@@ -70,6 +70,7 @@ import { DIFF_PATH } from '@/executor/handlers/pi/cloud-shared'
7070
7171const OLD_SHA = 'a' . repeat ( 40 )
7272const NEW_SHA = 'c' . repeat ( 40 )
73+ const SECOND_SHA = 'd' . repeat ( 40 )
7374const snapshot = {
7475 headSha : OLD_SHA ,
7576 headRef : 'feature' ,
@@ -160,13 +161,15 @@ function makeRunner(options: {
160161 prepareStdout ?: string | string [ ]
161162 pushResult ?: ReturnType < typeof commandResult >
162163 roundFile ?: string
164+ diff ?: string | string [ ]
163165} ) {
164166 const runCalls : Array < {
165167 command : string
166168 envs ?: Record < string , string >
167169 timeoutMs ?: number
168170 } > = [ ]
169171 let prepareCall = 0
172+ let diffRead = 0
170173 const runner = {
171174 run : vi . fn (
172175 async (
@@ -191,7 +194,7 @@ function makeRunner(options: {
191194 : options . prepareStdout
192195 return commandResult (
193196 configuredPrepare ??
194- `__CHANGED__ =src/a.ts\n__DIFF_BYTES__ =20\n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n`
197+ `__CUMULATIVE_CHANGED__ =src/a.ts\n__CUMULATIVE_DIFF_BYTES__ =20\n__CHANGED__=src/a.ts \n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n`
195198 )
196199 }
197200 if ( command . includes ( 'CURRENT_DIGEST=' ) ) {
@@ -202,7 +205,12 @@ function makeRunner(options: {
202205 ) ,
203206 writeFile : vi . fn ( ) ,
204207 readFile : vi . fn ( async ( path : string ) => {
205- if ( path === DIFF_PATH ) return 'diff --git a/src/a.ts b/src/a.ts'
208+ if ( path === DIFF_PATH ) {
209+ if ( Array . isArray ( options . diff ) ) {
210+ return options . diff [ diffRead ++ ] ?? options . diff . at ( - 1 ) ?? ''
211+ }
212+ return options . diff ?? 'diff --git a/src/a.ts b/src/a.ts'
213+ }
206214 if ( path === BABYSIT_ROUND_PATH ) {
207215 return (
208216 options . roundFile ??
@@ -347,7 +355,7 @@ describe('runBabysitPiWithOptions', () => {
347355 }
348356 if ( command . includes ( 'git -c core.hooksPath=/dev/null add -A' ) ) {
349357 return commandResult (
350- `__CHANGED__ =src/a.ts\n__DIFF_BYTES__ =20\n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n`
358+ `__CUMULATIVE_CHANGED__ =src/a.ts\n__CUMULATIVE_DIFF_BYTES__ =20\n__CHANGED__=src/a.ts \n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n`
351359 )
352360 }
353361 if ( command . includes ( 'CURRENT_DIGEST=' ) ) return commandResult ( '__PUSHED__=1\n' )
@@ -402,6 +410,63 @@ describe('runBabysitPiWithOptions', () => {
402410 } )
403411 } )
404412
413+ it ( 'reports only the last pushed round while enforcing cumulative markers' , async ( ) => {
414+ mockFetchSnapshot
415+ . mockResolvedValueOnce ( snapshot )
416+ . mockResolvedValueOnce ( snapshot )
417+ . mockResolvedValueOnce ( { ...snapshot , headSha : NEW_SHA } )
418+ . mockResolvedValueOnce ( { ...snapshot , headSha : NEW_SHA } )
419+ . mockResolvedValueOnce ( { ...snapshot , headSha : NEW_SHA } )
420+ . mockResolvedValueOnce ( { ...snapshot , headSha : SECOND_SHA } )
421+ . mockResolvedValueOnce ( { ...snapshot , headSha : SECOND_SHA } )
422+ mockFetchThreads . mockResolvedValue ( {
423+ actionable : [ ] ,
424+ skipped : [ ] ,
425+ totalUnresolved : 0 ,
426+ latestReview : null ,
427+ } )
428+ mockFetchChecks
429+ . mockResolvedValueOnce ( failingChecks )
430+ . mockResolvedValueOnce ( failingChecks )
431+ . mockResolvedValueOnce ( greenChecks )
432+ mockReplyAndResolve . mockResolvedValue ( {
433+ repliesPosted : 0 ,
434+ threadsResolved : 0 ,
435+ replyFailures : [ ] ,
436+ resolveFailures : [ ] ,
437+ headMoved : false ,
438+ awaitingConfirmation : false ,
439+ } )
440+ const { runner, runCalls } = makeRunner ( {
441+ prepareStdout : [
442+ `__CUMULATIVE_CHANGED__=src/a.ts\n__CUMULATIVE_DIFF_BYTES__=20\n__CHANGED__=src/a.ts\n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n` ,
443+ `__CUMULATIVE_CHANGED__=src/a.ts\n__CUMULATIVE_CHANGED__=src/b.ts\n__CUMULATIVE_DIFF_BYTES__=40\n__CHANGED__=src/b.ts\n__NEW_SHA__=${ SECOND_SHA } \n__NEEDS_PUSH__=1\n` ,
444+ ] ,
445+ roundFile : JSON . stringify ( { threads : [ ] } ) ,
446+ diff : [ 'round-one-diff' , 'round-two-diff' ] ,
447+ } )
448+ mockWithPiSandbox . mockImplementation ( async ( callback ) => callback ( runner ) )
449+
450+ const result = await runBabysitPiWithOptions (
451+ params ( ) ,
452+ { onEvent : vi . fn ( ) } ,
453+ { convergenceWaitMs : 0 , roundWaitMs : 0 }
454+ )
455+
456+ expect ( result ) . toMatchObject ( {
457+ stopReason : 'clean' ,
458+ rounds : 2 ,
459+ commitsPushed : 2 ,
460+ changedFiles : [ 'src/b.ts' ] ,
461+ diff : 'round-two-diff' ,
462+ } )
463+ expect (
464+ runCalls
465+ . filter ( ( { command } ) => command . includes ( 'CURRENT_DIGEST=' ) )
466+ . map ( ( { envs } ) => envs ?. PINNED_SHA )
467+ ) . toEqual ( [ OLD_SHA , NEW_SHA ] )
468+ } )
469+
405470 it ( 'refuses .github changes before the credentialed push' , async ( ) => {
406471 mockFetchSnapshot . mockResolvedValue ( snapshot )
407472 mockFetchThreads . mockResolvedValue ( {
@@ -412,7 +477,7 @@ describe('runBabysitPiWithOptions', () => {
412477 } )
413478 mockFetchChecks . mockResolvedValue ( greenChecks )
414479 const { runner, runCalls } = makeRunner ( {
415- prepareStdout : `__CHANGED__ =.github/workflows/ci.yml\n__DIFF_BYTES__ =20\n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n` ,
480+ prepareStdout : `__CUMULATIVE_CHANGED__ =.github/workflows/ci.yml\n__CUMULATIVE_DIFF_BYTES__ =20\n__CHANGED__=.github/workflows/ci.yml \n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n` ,
416481 } )
417482 mockWithPiSandbox . mockImplementation ( async ( callback ) => callback ( runner ) )
418483
@@ -672,10 +737,46 @@ describe('runBabysitPiWithOptions', () => {
672737 rounds : 2 ,
673738 commitsPushed : 0 ,
674739 threadsResolved : 0 ,
740+ threadsClean : false ,
741+ checksGreen : true ,
675742 } )
676743 expect ( mockFetchThreads ) . toHaveBeenCalledTimes ( 3 )
677744 } )
678745
746+ it ( 'preserves clean threads when checks are stuck' , async ( ) => {
747+ mockFetchSnapshot . mockResolvedValue ( snapshot )
748+ mockFetchThreads . mockResolvedValue ( {
749+ actionable : [ ] ,
750+ skipped : [ ] ,
751+ totalUnresolved : 0 ,
752+ latestReview : null ,
753+ } )
754+ mockFetchChecks . mockResolvedValue ( failingChecks )
755+ mockReplyAndResolve . mockResolvedValue ( {
756+ repliesPosted : 0 ,
757+ threadsResolved : 0 ,
758+ replyFailures : [ ] ,
759+ resolveFailures : [ ] ,
760+ headMoved : false ,
761+ awaitingConfirmation : false ,
762+ } )
763+ const { runner } = makeRunner ( {
764+ prepareStdout : '__NO_CHANGES__=1\n' ,
765+ roundFile : JSON . stringify ( { threads : [ ] } ) ,
766+ } )
767+ mockWithPiSandbox . mockImplementation ( async ( callback ) => callback ( runner ) )
768+
769+ const result = await runBabysitPiWithOptions ( params ( ) , { onEvent : vi . fn ( ) } , { roundWaitMs : 0 } )
770+
771+ expect ( result ) . toMatchObject ( {
772+ stopReason : 'stuck_checks' ,
773+ rounds : 2 ,
774+ commitsPushed : 0 ,
775+ threadsClean : true ,
776+ checksGreen : false ,
777+ } )
778+ } )
779+
679780 it ( 'does not count a push round toward unchanged-pin stuck detection' , async ( ) => {
680781 mockFetchSnapshot
681782 . mockResolvedValueOnce ( snapshot )
@@ -698,7 +799,7 @@ describe('runBabysitPiWithOptions', () => {
698799 } )
699800 const { runner } = makeRunner ( {
700801 prepareStdout : [
701- `__CHANGED__ =src/a.ts\n__DIFF_BYTES__ =20\n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n` ,
802+ `__CUMULATIVE_CHANGED__ =src/a.ts\n__CUMULATIVE_DIFF_BYTES__ =20\n__CHANGED__=src/a.ts \n__NEW_SHA__=${ NEW_SHA } \n__NEEDS_PUSH__=1\n` ,
702803 '__NO_CHANGES__=1\n' ,
703804 '__NO_CHANGES__=1\n' ,
704805 ] ,
0 commit comments