@@ -6694,6 +6694,103 @@ describe('runCreateBatch', () => {
66946694 ) . resolves . toBeDefined ( ) ;
66956695 } ) ;
66966696
6697+ // Regression test: create-batch --run must keep launching new triggers
6698+ // up to --max-concurrency as slots free up, not collapse to serial
6699+ // after the first wave. Uses equal-delay trigger responses so the
6700+ // first three jobs settle in the same microtask batch, the exact
6701+ // condition that exposed the bug (race only reacts to one settlement,
6702+ // then blocks the scheduler on the whole next job before moving on).
6703+ it ( '--run keeps concurrency at --max-concurrency for tail jobs, not just the first wave' , async ( ) => {
6704+ const { credentialsPath } = makeCreds ( ) ;
6705+ const specs = Array . from ( { length : 6 } , ( _ , i ) => ( { ...FE_SPEC , name : `spec-${ i } ` } ) ) ;
6706+ const plansFile = writePlansJsonl ( specs ) ;
6707+ const CREATE_RESP = {
6708+ results : specs . map ( ( _ , i ) => ( {
6709+ specIndex : i ,
6710+ testId : `test_tail_${ i } ` ,
6711+ status : 'created' as const ,
6712+ } ) ) ,
6713+ summary : { total : 6 , created : 6 , failed : 0 } ,
6714+ } ;
6715+ const TRIGGER_DELAY_MS = 60 ;
6716+ const limit = 3 ;
6717+ let activeCount = 0 ;
6718+ let triggerCallIndex = 0 ;
6719+ const activeAtStart : number [ ] = [ ] ;
6720+
6721+ type FetchInput2 = Parameters < typeof globalThis . fetch > [ 0 ] ;
6722+ const fetchImpl = ( async ( input : FetchInput2 ) => {
6723+ const url =
6724+ typeof input === 'string'
6725+ ? input
6726+ : input instanceof URL
6727+ ? input . toString ( )
6728+ : ( input as { url : string } ) . url ;
6729+ if ( url . includes ( '/tests/batch' ) ) {
6730+ return new Response ( JSON . stringify ( CREATE_RESP ) , {
6731+ status : 200 ,
6732+ headers : { 'content-type' : 'application/json' } ,
6733+ } ) ;
6734+ }
6735+ if ( url . includes ( '/runs' ) ) {
6736+ const callIdx = triggerCallIndex ++ ;
6737+ activeCount ++ ;
6738+ activeAtStart [ callIdx ] = activeCount ;
6739+ await new Promise ( resolve => setTimeout ( resolve , TRIGGER_DELAY_MS ) ) ;
6740+ activeCount -- ;
6741+ return new Response (
6742+ JSON . stringify ( {
6743+ runId : `run_tail_${ callIdx } ` ,
6744+ status : 'queued' as const ,
6745+ enqueuedAt : '2026-06-09T10:00:00.000Z' ,
6746+ codeVersion : 'v1' ,
6747+ targetUrl : 'https://example.com' ,
6748+ } ) ,
6749+ { status : 200 , headers : { 'content-type' : 'application/json' } } ,
6750+ ) ;
6751+ }
6752+ return new Response (
6753+ JSON . stringify ( {
6754+ error : { code : 'NOT_FOUND' , message : 'not found' , nextAction : '' , requestId : 'r1' } ,
6755+ } ) ,
6756+ { status : 404 , headers : { 'content-type' : 'application/json' } } ,
6757+ ) ;
6758+ } ) as typeof globalThis . fetch ;
6759+
6760+ try {
6761+ await runCreateBatch (
6762+ {
6763+ profile : 'default' ,
6764+ output : 'json' ,
6765+ debug : false ,
6766+ plans : plansFile ,
6767+ run : true ,
6768+ wait : false ,
6769+ dryRun : false ,
6770+ maxConcurrency : limit ,
6771+ } ,
6772+ {
6773+ credentialsPath,
6774+ fetchImpl : fetchImpl as ReturnType < typeof makeFetch > ,
6775+ stdout : ( ) => undefined ,
6776+ stderr : ( ) => undefined ,
6777+ } ,
6778+ ) ;
6779+ } catch {
6780+ // CLIError exit 1 expected: trigger status is 'queued', not 'passed'.
6781+ }
6782+
6783+ expect ( activeAtStart ) . toHaveLength ( 6 ) ;
6784+ // First wave fills up to the limit; true under the bug too.
6785+ expect ( Math . max ( ...activeAtStart . slice ( 0 , limit ) ) ) . toBe ( limit ) ;
6786+ // Tail jobs (index >= limit) must ALSO reach the concurrency limit.
6787+ // Under the bug, the scheduler blocks on each whole job after the
6788+ // first wave, so every tail job launches alone (active === 1).
6789+ for ( const snapshot of activeAtStart . slice ( limit ) ) {
6790+ expect ( snapshot ) . toBe ( limit ) ;
6791+ }
6792+ } ) ;
6793+
66976794 // Per codex round-1 P2: a 200 OK with `summary.created === 0` on a
66986795 // non-empty batch must not exit 0. Without this, a misconfigured
66996796 // batch job (every spec invalid) silently lands nothing in DDB while
0 commit comments