From b0d78d8632c27d01cda3d30b0653716cf908e8ae Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Mon, 20 Jul 2026 10:32:45 +0000 Subject: [PATCH] fix(mosaic): de-flake mutator-class lease gate TTL-expiry test (#861) --- .../mutator-gate.acceptance.spec.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts b/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts index ca809b18..4fb90ea6 100644 --- a/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts +++ b/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts @@ -661,13 +661,27 @@ describe('whole mutator-class lease gate', () => { test('observer revocation and monotonic TTL expiry deny the next mutator', async () => { const { socket } = await startBroker(); const sessionId = await register(socket); - const pending = await beginVerification(socket, sessionId, 'claude', 1, 1); - await promote(socket, sessionId, pending.receipt_challenge!); + // Establish the lease with a normal (non-racing) TTL first and prove it + // authorizes. This "still valid" check is setup, not a TTL-expiry + // assertion, so it must not share a lease with a 1-second TTL: on a + // contended push-CI host, scheduling delay alone between promote() and + // this authorize() call can consume that entire 1-second margin and + // spuriously deny it (CI#1945). Using a generous TTL here removes that + // real-time race without touching lease-gate security semantics. + const pending = await beginVerification(socket, sessionId, 'claude'); + await promote(socket, sessionId, pending.receipt_challenge!); expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({ ok: true, decision: 'allow', }); + + // A dedicated, isolated short-TTL lease drives the deliberate monotonic + // expiry demonstration below. It is never used for anything but the + // wait-then-expire assertion, so there is no setup work racing its + // 1-second window. + const shortLived = await beginVerification(socket, sessionId, 'claude', 1, 1, 2); + await promote(socket, sessionId, shortLived.receipt_challenge!); await new Promise((resolve) => setTimeout(resolve, 1_100)); expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({ ok: false, @@ -675,7 +689,7 @@ describe('whole mutator-class lease gate', () => { decision: 'deny', }); - const refreshed = await beginVerification(socket, sessionId, 'claude', 1, 300, 2); + const refreshed = await beginVerification(socket, sessionId, 'claude', 1, 300, 3); await promote(socket, sessionId, refreshed.receipt_challenge!); expect( await request(socket, {