From 6d2274e0dc6e81aea60d538833a0d50de86c382c Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 12 Jun 2026 15:27:19 -0700 Subject: [PATCH] fix: ios-qa daemon scenarios use unique pidfiles under --concurrent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All scenarios shared join(workDir, 'daemon.pid') through a module-scope workDir binding that beforeEach reassigns mid-flight under bun --concurrent. First daemon claims; siblings get already_running against the test process's own always-alive pid and fail in milliseconds — the failure mode seen at 15-way gate concurrency. Per-claim unique pidfiles keep the single-instance semantics under test. Co-Authored-By: Claude Fable 5 --- test/skill-e2e-ios.test.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/skill-e2e-ios.test.ts b/test/skill-e2e-ios.test.ts index 70676d8ab..0f6c3b721 100644 --- a/test/skill-e2e-ios.test.ts +++ b/test/skill-e2e-ios.test.ts @@ -36,6 +36,16 @@ afterEach(() => { rmSync(workDir, { recursive: true, force: true }); }); +// Under `bun test --concurrent`, overlapping tests read the SAME shared +// `workDir` binding (beforeEach reassigns it mid-flight), so a fixed +// 'daemon.pid' name collides: the first daemon claims it and every sibling +// gets already_running against the test process's own (always-alive) pid — +// the exact failure seen in full gate runs at 15-way concurrency. Unique +// per-claim pidfiles keep the single-instance semantics under test while +// removing the cross-test collision. +let pidfileSeq = 0; +const uniquePidfile = () => join(workDir, `daemon-${++pidfileSeq}.pid`); + interface StubState { loggedIn: boolean; username: string; @@ -205,7 +215,7 @@ class AppState { const daemon = await startDaemon({ loopbackPort: 0, tailnetEnabled: false, - pidfilePath: join(workDir, 'daemon.pid'), + pidfilePath: uniquePidfile(), tunnelProvider: async () => tunnel, }); if ('error' in daemon) throw new Error(daemon.error); @@ -249,7 +259,7 @@ describe('ios-qa E2E (agent-flow simulation)', () => { const daemon = await startDaemon({ loopbackPort: 0, tailnetEnabled: false, - pidfilePath: join(workDir, 'daemon.pid'), + pidfilePath: uniquePidfile(), tunnelProvider: async () => tunnel, }); if ('error' in daemon) throw new Error(daemon.error); @@ -314,7 +324,7 @@ describe('ios-qa E2E (agent-flow simulation)', () => { const daemon = await startDaemon({ loopbackPort: 0, tailnetEnabled: false, - pidfilePath: join(workDir, 'daemon.pid'), + pidfilePath: uniquePidfile(), tunnelProvider: async () => tunnel, }); if ('error' in daemon) throw new Error(daemon.error); @@ -352,7 +362,7 @@ describe('ios-qa E2E (agent-flow simulation)', () => { const daemon = await startDaemon({ loopbackPort: 0, tailnetEnabled: true, - pidfilePath: join(workDir, 'daemon.pid'), + pidfilePath: uniquePidfile(), tunnelProvider: async () => tunnel, probeImpl: async () => ({ ok: true, ownIdentity: 'mac@e2e' }), whoIsImpl: async () => ({ identity: 'agent@e2e', raw: {} }), @@ -430,7 +440,7 @@ describe('ios-qa E2E (agent-flow simulation)', () => { const daemon = await startDaemon({ loopbackPort: 0, tailnetEnabled: true, - pidfilePath: join(workDir, 'daemon.pid'), + pidfilePath: uniquePidfile(), tunnelProvider: async () => tunnel, probeImpl: async () => ({ ok: true, ownIdentity: 'mac@e2e' }), whoIsImpl: async () => ({ identity: 'readonly@e2e', raw: {} }),