diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 9f16c4b..a6f68d3 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -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, @@ -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; }); }); diff --git a/src/commands/test.ts b/src/commands/test.ts index 1c6edd8..af874e0 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -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 ` 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',