@@ -14,6 +14,7 @@ import {
1414 repairSummary ,
1515 verifyCompactionSummary ,
1616 verifyOrRepair ,
17+ verifyAbortNoticeLabel ,
1718 VERIFY_REPAIR_HEADING ,
1819} from "./compaction-verify.js" ;
1920import type {
@@ -421,7 +422,7 @@ describe("verifyCompactionSummary", () => {
421422 }
422423 } ) ;
423424
424- test ( "uppercase HTTP URL scores hostname case-insensitively " , ( ) => {
425+ test ( "a URL path is required; hostname alone does not cover it " , ( ) => {
425426 const facts = extractContinuationFacts ( [
426427 textTurn ( "user" , "Fix auth now" ) ,
427428 {
@@ -442,6 +443,12 @@ describe("verifyCompactionSummary", () => {
442443 "Fix auth now. Called api.com." ,
443444 facts ,
444445 ) . misses . some ( ( m ) => m . kind === "exactName" ) ,
446+ ) . toBe ( true ) ;
447+ expect (
448+ verifyCompactionSummary (
449+ "Fix auth now. Called API.COM/v1." ,
450+ facts ,
451+ ) . misses . some ( ( m ) => m . kind === "exactName" ) ,
445452 ) . toBe ( false ) ;
446453 expect (
447454 verifyCompactionSummary (
@@ -453,6 +460,12 @@ describe("verifyCompactionSummary", () => {
453460} ) ;
454461
455462describe ( "verifyOrRepair" , ( ) => {
463+ test ( "names contradiction and repair overflow instead of a bare verify" , ( ) => {
464+ expect ( verifyAbortNoticeLabel ( "contradiction" ) ) . toBe ( "contradiction" ) ;
465+ expect ( verifyAbortNoticeLabel ( "repair-overflow" ) ) . toBe ( "repair overflow" ) ;
466+ expect ( verifyAbortNoticeLabel ( undefined ) ) . toBe ( "kept-prior" ) ;
467+ } ) ;
468+
456469 test ( "repairs a lossy handoff instead of shipping it" , ( ) => {
457470 const facts = extractContinuationFacts ( droppedTurns ( ) ) ;
458471 const outcome = verifyOrRepair (
@@ -477,6 +490,7 @@ describe("verifyOrRepair", () => {
477490 ) ;
478491 expect ( outcome . aborted ) . toBe ( true ) ;
479492 expect ( outcome . repaired ) . toBe ( false ) ;
493+ expect ( outcome . abortReason ) . toBe ( "contradiction" ) ;
480494 } ) ;
481495
482496 test ( "truncating repair that still misses exactName aborts" , ( ) => {
@@ -495,9 +509,10 @@ describe("verifyOrRepair", () => {
495509 timestamp : 2 ,
496510 } ,
497511 ] ) ;
498- const outcome = verifyOrRepair ( "Fix auth now. Work continues." , facts , 90 ) ;
512+ const outcome = verifyOrRepair ( "Fix auth now. Work continues." , facts , 80 ) ;
499513 expect ( outcome . aborted ) . toBe ( true ) ;
500514 expect ( outcome . repaired ) . toBe ( false ) ;
515+ expect ( outcome . abortReason ) . toBe ( "repair-overflow" ) ;
501516 expect ( outcome . misses . some ( ( m ) => m . kind === "exactName" ) ) . toBe ( true ) ;
502517 } ) ;
503518
@@ -527,6 +542,26 @@ describe("verifyOrRepair", () => {
527542 expect ( outcome . summary ) . toContain ( "exact-file.ts" ) ;
528543 } ) ;
529544
545+ test ( "shrinks model prose so a carry-forward that fits the cap still verifies" , ( ) => {
546+ const facts = extractContinuationFacts ( droppedTurns ( ) ) ;
547+ const prose = "Padded model prose. " . repeat ( 40 ) ;
548+ const misses = verifyCompactionSummary ( prose , facts ) . misses ;
549+ const full = repairSummary ( prose , facts , misses ) ;
550+ const appendixAt = full . indexOf ( VERIFY_REPAIR_HEADING ) ;
551+ expect ( appendixAt ) . toBeGreaterThan ( 200 ) ;
552+ const appendix = full . slice ( appendixAt ) . replace ( / ^ \n + / , "" ) ;
553+ const cap = appendix . length + 8 ;
554+ expect ( full . length ) . toBeGreaterThan ( cap ) ;
555+ const outcome = verifyOrRepair ( prose , facts , cap ) ;
556+ expect ( outcome . aborted ) . toBe ( false ) ;
557+ expect ( outcome . repaired ) . toBe ( true ) ;
558+ expect ( outcome . summary . length ) . toBeLessThanOrEqual ( cap ) ;
559+ expect ( outcome . summary ) . toContain ( VERIFY_REPAIR_HEADING ) ;
560+ expect ( verifyCompactionSummary ( outcome . summary , facts ) . supported ) . toBe (
561+ true ,
562+ ) ;
563+ } ) ;
564+
530565 test ( "a constraint-only residual after a sliced repair aborts" , ( ) => {
531566 const facts = extractContinuationFacts ( [
532567 textTurn ( "user" , "Fix auth now" ) ,
@@ -662,10 +697,200 @@ describe("pruning compactor verify pass", () => {
662697 expect ( allText ( result . output ) ) . toContain ( "auth.ts" ) ;
663698 } else if ( kind === "aborted" ) {
664699 expect ( result . output ) . toBe ( turns ) ;
700+ expect ( result . record . decisions . verifyAbortReason ) . toBe ( "contradiction" ) ;
665701 } else {
666702 expect ( allText ( result . output ) ) . not . toContain ( VERIFY_REPAIR_HEADING ) ;
667703 }
668704 } ) ;
705+
706+ test ( "an over-cap repair commits the fold and the handoff keeps critical facts" , async ( ) => {
707+ const folded : ConversationTurn [ ] = [
708+ textTurn ( "user" , "Fix auth now" ) ,
709+ {
710+ role : "assistant" ,
711+ content : [
712+ {
713+ type : "tool_call" ,
714+ id : "c1" ,
715+ name : "read_file" ,
716+ arguments : { path : "src/very-long-unique-path/exact-file.ts" } ,
717+ } ,
718+ ] ,
719+ timestamp : 2 ,
720+ } ,
721+ {
722+ role : "user" ,
723+ content : [
724+ {
725+ type : "tool_result" ,
726+ callId : "c1" ,
727+ isError : true ,
728+ content : [
729+ {
730+ type : "text" ,
731+ text : "ZEBRA-BLOCKER-UNIQUE token refresh exploded" ,
732+ } ,
733+ ] ,
734+ } ,
735+ ] ,
736+ timestamp : 3 ,
737+ } ,
738+ textTurn (
739+ "assistant" ,
740+ "Next step is to patch the zebra handler QUOKKA-NEXT" ,
741+ ) ,
742+ textTurn ( "user" , "Do not commit generated artifacts ever." ) ,
743+ ] ;
744+ const turns : ConversationTurn [ ] = [
745+ ...folded ,
746+ textTurn ( "user" , "recent ask" ) ,
747+ textTurn ( "assistant" , "recent reply" ) ,
748+ ] ;
749+ const compactor = createPruningCompactor ( {
750+ summaryMaxChars : 80 ,
751+ compactionShape : { tailBudgetTokens : 10 } ,
752+ summarize : async ( ) => "Work continues." ,
753+ } ) ;
754+ const result = await compactor . apply ( turns , mockStrategyCtx ) ;
755+ expect ( result . record . decisions . verifyAborted ) . toBeUndefined ( ) ;
756+ expect ( result . record . reason . startsWith ( "compacted" ) ) . toBe ( true ) ;
757+ expect ( result . output . length ) . toBeLessThan ( turns . length ) ;
758+ const file = handoffFileText ( result ) ;
759+ expect ( file ) . toContain ( "exact-file.ts" ) ;
760+ expect ( file ) . toContain ( "ZEBRA-BLOCKER-UNIQUE" ) ;
761+ expect ( file ) . toContain ( "QUOKKA-NEXT" ) ;
762+ expect ( file ) . toContain ( "generated artifacts" ) ;
763+ expect ( file . toLowerCase ( ) ) . toContain ( "fix auth" ) ;
764+ const spine = allText ( result . output ) ;
765+ expect ( spine ) . toContain ( COMPACTED_PREFIX ) ;
766+ expect ( spine ) . not . toMatch ( / \b n o \s + e r r o r s ? \b / i) ;
767+ } ) ;
768+
769+ test ( "shrink-to-fit does not ship a spine that denies recorded failures" , async ( ) => {
770+ const folded : ConversationTurn [ ] = [
771+ textTurn ( "user" , "Fix auth now" ) ,
772+ {
773+ role : "assistant" ,
774+ content : [
775+ {
776+ type : "tool_call" ,
777+ id : "c1" ,
778+ name : "read_file" ,
779+ arguments : { path : "src/auth.ts" } ,
780+ } ,
781+ ] ,
782+ timestamp : 2 ,
783+ } ,
784+ {
785+ role : "user" ,
786+ content : [
787+ {
788+ type : "tool_result" ,
789+ callId : "c1" ,
790+ isError : true ,
791+ content : [ { type : "text" , text : "token refresh assertion failed" } ] ,
792+ } ,
793+ ] ,
794+ timestamp : 3 ,
795+ } ,
796+ textTurn ( "assistant" , "Fix the token refresh assertion next." ) ,
797+ textTurn ( "user" , "No errors remain" ) ,
798+ textTurn ( "user" , "Keep going on the handler" ) ,
799+ ] ;
800+ const turns : ConversationTurn [ ] = [
801+ ...folded ,
802+ textTurn ( "user" , "recent ask" ) ,
803+ textTurn ( "assistant" , "recent reply" ) ,
804+ ] ;
805+ const prose = "Padded model prose. " . repeat ( 40 ) ;
806+ const facts = extractContinuationFacts ( folded ) ;
807+ const misses = verifyCompactionSummary ( prose , facts ) . misses ;
808+ const full = repairSummary ( prose , facts , misses ) ;
809+ const appendixAt = full . indexOf ( VERIFY_REPAIR_HEADING ) ;
810+ const appendix = full . slice ( appendixAt ) . replace ( / ^ \n + / , "" ) ;
811+ const cap = appendix . length + 8 ;
812+ expect ( full . length ) . toBeGreaterThan ( cap ) ;
813+ const shrunk = verifyOrRepair ( prose , facts , cap ) ;
814+ expect ( shrunk . aborted ) . toBe ( false ) ;
815+ expect ( shrunk . summary . length ) . toBeLessThanOrEqual ( cap ) ;
816+
817+ const apply = ( summaryMaxChars : number ) =>
818+ createPruningCompactor ( {
819+ summaryMaxChars,
820+ compactionShape : { tailBudgetTokens : 10 } ,
821+ summarize : async ( ) => prose ,
822+ } ) . apply ( turns , mockStrategyCtx ) ;
823+
824+ const fitted = await apply ( cap ) ;
825+ const fittedSpine = allText ( fitted . output ) ;
826+ const committed = fitted . output . length < turns . length ;
827+ if ( committed ) {
828+ expect ( fittedSpine ) . not . toMatch ( / \b n o \s + e r r o r s ? \s + r e m a i n \b / i) ;
829+ } else {
830+ expect ( fitted . output ) . toBe ( turns ) ;
831+ expect ( fitted . record . decisions . verifyAbortReason ) . toBe ( "contradiction" ) ;
832+ }
833+ expect ( committed && / \b n o \s + e r r o r s ? \s + r e m a i n \b / i. test ( fittedSpine ) ) . toBe (
834+ false ,
835+ ) ;
836+
837+ const tight = await apply ( 80 ) ;
838+ expect ( tight . output ) . toBe ( turns ) ;
839+ expect ( tight . record . decisions . verifyAbortReason ) . toBe ( "contradiction" ) ;
840+ } ) ;
841+
842+ test ( "an over-cap fold does not commit without the exact URL path" , async ( ) => {
843+ const url = "https://api.example/hooks/QUOKKA-CALLBACK" ;
844+ const folded : ConversationTurn [ ] = [
845+ textTurn ( "user" , "Register the callback host api.example" ) ,
846+ {
847+ role : "assistant" ,
848+ content : [
849+ {
850+ type : "tool_call" ,
851+ id : "c1" ,
852+ name : "web_fetch" ,
853+ arguments : { url } ,
854+ } ,
855+ ] ,
856+ timestamp : 2 ,
857+ } ,
858+ textTurn ( "assistant" , "Next step is to patch the handler QUOKKA-NEXT" ) ,
859+ textTurn ( "user" , "Do not commit generated artifacts ever." ) ,
860+ ] ;
861+ const turns : ConversationTurn [ ] = [
862+ ...folded ,
863+ textTurn ( "user" , "recent ask" ) ,
864+ textTurn ( "assistant" , "recent reply" ) ,
865+ ] ;
866+ const prose = "Work continues." ;
867+ const facts = extractContinuationFacts ( folded ) ;
868+ expect ( facts . exactNames ) . toContain ( url ) ;
869+ const misses = verifyCompactionSummary ( prose , facts ) . misses ;
870+ const full = repairSummary ( prose , facts , misses ) ;
871+ const exactAt = full . indexOf ( "Exact references:" ) ;
872+ expect ( exactAt ) . toBeGreaterThan ( 0 ) ;
873+ expect ( full . slice ( 0 , exactAt ) ) . toContain ( "api.example" ) ;
874+ expect ( full . slice ( 0 , exactAt ) ) . not . toContain ( "QUOKKA-CALLBACK" ) ;
875+
876+ const result = await createPruningCompactor ( {
877+ summaryMaxChars : exactAt ,
878+ compactionShape : { tailBudgetTokens : 10 } ,
879+ summarize : async ( ) => prose ,
880+ } ) . apply ( turns , mockStrategyCtx ) ;
881+
882+ const file = handoffFileText ( result ) ;
883+ const spine = allText ( result . output ) ;
884+ const keptPrior = result . output === turns ;
885+ const pathKept =
886+ file . includes ( "hooks/QUOKKA-CALLBACK" ) ||
887+ spine . includes ( "hooks/QUOKKA-CALLBACK" ) ;
888+ expect ( keptPrior || pathKept ) . toBe ( true ) ;
889+ if ( ! keptPrior ) {
890+ expect ( result . record . decisions . verifyAborted ) . toBeUndefined ( ) ;
891+ expect ( result . record . reason . startsWith ( "compacted" ) ) . toBe ( true ) ;
892+ }
893+ } ) ;
669894} ) ;
670895
671896describe ( "CL-9007 budgeted tail (shared auto+manual pipeline)" , ( ) => {
0 commit comments