Skip to content

[Hackathon] fix(create-batch): --run JSON output silently drops per-spec creation failures #483

Open

Description

@yeziR4

Verified against (commit SHA or released version)

988c035 (CLI 0.14.0), built and executed on Windows / Node.js v22.22.2, 2026-10-17

Discord Username / User ID

___h___r

What does this improvement do?

Preserve per-spec creation failures in the JSON output of test create-batch --run, with and without --wait.

When POST /tests/batch partially succeeds, the response contains each input’s specIndex, status/error and a total/created/failed summary. Adding --run --output json suppresses this entire response and prints only runs for successfully created tests. Rejected inputs disappear from both stdout and stderr. Automation cannot identify which plans failed or retrieve their diagnostics.

This report concerns lost diagnostics/accounting, not changing the documented successful exit code for partial creation success.

Details / implementation notes

Trigger and observed behavior

Two valid frontend plan records target an existing project and a missing project. The batch endpoint supports partial success (the existing CliBatchSpecResult contract includes created, not_found and validation_error).

The offline HTTP fixture returns 404 for the missing project's environment lookup, a normal public default environment for the other project, then this creation response:

{"results":[{"specIndex":0,"testId":"test_created","status":"created"},{"specIndex":1,"status":"not_found","error":{"code":"NOT_FOUND","message":"Project project_missing was not found"}}],"summary":{"total":2,"created":1,"failed":1}}

The successful test's run is accepted and passes.

testsprite test create-batch --plans plans.jsonl --output json
testsprite test create-batch --plans plans.jsonl --run --output json
testsprite test create-batch --plans plans.jsonl --run --wait --output json
testsprite test create-batch --plans plans.jsonl --run --output text
Scenario Actual output
Create-only JSON Both creation outcomes, error and summary preserved
--run JSON Only the successful test's queued run remains
--run --wait JSON Only the successful test's passed run remains
--run text Shows total 2 / created 1 / failed 1 and failed input index

All four exit 0. For both JSON run modes, stderr contains only the generated idempotency key. The rejected spec's status, error and index are lost.

Actual --run --wait JSON:

{"results":[{"testId":"test_created","runId":"run_created","status":"passed","codeVersion":"v1","videoUrl":null,"failureKind":null}]}

Expected / proposed scope

Keep one parseable JSON envelope containing both the creation receipt and execution outcomes. An additive field such as creation: {results, summary} could retain existing run results while preserving accounting for every input. Exact naming is for maintainer agreement. Preserve the partial-success exit policy. Cover partial success, all success, all creation failures, and execution failures/interrupt output as appropriate.

Source evidence

All links are pinned to the tested commit:

Verification and duplicate check

Built the pinned checkout successfully. Executed all four scenarios with real Commander parsing, HTTP client, orchestration and polling; only HTTP responses were injected. No product source was changed or live test credits used. Existing src/commands/test.create-batch-run.spec.ts: 49 tests passed.

Searched open/closed issues and PRs for create-batch, partial, creation, receipt, results and specIndex; rechecked on 2026-10-17 before submission. No matching report found. #426 is signal handling during retry sleep; this case has no interrupt, retry or exceptional termination. #387 is incompatible conflict attachment; this case has no conflict. #161 / #267 are no-wait exit codes; this report preserves that policy. #480 concerns timeout rounding; this case uses immediate accepted/passed responses.

This is an executed offline reproduction of a supported response contract, not a claim of a hosted production incident. I will wait for acceptance and assignment before implementation. AI disclosure: Codex assisted with source review, reproduction and duplicate checking.

Executable four-scenario reproduction

Clone the repository into a folder named testsprite-cli, check out the commit above, and run npm ci then npm run build inside it. Save the script beside that folder as repro-batch-receipt.mjs and run node repro-batch-receipt.mjs. It writes local fixture files and prints outputs/traces. Assertions pin the current faulty behavior.

import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { Command } from './testsprite-cli/node_modules/commander/index.js';
import { createTestCommand } from './testsprite-cli/dist/commands/test.js';
const dir=resolve('work/batch-receipt-fixture');
mkdirSync(dir,{recursive:true});
const plans=join(dir,'plans.jsonl');
const spec={projectId:'project_fixture',type:'frontend',name:'Valid plan',planSteps:[{type:'action',description:'Open home page'}]};
writeFileSync(plans,[spec,{...spec,projectId:'project_missing',name:'Plan in missing project'}].map(x=>JSON.stringify(x)).join('\n'));
const stamp='2026-10-17T00:00:00Z';
const receipt={results:[{specIndex:0,testId:'test_created',status:'created'},{specIndex:1,status:'not_found',error:{code:'NOT_FOUND',message:'Project project_missing was not found'}}],summary:{total:2,created:1,failed:1}};
const outputs=[];
for(const mode of ['create-only-json','run-json','wait-json','run-text']){
 const stdout=[],stderr=[],requests=[];
 const deps={env:{TESTSPRITE_API_KEY:'sk-user-offline-fixture',TESTSPRITE_API_URL:'https://mock.example.com'},credentialsPath:join(dir,'absent-credentials'),stdout:x=>stdout.push(x),stderr:x=>stderr.push(x),fetchImpl:async(input,init={})=>{
  const url=new URL(typeof input==='string'?input:input.url??input.href);
  assert.equal(url.hostname,'mock.example.com');
  const path=url.pathname.replace('/api/cli/v1','');
  const method=init.method??'GET';requests.push({method,path});
  let body;
  if(path==='/tests/batch'&&method==='POST')body=receipt;
  else if(path==='/projects/project_missing/env')return new Response(JSON.stringify({error:{code:'NOT_FOUND',message:'Project project_missing was not found',requestId:'fixture'}}),{status:404,headers:{'content-type':'application/json'}});
  else if(path==='/projects/project_fixture/env')body={environments:[{name:'default',url:'https://example.com',isDefault:true,isTemporary:false}]};
  else if(path==='/tests/test_created/runs'&&method==='POST')body={runId:'run_created',status:'queued',enqueuedAt:stamp,codeVersion:'v1',targetUrl:'https://example.com'};
  else if(path==='/runs/run_created')body={runId:'run_created',testId:'test_created',projectId:'project_fixture',userId:'fixture',status:'passed',source:'cli',createdAt:stamp,startedAt:stamp,finishedAt:stamp,codeVersion:'v1',targetUrl:'https://example.com',createdFrom:'cli',failedStepIndex:null,failureKind:null,error:null,videoUrl:null,stepSummary:{total:1,completed:1,passedCount:1,failedCount:0}};
  else throw Error('Unexpected request: '+method+' '+path);
  return new Response(JSON.stringify(body),{status:200,headers:{'content-type':'application/json'}});
 }};
 const args=['--output',mode==='run-text'?'text':'json','test','create-batch','--plans',plans];
 if(mode!=='create-only-json')args.push('--run');
 if(mode==='wait-json')args.push('--wait');
 await new Command('testsprite').option('--output <mode>').addCommand(createTestCommand(deps)).parseAsync(args,{from:'user'});
 if(mode==='create-only-json')assert.equal(JSON.parse(stdout[0]).results[1].status,'not_found');
 else if(mode!=='run-text'){
  const result=JSON.parse(stdout[0]);
  assert.equal(stdout.length,1);assert.equal(result.results.length,1);
  assert.equal(result.summary,undefined);
  assert.ok(!JSON.stringify({stdout,stderr}).includes('Project project_missing was not found'));
 }
 outputs.push({mode,exitCode:0,stdout,stderr,requests});
}
console.log(JSON.stringify({node:process.version,receipt,outputs},null,2));

Confirmations

  • I have searched existing issues and this is not a duplicate.
  • I have provided my Discord identity above for reward coordination.

Activity

  1. zeshi-du commented on Oct 20, 2026

    @zeshi-du
    Contributor

    Thanks @yeziR4 — confirmed on v0.15.0: with --run --output json, runBatchRun prints only { results: <runs> }, so rejected specs never reach stdout. Accepted.

    Scope:

    • Additive only: add a top-level creation: { results, summary } to the --run JSON envelope, same shape as the create-only output. Keep results as the run rows; rename nothing. The name creation is settled.
    • Exit codes and text output unchanged (partial creation success still exits 0; the all-failed error envelope stays).
    • Cover every JSON emit point in runBatchRun: normal, interrupted/partial, dry-run, with and without --wait.
    • Tests: partial success, all success, interrupted, and at least one that fails before the fix. Update DOCUMENTATION.md and any matching schema in schemas/.

    Keep the diff small; leave idempotency keys, wait budget and conflict handling alone. Other accepted issues touch the same function, so we merge in a set order and you may need to rebase. Comment /assign to claim it.

  2. added
    acceptedTriaged and accepted — a PR for this will be reviewed
    and removed
    needs-triageNew issue/PR awaiting first triage
    on Oct 20, 2026
  3. Lexiie commented on Oct 20, 2026

    @Lexiie
    Contributor

    /assign

  4. testsprite-hob commented on Oct 20, 2026

    @testsprite-hob

    Assigned to @Lexiie. Thanks for taking this on. (2 more slots available.)

    Your open assigned issues (1/3):
    (none)

  5. yeziR4 commented on Oct 20, 2026

    @yeziR4
    Author

    Thanks for confirming and accepting this report. I reported and reproduced it with the intention of implementing it after acceptance, and I see it has since been claimed and PR #589 opened. Could you clarify the Season 4 bonus credit policy when the original reporter and PR author differ? The Discord announcement says “If two people file the same thing, the first to file keeps the credit,” while payout eligibility is described in terms of PRs. Does the original reporter retain any bonus credit, and do reporters have any priority to claim their accepted reports? I want to follow the intended process and avoid duplicate implementation work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

acceptedTriaged and accepted — a PR for this will be reviewedhackathonCLI hackathon submissionsin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions