137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
|
import { resolveBaselineReleaseRef } from './release-checkout'
|
|
import {
|
|
JOURNEY_INPUTS,
|
|
JOURNEY_STEPS,
|
|
runTerminalSkewJourney,
|
|
type JourneyRecord
|
|
} from './terminal-skew-journey'
|
|
import {
|
|
loadTerminalWireBuild,
|
|
WORKING_TREE,
|
|
type TerminalWireBuild
|
|
} from './versioned-terminal-wire'
|
|
|
|
// Why: a cold CI run extracts the baseline checkout before the first journey.
|
|
const SUITE_TIMEOUT_MS = 180_000
|
|
|
|
/**
|
|
* The frames one journey must produce, named rather than numbered so a diff reads
|
|
* as a protocol change. Any deviation is a change in what a peer publishes or
|
|
* accepts, and needs a human decision against docs/reference/remote-wire-compatibility.md.
|
|
*/
|
|
const EXPECTED_JOURNEY_FRAMES = [
|
|
'C>H Subscribe',
|
|
'H>C SnapshotStart',
|
|
'H>C SnapshotChunk',
|
|
'H>C SnapshotEnd',
|
|
'C>H Input',
|
|
'H>C Output',
|
|
'C>H SnapshotRequest',
|
|
'H>C SnapshotStart',
|
|
'H>C SnapshotChunk',
|
|
'H>C SnapshotEnd',
|
|
'C>H Subscribe',
|
|
'H>C SnapshotStart',
|
|
'H>C SnapshotChunk',
|
|
'H>C SnapshotEnd',
|
|
'C>H Input',
|
|
'C>H Unsubscribe'
|
|
]
|
|
|
|
let baselineRef: string
|
|
let current: TerminalWireBuild
|
|
let baseline: TerminalWireBuild
|
|
|
|
beforeAll(async () => {
|
|
baselineRef = resolveBaselineReleaseRef()
|
|
current = await loadTerminalWireBuild(WORKING_TREE)
|
|
baseline = await loadTerminalWireBuild(baselineRef)
|
|
}, SUITE_TIMEOUT_MS)
|
|
|
|
afterEach(() => {
|
|
// Each journey installs and removes its own window stub; fail loudly if one leaked.
|
|
expect(typeof globalThis.window).toBe('undefined')
|
|
})
|
|
|
|
function expectJourneyActuallyRan(record: JourneyRecord): void {
|
|
// The anti-vacuous-pass oracle. A harness that connects and then does nothing
|
|
// fails here, because "nothing threw" is never enough to call a pairing green.
|
|
expect(record.completed).toEqual([...JOURNEY_STEPS])
|
|
expect(record.frameSequence).toEqual(EXPECTED_JOURNEY_FRAMES)
|
|
expect(record.subscribedEvents).toHaveLength(2)
|
|
expect(record.snapshotStarts).toHaveLength(3)
|
|
expect(record.missingRuntimeMethods).toEqual([])
|
|
}
|
|
|
|
function expectWireCompatible(record: JourneyRecord): void {
|
|
// Rule 2 — no frame may be refused by the receiving build's decoder. An opcode
|
|
// the peer does not know is dropped silently, so this is the only signal.
|
|
expect(record.rejected).toEqual([])
|
|
expect(record.clientErrors).toEqual([])
|
|
|
|
// The subscribe handshake still negotiates the optional output-pause opcode,
|
|
// which is what keeps opcode 16 legal to send on this pairing.
|
|
for (const event of record.subscribedEvents) {
|
|
expect(event.capabilities).toEqual({ outputPause: 1 })
|
|
}
|
|
|
|
// Input reached the process, before and after the reconnect.
|
|
expect(record.inputAtProcess).toEqual([JOURNEY_INPUTS.first, JOURNEY_INPUTS.second])
|
|
|
|
// Rule 3 — what the host publishes, as the client actually rendered it.
|
|
expect(record.snapshotsRendered[0]).toBe(JOURNEY_INPUTS.initialBuffer)
|
|
expect(record.dataRendered.join('')).toBe(JOURNEY_INPUTS.output)
|
|
expect(record.revealSnapshot?.data).toBe(
|
|
`${JOURNEY_INPUTS.initialBuffer}${JOURNEY_INPUTS.output}`
|
|
)
|
|
expect(record.revealSnapshot).toMatchObject({ cols: 120, rows: 40 })
|
|
for (const start of record.snapshotStarts) {
|
|
expect(start).toMatchObject({ kind: 'scrollback', cols: 120, rows: 40, source: 'headless' })
|
|
}
|
|
}
|
|
|
|
describe('cross-version remote terminal wire', () => {
|
|
it(
|
|
'skews current code against a real published release',
|
|
() => {
|
|
expect(baselineRef).toMatch(/^v?\d/)
|
|
expect(baseline.revision).toMatch(/^[0-9a-f]{40}$/)
|
|
expect(baseline.revision).not.toBe(current.revision)
|
|
},
|
|
SUITE_TIMEOUT_MS
|
|
)
|
|
|
|
it(
|
|
'current client against current server completes the journey',
|
|
async () => {
|
|
const record = await runTerminalSkewJourney({ hostBuild: current, clientBuild: current })
|
|
expectJourneyActuallyRan(record)
|
|
expectWireCompatible(record)
|
|
},
|
|
SUITE_TIMEOUT_MS
|
|
)
|
|
|
|
it(
|
|
'old client against new server completes the journey',
|
|
async () => {
|
|
const record = await runTerminalSkewJourney({ hostBuild: current, clientBuild: baseline })
|
|
expect(record.clientRevision).toBe(baseline.revision)
|
|
expectJourneyActuallyRan(record)
|
|
expectWireCompatible(record)
|
|
},
|
|
SUITE_TIMEOUT_MS
|
|
)
|
|
|
|
it(
|
|
'new client against old server completes the journey',
|
|
async () => {
|
|
const record = await runTerminalSkewJourney({ hostBuild: baseline, clientBuild: current })
|
|
expect(record.hostRevision).toBe(baseline.revision)
|
|
expectJourneyActuallyRan(record)
|
|
expectWireCompatible(record)
|
|
},
|
|
SUITE_TIMEOUT_MS
|
|
)
|
|
})
|