Files
orca/tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts
T
Brennan BensonandBrennan Benson 11d8673112 test(cross-version-wire): derive skew expectations from the baseline under test (#17178)
* test(cross-version-wire): derive skew expectations from the baseline under test

The cross-version wire job pairs current code against whichever release tag is
newest, so a hand-written "the old side does not have X" assertion expires by
itself: v1.4.192 was the first tag containing the SnapshotStart `terminalOwner`
field, and cutting it turned the new-client/old-server pairing red on unrelated
pull requests with no code change anywhere.

Read what each build publishes from that build. Each host is now paired against
a client of its own version to produce a reference, and the skewed pairings are
compared against that reference, so the expectation is whatever the release
actually shipped. The same class of assertion in the agent-session suite —
"the old build advertises no structured capability and registers no structured
method" — becomes "each build's advertisement agrees with what it registers",
and the "client too old to know this capability" is derived by removing the
capability from the baseline's own list.

The guard is unchanged in strength: a field the old host still publishes may not
be dropped, skew may not change what a host puts on the wire, and a new pairing
asserts the oracle still stalls when a peer cannot decode an opcode the other
side sends.

* test(cross-version-wire): exercise release structured methods

* test(cross-version-wire): load the registered method manifest

* test(cross-version-wire): assert execution, not registration, on both host gates

The release-shaped checkout gate accepted any reply that was not
method_not_found, so a registered-but-throwing handler passed it. The
capability gate asserted a shared host spy had been called at all, so the
second method mapped to that spy could stop reaching the host unnoticed.

* test(cross-version): make the release-shaped skew cover the whole agent-session manifest

The release-shaped checkout is the only place the "registered means usable"
claim is executable today — the baseline release registers none of these
methods — and it was exercising one of sixteen. A handler registered and
returning an execution error passed the suite.

- Declare each method's result in the manifest, so "answered" is the contract
  rather than "did not say method_not_found".
- Give each build a seam to install a host into its own module slot; a release
  checkout has its own copy, so the working tree's host was never this
  dispatcher's, and every host-backed method answered
  structured_agent_session_unsupported — the capability gate's own words.
- Run one execution contract over both skews instead of two divergent loops.
- Pair the AI Vault never-called spy with a positive control; renaming the
  runtime method it watches left it green.

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-08-29 13:42:50 -07:00

230 lines
9.3 KiB
TypeScript

// Cross-version coverage for the remote terminal stream, paired in both skew
// directions: current working tree against the newest published release.
//
// What each build publishes is read from that build, never written down here. The
// baseline is whichever release tag is newest, so a list of "fields the old side
// does not have yet" stops being true the moment a release ships one of them — the
// suite then reddens on whatever pull request is in flight, with no code change
// anywhere. Every version-dependent expectation below therefore comes from a
// same-version reference pairing of the build that publishes the frame.
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { comparePublishedFields, publishedFieldNames } from './published-field-shape'
import { resolveBaselineReleaseRef, selectLatestStableReleaseTag } from './release-checkout'
import {
CrossVersionJourneyStall,
JOURNEY_INPUTS,
JOURNEY_STEPS,
runTerminalSkewJourney,
type JourneyRecord
} from './terminal-skew-journey'
import {
loadTerminalWireBuild,
withoutOpcodeSupport,
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
/** What a current host publishes to a client of its own version. */
let currentReference: JourneyRecord
/** What the baseline host publishes to a client of its own version. */
let baselineReference: JourneyRecord
beforeAll(async () => {
baselineRef = resolveBaselineReleaseRef()
current = await loadTerminalWireBuild(WORKING_TREE)
baseline = await loadTerminalWireBuild(baselineRef)
currentReference = await runTerminalSkewJourney({ hostBuild: current, clientBuild: current })
baselineReference = await runTerminalSkewJourney({ hostBuild: baseline, clientBuild: baseline })
}, 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('ignores legacy, mobile, and prerelease tags when selecting the baseline', () => {
expect(
selectLatestStableReleaseTag([
'v799',
'mobile-v9.0.0',
'v1.4.177-rc.3',
'v1.4.175',
'v1.4.176'
])
).toBe('v1.4.176')
})
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, and is the reference for a current host', () => {
expectJourneyActuallyRan(currentReference)
expectWireCompatible(currentReference)
// Current code's own contract in both roles, so it is safe to state literally.
expect(currentReference.snapshotStarts).toEqual([
expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }),
expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }),
expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' })
])
})
it('old client against old server completes the journey, and is the reference for an old host', () => {
expect(baselineReference.hostRevision).toBe(baseline.revision)
expect(baselineReference.clientRevision).toBe(baseline.revision)
expectJourneyActuallyRan(baselineReference)
expectWireCompatible(baselineReference)
// Anti-vacuous: a reference read from a pairing that published nothing would
// make every comparison against it trivially true.
expect(publishedFieldNames(baselineReference.snapshotStarts).length).toBeGreaterThan(4)
})
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)
// Direction: the NEW host publishes here, and the old client only reads. Skew
// must not change what that host puts on the wire, so the expectation is the
// current host's own reference — whatever fields it carries today.
expect(record.snapshotStarts).toEqual(currentReference.snapshotStarts)
},
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)
// Direction: the OLD host publishes here, and the new client only reads. Which
// optional fields that release shipped is a property of the release, so it is
// read from the baseline's own pairing rather than named here.
expect(record.snapshotStarts).toEqual(baselineReference.snapshotStarts)
},
SUITE_TIMEOUT_MS
)
it('adds SnapshotStart fields rather than dropping ones the old host still publishes', () => {
const skew = comparePublishedFields({
older: publishedFieldNames(baselineReference.snapshotStarts),
newer: publishedFieldNames(currentReference.snapshotStarts)
})
// Rule 1 is additive-only. A field the old host still publishes is one an old
// client may still read, so dropping it breaks that client with no opcode
// change for the decoder check to catch.
expect(
skew.removed,
`current code stopped publishing SnapshotStart fields ${baselineRef} still publishes ` +
`(it added: ${skew.added.join(', ') || 'nothing'})`
).toEqual([])
})
it(
'still fails a pairing whose peer cannot decode an opcode the other side sends',
async () => {
// The regression case for the guard itself: relaxing a stale field list must
// not relax the real incompatibility. A short barrier only bounds a stall
// that is already certain — the frame either arrives at once, or never.
const inputOpcode = Number(current.codec.TerminalStreamOpcode.Input)
const stall = await runTerminalSkewJourney({
hostBuild: withoutOpcodeSupport(current, 'Input'),
clientBuild: current,
barrierTimeoutMs: 2_000
}).then(
() => null,
(error: unknown) => error
)
expect(stall).toBeInstanceOf(CrossVersionJourneyStall)
const stalled = stall as CrossVersionJourneyStall
expect(stalled.step).toBe('input-reaches-process')
expect(stalled.record.completed).not.toContain('input-reaches-process')
expect(stalled.record.inputAtProcess).toEqual([])
expect(stalled.record.rejected).toContainEqual(
expect.objectContaining({ direction: 'client-to-host', rawOpcode: inputOpcode })
)
},
SUITE_TIMEOUT_MS
)
})