From 46a693a4db74fb0b68b09af8b072cbbfe1b78f08 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 22:34:20 -0400 Subject: [PATCH] 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. --- config/reliability-gates.jsonc | 8 ++-- src/relay/pty-handler-liveness-probe.test.ts | 46 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 9d5ed04ec7d..c2bbf962838 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -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", diff --git a/src/relay/pty-handler-liveness-probe.test.ts b/src/relay/pty-handler-liveness-probe.test.ts index ca5154c8c7c..3ece338bea6 100644 --- a/src/relay/pty-handler-liveness-probe.test.ts +++ b/src/relay/pty-handler-liveness-probe.test.ts @@ -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 { + 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') + }) + }) })