Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/memory/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,20 @@ function deferred<T>() {
}

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', () => {
Expand Down
146 changes: 84 additions & 62 deletions packages/core/src/services/backgroundShellRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading