From 2c63e10d9a8042b4b6d0765ca7dc8a510c963440 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 20 Oct 2026 15:55:43 +0700 Subject: [PATCH 01/21] fix(create-batch): preserve per-spec creation failures in --run JSON output (#483) --- src/commands/test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index 8bf5ce5b..fcb1ca9a 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4985,7 +4985,10 @@ async function runBatchRun( return { ...r, dashboardUrl: state.dashboardUrl }; }) : batchRunResults; - out.print({ results: enrichedResults }); + out.print({ + results: enrichedResults, + creation: { results: createResponse.results, summary: createResponse.summary }, + }); } else if (!opts.wait) { // Text mode, no --wait: statuses are non-terminal by design ('queued'), // so a pass/fail summary would misread a successful dispatch as From 144e23bfa7a748091ce32464bbd952b1114531a8 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 22 Oct 2026 16:54:35 +0700 Subject: [PATCH 02/21] chore: trigger CI entry check From 600faa54873359483d7aa3c9dde2200b0c24cebb Mon Sep 17 00:00:00 2001 From: Lex Date: Mon, 2 Nov 2026 21:25:58 +0800 Subject: [PATCH 03/21] fix(create-batch): preserve creation receipt on partial JSON output --- src/commands/test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index fcb1ca9a..0863f277 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4942,8 +4942,12 @@ async function runBatchRun( codeVersion: '', }, ); - out.print({ results: partialResults }, () => - partialResults.map(r => `${r.testId} ${r.runId || '-'} ${r.status}`).join('\n'), + out.print( + { + results: partialResults, + creation: { results: createResponse.results, summary: createResponse.summary }, + }, + () => partialResults.map(r => `${r.testId} ${r.runId || '-'} ${r.status}`).join('\n'), ); const unfinished = partialResults .filter(r => r.status === 'running' && r.runId) From a8e4cfbbca4fba8b79698854628a3a5e7bc6bb89 Mon Sep 17 00:00:00 2001 From: Lex Date: Mon, 2 Nov 2026 21:26:01 +0800 Subject: [PATCH 04/21] test(create-batch): cover creation receipt on interruption --- src/commands/test.create-batch-run.spec.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index be439ca8..b67119dd 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1743,14 +1743,21 @@ it('a paused workspace outranks a mixed-in credits refusal from create-batch --r it('marks a dispatched batch run for recovery guidance on fan-out interruption', async () => { const { credentialsPath } = makeCreds(); - const plansFile = writePlansJsonl([FE_SPEC]); + const plansFile = writePlansJsonl([FE_SPEC, FE_SPEC]); const interruption = new InterruptError('SIGINT'); const stdout: string[] = []; const stderr: string[] = []; + const creationResponse = { + results: [ + { specIndex: 0, testId: 'test_one', status: 'created' }, + { specIndex: 1, testId: null, status: 'error', error: { code: 'VALIDATION_ERROR', message: 'invalid spec' } }, + ], + summary: { total: 2, created: 1, failed: 1 }, + }; const fetchImpl = (async (input: FetchInput) => { const url = String(input); if (url.includes('/tests/batch')) { - return new Response(JSON.stringify(makeBatchCreateResponse(['test_one']))); + return new Response(JSON.stringify(creationResponse)); } if (url.includes('/tests/test_one/runs')) { return new Response(JSON.stringify(makeTriggerResponse('test_one', 'run_one'))); @@ -1785,6 +1792,13 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', expect((error as InterruptError).runWaitContext).toBe(true); expect(JSON.parse(stdout.at(-1)!) as unknown).toMatchObject({ results: [{ testId: 'test_one', runId: 'run_one', status: 'running' }], + creation: { + results: [ + { specIndex: 0, testId: 'test_one', status: 'created' }, + { specIndex: 1, testId: null, status: 'error' }, + ], + summary: { total: 2, created: 1, failed: 1 }, + }, }); expect(stderr.join('\n')).toContain('testsprite test wait run_one'); }); From f177758b329a36db1dffd1c0c942318238cd2012 Mon Sep 17 00:00:00 2001 From: Lex Date: Mon, 2 Nov 2026 21:31:40 +0800 Subject: [PATCH 05/21] style(create-batch): format creation receipt output --- src/commands/test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index 0863f277..44fa8cce 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4945,7 +4945,10 @@ async function runBatchRun( out.print( { results: partialResults, - creation: { results: createResponse.results, summary: createResponse.summary }, + creation: { + results: createResponse.results, + summary: createResponse.summary, + }, }, () => partialResults.map(r => `${r.testId} ${r.runId || '-'} ${r.status}`).join('\n'), ); @@ -4991,7 +4994,10 @@ async function runBatchRun( : batchRunResults; out.print({ results: enrichedResults, - creation: { results: createResponse.results, summary: createResponse.summary }, + creation: { + results: createResponse.results, + summary: createResponse.summary, + }, }); } else if (!opts.wait) { // Text mode, no --wait: statuses are non-terminal by design ('queued'), From c4c120d469d23e55b4c7099c0d46c343aa5adadc Mon Sep 17 00:00:00 2001 From: Lex Date: Mon, 2 Nov 2026 21:31:43 +0800 Subject: [PATCH 06/21] style(create-batch): format interruption regression test --- src/commands/test.create-batch-run.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index b67119dd..24f33def 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1750,7 +1750,12 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', const creationResponse = { results: [ { specIndex: 0, testId: 'test_one', status: 'created' }, - { specIndex: 1, testId: null, status: 'error', error: { code: 'VALIDATION_ERROR', message: 'invalid spec' } }, + { + specIndex: 1, + testId: null, + status: 'error', + error: { code: 'VALIDATION_ERROR', message: 'invalid spec' }, + }, ], summary: { total: 2, created: 1, failed: 1 }, }; From b98e5c13bad73fd00e7168030b1383e1d119807e Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:55:31 +0700 Subject: [PATCH 07/21] test(create-batch): use valid creation failure status --- src/commands/test.create-batch-run.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index 24f33def..e7bb0803 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1753,7 +1753,7 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', { specIndex: 1, testId: null, - status: 'error', + status: 'validation_error', error: { code: 'VALIDATION_ERROR', message: 'invalid spec' }, }, ], From 86788a0fd55656edd378ef4139a05f78b4c3875d Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:55:37 +0700 Subject: [PATCH 08/21] test(create-batch): assert creation failure contract --- src/commands/test.create-batch-run.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index e7bb0803..b7cd0267 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1800,7 +1800,7 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', creation: { results: [ { specIndex: 0, testId: 'test_one', status: 'created' }, - { specIndex: 1, testId: null, status: 'error' }, + { specIndex: 1, testId: null, status: 'validation_error' }, ], summary: { total: 2, created: 1, failed: 1 }, }, From 116efd05df95d9071790b6f3208c90b095063c98 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:56:21 +0700 Subject: [PATCH 09/21] fix(create-batch): preserve enriched creation receipt in run output --- src/commands/test.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index 44fa8cce..9b7dc441 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4325,6 +4325,17 @@ async function runBatchRun( .filter(r => r.status === 'created' && r.testId !== undefined) .map(r => r.testId as string); + // Match the create-only JSON receipt: preserve server links, normalize + // explicitly suppressed links, and use the same create-time fallback map. + const creationResults: CliBatchSpecResult[] = + !opts.dryRun && testIdToDashboardState !== undefined + ? createResponse.results.map(r => { + if (r.status !== 'created' || r.testId === undefined) return r; + const state = testIdToDashboardState.get(r.testId); + return state ? { ...r, dashboardUrl: state.dashboardUrl } : r; + }) + : createResponse.results; + if (testIds.length === 0) { // All specs failed at create time — already threw above; unreachable. return; @@ -4348,6 +4359,10 @@ async function runBatchRun( testIds, ...(opts.wait ? { thenPoll: `/api/cli/v1/runs/?waitSeconds=25` } : {}), results: dryRunResults, + creation: { + results: creationResults, + summary: createResponse.summary, + }, }; out.print(envelope); return; @@ -4946,7 +4961,7 @@ async function runBatchRun( { results: partialResults, creation: { - results: createResponse.results, + results: creationResults, summary: createResponse.summary, }, }, @@ -4995,7 +5010,7 @@ async function runBatchRun( out.print({ results: enrichedResults, creation: { - results: createResponse.results, + results: creationResults, summary: createResponse.summary, }, }); From f343969d3b0143288c89d3d91ec4479e63917519 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:56:31 +0700 Subject: [PATCH 10/21] types(create-batch): model creation receipt in run envelopes --- src/commands/test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/test.ts b/src/commands/test.ts index 9b7dc441..f51eca3d 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -3016,7 +3016,22 @@ export interface CliBatchRunResult { /** Envelope emitted by `test create-batch --run` in JSON mode. */ export interface CliCreateBatchRunResponse { + /** Per-test run results; remains unchanged for existing consumers. */ results: CliBatchRunResult[]; + /** Original per-spec creation receipt, matching create-only JSON output. */ + creation: CliCreateBatchResponse; +} + +/** Dry-run variant of the `test create-batch --run` JSON envelope. */ +export interface CliCreateBatchDryRunResponse extends CliCreateBatchRunResponse { + dryRun: true; + method: 'POST'; + pathTemplate: '/api/cli/v1/tests/{testId}/runs'; + maxConcurrency: number | null; + wait: boolean; + timeoutSeconds: number; + testIds: string[]; + thenPoll?: string; } /** From 40baea380497b460c291f85191faaacef14b341a Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:56:53 +0700 Subject: [PATCH 11/21] test(create-batch): cover creation receipt and dashboard links --- src/commands/test.create-batch-run.spec.ts | 39 ++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index b7cd0267..1717e9cf 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -190,7 +190,19 @@ describe('runCreateBatch --run --wait: happy path', () => { const fetchImpl = makeFetch(url => { // Batch create if (url.includes('/tests/batch')) { - return { body: makeBatchCreateResponse(testIds) }; + return { + body: { + results: testIds.map((testId, specIndex) => ({ + specIndex, + testId, + status: 'created' as const, + ...(specIndex === 0 + ? { dashboardUrl: 'https://portal.testsprite.com/tests/test_a1' } + : {}), + })), + summary: { total: testIds.length, created: testIds.length, failed: 0 }, + }, + }; } // Trigger POST /tests/{id}/runs const triggerMatch = /\/tests\/(test_[a-z0-9]+)\/runs$/.exec(url); @@ -238,8 +250,21 @@ describe('runCreateBatch --run --wait: happy path', () => { ); // JSON output is the batch-run envelope - const printed = JSON.parse(stdout.join('')) as { results: unknown[] }; + const printed = JSON.parse(stdout.join('')) as { + results: unknown[]; + creation: { + results: Array<{ testId?: string; status: string; dashboardUrl?: string }>; + summary: { total: number; created: number; failed: number }; + }; + }; expect(printed.results).toHaveLength(3); + expect(printed.creation.summary).toEqual({ total: 3, created: 3, failed: 0 }); + expect(printed.creation.results).toHaveLength(3); + expect(printed.creation.results[0]).toMatchObject({ + testId: 'test_a1', + status: 'created', + dashboardUrl: 'https://portal.testsprite.com/tests/test_a1', + }); const results = printed.results as Array<{ testId: string; status: string; runId: string }>; expect(results.every(r => r.status === 'passed')).toBe(true); expect(results.map(r => r.testId).sort()).toEqual(['test_a1', 'test_a2', 'test_a3'].sort()); @@ -1136,9 +1161,17 @@ describe('runCreateBatch --run --dry-run', () => { expect(envelope.dryRun).toBe(true); expect(Array.isArray(envelope.testIds)).toBe(true); expect((envelope.testIds as string[]).sort()).toEqual(testIds.sort()); - // results array is present + // Both run descriptors and the original create receipt are present. expect(Array.isArray(envelope.results)).toBe(true); expect((envelope.results as unknown[]).length).toBe(testIds.length); + expect(envelope.creation).toMatchObject({ + results: testIds.map((testId, specIndex) => ({ + specIndex, + testId, + status: 'created', + })), + summary: { total: 2, created: 2, failed: 0 }, + }); }); }); From 822b7fe5c6f8ce11280967f8125ace00e657bcb3 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:57:15 +0700 Subject: [PATCH 12/21] test(create-batch): cover partial creation receipts with and without wait --- src/commands/test.create-batch-run.spec.ts | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index 1717e9cf..77f14ee0 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1093,6 +1093,89 @@ describe('runCreateBatch --run --wait: CONFLICT auto-resume', () => { }); }); +// --------------------------------------------------------------------------- +// Partial creation success — the receipt must survive normal completion with +// and without --wait, independently of the run-result status. +// --------------------------------------------------------------------------- + +describe('runCreateBatch --run: partial creation receipt', () => { + it.each([true, false])( + 'preserves created and validation_error rows on normal completion (wait=%s)', + async wait => { + const { credentialsPath } = makeCreds(); + const plansFile = writePlansJsonl([FE_SPEC, FE_SPEC]); + const creationResponse = { + results: [ + { specIndex: 0, testId: 'test_partial_created', status: 'created' }, + { + specIndex: 1, + status: 'validation_error', + error: { code: 'VALIDATION_ERROR', message: 'invalid plan', field: 'planSteps' }, + }, + ], + summary: { total: 2, created: 1, failed: 1 }, + }; + + const fetchImpl = makeFetch(url => { + if (url.includes('/tests/batch')) return { body: creationResponse }; + if (url.endsWith('/tests/test_partial_created/runs')) { + return { body: makeTriggerResponse('test_partial_created', 'run_partial_created') }; + } + if (url.includes('/runs/run_partial_created')) { + return { body: makePassedRun('test_partial_created', 'run_partial_created') }; + } + return { + status: 404, + body: { + error: { code: 'NOT_FOUND', message: 'not found', nextAction: '', requestId: 'r1' }, + }, + }; + }); + + const stdout: string[] = []; + await runCreateBatch( + { + profile: 'default', + output: 'json', + debug: false, + dryRun: false, + plans: plansFile, + run: true, + wait, + maxConcurrency: 1, + timeoutSeconds: 60, + }, + { + credentialsPath, + fetchImpl, + stdout: line => stdout.push(line), + stderr: () => {}, + sleep: instantSleep, + }, + ); + + const printed = JSON.parse(stdout.join('')) as { + results: Array<{ testId: string; status: string }>; + creation: { + results: Array<{ specIndex: number; testId?: string; status: string; error?: unknown }>; + summary: { total: number; created: number; failed: number }; + }; + }; + expect(printed.results).toHaveLength(1); + expect(printed.results[0]?.status).toBe(wait ? 'passed' : 'queued'); + expect(printed.creation.summary).toEqual({ total: 2, created: 1, failed: 1 }); + expect(printed.creation.results).toMatchObject([ + { specIndex: 0, testId: 'test_partial_created', status: 'created' }, + { + specIndex: 1, + status: 'validation_error', + error: { code: 'VALIDATION_ERROR', field: 'planSteps' }, + }, + ]); + }, + ); +}); + // --------------------------------------------------------------------------- // --dry-run mode // --------------------------------------------------------------------------- From 9ae648e7176776c984d5cd93b63da4dba6aef1eb Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:57:24 +0700 Subject: [PATCH 13/21] test(create-batch): assert creation receipt without wait --- src/commands/test.create-batch-run.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index 77f14ee0..ba1f33e5 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -442,10 +442,16 @@ describe('runCreateBatch --run (no --wait): exit-code contract', () => { const printed = JSON.parse(stdout.join('')) as { results: Array<{ testId: string; status: string; error?: unknown }>; + creation: { + results: Array<{ specIndex: number; testId?: string; status: string }>; + summary: { total: number; created: number; failed: number }; + }; }; expect(printed.results).toHaveLength(3); expect(printed.results.every(r => r.status === 'queued')).toBe(true); expect(printed.results.every(r => r.error === undefined)).toBe(true); + expect(printed.creation.results).toHaveLength(3); + expect(printed.creation.summary).toEqual({ total: 3, created: 3, failed: 0 }); }); it('all triggers succeed (queued) → text summary reports "3/3 triggered", exit 0 (no "0/N passed")', async () => { From c78484c600bab6b1e328669dc7573c4bd24aa6a2 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:57:33 +0700 Subject: [PATCH 14/21] types(create-batch): type-check dry-run run envelope --- src/commands/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index f51eca3d..c765cdb9 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4364,7 +4364,7 @@ async function runBatchRun( status: 'queued', codeVersion: 'v1', })); - const envelope = { + const envelope: CliCreateBatchDryRunResponse = { dryRun: true, method: 'POST', pathTemplate: '/api/cli/v1/tests/{testId}/runs', From 2cb6d130b2d2a39c22de73aa0072324a80418fc4 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:58:11 +0700 Subject: [PATCH 15/21] docs(create-batch): document creation receipt in run output --- DOCUMENTATION.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index da696075..2ee593e3 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -410,6 +410,23 @@ testsprite test create --plan-from ./checkout.plan.json --dry-run --output json Bulk-create frontend tests from a JSONL plan-steps file (or a directory of plan files with `--plan-from-dir`). Optional `--run --max-concurrency ` fans out triggers. Without `--wait`, each run is dispatched (`status: "queued"`) and the command exits 0 when every trigger is accepted — mirroring single `test run` without `--wait`; a trigger error still exits non-zero. With `--wait`, it polls every run to terminal and exits non-zero if any run does not pass. +**JSON output with `--run`.** The top-level `results` array remains the per-test run results. A separate additive `creation` object preserves the original batch-create receipt, including failures for individual specs that were not created: + +```json +{ + "results": [{ "testId": "test_xxxxxxxx", "runId": "run_xxxxxxxx", "status": "passed" }], + "creation": { + "results": [ + { "specIndex": 0, "testId": "test_xxxxxxxx", "status": "created", "dashboardUrl": "https://..." }, + { "specIndex": 1, "status": "validation_error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid plan" } } + ], + "summary": { "total": 2, "created": 1, "failed": 1 } + } +} +``` + +The `creation` object has the same `{ results, summary }` shape as create-only JSON output. Each creation result retains its `specIndex`, status, optional error and optional `dashboardUrl`; the dashboard URL is omitted when unavailable or explicitly suppressed. This receipt is also included in interrupted/partial output and `--dry-run` JSON envelopes. Existing run-result rows, text output and exit codes are unchanged. + ```bash testsprite test create-batch --plans ./plans.jsonl --run --max-concurrency 4 --output json testsprite test create-batch --plan-from-dir ./plans/ --dry-run --output json From 981054cd6ec36fb2063fc1d528781774b4d04ea4 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:58:33 +0700 Subject: [PATCH 16/21] test(create-batch): align failed row with response contract --- src/commands/test.create-batch-run.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index ba1f33e5..dbb50c01 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1874,7 +1874,6 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', { specIndex: 0, testId: 'test_one', status: 'created' }, { specIndex: 1, - testId: null, status: 'validation_error', error: { code: 'VALIDATION_ERROR', message: 'invalid spec' }, }, @@ -1922,7 +1921,7 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', creation: { results: [ { specIndex: 0, testId: 'test_one', status: 'created' }, - { specIndex: 1, testId: null, status: 'validation_error' }, + { specIndex: 1, status: 'validation_error' }, ], summary: { total: 2, created: 1, failed: 1 }, }, From 377a6ce278199eb9f8164e823218f5544720e881 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:58:48 +0700 Subject: [PATCH 17/21] test(create-batch): assert failed creation diagnostics --- src/commands/test.create-batch-run.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index dbb50c01..8b84e23e 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1921,7 +1921,11 @@ it('marks a dispatched batch run for recovery guidance on fan-out interruption', creation: { results: [ { specIndex: 0, testId: 'test_one', status: 'created' }, - { specIndex: 1, status: 'validation_error' }, + { + specIndex: 1, + status: 'validation_error', + error: { code: 'VALIDATION_ERROR', message: 'invalid spec' }, + }, ], summary: { total: 2, created: 1, failed: 1 }, }, From c9d428423be5e4844c7f9f4550a112a3abb50eed Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:59:43 +0700 Subject: [PATCH 18/21] fix(create-batch): keep text dry-run output unchanged --- src/commands/test.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index c765cdb9..60ef8238 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4364,22 +4364,30 @@ async function runBatchRun( status: 'queued', codeVersion: 'v1', })); - const envelope: CliCreateBatchDryRunResponse = { - dryRun: true, - method: 'POST', - pathTemplate: '/api/cli/v1/tests/{testId}/runs', + const envelope = { + dryRun: true as const, + method: 'POST' as const, + pathTemplate: '/api/cli/v1/tests/{testId}/runs' as const, maxConcurrency: opts.maxConcurrency ?? null, wait: opts.wait ?? false, timeoutSeconds, testIds, ...(opts.wait ? { thenPoll: `/api/cli/v1/runs/?waitSeconds=25` } : {}), results: dryRunResults, - creation: { - results: creationResults, - summary: createResponse.summary, - }, }; - out.print(envelope); + if (opts.output === 'json') { + const jsonEnvelope: CliCreateBatchDryRunResponse = { + ...envelope, + creation: { + results: creationResults, + summary: createResponse.summary, + }, + }; + out.print(jsonEnvelope); + } else { + // Keep the pre-existing text-mode dry-run descriptor unchanged. + out.print(envelope); + } return; } From 87670567365c8fa2150ec65c883ccd38056799d8 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 12:59:56 +0700 Subject: [PATCH 19/21] test(create-batch): preserve text dry-run descriptor shape --- src/commands/test.create-batch-run.spec.ts | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index 8b84e23e..ee7510db 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1262,6 +1262,40 @@ describe('runCreateBatch --run --dry-run', () => { summary: { total: 2, created: 2, failed: 0 }, }); }); + + it('text dry-run keeps the existing descriptor shape without a creation field', async () => { + const { credentialsPath } = makeCreds(); + const plansFile = writePlansJsonl([FE_SPEC]); + const fetchImpl = makeFetch(url => { + if (url.includes('/tests/batch')) return { body: makeBatchCreateResponse(['test_text_dry']) }; + return { status: 404, body: {} }; + }); + const stdout: string[] = []; + + await runCreateBatch( + { + profile: 'default', + output: 'text', + debug: false, + dryRun: true, + plans: plansFile, + run: true, + wait: false, + timeoutSeconds: 60, + }, + { + credentialsPath, + fetchImpl, + stdout: line => stdout.push(line), + stderr: () => {}, + }, + ); + + const envelope = JSON.parse(stdout.join('')) as Record; + expect(envelope.dryRun).toBe(true); + expect(envelope.results).toBeDefined(); + expect(envelope).not.toHaveProperty('creation'); + }); }); // --------------------------------------------------------------------------- From 12a2a4eee1c5f550c86badde6109dd6dd5f3ac66 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 13:01:36 +0700 Subject: [PATCH 20/21] docs(create-batch): format JSON example --- DOCUMENTATION.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 2ee593e3..dca6a803 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -414,13 +414,35 @@ Bulk-create frontend tests from a JSONL plan-steps file (or a directory of plan ```json { - "results": [{ "testId": "test_xxxxxxxx", "runId": "run_xxxxxxxx", "status": "passed" }], + "results": [ + { + "testId": "test_xxxxxxxx", + "runId": "run_xxxxxxxx", + "status": "passed" + } + ], "creation": { "results": [ - { "specIndex": 0, "testId": "test_xxxxxxxx", "status": "created", "dashboardUrl": "https://..." }, - { "specIndex": 1, "status": "validation_error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid plan" } } + { + "specIndex": 0, + "testId": "test_xxxxxxxx", + "status": "created", + "dashboardUrl": "https://..." + }, + { + "specIndex": 1, + "status": "validation_error", + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid plan" + } + } ], - "summary": { "total": 2, "created": 1, "failed": 1 } + "summary": { + "total": 2, + "created": 1, + "failed": 1 + } } } ``` From 695b70e3b4388df196465222a023243e475c4cf2 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 10 Nov 2026 14:17:54 +0700 Subject: [PATCH 21/21] test(create-batch): assert text dry-run output without JSON parsing --- src/commands/test.create-batch-run.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/test.create-batch-run.spec.ts b/src/commands/test.create-batch-run.spec.ts index ee7510db..bb3d2fad 100644 --- a/src/commands/test.create-batch-run.spec.ts +++ b/src/commands/test.create-batch-run.spec.ts @@ -1291,10 +1291,10 @@ describe('runCreateBatch --run --dry-run', () => { }, ); - const envelope = JSON.parse(stdout.join('')) as Record; - expect(envelope.dryRun).toBe(true); - expect(envelope.results).toBeDefined(); - expect(envelope).not.toHaveProperty('creation'); + const output = stdout.join(''); + // Text mode uses the human-readable formatter, not the JSON envelope. + expect(() => JSON.parse(output)).toThrow(); + expect(output).not.toContain('creation'); }); });