Skip to content

Commit 8e79ee0

Browse files
Contributorcursoragent
andcommitted
test(rerun): restore DEV-331 InterruptError batch --wait regression
Re-add the R-BAT batch rerun --wait InterruptError test removed during the TimeoutError stdout refactor. Reviewer requires no main describe/it deletions unless explicitly agreed obsolete. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 943b761 commit 8e79ee0

1 file changed

Lines changed: 97 additions & 1 deletion

File tree

src/commands/test.rerun.spec.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
99
import { tmpdir } from 'node:os';
1010
import { join } from 'node:path';
1111
import { describe, expect, it } from 'vitest';
12-
import { ApiError, RequestTimeoutError } from '../lib/errors.js';
12+
import { ApiError, InterruptError, RequestTimeoutError } from '../lib/errors.js';
13+
import { ShutdownController } from '../lib/interrupt.js';
1314
import type { RunResponse, RerunResponse, BatchRerunResponse } from '../lib/runs.types.js';
1415
import type { FetchImpl } from '../lib/http.js';
1516
import { runTestRerun, resolveWaitRequestTimeoutMs } from './test.js';
@@ -4952,3 +4953,98 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t
49524953
void fetchCallCount;
49534954
});
49544955
});
4956+
4957+
// ---------------------------------------------------------------------------
4958+
// DEV-331 piece 1 — graceful detach during batch rerun --wait (SIG-6)
4959+
// ---------------------------------------------------------------------------
4960+
4961+
describe('R-BAT: batch rerun --wait — InterruptError partial lists all dispatched runIds (DEV-331)', () => {
4962+
it('interrupt mid fan-out → stdout partial covers every accepted runId, honest stderr, exit 130', async () => {
4963+
const creds = makeCreds();
4964+
const shutdown = new ShutdownController();
4965+
const batchResp: BatchRerunResponse = {
4966+
accepted: [
4967+
{ testId: 'test_1', runId: 'run_b1', enqueuedAt: '2026-06-03T10:00:00.000Z' },
4968+
{ testId: 'test_2', runId: 'run_b2', enqueuedAt: '2026-06-03T10:00:00.000Z' },
4969+
],
4970+
deferred: [],
4971+
conflicts: [],
4972+
closure: { byProject: [] },
4973+
};
4974+
4975+
// Batch trigger resolves; every run poll hangs until the composed signal aborts.
4976+
const fetchImpl: FetchImpl = (async (input: unknown, init: RequestInit = {}) => {
4977+
const url =
4978+
typeof input === 'string'
4979+
? input
4980+
: input instanceof URL
4981+
? input.toString()
4982+
: (input as { url: string }).url;
4983+
if (url.includes('/tests/batch/rerun')) {
4984+
return new Response(JSON.stringify(batchResp), {
4985+
status: 202,
4986+
headers: { 'content-type': 'application/json' },
4987+
});
4988+
}
4989+
return new Promise<Response>((_resolve, reject) => {
4990+
const signal = init.signal;
4991+
const rejectWithReason = (): void => {
4992+
const reason: unknown = signal?.reason;
4993+
reject(reason instanceof Error ? reason : new Error('aborted'));
4994+
};
4995+
if (signal?.aborted) {
4996+
rejectWithReason();
4997+
return;
4998+
}
4999+
signal?.addEventListener('abort', rejectWithReason, { once: true });
5000+
});
5001+
}) as FetchImpl;
5002+
5003+
const stdoutLines: string[] = [];
5004+
const stderrLines: string[] = [];
5005+
const pending = runTestRerun(
5006+
{
5007+
testIds: ['test_1', 'test_2'],
5008+
all: false,
5009+
wait: true,
5010+
timeoutSeconds: 600,
5011+
autoHeal: false,
5012+
autoHealExplicit: false,
5013+
skipDependencies: false,
5014+
maxConcurrency: 10,
5015+
output: 'json',
5016+
profile: 'default',
5017+
dryRun: false,
5018+
debug: false,
5019+
verbose: false,
5020+
},
5021+
{
5022+
...creds,
5023+
sleep: instantSleep,
5024+
fetchImpl,
5025+
stdout: line => stdoutLines.push(line),
5026+
stderr: line => stderrLines.push(line),
5027+
shutdown,
5028+
},
5029+
);
5030+
setTimeout(() => shutdown.interrupt('SIGINT'), 10);
5031+
5032+
const err = await pending.catch(e => e);
5033+
expect(err).toBeInstanceOf(InterruptError);
5034+
expect((err as InterruptError).exitCode).toBe(130);
5035+
5036+
// SIG-6: the partial lists ALL dispatched runIds, marked running.
5037+
const stdoutJson = JSON.parse(stdoutLines.join('\n')) as {
5038+
accepted: Array<{ runId: string; status: string }>;
5039+
};
5040+
const byRunId = new Map(stdoutJson.accepted.map(r => [r.runId, r.status]));
5041+
expect(byRunId.get('run_b1')).toBe('running');
5042+
expect(byRunId.get('run_b2')).toBe('running');
5043+
5044+
const stderrBlock = stderrLines.join('\n');
5045+
expect(stderrBlock).toContain('Interrupted (SIGINT)');
5046+
expect(stderrBlock).toContain('billing');
5047+
expect(stderrBlock).toContain('run_b1');
5048+
expect(stderrBlock).toContain('run_b2');
5049+
});
5050+
});

0 commit comments

Comments
 (0)