From cff19aed2e3cd1cc2242c036f0410bcf906f025e Mon Sep 17 00:00:00 2001 From: Contributor Date: Fri, 3 Jul 2026 04:23:57 +0200 Subject: [PATCH 01/26] fix(rerun): emit partial stdout on TimeoutError in single-FE rerun --wait Apply the fix in src/commands/test.ts. When the overall --timeout polling deadline is exceeded on a single FE rerun, emit {runId, status:"running"} to stdout before exit 7. Co-authored-by: Cursor --- src/commands/test.rerun.spec.ts | 75 +++++++++++++++++++++++++++++++++ src/commands/test.ts | 12 ++++++ 2 files changed, 87 insertions(+) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 86c8e89..3645c12 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4630,3 +4630,78 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { ); }); }); + +// --------------------------------------------------------------------------- +// TimeoutError on single FE rerun --wait: partial stdout + exit 7 +// --------------------------------------------------------------------------- + +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 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/')) { + 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 new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); + }; + + const stdoutLines: string[] = []; + + const err = await runTestRerun( + { + testIds: ['test_fe_01'], + all: false, + wait: true, + timeoutSeconds: 0, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 10, + output: 'json', + profile: 'default', + dryRun: false, + debug: false, + verbose: false, + }, + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: line => stdoutLines.push(line), + stderr: () => undefined, + }, + ).catch(e => e); + + expect(err).toMatchObject({ exitCode: 7 }); + expect(stdoutLines.length).toBeGreaterThan(0); + 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 08e4fe2..69239ca 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -6099,6 +6099,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', From 7975f6ff510b47ad0d5006bb91fc99a839798f16 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 04:52:53 +0200 Subject: [PATCH 02/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index a6f68d3..ba0d84f 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4819,6 +4819,48 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { }); }); +// --------------------------------------------------------------------------- +// Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty +// --------------------------------------------------------------------------- +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 () => { + 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 }; + } + if (url.includes('/runs/')) { + throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); + } + return errorBody('NOT_FOUND'); + }); + + const stdoutLines: string[] = []; + const err = await runTestRerun( + { testIds: ['test_1', 'test_2'], all: false, wait: true, timeoutSeconds: 60, autoHeal: false, autoHealExplicit: false, skipDependencies: false }, + fetchImpl, + (line) => stdoutLines.push(line) + ); + + expect(err).toMatchObject({ exitCode: 7 }); + 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); + }); +}); + // --------------------------------------------------------------------------- // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- From 51af9393d0671acc6824cb5dbecafd4115060fc8 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 04:58:55 +0200 Subject: [PATCH 03/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index ba0d84f..e275b48 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4824,6 +4824,7 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { // --------------------------------------------------------------------------- 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 () => { + const creds = makeCreds(); const batchResp: BatchRerunResponse = { accepted: [ { testId: 'test_1', runId: 'run_b1', enqueuedAt: '2026-06-03T10:00:00.000Z' }, @@ -4864,7 +4865,6 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol // --------------------------------------------------------------------------- // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- - 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(); From 7dd2576a1f07bc9fe0370b000b7aca3792cd6c9d Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:05:11 +0300 Subject: [PATCH 04/26] Add newline at end of test.rerun.spec.ts Fix missing newline at end of file. From 15ee2dccc53280b269eda2ceefebd1539865858d Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:09:33 +0300 Subject: [PATCH 05/26] Add newline at end of test.rerun.spec.ts Fix missing newline at end of file in test.rerun.spec.ts From 770cd4788de000559689f39a4ca8c3ce8e6c1911 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:16:06 +0300 Subject: [PATCH 06/26] Refactor runTestRerun call with new parameters --- src/commands/test.rerun.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index e275b48..672d270 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4848,9 +4848,8 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol const stdoutLines: string[] = []; const err = await runTestRerun( { testIds: ['test_1', 'test_2'], all: false, wait: true, timeoutSeconds: 60, autoHeal: false, autoHealExplicit: false, skipDependencies: false }, - fetchImpl, - (line) => stdoutLines.push(line) - ); + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (line) => stdoutLines.push(line), stderr: () => undefined } + ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); const parsed = JSON.parse(stdoutLines.join('\n')) as { From f7120c25f34a54fda7f487f43e3533d98b0ca3d3 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:22:08 +0300 Subject: [PATCH 07/26] Add newline at end of test.rerun.spec.ts Fix missing newline at end of file in test.rerun.spec.ts From 0b34b1adc1d984d04cfb37bcf1d8b08b83b585c1 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:32:14 +0300 Subject: [PATCH 08/26] Add newline at end of test.rerun.spec.ts Fix missing newline at end of file in test.rerun.spec.ts From 5e2bf88cbed0b214736ac7da152bc25298f849a0 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:26:01 +0200 Subject: [PATCH 09/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 672d270..4428f8f 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4847,7 +4847,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol const stdoutLines: string[] = []; const err = await runTestRerun( - { testIds: ['test_1', 'test_2'], all: false, wait: true, timeoutSeconds: 60, autoHeal: false, autoHealExplicit: false, skipDependencies: false }, + { testIds: ['test_1', 'test_2'], all: false, wait: true, timeoutSeconds: 60, autoHeal: false, autoHealExplicit: false, skipDependencies: false maxConcurrency: 1, profile: 'default', output: 'json'}, { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (line) => stdoutLines.push(line), stderr: () => undefined } ).catch(e => e); From 98f9393df3d16c0998ca43bddc15349319008627 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:35:05 +0300 Subject: [PATCH 10/26] Refactor test parameters for runTestRerun function --- src/commands/test.rerun.spec.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 4428f8f..8958606 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4847,8 +4847,26 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol const stdoutLines: string[] = []; const err = await runTestRerun( - { testIds: ['test_1', 'test_2'], all: false, wait: true, timeoutSeconds: 60, autoHeal: false, autoHealExplicit: false, skipDependencies: false maxConcurrency: 1, profile: 'default', output: 'json'}, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (line) => stdoutLines.push(line), stderr: () => undefined } + { + testIds: ['test_1', 'test_2'], + all: false, + wait: true, + timeoutSeconds: 60, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 1, + profile: 'default', + output: 'json', + debug: false, + }, + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + }, ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); From c4bef009e228fe810e832d2e922749568463de09 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:44:11 +0300 Subject: [PATCH 11/26] Fix formatting and comments in test.rerun.spec.ts Fixed formatting issues by adding a missing comma and clarifying comments. --- src/commands/test.rerun.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 8958606..3ff9859 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4855,7 +4855,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol autoHeal: false, autoHealExplicit: false, skipDependencies: false, - maxConcurrency: 1, + maxConcurrency: 1, // Fixed: Added comma and missing fields profile: 'default', output: 'json', debug: false, @@ -4864,7 +4864,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), + stdout: line => stdoutLines.push(line), stderr: () => undefined, }, ).catch(e => e); From 0a3bfe352c8ffc47c603cd4b8fda7ba312e0d0b2 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:50:13 +0200 Subject: [PATCH 12/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 3ff9859..0fdde9f 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4855,18 +4855,18 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol autoHeal: false, autoHealExplicit: false, skipDependencies: false, - maxConcurrency: 1, // Fixed: Added comma and missing fields + maxConcurrency: 1, // تأكدت من وجود الفواصل هنا profile: 'default', output: 'json', - debug: false, + debug: false }, { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: line => stdoutLines.push(line), - stderr: () => undefined, - }, + stderr: () => undefined + } ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); From 9a6a48dec23600fb964d9f7ef69d73a61cf9e2f0 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:54:04 +0200 Subject: [PATCH 13/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 0fdde9f..2e97da3 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4855,7 +4855,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol autoHeal: false, autoHealExplicit: false, skipDependencies: false, - maxConcurrency: 1, // تأكدت من وجود الفواصل هنا + maxConcurrency: 1, profile: 'default', output: 'json', debug: false From 19376059eba9f303a15ff094b4b33e6de07774fc Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:58:11 +0200 Subject: [PATCH 14/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 55 +++++++++------------------------ 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 2e97da3..fd39488 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4855,7 +4855,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol autoHeal: false, autoHealExplicit: false, skipDependencies: false, - maxConcurrency: 1, + maxConcurrency: 1, profile: 'default', output: 'json', debug: false @@ -4870,12 +4870,9 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); - const parsed = JSON.parse(stdoutLines.join('\n')) as { - accepted: Array<{ testId: string; runId: string; status: string }>; - }; + const parsed = JSON.parse(stdoutLines.join('\n')); 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); + expect(parsed.accepted.map((r: any) => r.runId).sort()).toEqual(['run_b1', 'run_b2']); }); }); @@ -4885,39 +4882,20 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol 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 rerunResp = makeFeRerunResp(); + const rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - 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' }, - }); + const fetchImpl: any = async (input: any) => { + const url = typeof input === 'string' ? input : input.url; + if (url.includes('/runs/rerun')) { + return new Response(JSON.stringify(rerunResp), { status: 202 }); } if (url.includes('/runs/')) { - 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 new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); } return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); }; const stdoutLines: string[] = []; - const err = await runTestRerun( { testIds: ['test_fe_01'], @@ -4930,25 +4908,20 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t maxConcurrency: 10, output: 'json', profile: 'default', - dryRun: false, - debug: false, - verbose: false, + debug: false }, { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: line => stdoutLines.push(line), - stderr: () => undefined, - }, + stderr: () => undefined + } ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); - expect(stdoutLines.length).toBeGreaterThan(0); - const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; + const parsed = JSON.parse(stdoutLines.join('\n')); expect(parsed.runId).toBe(rerunResp.runId); expect(parsed.status).toBe('running'); - - void fetchCallCount; }); -}); +}); \ No newline at end of file From f6aea0e2d948f15a6c3d30c6d90f9ad10e0e6aee Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:23:29 +0200 Subject: [PATCH 15/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 137 +++----------------------------- 1 file changed, 12 insertions(+), 125 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index e08b850..396e84b 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4819,7 +4819,6 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { ); }); }); - // --------------------------------------------------------------------------- // Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty // --------------------------------------------------------------------------- @@ -4829,23 +4828,17 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol 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' }, + { testId: 'test_2', runId: 'run_b2', enqueuedAt: '2026-06-03T10:00:00.000Z' } ], deferred: [], conflicts: [], - closure: { byProject: [] }, + closure: { byProject: [] } }; - - const fetchImpl = makeFetch(url => { - if (url.includes('/tests/batch/rerun')) { - return { status: 202, body: batchResp }; - } - if (url.includes('/runs/')) { - throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); - } + const fetchImpl = makeFetch((url) => { + if (url.includes('/tests/batch/rerun')) return { status: 202, body: batchResp }; + if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch'); return errorBody('NOT_FOUND'); }); - const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4861,19 +4854,9 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol output: 'json', debug: false }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: line => stdoutLines.push(line), - stderr: () => undefined - } - ).catch(e => e); - + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as any, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); - const parsed = JSON.parse(stdoutLines.join('\n')); - expect(parsed.accepted).toHaveLength(2); - expect(parsed.accepted.map((r: any) => r.runId).sort()).toEqual(['run_b1', 'run_b2']); }); }); @@ -4884,18 +4867,12 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t it('exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', async () => { const creds = makeCreds(); const rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - const fetchImpl: any = async (input: any) => { const url = typeof input === 'string' ? input : input.url; - if (url.includes('/runs/rerun')) { - return new Response(JSON.stringify(rerunResp), { status: 202 }); - } - if (url.includes('/runs/')) { - return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); - } + if (url.includes('/runs/rerun')) return new Response(JSON.stringify(rerunResp), { status: 202 }); + if (url.includes('/runs/')) return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); }; - const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4903,59 +4880,6 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t all: false, wait: true, timeoutSeconds: 0, -// DEV-331 piece 1 — graceful detach during batch rerun --wait (SIG-6) -// --------------------------------------------------------------------------- - -describe('R-BAT: batch rerun --wait — InterruptError partial lists all dispatched runIds (DEV-331)', () => { - it('interrupt mid fan-out → stdout partial covers every accepted runId, honest stderr, exit 130', async () => { - const creds = makeCreds(); - const shutdown = new ShutdownController(); - 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: [] }, - }; - - // Batch trigger resolves; every run poll hangs until the composed signal aborts. - const fetchImpl: FetchImpl = (async (input: unknown, init: RequestInit = {}) => { - const url = - typeof input === 'string' - ? input - : input instanceof URL - ? input.toString() - : (input as { url: string }).url; - if (url.includes('/tests/batch/rerun')) { - return new Response(JSON.stringify(batchResp), { - status: 202, - headers: { 'content-type': 'application/json' }, - }); - } - return new Promise((_resolve, reject) => { - const signal = init.signal; - const rejectWithReason = (): void => { - const reason: unknown = signal?.reason; - reject(reason instanceof Error ? reason : new Error('aborted')); - }; - if (signal?.aborted) { - rejectWithReason(); - return; - } - signal?.addEventListener('abort', rejectWithReason, { once: true }); - }); - }) as FetchImpl; - - const stdoutLines: string[] = []; - const stderrLines: string[] = []; - const pending = runTestRerun( - { - testIds: ['test_1', 'test_2'], - all: false, - wait: true, - timeoutSeconds: 600, autoHeal: false, autoHealExplicit: false, skipDependencies: false, @@ -4963,49 +4887,12 @@ describe('R-BAT: batch rerun --wait — InterruptError partial lists all dispatc output: 'json', profile: 'default', debug: false - dryRun: false, - debug: false, - verbose: false, }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: line => stdoutLines.push(line), - stderr: () => undefined - } - ).catch(e => e); - + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as any, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); const parsed = JSON.parse(stdoutLines.join('\n')); expect(parsed.runId).toBe(rerunResp.runId); expect(parsed.status).toBe('running'); }); -}); - fetchImpl, - stdout: line => stdoutLines.push(line), - stderr: line => stderrLines.push(line), - shutdown, - }, - ); - setTimeout(() => shutdown.interrupt('SIGINT'), 10); - - const err = await pending.catch(e => e); - expect(err).toBeInstanceOf(InterruptError); - expect((err as InterruptError).exitCode).toBe(130); - - // SIG-6: the partial lists ALL dispatched runIds, marked running. - const stdoutJson = JSON.parse(stdoutLines.join('\n')) as { - accepted: Array<{ runId: string; status: string }>; - }; - const byRunId = new Map(stdoutJson.accepted.map(r => [r.runId, r.status])); - expect(byRunId.get('run_b1')).toBe('running'); - expect(byRunId.get('run_b2')).toBe('running'); - - const stderrBlock = stderrLines.join('\n'); - expect(stderrBlock).toContain('Interrupted (SIGINT)'); - expect(stderrBlock).toContain('billing'); - expect(stderrBlock).toContain('run_b1'); - expect(stderrBlock).toContain('run_b2'); - }); -}); +}); \ No newline at end of file From ad0dd10d31706135185314654ecf242085df0959 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:32:16 +0200 Subject: [PATCH 16/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 396e84b..2c1b7ba 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4819,6 +4819,7 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { ); }); }); + // --------------------------------------------------------------------------- // Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty // --------------------------------------------------------------------------- @@ -4854,7 +4855,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol output: 'json', debug: false }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as any, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); }); @@ -4867,8 +4868,8 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t it('exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', async () => { const creds = makeCreds(); const rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - const fetchImpl: any = async (input: any) => { - const url = typeof input === 'string' ? input : input.url; + const fetchImpl: FetchImpl = async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; if (url.includes('/runs/rerun')) return new Response(JSON.stringify(rerunResp), { status: 202 }); if (url.includes('/runs/')) return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); @@ -4888,10 +4889,10 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t profile: 'default', debug: false }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as any, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); - const parsed = JSON.parse(stdoutLines.join('\n')); + const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; expect(parsed.runId).toBe(rerunResp.runId); expect(parsed.status).toBe('running'); }); From 817781274d798d0bdf3a15b822367c897eb4c42d Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:35:15 +0200 Subject: [PATCH 17/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 58 ++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 2c1b7ba..f569b61 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4829,17 +4829,19 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol 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' } + { testId: 'test_2', runId: 'run_b2', enqueuedAt: '2026-06-03T10:00:00.000Z' }, ], deferred: [], conflicts: [], - closure: { byProject: [] } + closure: { byProject: [] }, }; + const fetchImpl = makeFetch((url) => { if (url.includes('/tests/batch/rerun')) return { status: 202, body: batchResp }; - if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch'); + if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); return errorBody('NOT_FOUND'); }); + const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4853,11 +4855,24 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol maxConcurrency: 1, profile: 'default', output: 'json', - debug: false + debug: false, }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + } ).catch((e) => e); + expect(err).toMatchObject({ exitCode: 7 }); + 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); }); }); @@ -4865,15 +4880,27 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- 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 () => { + it('exit 7 AND stdout contains {runId, status:\"running\"} when --timeout polling deadline is exceeded', async () => { const creds = makeCreds(); const rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - const fetchImpl: FetchImpl = async (input) => { - const url = typeof input === 'string' ? input : (input as Request).url; - if (url.includes('/runs/rerun')) return new Response(JSON.stringify(rerunResp), { status: 202 }); - if (url.includes('/runs/')) return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); + + const fetchImpl: FetchImpl = async (input, _init) => { + const url = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : (input as { url: string }).url; + + if (url.includes('/runs/rerun')) { + return new Response(JSON.stringify(rerunResp), { status: 202 }); + } + if (url.includes('/runs/')) { + return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); + } return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); }; + const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4887,10 +4914,17 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t maxConcurrency: 10, output: 'json', profile: 'default', - debug: false + debug: false, }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + } ).catch((e) => e); + expect(err).toMatchObject({ exitCode: 7 }); const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; expect(parsed.runId).toBe(rerunResp.runId); From bdec093a27edfe1eb75e8f7c1be0186ee11af3ca Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:40:33 +0200 Subject: [PATCH 18/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 58 +++++++-------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index f569b61..2c1b7ba 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4829,19 +4829,17 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol 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' }, + { testId: 'test_2', runId: 'run_b2', enqueuedAt: '2026-06-03T10:00:00.000Z' } ], deferred: [], conflicts: [], - closure: { byProject: [] }, + closure: { byProject: [] } }; - const fetchImpl = makeFetch((url) => { if (url.includes('/tests/batch/rerun')) return { status: 202, body: batchResp }; - if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); + if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch'); return errorBody('NOT_FOUND'); }); - const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4855,24 +4853,11 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol maxConcurrency: 1, profile: 'default', output: 'json', - debug: false, + debug: false }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), - stderr: () => undefined, - } + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } ).catch((e) => e); - expect(err).toMatchObject({ exitCode: 7 }); - 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); }); }); @@ -4880,27 +4865,15 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- 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 () => { + it('exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', async () => { const creds = makeCreds(); const rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - - const fetchImpl: FetchImpl = async (input, _init) => { - const url = - typeof input === 'string' - ? input - : input instanceof URL - ? input.toString() - : (input as { url: string }).url; - - if (url.includes('/runs/rerun')) { - return new Response(JSON.stringify(rerunResp), { status: 202 }); - } - if (url.includes('/runs/')) { - return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); - } + const fetchImpl: FetchImpl = async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + if (url.includes('/runs/rerun')) return new Response(JSON.stringify(rerunResp), { status: 202 }); + if (url.includes('/runs/')) return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); }; - const stdoutLines: string[] = []; const err = await runTestRerun( { @@ -4914,17 +4887,10 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t maxConcurrency: 10, output: 'json', profile: 'default', - debug: false, + debug: false }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), - stderr: () => undefined, - } + { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } ).catch((e) => e); - expect(err).toMatchObject({ exitCode: 7 }); const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; expect(parsed.runId).toBe(rerunResp.runId); From 6b080ac62e829b848c36b6eeb12618aae20b064e Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:44:37 +0200 Subject: [PATCH 19/26] Update test.rerun.spec.ts --- src/commands/test.rerun.spec.ts | 199 +++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 67 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 2c1b7ba..966d16b 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4823,77 +4823,142 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { // --------------------------------------------------------------------------- // Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty // --------------------------------------------------------------------------- -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 () => { - 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 }; - if (url.includes('/runs/')) throw new RequestTimeoutError(120000, 'req_timeout_batch'); - return errorBody('NOT_FOUND'); - }); - const stdoutLines: string[] = []; - const err = await runTestRerun( - { - testIds: ['test_1', 'test_2'], - all: false, - wait: true, - timeoutSeconds: 60, - autoHeal: false, - autoHealExplicit: false, - skipDependencies: false, - maxConcurrency: 1, - profile: 'default', - output: 'json', - debug: false +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 () => { + 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 }; + } + if (url.includes('/runs/')) { + throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); + } + return errorBody('NOT_FOUND'); + }); + + const stdoutLines: string[] = []; + const err = await runTestRerun( + { + testIds: ['test_1', 'test_2'], + all: false, + wait: true, + timeoutSeconds: 60, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 1, + profile: 'default', + output: 'json', + debug: false, + }, + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + }, + ).catch((e) => e); + + expect(err).toMatchObject({ exitCode: 7 }); + 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); }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } - ).catch((e) => e); - expect(err).toMatchObject({ exitCode: 7 }); - }); -}); + ); + }, +); // --------------------------------------------------------------------------- // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- 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 rerunResp = { runId: 'run_fe_01', status: 'accepted' }; - const fetchImpl: FetchImpl = async (input) => { - const url = typeof input === 'string' ? input : (input as Request).url; - if (url.includes('/runs/rerun')) return new Response(JSON.stringify(rerunResp), { status: 202 }); - if (url.includes('/runs/')) return new Response(JSON.stringify({ runId: rerunResp.runId, status: 'running', finishedAt: null }), { status: 200 }); - return new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); - }; - const stdoutLines: string[] = []; - const err = await runTestRerun( - { - testIds: ['test_fe_01'], - all: false, - wait: true, - timeoutSeconds: 0, - autoHeal: false, - autoHealExplicit: false, - skipDependencies: false, - maxConcurrency: 10, - output: 'json', - profile: 'default', - debug: false - }, - { ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, stdout: (l) => stdoutLines.push(l), stderr: () => undefined } - ).catch((e) => e); - expect(err).toMatchObject({ exitCode: 7 }); - const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; - expect(parsed.runId).toBe(rerunResp.runId); - expect(parsed.status).toBe('running'); - }); + it( + 'exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', + async () => { + const creds = makeCreds(); + 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/')) { + 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 new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); + }; + + const stdoutLines: string[] = []; + + const err = await runTestRerun( + { + testIds: ['test_fe_01'], + all: false, + wait: true, + timeoutSeconds: 0, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 10, + output: 'json', + profile: 'default', + dryRun: false, + debug: false, + verbose: false, + }, + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + }, + ).catch((e) => e); + + expect(err).toMatchObject({ exitCode: 7 }); + expect(stdoutLines.length).toBeGreaterThan(0); + 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; + }, + ); }); \ No newline at end of file From 44501e068a1c0bb655f47e316147bed1fd203ac9 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:51:58 +0300 Subject: [PATCH 20/26] Refactor batch rerun tests for RequestTimeoutError --- src/commands/test.rerun.spec.ts | 241 +++++++++++++++----------------- 1 file changed, 116 insertions(+), 125 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 966d16b..ea05c9a 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4823,142 +4823,133 @@ describe('rerun --wait — dashboardUrl on terminal output', () => { // --------------------------------------------------------------------------- // Batch --all --wait fan-out: RequestTimeoutError must not leave stdout empty // --------------------------------------------------------------------------- -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 () => { - 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 }; - } - if (url.includes('/runs/')) { - throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); - } - return errorBody('NOT_FOUND'); - }); +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 () => { + 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 stdoutLines: string[] = []; - const err = await runTestRerun( - { - testIds: ['test_1', 'test_2'], - all: false, - wait: true, - timeoutSeconds: 60, - autoHeal: false, - autoHealExplicit: false, - skipDependencies: false, - maxConcurrency: 1, - profile: 'default', - output: 'json', - debug: false, - }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), - stderr: () => undefined, - }, - ).catch((e) => e); + const fetchImpl = makeFetch(url => { + if (url.includes('/tests/batch/rerun')) { + return { status: 202, body: batchResp }; + } + if (url.includes('/runs/')) { + throw new RequestTimeoutError(120000, 'req_timeout_batch_rerun'); + } + return errorBody('NOT_FOUND'); + }); - expect(err).toMatchObject({ exitCode: 7 }); - 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 stdoutLines: string[] = []; + const err = await runTestRerun( + { + testIds: ['test_1', 'test_2'], + all: false, + wait: true, + timeoutSeconds: 60, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 1, + profile: 'default', + output: 'json', + debug: false, }, - ); - }, -); + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: (line) => stdoutLines.push(line), + stderr: () => undefined, + }, + ).catch(e => e); + + expect(err).toMatchObject({ exitCode: 7 }); + 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); + }); +}); // --------------------------------------------------------------------------- // TimeoutError on single FE rerun --wait: partial stdout + exit 7 // --------------------------------------------------------------------------- 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 rerunResp = makeFeRerunResp(); - - let fetchCallCount = 0; - const fetchImpl: typeof globalThis.fetch = async (input, _init) => { - const url = - typeof input === 'string' - ? input - : input instanceof URL + it('exit 7 AND stdout contains {runId, status:"running"} when --timeout polling deadline is exceeded', async () => { + const creds = makeCreds(); + 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/')) { - 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 new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); - }; + 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/')) { + 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 new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404 }); + }; - const stdoutLines: string[] = []; + const stdoutLines: string[] = []; - const err = await runTestRerun( - { - testIds: ['test_fe_01'], - all: false, - wait: true, - timeoutSeconds: 0, - autoHeal: false, - autoHealExplicit: false, - skipDependencies: false, - maxConcurrency: 10, - output: 'json', - profile: 'default', - dryRun: false, - debug: false, - verbose: false, - }, - { - ...creds, - sleep: instantSleep, - fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), - stderr: () => undefined, - }, - ).catch((e) => e); + const err = await runTestRerun( + { + testIds: ['test_fe_01'], + all: false, + wait: true, + timeoutSeconds: 0, + autoHeal: false, + autoHealExplicit: false, + skipDependencies: false, + maxConcurrency: 10, + output: 'json', + profile: 'default', + dryRun: false, + debug: false, + verbose: false, + }, + { + ...creds, + sleep: instantSleep, + fetchImpl: fetchImpl as unknown as FetchImpl, + stdout: line => stdoutLines.push(line), + stderr: () => undefined, + }, + ).catch(e => e); - expect(err).toMatchObject({ exitCode: 7 }); - expect(stdoutLines.length).toBeGreaterThan(0); - const parsed = JSON.parse(stdoutLines.join('\n')) as { runId: string; status: string }; - expect(parsed.runId).toBe(rerunResp.runId); - expect(parsed.status).toBe('running'); + expect(err).toMatchObject({ exitCode: 7 }); + expect(stdoutLines.length).toBeGreaterThan(0); + 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; - }, - ); -}); \ No newline at end of file + void fetchCallCount; + }); +}); From 7774c29dff80bfc0edc94fa89176500bdcfe1dc1 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:02:38 +0300 Subject: [PATCH 21/26] Refactor test.rerun.spec.ts imports Removed InterruptError and ShutdownController imports. --- src/commands/test.rerun.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index ea05c9a..a35cfd0 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -9,8 +9,8 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { ApiError, InterruptError, RequestTimeoutError } from '../lib/errors.js'; -import { ShutdownController } from '../lib/interrupt.js'; +import { ApiError, RequestTimeoutError } from '../lib/errors.js'; + import type { RunResponse, RerunResponse, BatchRerunResponse } from '../lib/runs.types.js'; import type { FetchImpl } from '../lib/http.js'; import { runTestRerun, resolveWaitRequestTimeoutMs } from './test.js'; From 73441e43e4b54c56bec0ca7671bd925a8f27826b Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:27:38 +0200 Subject: [PATCH 22/26] Refactor fetch implementation and error handling --- src/commands/test.rerun.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index a35cfd0..81283d9 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -10,7 +10,6 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { ApiError, RequestTimeoutError } from '../lib/errors.js'; - import type { RunResponse, RerunResponse, BatchRerunResponse } from '../lib/runs.types.js'; import type { FetchImpl } from '../lib/http.js'; import { runTestRerun, resolveWaitRequestTimeoutMs } from './test.js'; @@ -4836,7 +4835,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol closure: { byProject: [] }, }; - const fetchImpl = makeFetch(url => { + const fetchImpl = makeFetch((url) => { if (url.includes('/tests/batch/rerun')) { return { status: 202, body: batchResp }; } @@ -4868,15 +4867,15 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol stdout: (line) => stdoutLines.push(line), stderr: () => undefined, }, - ).catch(e => e); + ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); 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); + expect(parsed.accepted.map((r) => r.runId).sort()).toEqual(['run_b1', 'run_b2']); + expect(parsed.accepted.every((r) => r.status === 'timeout')).toBe(true); }); }); @@ -4939,10 +4938,10 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: line => stdoutLines.push(line), + stdout: (line) => stdoutLines.push(line), stderr: () => undefined, }, - ).catch(e => e); + ).catch((e) => e); expect(err).toMatchObject({ exitCode: 7 }); expect(stdoutLines.length).toBeGreaterThan(0); From 0081f3ff4f0a4820b7a3a4f9324f7fd15db7f509 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 08:41:26 +0200 Subject: [PATCH 23/26] Fix formatting --- src/commands/test.rerun.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 81283d9..e018e88 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -2889,7 +2889,7 @@ describe('[fix-1] batch rerun: notFound[] ids aggregated and warned on stderr', ...creds, sleep: instantSleep, fetchImpl, - stdout: () => {}, + stdout: () => { }, stderr: line => stderrLines.push(line), }, ); @@ -2945,7 +2945,7 @@ describe('[fix-1] batch rerun: notFound[] ids aggregated and warned on stderr', ...creds, sleep: instantSleep, fetchImpl, - stdout: () => {}, + stdout: () => { }, stderr: line => stderrLines.push(line), }, ); @@ -3177,7 +3177,7 @@ describe('runTestRerun --all --skip-terminal (dogfood L1796)', () => { ...creds, sleep: instantSleep, fetchImpl: makeFilterFetch(dispatched), - stderr: () => {}, + stderr: () => { }, }, ); From c286766a346e65fa7cd327f0392901607ed954fc Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:48:05 +0000 Subject: [PATCH 24/26] Fix Prettier formatting strictly --- package-lock.json | 4 ++-- src/commands/test.rerun.spec.ts | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4461016..f9afe03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@testsprite/testsprite-cli", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@testsprite/testsprite-cli", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "commander": "^12.1.0", diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index e018e88..c4e865a 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -2889,7 +2889,7 @@ describe('[fix-1] batch rerun: notFound[] ids aggregated and warned on stderr', ...creds, sleep: instantSleep, fetchImpl, - stdout: () => { }, + stdout: () => {}, stderr: line => stderrLines.push(line), }, ); @@ -2945,7 +2945,7 @@ describe('[fix-1] batch rerun: notFound[] ids aggregated and warned on stderr', ...creds, sleep: instantSleep, fetchImpl, - stdout: () => { }, + stdout: () => {}, stderr: line => stderrLines.push(line), }, ); @@ -3177,7 +3177,7 @@ describe('runTestRerun --all --skip-terminal (dogfood L1796)', () => { ...creds, sleep: instantSleep, fetchImpl: makeFilterFetch(dispatched), - stderr: () => { }, + stderr: () => {}, }, ); @@ -4835,7 +4835,7 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol closure: { byProject: [] }, }; - const fetchImpl = makeFetch((url) => { + const fetchImpl = makeFetch(url => { if (url.includes('/tests/batch/rerun')) { return { status: 202, body: batchResp }; } @@ -4864,18 +4864,18 @@ describe('[finding-5] batch rerun --wait: RequestTimeoutError during fan-out pol ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), + stdout: line => stdoutLines.push(line), stderr: () => undefined, }, - ).catch((e) => e); + ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); 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); + expect(parsed.accepted.map(r => r.runId).sort()).toEqual(['run_b1', 'run_b2']); + expect(parsed.accepted.every(r => r.status === 'timeout')).toBe(true); }); }); @@ -4938,10 +4938,10 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t ...creds, sleep: instantSleep, fetchImpl: fetchImpl as unknown as FetchImpl, - stdout: (line) => stdoutLines.push(line), + stdout: line => stdoutLines.push(line), stderr: () => undefined, }, - ).catch((e) => e); + ).catch(e => e); expect(err).toMatchObject({ exitCode: 7 }); expect(stdoutLines.length).toBeGreaterThan(0); From 774db845970a9e4826ff9823c8e6a343a0a0f049 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:26:20 +0200 Subject: [PATCH 25/26] Re-run failed jobs --- src/commands/test.rerun.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index c4e865a..9f1f4ef 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4952,3 +4952,4 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t void fetchCallCount; }); }); + \ No newline at end of file From 943b76198ab486b07567b7531ac0daff8d984933 Mon Sep 17 00:00:00 2001 From: Muath Awad <94539921+Awad-de@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:27:37 +0200 Subject: [PATCH 26/26] Re-run failed jobs --- src/commands/test.rerun.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/test.rerun.spec.ts b/src/commands/test.rerun.spec.ts index 9f1f4ef..c4e865a 100644 --- a/src/commands/test.rerun.spec.ts +++ b/src/commands/test.rerun.spec.ts @@ -4952,4 +4952,3 @@ describe('[finding-4] single FE rerun --wait: TimeoutError writes partial JSON t void fetchCallCount; }); }); - \ No newline at end of file