Skip to content
Open
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
64 changes: 38 additions & 26 deletions src/commands/test.rerun.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4820,38 +4820,51 @@ describe('rerun --wait — dashboardUrl on terminal output', () => {
});

// ---------------------------------------------------------------------------
// Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty
// TimeoutError on single FE rerun --wait: partial stdout + exit 7
// ---------------------------------------------------------------------------

describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out poll writes JSON stdout + exit 7', () => {
it('stdout contains accepted[] with runIds when member polls throw RequestTimeoutError', async () => {
describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON to stdout', () => {
it('exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', async () => {
const creds = makeCreds();
const batchResp: BatchRerunResponse = {
accepted: [
{ testId: 'test_1', runId: 'run_b1', enqueuedAt: '2026-06-03T10:00:00.000Z' },
{ testId: 'test_2', runId: 'run_b2', enqueuedAt: '2026-06-03T10:00:00.000Z' },
],
deferred: [],
conflicts: [],
closure: { byProject: [] },
};
const fetchImpl = makeFetch(url => {
if (url.includes('/tests/batch/rerun')) {
return { status: 202, body: batchResp };
const rerunResp = makeFeRerunResp();

let fetchCallCount = 0;
const fetchImpl: typeof globalThis.fetch = async (input, _init) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: (input as { url: string }).url;
fetchCallCount++;
if (url.includes('/tests/test_fe_01/runs/rerun')) {
return new Response(JSON.stringify(rerunResp), {
status: 202,
headers: { 'content-type': 'application/json' },
});
}
if (url.includes('/runs/')) {
throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun');
const runningRun: RunResponse = {
...makeTerminalRun(rerunResp.runId, 'passed'),
status: 'running',
finishedAt: null,
};
return new Response(JSON.stringify(runningRun), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return errorBody('NOT_FOUND');
});
return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 });
};

const stdoutLines: string[] = [];

const err = await runTestRerun(
{
testIds: ['test_1', 'test_2'],
testIds: ['test_fe_01'],
all: false,
wait: true,
timeoutSeconds: 60,
timeoutSeconds: 0,
autoHeal: false,
autoHealExplicit: false,
skipDependencies: false,
Expand All @@ -4873,11 +4886,10 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol

expect(err).toMatchObject({ exitCode: 7 });
expect(stdoutLines.length).toBeGreaterThan(0);
const parsed = JSON.parse(stdoutLines.join('\n')) as {
accepted: Array<{ testId: string; runId: string; status: string }>;
};
expect(parsed.accepted).toHaveLength(2);
expect(parsed.accepted.map(r => r.runId).sort()).toEqual(['run_b1', 'run_b2']);
expect(parsed.accepted.every(r => r.status === 'timeout')).toBe(true);
const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string };
expect(parsed.runId).toBe(rerunResp.runId);
expect(parsed.status).toBe('running');

void fetchCallCount;
});
});
12 changes: 12 additions & 0 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6974,6 +6974,18 @@ export async function runTestRerun(
} catch (err) {
if (err instanceof TimeoutError) {
ticker.finalize(`Run ${rerunResp.runId} — timed out after ${opts.timeoutSeconds}s`);
// Mirror the RequestTimeoutError path: emit a partial run to stdout so
// JSON consumers and AI agents can grab the runId and chain into
// `testsprite test wait <runId>` without parsing the stderr error envelope.
const timeoutPartial = { runId: rerunResp.runId, status: 'running' as const };
out.print(timeoutPartial, data => {
const p = data as typeof timeoutPartial;
return [
`runId ${p.runId}`,
`status ${p.status} (timed out after ${opts.timeoutSeconds}s)`,
`hint Re-attach with: testsprite test wait ${p.runId}`,
].join('\n');
});
throw ApiError.fromEnvelope({
error: {
code: 'UNSUPPORTED',
Expand Down
Loading