@@ -1020,6 +1020,149 @@ test("executeSuite stops on a failed repetition and keeps averages from successf
10201020 ) ;
10211021} ) ;
10221022
1023+ test ( "executeSuite repeats asserts with persisted temp state and retries failed repetitions" , async ( ) => {
1024+ const outputDir = await createTempDir ( ) ;
1025+ const stateDir = await createTempDir ( ) ;
1026+ const stableCounterPath = path . join ( stateDir , "stable-counter.json" ) ;
1027+ const flakyCounterPath = path . join ( stateDir , "flaky-counter.json" ) ;
1028+ const runner = createRunnerInfo ( "open" , { type : "opencode" , model : "openai/gpt-5" } ) ;
1029+
1030+ await writeFile ( stableCounterPath , JSON . stringify ( { value : 0 } , null , 2 ) , "utf8" ) ;
1031+ await writeFile ( flakyCounterPath , JSON . stringify ( { value : 0 } , null , 2 ) , "utf8" ) ;
1032+
1033+ const stableSeen : number [ ] = [ ] ;
1034+ const flakySeen : number [ ] = [ ] ;
1035+ const suitePath = path . join ( stateDir , "repeat-suite.ts" ) ;
1036+
1037+ const adapter : RunnerAdapter = {
1038+ async run ( input : RunInput ) : Promise < RunHandle > {
1039+ return {
1040+ startedAt : "2026-04-03T10:00:00.000Z" ,
1041+ endedAt : "2026-04-03T10:00:01.000Z" ,
1042+ durationMs : 1_000 ,
1043+ stdoutPath : path . join ( input . artifactsDir , "stdout.log" ) ,
1044+ stderrPath : path . join ( input . artifactsDir , "stderr.log" ) ,
1045+ } ;
1046+ } ,
1047+ async collect ( handle : RunHandle ) : Promise < RawRunArtifacts > {
1048+ return {
1049+ stdout : "" ,
1050+ stderr : "" ,
1051+ stdoutPath : handle . stdoutPath ,
1052+ stderrPath : handle . stderrPath ,
1053+ startedAt : handle . startedAt ,
1054+ endedAt : handle . endedAt ,
1055+ durationMs : handle . durationMs ,
1056+ } ;
1057+ } ,
1058+ async normalize ( input : RunInput ) {
1059+ return createSessionReport ( {
1060+ runner,
1061+ prompt : input . prompt ,
1062+ finalOutput : input . prompt ,
1063+ } ) ;
1064+ } ,
1065+ async explain ( ) {
1066+ throw new Error ( "not used in repeat-state test" ) ;
1067+ } ,
1068+ } ;
1069+
1070+ const cases : TestCase [ ] = [
1071+ {
1072+ id : "repeat-counter-stable" ,
1073+ prompt : "Print the stable repeat counter." ,
1074+ async assert ( ) {
1075+ const next = await incrementCounter ( stableCounterPath ) ;
1076+ stableSeen . push ( next ) ;
1077+ console . log ( next ) ;
1078+ } ,
1079+ } ,
1080+ {
1081+ id : "repeat-counter-flaky" ,
1082+ prompt : "Print the flaky repeat counter." ,
1083+ async assert ( ) {
1084+ const next = await incrementCounter ( flakyCounterPath ) ;
1085+ flakySeen . push ( next ) ;
1086+ console . log ( next ) ;
1087+
1088+ if ( next === 3 || next === 4 ) {
1089+ throw new Error ( `intentional failure at counter ${ String ( next ) } ` ) ;
1090+ }
1091+ } ,
1092+ } ,
1093+ ] ;
1094+
1095+ const result = await executeSuite ( suitePath , cases , {
1096+ cwd : outputDir ,
1097+ outputDir,
1098+ repeat : 4 ,
1099+ repeatFailure : 2 ,
1100+ isInteractive : false ,
1101+ config : {
1102+ runners : {
1103+ open : { agent : { type : "opencode" , model : "openai/gpt-5" } } ,
1104+ } ,
1105+ } ,
1106+ executeRunnerFn : ( testCase , runnerInfo , _unusedAdapter , options ) => {
1107+ return executeRunner ( testCase , runnerInfo , adapter , options ) ;
1108+ } ,
1109+ } ) ;
1110+
1111+ const stableResult = result . cases . find (
1112+ ( caseResult ) => caseResult . caseId === "repeat-counter-stable" ,
1113+ ) ?. runnerResults [ 0 ] ;
1114+ const flakyResult = result . cases . find (
1115+ ( caseResult ) => caseResult . caseId === "repeat-counter-flaky" ,
1116+ ) ?. runnerResults [ 0 ] ;
1117+
1118+ expect ( stableSeen ) . toEqual ( [ 1 , 2 , 3 , 4 ] ) ;
1119+ expect ( flakySeen ) . toEqual ( [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
1120+ expect ( await readCounter ( stableCounterPath ) ) . toBe ( 4 ) ;
1121+ expect ( await readCounter ( flakyCounterPath ) ) . toBe ( 6 ) ;
1122+
1123+ expect ( stableResult ) . toMatchObject ( {
1124+ passed : true ,
1125+ status : "passed" ,
1126+ repeatTarget : 4 ,
1127+ completedRepetitions : 4 ,
1128+ successfulRepetitions : 4 ,
1129+ } ) ;
1130+ expect ( stableResult ?. repetitions ?. map ( ( entry ) => entry . attempt ) ) . toEqual ( [ 1 , 1 , 1 , 1 ] ) ;
1131+
1132+ expect ( flakyResult ) . toMatchObject ( {
1133+ passed : true ,
1134+ status : "passed" ,
1135+ repeatTarget : 4 ,
1136+ completedRepetitions : 4 ,
1137+ successfulRepetitions : 4 ,
1138+ attempt : 1 ,
1139+ } ) ;
1140+ expect ( flakyResult ?. repetitions ) . toHaveLength ( 4 ) ;
1141+ expect ( flakyResult ?. repetitions ?. map ( ( entry ) => entry . attempt ) ) . toEqual ( [ 1 , 1 , 3 , 1 ] ) ;
1142+ expect ( flakyResult ?. repetitions ?. [ 2 ] ?. attempts ?. map ( ( entry ) => entry . passed ) ) . toEqual ( [
1143+ false ,
1144+ false ,
1145+ true ,
1146+ ] ) ;
1147+ expect ( flakyResult ?. repetitions ?. [ 2 ] ?. attempts ?. map ( ( entry ) => entry . error ?. message ) ) . toEqual ( [
1148+ "intentional failure at counter 3" ,
1149+ "intentional failure at counter 4" ,
1150+ undefined ,
1151+ ] ) ;
1152+ expect ( flakyResult ?. repetitions ?. [ 2 ] ?. attempts ?. [ 0 ] ?. artifactDir ) . toBe (
1153+ path . join ( result . outputDir , "repeat-counter-flaky" , runner . pathKey , "repeat-3" ) ,
1154+ ) ;
1155+ expect ( flakyResult ?. repetitions ?. [ 2 ] ?. attempts ?. [ 1 ] ?. artifactDir ) . toBe (
1156+ path . join ( result . outputDir , "repeat-counter-flaky" , runner . pathKey , "repeat-3" , "attempt-2" ) ,
1157+ ) ;
1158+ expect ( flakyResult ?. repetitions ?. [ 2 ] ?. attempts ?. [ 2 ] ?. artifactDir ) . toBe (
1159+ path . join ( result . outputDir , "repeat-counter-flaky" , runner . pathKey , "repeat-3" , "attempt-3" ) ,
1160+ ) ;
1161+ expect ( flakyResult ?. leafArtifactDir ) . toBe (
1162+ path . join ( result . outputDir , "repeat-counter-flaky" , runner . pathKey , "repeat-4" ) ,
1163+ ) ;
1164+ } ) ;
1165+
10231166test ( "executeSuite filters cases by tags with OR semantics and preserves result metadata" , async ( ) => {
10241167 const outputDir = await createTempDir ( ) ;
10251168 const executed : string [ ] = [ ] ;
@@ -1372,6 +1515,22 @@ async function createTempDir(): Promise<string> {
13721515 return tempDir ;
13731516}
13741517
1518+ async function incrementCounter ( filePath : string ) : Promise < number > {
1519+ const current = await readCounter ( filePath ) ;
1520+ const next = current + 1 ;
1521+ await writeFile ( filePath , JSON . stringify ( { value : next } , null , 2 ) , "utf8" ) ;
1522+ return next ;
1523+ }
1524+
1525+ async function readCounter ( filePath : string ) : Promise < number > {
1526+ const contents = JSON . parse ( await readFile ( filePath , "utf8" ) ) as { value ?: unknown } ;
1527+ if ( typeof contents . value !== "number" ) {
1528+ throw new Error ( `Invalid counter file: ${ filePath } ` ) ;
1529+ }
1530+
1531+ return contents . value ;
1532+ }
1533+
13751534function createSnapshotRuntime ( filePath : string ) : SnapshotRuntimeOptions {
13761535 return {
13771536 enabled : true ,
0 commit comments