From 113515f25adbec985dc0ea76d2dc1309c50cdfaf Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 10 Sep 2026 19:35:48 -0700 Subject: [PATCH] test(runtime): pin the retraction boundary against a stale inventory omission Mutation testing left a survivor: writing the boundary unconditionally, instead of only when it advances the ledger, passed the whole runtime suite. It is not inert. A visibility-resume inventory reserves its received frame before it lists, so an omission it reports can be older than a stream frame that landed meanwhile; without the guard that stale omission rewinds the ledger, forgetting the stream frame's version, and a delayed list reserved in between is then readmitted instead of outranked. This pins that ordering. The one remaining survivor is the boundary's `snapshotVersion`, and it is inert: the ledger's version is read at exactly two sites, both reachable only when the incoming frame's epoch equals the stored one, and a retraction epoch never equals a live publication. --- ...moved-frame-retires-live-publisher.test.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts b/src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts index 956ca22a5e9..b0574285eff 100644 --- a/src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts @@ -2,10 +2,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { decideWebSessionTabsSnapshot } from './web-session-tabs-sync' import { + recordReceivedWebSessionTabsRemoval, recordReceivedWebSessionTabsSnapshot, shouldApplyRecoveredWebSessionTabsSnapshot } from './web-session-tabs-sync/tracking' -import { nextReceivedSessionTabsFrame } from './web-session-tabs-sync/state' +import { + nextReceivedSessionTabsFrame, + VISIBILITY_INVENTORY_REMOVAL_EPOCH +} from './web-session-tabs-sync/state' import { resetWebSessionTabsSyncTestState } from './web-session-tabs-sync-test-harness' vi.mock('../store', () => ({ useAppStore: { setState: vi.fn() } })) @@ -133,6 +137,41 @@ describe('a removal frame must not retire the publisher that is still live', () expect(admits(delayed, delayedReceived)).toBe(false) }) + /** + * The boundary is evidence, so a retraction may only ever advance it. A visibility-resume + * inventory reserves its received frame before it lists, so an omission it reports can be older + * than a stream frame that landed meanwhile. Letting that stale omission rewind the ledger would + * forget the stream frame's version and readmit a delayed frame the ledger had already outranked. + */ + it('does not let an inventory omission older than the last stream frame rewind the boundary', () => { + const inventoryReceived = nextReceivedSessionTabsFrame() + const delayedReceived = nextReceivedSessionTabsFrame() + const streamReceived = recordReceivedWebSessionTabsSnapshot(ENVIRONMENT_ID, liveFrame(3)) + expect(delayedReceived).toBeGreaterThan(inventoryReceived) + expect(streamReceived).toBeGreaterThan(delayedReceived) + + // The inventory sweep finally reports this worktree missing, on its older frame. + recordReceivedWebSessionTabsRemoval( + ENVIRONMENT_ID, + WORKTREE, + inventoryReceived, + VISIBILITY_INVENTORY_REMOVAL_EPOCH + ) + + // A list reserved before the stream frame lands last, carrying a genuinely stale version. + const delayed = liveFrame(1) + recordReceivedWebSessionTabsSnapshot( + ENVIRONMENT_ID, + delayed, + delayedReceived, + undefined, + 'bootstrap' + ) + expect( + shouldApplyRecoveredWebSessionTabsSnapshot(ENVIRONMENT_ID, delayed, delayedReceived) + ).toBe(false) + }) + // Why this stays fenced: a genuinely superseded generation is retired by a *successor's* // publication, which is a handover. Only the removal path must stop retiring. it('still fences a predecessor generation that a successor replaced', () => {