@@ -15,6 +15,7 @@ let origin: string;
1515let scenario : "success" | "submit-error" | "task-failed" | "empty" | "download-error" ;
1616let requests : Array < { method : string ; path : string ; body : unknown ; async : boolean } > ;
1717let legacyResults : boolean ;
18+ let asyncImageShape : "default" | "results" | "mixed" | "both" | "partial-empty" | "all-empty" ;
1819let submissions : number ;
1920let downloads : number ;
2021
@@ -25,6 +26,7 @@ beforeEach(async () => {
2526 submissions = 0 ;
2627 downloads = 0 ;
2728 legacyResults = false ;
29+ asyncImageShape = "default" ;
2830 server = createServer ( async ( request , response ) => {
2931 if ( request . url ?. startsWith ( "/files/" ) ) {
3032 downloads ++ ;
@@ -74,30 +76,21 @@ beforeEach(async () => {
7476 return ;
7577 }
7678 const taskId = request . url ! . split ( "/" ) . at ( - 1 ) ! ;
79+ const taskNumber = taskId . slice ( 5 ) ;
80+ const fileUrl = `${ origin } /files/${ taskNumber } .png` ;
81+ const choiceImage = { message : { content : [ { image : fileUrl } ] } } ;
82+ const resultImage = { url : fileUrl } ;
83+ const ignoredImage = { url : `${ origin } /files/ignored-${ taskNumber } .png` } ;
84+ const useResults = legacyResults || asyncImageShape === "results" ;
85+ const images = taskImages ( taskNumber , choiceImage , resultImage , ignoredImage , useResults ) ;
7786 response . end (
7887 JSON . stringify ( {
7988 output : {
8089 task_id : taskId ,
8190 task_status : scenario === "task-failed" ? "FAILED" : "SUCCEEDED" ,
8291 ...( scenario === "task-failed"
8392 ? { code : "TaskFailure" , message : "Original task failure" }
84- : legacyResults
85- ? {
86- results :
87- scenario === "empty" ? [ ] : [ { url : `${ origin } /files/${ taskId . slice ( 5 ) } .png` } ] ,
88- }
89- : {
90- choices :
91- scenario === "empty"
92- ? [ ]
93- : [
94- {
95- message : {
96- content : [ { image : `${ origin } /files/${ taskId . slice ( 5 ) } .png` } ] ,
97- } ,
98- } ,
99- ] ,
100- } ) ,
93+ : images ) ,
10194 } ,
10295 } ) ,
10396 ) ;
@@ -117,6 +110,25 @@ afterEach(async () => {
117110 rmSync ( directory , { recursive : true , force : true } ) ;
118111} ) ;
119112
113+ function taskImages (
114+ taskNumber : string ,
115+ choiceImage : { message : { content : Array < { image : string } > } } ,
116+ resultImage : { url : string } ,
117+ ignoredImage : { url : string } ,
118+ useResults : boolean ,
119+ ) {
120+ if ( asyncImageShape === "all-empty" ) return { results : [ ] } ;
121+ if ( asyncImageShape === "partial-empty" ) {
122+ return taskNumber === "2" ? { results : [ ] } : { results : [ resultImage ] } ;
123+ }
124+ if ( asyncImageShape === "both" ) return { choices : [ choiceImage ] , results : [ ignoredImage ] } ;
125+ if ( asyncImageShape === "mixed" ) {
126+ return taskNumber === "1" ? { choices : [ choiceImage ] } : { results : [ resultImage ] } ;
127+ }
128+ if ( useResults ) return { results : scenario === "empty" ? [ ] : [ resultImage ] } ;
129+ return { choices : scenario === "empty" ? [ ] : [ choiceImage ] } ;
130+ }
131+
120132const commands = [
121133 { name : "generate" , command : generate } ,
122134 { name : "edit" , command : edit } ,
@@ -381,3 +393,117 @@ for (const entry of commands) {
381393 expect ( result . stdout ) . not . toContain ( "cGF5bG9hZA==" ) ;
382394 } ) ;
383395}
396+
397+ function savedPaths ( count : number ) {
398+ const paths : string [ ] = [ ] ;
399+ for ( let index = 1 ; index <= count ; index += 1 ) {
400+ paths . push ( join ( directory , "explicit" , `fixed_${ String ( index ) . padStart ( 3 , "0" ) } .png` ) ) ;
401+ }
402+ return paths ;
403+ }
404+
405+ function fileUrls ( numbers : number [ ] ) {
406+ return numbers . map ( ( number ) => `${ origin } /files/${ number } .png` ) ;
407+ }
408+
409+ for ( const entry of commands ) {
410+ test ( `${ entry . name } async concurrent results keeps every task image` , async ( ) => {
411+ asyncImageShape = "results" ;
412+ const snapshots = [ ] ;
413+ for ( const format of [ "json" , "text" ] as const ) {
414+ submissions = 0 ;
415+ downloads = 0 ;
416+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 3 ) , {
417+ format,
418+ quiet : true ,
419+ } ) ;
420+ expect ( result . exitCode , result . stderr ) . toBe ( 0 ) ;
421+ const data = format === "json" ? JSON . parse ( result . stdout ) : parseYaml ( result . stdout ) ;
422+ const saved = savedPaths ( 3 ) ;
423+ expect ( data ) . toEqual ( {
424+ urls : fileUrls ( [ 1 , 2 , 3 ] ) ,
425+ saved,
426+ total : 3 ,
427+ ...( entry . name === "generate" ? { task_ids : [ "task-1" , "task-2" , "task-3" ] } : { } ) ,
428+ } ) ;
429+ for ( const path of saved ) expect ( readFileSync ( path , "utf8" ) ) . toBe ( "payload" ) ;
430+ expect ( downloads ) . toBe ( 3 ) ;
431+ snapshots . push ( data ) ;
432+ }
433+ expect ( snapshots [ 1 ] ) . toEqual ( snapshots [ 0 ] ) ;
434+ } ) ;
435+
436+ test ( `${ entry . name } async mixed choices and results keeps both tasks` , async ( ) => {
437+ asyncImageShape = "mixed" ;
438+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 2 ) , {
439+ format : "json" ,
440+ quiet : true ,
441+ } ) ;
442+ expect ( result . exitCode , result . stderr ) . toBe ( 0 ) ;
443+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( {
444+ urls : fileUrls ( [ 1 , 2 ] ) ,
445+ saved : savedPaths ( 2 ) ,
446+ total : 2 ,
447+ ...( entry . name === "generate" ? { task_ids : [ "task-1" , "task-2" ] } : { } ) ,
448+ } ) ;
449+ expect ( downloads ) . toBe ( 2 ) ;
450+ } ) ;
451+
452+ test ( `${ entry . name } async task with both fields keeps choices only` , async ( ) => {
453+ asyncImageShape = "both" ;
454+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 2 ) , {
455+ format : "json" ,
456+ quiet : true ,
457+ } ) ;
458+ expect ( result . exitCode , result . stderr ) . toBe ( 0 ) ;
459+ const data = JSON . parse ( result . stdout ) ;
460+ expect ( data . urls ) . toEqual ( fileUrls ( [ 1 , 2 ] ) ) ;
461+ expect ( data . total ) . toBe ( 2 ) ;
462+ expect ( data . saved ) . toEqual ( savedPaths ( 2 ) ) ;
463+ expect ( JSON . stringify ( data ) ) . not . toContain ( "ignored-" ) ;
464+ expect ( downloads ) . toBe ( 2 ) ;
465+ } ) ;
466+
467+ test ( `${ entry . name } async partial empty results keeps the remaining images` , async ( ) => {
468+ asyncImageShape = "partial-empty" ;
469+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 3 ) , {
470+ format : "json" ,
471+ quiet : true ,
472+ } ) ;
473+ expect ( result . exitCode , result . stderr ) . toBe ( 0 ) ;
474+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( {
475+ urls : fileUrls ( [ 1 , 3 ] ) ,
476+ saved : savedPaths ( 2 ) ,
477+ total : 2 ,
478+ ...( entry . name === "generate" ? { task_ids : [ "task-1" , "task-2" , "task-3" ] } : { } ) ,
479+ } ) ;
480+ expect ( downloads ) . toBe ( 2 ) ;
481+ } ) ;
482+
483+ test ( `${ entry . name } async empty results still fails without images` , async ( ) => {
484+ asyncImageShape = "all-empty" ;
485+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 2 ) , {
486+ format : "json" ,
487+ quiet : true ,
488+ } ) ;
489+ expect ( result . exitCode ) . toBe ( 1 ) ;
490+ expect ( result . stdout ) . toBe ( "" ) ;
491+ expect ( result . stderr ) . toContain ( "All tasks completed but no images returned." ) ;
492+ expect ( downloads ) . toBe ( 0 ) ;
493+ } ) ;
494+
495+ test ( `${ entry . name } async concurrent choices still collects every image` , async ( ) => {
496+ const result = await invoke ( entry . command , flagsFor ( entry . name , "wait" , 3 ) , {
497+ format : "json" ,
498+ quiet : true ,
499+ } ) ;
500+ expect ( result . exitCode , result . stderr ) . toBe ( 0 ) ;
501+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( {
502+ urls : fileUrls ( [ 1 , 2 , 3 ] ) ,
503+ saved : savedPaths ( 3 ) ,
504+ total : 3 ,
505+ ...( entry . name === "generate" ? { task_ids : [ "task-1" , "task-2" , "task-3" ] } : { } ) ,
506+ } ) ;
507+ expect ( downloads ) . toBe ( 3 ) ;
508+ } ) ;
509+ }
0 commit comments