Skip to content

Commit 03251d7

Browse files
ahndohunclaude
andauthored
fix(test): stop run --all --wait from polling queued runs past the shared deadline (#130)
runTestRunAll computes each member's poll budget as Math.max(1, ceil(batchDeadlineMs - now)), so a run whose turn arrives after the shared --timeout deadline still gets a fresh >=1s poll. With --max-concurrency bounding the fan-out, a batch could overshoot the documented shared deadline and report a late 'passed' for a member that should have been reported as 'timeout' (exit 7). Guard the poll helper the same way the create-batch --run --wait path already does: if the shared deadline is exhausted before a member's poll starts, return the existing timeout-shaped member result without calling pollRunUntilTerminal. Regression test drives the clock past the deadline during the first member's poll and asserts the second member is never polled and reports 'timeout'. Co-authored-by: ahndohun <19940813+ahndohun@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 03c4f11 commit 03251d7

2 files changed

Lines changed: 69 additions & 1 deletion

File tree

src/commands/test.run.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2487,6 +2487,61 @@ describe('runTestRunAll — batch fresh run', () => {
24872487
expect(payload.accepted.every(r => r.status === 'passed')).toBe(true);
24882488
});
24892489

2490+
it('run --all --wait: does not start a fresh poll for a queued run after the shared deadline expired', async () => {
2491+
const { credentialsPath } = makeCreds();
2492+
const baseNow = new Date('2026-06-09T10:00:00.000Z').getTime();
2493+
let nowMs = baseNow;
2494+
const runFetches: string[] = [];
2495+
const stdoutLines: string[] = [];
2496+
let caughtError: unknown;
2497+
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => nowMs);
2498+
2499+
const fetchImpl = makeFetch((url, init) => {
2500+
const method = init.method ?? 'GET';
2501+
if (method === 'POST') return { body: BATCH_FRESH_RESP };
2502+
2503+
const runId = url.split('/runs/')[1]?.split('?')[0] ?? 'run_unknown';
2504+
runFetches.push(runId);
2505+
if (runId === 'run_fresh_01') {
2506+
nowMs = baseNow + 2000;
2507+
return { body: makePassedRun(runId, 'test_be_01') };
2508+
}
2509+
return { body: makePassedRun(runId, 'test_be_02') };
2510+
});
2511+
2512+
try {
2513+
await runTestRunAll(
2514+
{
2515+
profile: 'default',
2516+
output: 'json',
2517+
debug: false,
2518+
projectId: 'project_be',
2519+
wait: true,
2520+
timeoutSeconds: 1,
2521+
maxConcurrency: 1,
2522+
},
2523+
{
2524+
credentialsPath,
2525+
fetchImpl,
2526+
stdout: line => stdoutLines.push(line),
2527+
stderr: () => undefined,
2528+
sleep: instantSleep,
2529+
},
2530+
);
2531+
} catch (err) {
2532+
caughtError = err;
2533+
} finally {
2534+
nowSpy.mockRestore();
2535+
}
2536+
2537+
const payload = JSON.parse(stdoutLines.join('\n')) as {
2538+
accepted: Array<{ runId: string; status: string }>;
2539+
};
2540+
expect(runFetches).toEqual(['run_fresh_01']);
2541+
expect(payload.accepted.find(r => r.runId === 'run_fresh_02')?.status).toBe('timeout');
2542+
expect((caughtError as { exitCode?: number } | undefined)?.exitCode).toBe(7);
2543+
});
2544+
24902545
it('--wait with a failed run → exit 1', async () => {
24912546
const { credentialsPath } = makeCreds();
24922547
const fetchImpl = makeFetch((url, init) => {

src/commands/test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5432,7 +5432,20 @@ export async function runTestRunAll(
54325432

54335433
async function pollFreshAccepted(entry: BatchRunFreshAccepted): Promise<CliBatchRunFreshResult> {
54345434
const runId = entry.runId;
5435-
const remainingSeconds = Math.max(1, Math.ceil((batchDeadlineMs - Date.now()) / 1000));
5435+
const remainingMs = batchDeadlineMs - Date.now();
5436+
if (remainingMs <= 0) {
5437+
return {
5438+
testId: entry.testId,
5439+
runId,
5440+
status: 'timeout',
5441+
error: {
5442+
code: 'UNSUPPORTED',
5443+
message: `Timed out after ${opts.timeoutSeconds}s`,
5444+
exitCode: 7,
5445+
},
5446+
};
5447+
}
5448+
const remainingSeconds = Math.ceil(remainingMs / 1000);
54365449
const resolveAlternate = makeBackendWaitFallback({
54375450
client,
54385451
resolveTestId: () => entry.testId,

0 commit comments

Comments
 (0)