@@ -801,7 +801,7 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
801801 expect ( result . record . reason ) . toBe ( "no compaction needed" ) ;
802802 } ) ;
803803
804- test ( "failing summarizer keeps prior context ; a later success writes one handoff" , async ( ) => {
804+ test ( "failing summarizer folds with a statistics-only stub ; a later success writes one handoff" , async ( ) => {
805805 const source : InferenceSource = {
806806 id : "test" ,
807807 provider : "openai" ,
@@ -825,10 +825,14 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
825825 } ) ;
826826 const turns = grow ( [ ] , 16 , "fail" ) ;
827827 const result1 = await compactor . apply ( turns , mockStrategyCtx ) ;
828- expect ( result1 . output ) . toBe ( turns ) ;
829- expect ( result1 . record . reason ) . toBe ( "summarize failed" ) ;
830- expect ( firstText ( defined ( result1 . output [ 0 ] ) ) ) . not . toContain (
831- COMPACTED_PREFIX ,
828+ expect ( result1 . output ) . not . toBe ( turns ) ;
829+ expect ( result1 . record . reason ) . toContain ( "statistics-only stub" ) ;
830+ expect ( result1 . record . decisions . summarizeFailed ) . toBe ( 1 ) ;
831+ expect ( result1 . record . decisions . summarizeFailureKind ) . toBe ( "failed" ) ;
832+ expect ( compactedTurns ( result1 . output ) ) . toHaveLength ( 1 ) ;
833+ const stubHandoff = defined ( defined ( result1 . blobs ) [ 0 ] ) ;
834+ expect ( new TextDecoder ( ) . decode ( stubHandoff . bytes ) ) . toContain (
835+ "Turns compacted:" ,
832836 ) ;
833837
834838 const result2 = await compactor . apply (
@@ -839,8 +843,11 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
839843 // CL-8744: the narrative lives in the fat handoff file, not the prompt.
840844 // The live output carries only the thin spine plus its pointer.
841845 expect ( allText ( result2 . output ) ) . not . toContain ( "UNIQUE_SUCCESS_SUMMARY" ) ;
842- const handoffBlob = defined ( defined ( result2 . blobs ) [ 0 ] ) ;
843- expect ( handoffBlob . contentType ) . toBe ( "text/markdown" ) ;
846+ const handoffBlob = defined (
847+ defined ( result2 . blobs ) . find (
848+ ( blob ) => blob . contentType === "text/markdown" ,
849+ ) ,
850+ ) ;
844851 expect ( new TextDecoder ( ) . decode ( handoffBlob . bytes ) ) . toContain (
845852 "UNIQUE_SUCCESS_SUMMARY" ,
846853 ) ;
@@ -849,6 +856,51 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
849856 ) ;
850857 expect ( hasConsecutiveSameRole ( result2 . output ) ) . toBe ( false ) ;
851858 } ) ;
859+
860+ test ( "empty summarizer output folds with a statistics-only stub" , async ( ) => {
861+ const source : InferenceSource = {
862+ id : "test" ,
863+ provider : "openai" ,
864+ model : "test-model" ,
865+ baseURL : "http://localhost:1" ,
866+ credentialId : "test" ,
867+ } ;
868+ const notices : string [ ] = [ ] ;
869+ const summarize = createModelSummarizer ( {
870+ getSource : ( ) => source ,
871+ complete : async ( ) => "" ,
872+ onFailure : ( text ) => notices . push ( text ) ,
873+ } ) ;
874+ const result = await smallCompactor ( {
875+ keepRecentTurns : 2 ,
876+ summaryMaxChars : 500 ,
877+ summarize,
878+ } ) . apply ( grow ( [ ] , 16 , "empty" ) , mockStrategyCtx ) ;
879+ expect ( result . record . decisions . summarizeFailed ) . toBe ( 1 ) ;
880+ expect ( result . record . decisions . summarizeFailureKind ) . toBe ( "empty" ) ;
881+ expect ( result . record . reason ) . toContain ( "statistics-only stub: empty" ) ;
882+ expect ( compactedTurns ( result . output ) ) . toHaveLength ( 1 ) ;
883+ expect ( notices ) . toHaveLength ( 1 ) ;
884+ expect ( notices [ 0 ] ) . toContain ( "statistics-only stub" ) ;
885+ expect ( notices [ 0 ] ) . toContain ( "empty" ) ;
886+ } ) ;
887+
888+ test ( "an aborted summarizer keeps prior context instead of stubbing" , async ( ) => {
889+ const err = new Error ( "aborted by lifecycle" ) ;
890+ err . name = "AbortError" ;
891+ const turns = grow ( [ ] , 16 , "abort" ) ;
892+ const result = await smallCompactor ( {
893+ keepRecentTurns : 2 ,
894+ summaryMaxChars : 500 ,
895+ summarize : async ( ) => {
896+ throw err ;
897+ } ,
898+ } ) . apply ( turns , mockStrategyCtx ) ;
899+ expect ( result . output ) . toBe ( turns ) ;
900+ expect ( result . record . reason ) . toBe ( "summarize failed" ) ;
901+ expect ( result . record . decisions . summarizeFailed ) . toBe ( 1 ) ;
902+ expect ( result . record . decisions . summarizedTurnCount ) . toBeUndefined ( ) ;
903+ } ) ;
852904} ) ;
853905
854906describe ( "buildTurnSummary via createPruningCompactor" , ( ) => {
0 commit comments