@@ -529,6 +529,147 @@ describe('callAutofixPr · Phase 2 completionChecker integration', () => {
529529 } )
530530} )
531531
532+ // Phase 2 (cont.): exercise the registered completionChecker arrow body
533+ // directly. The earlier suite verifies it was registered but never invokes
534+ // the arrow itself, leaving the throttle / metadata-guard / gh-CLI dispatch
535+ // branches uncovered.
536+ describe ( 'callAutofixPr · Phase 2 completionChecker arrow body' , ( ) => {
537+ // Pull the most recent registered checker — beforeAll registers once at
538+ // module load; nothing else re-registers across this file's tests.
539+ function getChecker ( ) : (
540+ metadata ?: unknown ,
541+ ) => Promise < string | null > {
542+ const calls = registerCompletionCheckerMock . mock . calls . filter (
543+ c => c [ 0 ] === 'autofix-pr' ,
544+ )
545+ const fn = calls [ calls . length - 1 ] ?. [ 1 ]
546+ if ( typeof fn !== 'function' ) {
547+ throw new Error ( 'completionChecker not registered' )
548+ }
549+ return fn
550+ }
551+
552+ test ( 'returns null when metadata is undefined (early guard)' , async ( ) => {
553+ const checker = getChecker ( )
554+ expect ( await checker ( undefined ) ) . toBeNull ( )
555+ } )
556+
557+ test ( 'returns null when checkPrAutofixOutcome reports not completed' , async ( ) => {
558+ const { checkPrAutofixOutcome } = await import ( '../prFetch.js' )
559+ ; ( checkPrAutofixOutcome as ReturnType < typeof mock > ) . mockImplementationOnce (
560+ ( ) => Promise . resolve ( { completed : false } ) ,
561+ )
562+ const checker = getChecker ( )
563+ // Distinct PR number to dodge the in-process throttle map carried over
564+ // from earlier tests.
565+ const result = await checker ( {
566+ owner : 'acme' ,
567+ repo : 'myrepo' ,
568+ prNumber : 1001 ,
569+ } )
570+ expect ( result ) . toBeNull ( )
571+ } )
572+
573+ test ( 'returns the summary string when checkPrAutofixOutcome reports completed' , async ( ) => {
574+ const { checkPrAutofixOutcome } = await import ( '../prFetch.js' )
575+ ; ( checkPrAutofixOutcome as ReturnType < typeof mock > ) . mockImplementationOnce (
576+ ( ) =>
577+ Promise . resolve ( {
578+ completed : true ,
579+ summary : 'acme/myrepo#1002 merged. Autofix monitoring complete.' ,
580+ } ) ,
581+ )
582+ const checker = getChecker ( )
583+ const result = await checker ( {
584+ owner : 'acme' ,
585+ repo : 'myrepo' ,
586+ prNumber : 1002 ,
587+ } )
588+ expect ( result ) . toBe (
589+ 'acme/myrepo#1002 merged. Autofix monitoring complete.' ,
590+ )
591+ } )
592+
593+ test ( 'passes initialHeadSha through to checkPrAutofixOutcome' , async ( ) => {
594+ const { checkPrAutofixOutcome } = await import ( '../prFetch.js' )
595+ const checkMock = checkPrAutofixOutcome as ReturnType < typeof mock >
596+ checkMock . mockClear ( )
597+ checkMock . mockImplementationOnce ( ( ) => Promise . resolve ( { completed : false } ) )
598+ const checker = getChecker ( )
599+ await checker ( {
600+ owner : 'acme' ,
601+ repo : 'myrepo' ,
602+ prNumber : 1003 ,
603+ initialHeadSha : 'sha-baseline-xyz' ,
604+ } )
605+ expect ( checkMock ) . toHaveBeenCalledWith ( {
606+ owner : 'acme' ,
607+ repo : 'myrepo' ,
608+ prNumber : 1003 ,
609+ initialHeadSha : 'sha-baseline-xyz' ,
610+ } )
611+ } )
612+
613+ test ( 'throttles back-to-back calls for the same PR within CHECK_INTERVAL_MS' , async ( ) => {
614+ const { checkPrAutofixOutcome } = await import ( '../prFetch.js' )
615+ const checkMock = checkPrAutofixOutcome as ReturnType < typeof mock >
616+ checkMock . mockClear ( )
617+ checkMock . mockImplementation ( ( ) => Promise . resolve ( { completed : false } ) )
618+ const checker = getChecker ( )
619+ const meta = { owner : 'acme' , repo : 'myrepo' , prNumber : 1004 }
620+ await checker ( meta )
621+ // Second call within the 5s throttle window must short-circuit to null
622+ // without invoking the gh CLI layer again.
623+ const callCountAfterFirst = checkMock . mock . calls . length
624+ const result = await checker ( meta )
625+ expect ( result ) . toBeNull ( )
626+ expect ( checkMock . mock . calls . length ) . toBe ( callCountAfterFirst )
627+ } )
628+
629+ test ( 'completionHook with metadata clears the throttle entry (re-launch can re-check immediately)' , async ( ) => {
630+ const { checkPrAutofixOutcome } = await import ( '../prFetch.js' )
631+ const checkMock = checkPrAutofixOutcome as ReturnType < typeof mock >
632+ checkMock . mockClear ( )
633+ checkMock . mockImplementation ( ( ) => Promise . resolve ( { completed : false } ) )
634+ const checker = getChecker ( )
635+ const meta = { owner : 'acme' , repo : 'myrepo' , prNumber : 1005 }
636+ await checker ( meta ) // populate throttle map
637+
638+ // Invoke the registered completion hook with the same metadata so the
639+ // throttle entry is wiped, then verify the next checker call dispatches
640+ // gh CLI again instead of short-circuiting.
641+ const hookCalls = registerCompletionHookMock . mock . calls . filter (
642+ c => c [ 0 ] === 'autofix-pr' ,
643+ )
644+ const hook = hookCalls [ hookCalls . length - 1 ] ?. [ 1 ] as (
645+ id : string ,
646+ metadata ?: unknown ,
647+ ) => void
648+ hook ( 'any-task-id' , meta )
649+
650+ const callCountBefore = checkMock . mock . calls . length
651+ await checker ( meta )
652+ expect ( checkMock . mock . calls . length ) . toBe ( callCountBefore + 1 )
653+ } )
654+
655+ test ( 'completionHook without metadata still clears the active monitor lock' , async ( ) => {
656+ // Lock is set via callAutofixPr; hook then invoked with undefined metadata
657+ // to exercise the `if (meta)` short-circuit branch (the lock-clear half
658+ // still has to run regardless of metadata presence).
659+ await callAutofixPr ( onDone , makeContext ( ) , '42' )
660+ expect ( getActiveMonitor ( ) ) . not . toBeNull ( )
661+ const hookCalls = registerCompletionHookMock . mock . calls . filter (
662+ c => c [ 0 ] === 'autofix-pr' ,
663+ )
664+ const hook = hookCalls [ hookCalls . length - 1 ] ?. [ 1 ] as (
665+ id : string ,
666+ metadata ?: unknown ,
667+ ) => void
668+ hook ( 'framework-task-id' , undefined )
669+ expect ( getActiveMonitor ( ) ) . toBeNull ( )
670+ } )
671+ } )
672+
532673// Phase 3: content extractor wiring + initialMessage tag instruction
533674describe ( 'callAutofixPr · Phase 3 content extractor integration' , ( ) => {
534675 test ( 'registerContentExtractor is called at module load with autofix-pr type' , ( ) => {
0 commit comments