diff --git a/packages/cli/src/commands/review/lib/manifest-repository-context.test.ts b/packages/cli/src/commands/review/lib/manifest-repository-context.test.ts index ba6dbe01c8c..8d40883d271 100644 --- a/packages/cli/src/commands/review/lib/manifest-repository-context.test.ts +++ b/packages/cli/src/commands/review/lib/manifest-repository-context.test.ts @@ -32,12 +32,15 @@ function temp(): string { } // Several fixtures hold 16k-entry trees; leaking them exhausts a tmpfs -// /tmp within a handful of runs. +// /tmp within a handful of runs. Deleting them is tens of thousands of +// unlinks, which has blown past the default 10s hook timeout on a loaded +// CI runner — give the teardown the time it needs rather than failing a +// green suite on cleanup. afterAll(() => { for (const root of worktrees) { rmSync(root, { recursive: true, force: true }); } -}); +}, 120_000); function write(path: string, content = ''): void { mkdirSync(dirname(path), { recursive: true }); diff --git a/packages/core/src/memory/extract.test.ts b/packages/core/src/memory/extract.test.ts index 32cb11b4741..9e8d6be68e2 100644 --- a/packages/core/src/memory/extract.test.ts +++ b/packages/core/src/memory/extract.test.ts @@ -44,13 +44,20 @@ function deferred() { } async function waitForMockCall(mock: { mock: { calls: unknown[] } }) { - for (let i = 0; i < 10; i++) { + // Wall-clock deadline, not a fixed tick count: the mock is invoked after + // real async work (index reads, cursor I/O), and ten zero-delay turns can + // elapse before that work completes on a loaded CI runner — the poll spun + // through its turns without waiting any actual time. + const deadline = Date.now() + 2000; + for (;;) { if (mock.mock.calls.length > 0) { return; } - await new Promise((resolve) => setTimeout(resolve, 0)); + if (Date.now() >= deadline) { + throw new Error('Expected mock to be called'); + } + await new Promise((resolve) => setTimeout(resolve, 5)); } - throw new Error('Expected mock to be called'); } describe('auto-memory extraction', () => { diff --git a/packages/core/src/services/backgroundShellRegistry.test.ts b/packages/core/src/services/backgroundShellRegistry.test.ts index ba07b80f640..a462c0fa918 100644 --- a/packages/core/src/services/backgroundShellRegistry.test.ts +++ b/packages/core/src/services/backgroundShellRegistry.test.ts @@ -669,73 +669,95 @@ describe('BackgroundShellRegistry', () => { }); }); + // Every register/complete in these loop tests also writes the status + // sidecar through atomicWriteFileSync, and a loaded CI runner has been + // measured spending ~700ms per sidecar write — ~50s for the ~70 writes + // of the longest loop, past the 15s default. The explicit timeout buys + // the I/O the time it costs; the assertions are unchanged. + const SIDECAR_IO_TIMEOUT = 120_000; describe('terminal-entry retention cap', () => { - it('retains only a bounded number of terminal entries (oldest by endTime evicted)', () => { - const reg = new BackgroundShellRegistry(); - // Register and complete one more entry than the cap allows. Use - // strictly increasing endTimes so eviction order is deterministic. - for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS + 2; i++) { - reg.register(makeEntry({ shellId: `s-${i}`, startTime: i * 10 })); - reg.complete(`s-${i}`, 0, i * 10 + 5); - } - expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); - // The two oldest (`s-0`, `s-1`) get pruned; the newest survives. - expect(reg.get('s-0')).toBeUndefined(); - expect(reg.get('s-1')).toBeUndefined(); - expect(reg.get(`s-${MAX_RETAINED_TERMINAL_SHELLS + 1}`)).toBeDefined(); - }); + it( + 'retains only a bounded number of terminal entries (oldest by endTime evicted)', + () => { + const reg = new BackgroundShellRegistry(); + // Register and complete one more entry than the cap allows. Use + // strictly increasing endTimes so eviction order is deterministic. + for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS + 2; i++) { + reg.register(makeEntry({ shellId: `s-${i}`, startTime: i * 10 })); + reg.complete(`s-${i}`, 0, i * 10 + 5); + } + expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); + // The two oldest (`s-0`, `s-1`) get pruned; the newest survives. + expect(reg.get('s-0')).toBeUndefined(); + expect(reg.get('s-1')).toBeUndefined(); + expect(reg.get(`s-${MAX_RETAINED_TERMINAL_SHELLS + 1}`)).toBeDefined(); + }, + SIDECAR_IO_TIMEOUT, + ); - it('never evicts running entries even when the cap is exceeded', () => { - const reg = new BackgroundShellRegistry(); - // Register one extra terminal entry beyond the cap, then a single - // running entry. The running entry must be retained regardless of - // its launch order — pruning a still-running shell would lose the - // user's only handle on a live process. - reg.register(makeEntry({ shellId: 'live', startTime: 1 })); - for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS + 1; i++) { + it( + 'never evicts running entries even when the cap is exceeded', + () => { + const reg = new BackgroundShellRegistry(); + // Register one extra terminal entry beyond the cap, then a single + // running entry. The running entry must be retained regardless of + // its launch order — pruning a still-running shell would lose the + // user's only handle on a live process. + reg.register(makeEntry({ shellId: 'live', startTime: 1 })); + for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS + 1; i++) { + reg.register( + makeEntry({ shellId: `done-${i}`, startTime: 100 + i * 10 }), + ); + reg.complete(`done-${i}`, 0, 100 + i * 10 + 5); + } + // Cap-of-32 terminals + 1 running survivor = 33 entries kept. + expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS + 1); + expect(reg.get('live')?.status).toBe('running'); + // The oldest terminal entry (lowest endTime) is the one evicted. + expect(reg.get('done-0')).toBeUndefined(); + }, + SIDECAR_IO_TIMEOUT, + ); + + it( + 'prunes after fail() too, not just complete()', + () => { + const reg = new BackgroundShellRegistry(); + for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS; i++) { + reg.register(makeEntry({ shellId: `done-${i}`, startTime: i * 10 })); + reg.complete(`done-${i}`, 0, i * 10 + 5); + } + const overflowStart = MAX_RETAINED_TERMINAL_SHELLS * 10 + 100; reg.register( - makeEntry({ shellId: `done-${i}`, startTime: 100 + i * 10 }), + makeEntry({ shellId: 'overflow', startTime: overflowStart }), ); - reg.complete(`done-${i}`, 0, 100 + i * 10 + 5); - } - // Cap-of-32 terminals + 1 running survivor = 33 entries kept. - expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS + 1); - expect(reg.get('live')?.status).toBe('running'); - // The oldest terminal entry (lowest endTime) is the one evicted. - expect(reg.get('done-0')).toBeUndefined(); - }); - - it('prunes after fail() too, not just complete()', () => { - const reg = new BackgroundShellRegistry(); - for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS; i++) { - reg.register(makeEntry({ shellId: `done-${i}`, startTime: i * 10 })); - reg.complete(`done-${i}`, 0, i * 10 + 5); - } - const overflowStart = MAX_RETAINED_TERMINAL_SHELLS * 10 + 100; - reg.register( - makeEntry({ shellId: 'overflow', startTime: overflowStart }), - ); - reg.fail('overflow', 'boom', overflowStart + 5); - expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); - expect(reg.get('done-0')).toBeUndefined(); - expect(reg.get('overflow')?.status).toBe('failed'); - }); + reg.fail('overflow', 'boom', overflowStart + 5); + expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); + expect(reg.get('done-0')).toBeUndefined(); + expect(reg.get('overflow')?.status).toBe('failed'); + }, + SIDECAR_IO_TIMEOUT, + ); - it('prunes after cancel() too, not just complete()', () => { - const reg = new BackgroundShellRegistry(); - for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS; i++) { - reg.register(makeEntry({ shellId: `done-${i}`, startTime: i * 10 })); - reg.complete(`done-${i}`, 0, i * 10 + 5); - } - const overflowStart = MAX_RETAINED_TERMINAL_SHELLS * 10 + 100; - reg.register( - makeEntry({ shellId: 'overflow', startTime: overflowStart }), - ); - reg.cancel('overflow', overflowStart + 5); - expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); - expect(reg.get('done-0')).toBeUndefined(); - expect(reg.get('overflow')?.status).toBe('cancelled'); - }); + it( + 'prunes after cancel() too, not just complete()', + () => { + const reg = new BackgroundShellRegistry(); + for (let i = 0; i < MAX_RETAINED_TERMINAL_SHELLS; i++) { + reg.register(makeEntry({ shellId: `done-${i}`, startTime: i * 10 })); + reg.complete(`done-${i}`, 0, i * 10 + 5); + } + const overflowStart = MAX_RETAINED_TERMINAL_SHELLS * 10 + 100; + reg.register( + makeEntry({ shellId: 'overflow', startTime: overflowStart }), + ); + reg.cancel('overflow', overflowStart + 5); + expect(reg.getAll()).toHaveLength(MAX_RETAINED_TERMINAL_SHELLS); + expect(reg.get('done-0')).toBeUndefined(); + expect(reg.get('overflow')?.status).toBe('cancelled'); + }, + SIDECAR_IO_TIMEOUT, + ); }); describe('cancel', () => {