test(relay): pin the live-record ownership fallback across owner-map eviction

The resident disjunct in stillOwnsPtyId let a PTY that outlives 4,096
admissions certify its own exit after its owner entry was evicted; nothing
checked in failed when it was removed. Round-5 review found the survivor.
This commit is contained in:
Jinwoo-H
2026-09-07 22:34:20 -04:00
parent 847912e54c
commit 46a693a4db
2 changed files with 51 additions and 3 deletions
+5 -3
View File
@@ -18744,7 +18744,9 @@
"does not certify a revived process with the exit of the one it replaced",
"does not let a superseded incarnation certify the id once both records are gone",
"evicts observations in the order it made them, not the order it was asked",
"forgets every observation when the relay restarts, even under the same mint epoch"
"forgets every observation when the relay restarts, even under the same mint epoch",
"certifies a long-lived resident whose owner entry was evicted",
"withholds a late observation once both the record and its owner entry are gone"
]
},
{
@@ -18790,12 +18792,12 @@
"command": "ORCA_BACKGROUND_LAUNCH=1 node_modules/.bin/vitest run --config config/vitest.config.ts src/relay/pty-handler-liveness-probe.test.ts src/relay/pty-handler-ssh-exit-certification.test.ts src/relay/pty-handler-ssh-recovery-convergence.test.ts src/main/providers/ssh-pty-liveness-probe.test.ts src/main/providers/ssh-pty-provider-liveness-readback.test.ts src/main/runtime/runtime-legacy-worker-terminal-exit-certification.test.ts",
"result": "passed",
"durationSeconds": 1.27,
"summary": "6 files, 56 tests passed. The convergence file fails 1/3 without the owner readback; twelve mutations of the probe, its incarnation binding, the published capabilities, the bounded map and the forwarder each fail at least one of these tests."
"summary": "6 files, 58 tests passed. The convergence file fails 1/3 without the owner readback; thirteen mutations of the probe, its incarnation binding, the published capabilities, the bounded map and the forwarder each fail at least one of these tests."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "Six unit/service-level files; the ledger-eviction case drives 4,097 spawn-and-exit cycles through the real handler and reads the oldest entry before the overflow and dominates the run at roughly 0.8 s."
"scope": "Six unit/service-level files; the ledger-eviction and owner-eviction cases each drive 4,096+ spawn-and-exit cycles through the real handler and reads the oldest entry before the overflow and dominates the run at roughly 0.8 s."
},
"flakeHistory": {
"status": "not-started",
@@ -374,4 +374,50 @@ describe('PtyHandler.probeLiveness', () => {
expect(await probe(id)).toBe('unverifiable')
})
})
describe('owner entries evicted from the bounded map while a record outlives them', () => {
async function churnOwnersPastTheCap(): Promise<void> {
for (let index = 0; index < OBSERVED_PTY_EXIT_HISTORY; index++) {
await spawnPty()
reportExitOfLatestPty()
}
}
function ownerEntry(id: string): string | undefined {
return (
handler as unknown as { ptyIdIncarnationOwners: { peek(key: string): string | undefined } }
).ptyIdIncarnationOwners.peek(id)
}
it('certifies a long-lived resident whose owner entry was evicted', async () => {
const { id } = await spawnPty()
const exit = mockPtyInstance.onExit.mock.calls.at(-1)?.[0] as (event: {
exitCode: number
}) => void
await churnOwnersPastTheCap()
expect(ownerEntry(id)).toBeUndefined()
expect(await probe(id)).toBe('live')
// The record is still in the pool, so it speaks for the id without the owner map: a PTY
// that outlives 4,096 admissions must still certify its own exit.
exit({ exitCode: 0 })
expect(await probe(id)).toBe('exited')
})
it('withholds a late observation once both the record and its owner entry are gone', async () => {
const { id } = await spawnPty()
const exit = mockPtyInstance.onExit.mock.calls.at(-1)?.[0] as (event: {
exitCode: number
}) => void
await forgetRecordWithoutObservingItsExit(id)
await churnOwnersPastTheCap()
expect(ownerEntry(id)).toBeUndefined()
// Forgetting fails closed: with no owner to compare against, nothing may record.
exit({ exitCode: 0 })
expect(await probe(id)).toBe('unverifiable')
})
})
})