@@ -34,6 +34,10 @@ vi.mock("../compaction.js", async () => {
3434
3535const mockSummarizeInStages = vi . mocked ( compactionModule . summarizeInStages ) ;
3636
37+ function summaryResult ( text : string ) {
38+ return { kind : "summary" as const , text } ;
39+ }
40+
3741const {
3842 collectToolFailures,
3943 formatToolFailuresSection,
@@ -1442,7 +1446,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
14421446
14431447 it ( "uses structured instructions when summarizing dropped history chunks" , async ( ) => {
14441448 mockSummarizeInStages . mockReset ( ) ;
1445- mockSummarizeInStages . mockResolvedValue ( "mock summary" ) ;
1449+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "mock summary" ) ) ;
14461450
14471451 const sessionManager = stubSessionManager ( ) ;
14481452 const model = createAnthropicModelFixture ( ) ;
@@ -1499,7 +1503,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
14991503
15001504 it ( "caps summarization reserve tokens to the model output limit" , async ( ) => {
15011505 mockSummarizeInStages . mockReset ( ) ;
1502- mockSummarizeInStages . mockResolvedValue ( "mock summary" ) ;
1506+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "mock summary" ) ) ;
15031507
15041508 const sessionManager = stubSessionManager ( ) ;
15051509 const model = createAnthropicModelFixture ( {
@@ -1538,7 +1542,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
15381542
15391543 it ( "adds Copilot IDE headers to built-in compaction summarization" , async ( ) => {
15401544 mockSummarizeInStages . mockReset ( ) ;
1541- mockSummarizeInStages . mockResolvedValue ( "mock summary" ) ;
1545+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "mock summary" ) ) ;
15421546
15431547 const sessionManager = stubSessionManager ( ) ;
15441548 const model = createAnthropicModelFixture ( {
@@ -1584,7 +1588,7 @@ describe("compaction-safeguard recent-turn preservation", () => {
15841588
15851589 it ( "does not retry summaries unless quality guard is explicitly enabled" , async ( ) => {
15861590 mockSummarizeInStages . mockReset ( ) ;
1587- mockSummarizeInStages . mockResolvedValue ( "summary missing headings" ) ;
1591+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "summary missing headings" ) ) ;
15881592
15891593 const sessionManager = stubSessionManager ( ) ;
15901594 const model = createAnthropicModelFixture ( ) ;
@@ -1633,20 +1637,22 @@ describe("compaction-safeguard recent-turn preservation", () => {
16331637 it ( "retries when generated summary misses headings even if preserved turns contain them" , async ( ) => {
16341638 mockSummarizeInStages . mockReset ( ) ;
16351639 mockSummarizeInStages
1636- . mockResolvedValueOnce ( "latest ask status" )
1640+ . mockResolvedValueOnce ( summaryResult ( "latest ask status" ) )
16371641 . mockResolvedValueOnce (
1638- [
1639- "## Decisions" ,
1640- "Keep current flow." ,
1641- "## Open TODOs" ,
1642- "None." ,
1643- "## Constraints/Rules" ,
1644- "Follow rules." ,
1645- "## Pending user asks" ,
1646- "latest ask status" ,
1647- "## Exact identifiers" ,
1648- "None." ,
1649- ] . join ( "\n" ) ,
1642+ summaryResult (
1643+ [
1644+ "## Decisions" ,
1645+ "Keep current flow." ,
1646+ "## Open TODOs" ,
1647+ "None." ,
1648+ "## Constraints/Rules" ,
1649+ "Follow rules." ,
1650+ "## Pending user asks" ,
1651+ "latest ask status" ,
1652+ "## Exact identifiers" ,
1653+ "None." ,
1654+ ] . join ( "\n" ) ,
1655+ ) ,
16501656 ) ;
16511657
16521658 const sessionManager = stubSessionManager ( ) ;
@@ -1733,32 +1739,36 @@ describe("compaction-safeguard recent-turn preservation", () => {
17331739 mockSummarizeInStages . mockReset ( ) ;
17341740 mockSummarizeInStages
17351741 . mockResolvedValueOnce (
1736- [
1737- "## Decisions" ,
1738- "Keep current flow." ,
1739- "## Open TODOs" ,
1740- "None." ,
1741- "## Constraints/Rules" ,
1742- "Follow rules." ,
1743- "## Pending user asks" ,
1744- "latest ask status" ,
1745- "## Exact identifiers" ,
1746- "None." ,
1747- ] . join ( "\n" ) ,
1742+ summaryResult (
1743+ [
1744+ "## Decisions" ,
1745+ "Keep current flow." ,
1746+ "## Open TODOs" ,
1747+ "None." ,
1748+ "## Constraints/Rules" ,
1749+ "Follow rules." ,
1750+ "## Pending user asks" ,
1751+ "latest ask status" ,
1752+ "## Exact identifiers" ,
1753+ "None." ,
1754+ ] . join ( "\n" ) ,
1755+ ) ,
17481756 )
17491757 . mockResolvedValueOnce (
1750- [
1751- "## Decisions" ,
1752- "Keep current flow." ,
1753- "## Open TODOs" ,
1754- "None." ,
1755- "## Constraints/Rules" ,
1756- "Follow rules." ,
1757- "## Pending user asks" ,
1758- "older context" ,
1759- "## Exact identifiers" ,
1760- "None." ,
1761- ] . join ( "\n" ) ,
1758+ summaryResult (
1759+ [
1760+ "## Decisions" ,
1761+ "Keep current flow." ,
1762+ "## Open TODOs" ,
1763+ "None." ,
1764+ "## Constraints/Rules" ,
1765+ "Follow rules." ,
1766+ "## Pending user asks" ,
1767+ "older context" ,
1768+ "## Exact identifiers" ,
1769+ "None." ,
1770+ ] . join ( "\n" ) ,
1771+ ) ,
17621772 ) ;
17631773
17641774 const sessionManager = stubSessionManager ( ) ;
@@ -1822,8 +1832,8 @@ describe("compaction-safeguard recent-turn preservation", () => {
18221832 const oversizedHistorySummary = "history detail " . repeat ( MAX_COMPACTION_SUMMARY_CHARS ) ;
18231833 const splitTurnPrefixSummary = "split-turn prefix context that must survive capping" ;
18241834 mockSummarizeInStages
1825- . mockResolvedValueOnce ( oversizedHistorySummary )
1826- . mockResolvedValueOnce ( splitTurnPrefixSummary )
1835+ . mockResolvedValueOnce ( summaryResult ( oversizedHistorySummary ) )
1836+ . mockResolvedValueOnce ( summaryResult ( splitTurnPrefixSummary ) )
18271837 . mockRejectedValueOnce ( new Error ( "retry transient failure" ) ) ;
18281838
18291839 const sessionManager = stubSessionManager ( ) ;
@@ -1949,18 +1959,20 @@ describe("compaction-safeguard recent-turn preservation", () => {
19491959 it ( "re-distills prior summaries on the LLM path instead of preserving them verbatim" , async ( ) => {
19501960 mockSummarizeInStages . mockReset ( ) ;
19511961 mockSummarizeInStages . mockResolvedValue (
1952- [
1953- "## Decisions" ,
1954- "Condensed prior context with latest status." ,
1955- "## Open TODOs" ,
1956- "None." ,
1957- "## Constraints/Rules" ,
1958- "Preserve identifiers." ,
1959- "## Pending user asks" ,
1960- "latest ask status" ,
1961- "## Exact identifiers" ,
1962- "None." ,
1963- ] . join ( "\n" ) ,
1962+ summaryResult (
1963+ [
1964+ "## Decisions" ,
1965+ "Condensed prior context with latest status." ,
1966+ "## Open TODOs" ,
1967+ "None." ,
1968+ "## Constraints/Rules" ,
1969+ "Preserve identifiers." ,
1970+ "## Pending user asks" ,
1971+ "latest ask status" ,
1972+ "## Exact identifiers" ,
1973+ "None." ,
1974+ ] . join ( "\n" ) ,
1975+ ) ,
19641976 ) ;
19651977
19661978 const sessionManager = stubSessionManager ( ) ;
@@ -2011,14 +2023,62 @@ describe("compaction-safeguard recent-turn preservation", () => {
20112023 expect ( JSON . stringify ( messages [ 0 ] ) ) . toContain ( "Old duplicated section" ) ;
20122024 } ) ;
20132025
2026+ it ( "preserves the prior summary when staged summarization returns a generic fallback" , async ( ) => {
2027+ mockSummarizeInStages . mockReset ( ) ;
2028+ mockSummarizeInStages . mockResolvedValue ( {
2029+ kind : "generic-fallback" ,
2030+ text : "Context contained 4 messages. Summary unavailable due to size limits." ,
2031+ } ) ;
2032+
2033+ const sessionManager = stubSessionManager ( ) ;
2034+ const model = createAnthropicModelFixture ( ) ;
2035+ setCompactionSafeguardRuntime ( sessionManager , {
2036+ model,
2037+ recentTurnsPreserve : 0 ,
2038+ } ) ;
2039+
2040+ const compactionHandler = createCompactionHandler ( ) ;
2041+ const mockContext = createCompactionContext ( {
2042+ sessionManager,
2043+ getApiKeyMock : vi . fn ( ) . mockResolvedValue ( "test-key" ) ,
2044+ } ) ;
2045+ const event = {
2046+ preparation : {
2047+ messagesToSummarize : [ { role : "user" , content : "latest ask status" , timestamp : 1 } ] ,
2048+ turnPrefixMessages : [ ] ,
2049+ firstKeptEntryId : "entry-1" ,
2050+ tokensBefore : 1_500 ,
2051+ fileOps : {
2052+ read : [ ] ,
2053+ edited : [ ] ,
2054+ written : [ ] ,
2055+ } ,
2056+ settings : { reserveTokens : 4_000 } ,
2057+ previousSummary : "## Goal\nKnown context that must survive the outage." ,
2058+ isSplitTurn : false ,
2059+ } ,
2060+ customInstructions : "" ,
2061+ signal : new AbortController ( ) . signal ,
2062+ } ;
2063+
2064+ const result = ( await compactionHandler ( event , mockContext ) ) as {
2065+ cancel ?: boolean ;
2066+ compaction ?: { summary ?: string } ;
2067+ } ;
2068+
2069+ expect ( result . cancel ) . not . toBe ( true ) ;
2070+ expect ( result . compaction ?. summary ) . toContain ( "Known context that must survive the outage." ) ;
2071+ expect ( result . compaction ?. summary ) . toContain ( "Summary unavailable due to size limits." ) ;
2072+ } ) ;
2073+
20142074 it ( "falls back to LLM when provider throws a provider-side AbortError with signal not aborted" , async ( ) => {
20152075 // Reproduce the undici AbortError("This operation was aborted") shape that
20162076 // arrives when the compaction provider's HTTP connection drops mid-stream while
20172077 // the caller has NOT yet fired their abort signal. Before the fix,
20182078 // isAbortError() matched this shape so tryProviderSummarize rethrew and the
20192079 // extension runner swallowed the error — the LLM fallback path was skipped.
20202080 mockSummarizeInStages . mockReset ( ) ;
2021- mockSummarizeInStages . mockResolvedValue ( "llm fallback summary" ) ;
2081+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "llm fallback summary" ) ) ;
20222082
20232083 const providerAbortErr = Object . assign ( new Error ( "This operation was aborted" ) , {
20242084 name : "AbortError" ,
@@ -2225,7 +2285,7 @@ describe("compaction-safeguard extension model fallback", () => {
22252285 // neither apiKey nor headers. `ok: true` must be trusted so compaction runs
22262286 // instead of wedging every message with a false "no credentials" cancel.
22272287 mockSummarizeInStages . mockReset ( ) ;
2228- mockSummarizeInStages . mockResolvedValue ( "mock summary" ) ;
2288+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "mock summary" ) ) ;
22292289
22302290 const sessionManager = stubSessionManager ( ) ;
22312291 const model = createAnthropicModelFixture ( { provider : "amazon-bedrock" } ) ;
@@ -2450,7 +2510,7 @@ describe("compaction-safeguard double-compaction guard", () => {
24502510
24512511 it ( "falls back to visible custom session branch entries before writing an empty boundary" , async ( ) => {
24522512 mockSummarizeInStages . mockReset ( ) ;
2453- mockSummarizeInStages . mockResolvedValue ( "branch summary" ) ;
2513+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "branch summary" ) ) ;
24542514
24552515 const now = Date . now ( ) ;
24562516 const sessionManager = {
@@ -2539,7 +2599,7 @@ describe("compaction-safeguard double-compaction guard", () => {
25392599
25402600 it ( "does not replay inter-session sessions_send branch turns as fallback history" , async ( ) => {
25412601 mockSummarizeInStages . mockReset ( ) ;
2542- mockSummarizeInStages . mockResolvedValue ( "branch summary" ) ;
2602+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "branch summary" ) ) ;
25432603
25442604 const now = Date . now ( ) ;
25452605 const sessionManager = {
@@ -2607,7 +2667,7 @@ describe("compaction-safeguard double-compaction guard", () => {
26072667 { toolName : "functions.sessions_send" , expectedRoles : [ "user" , "assistant" ] } ,
26082668 ] ) ( "preserves unfinished inter-session work after a $toolName result" , async ( scenario ) => {
26092669 mockSummarizeInStages . mockReset ( ) ;
2610- mockSummarizeInStages . mockResolvedValue ( "unfinished branch summary" ) ;
2670+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "unfinished branch summary" ) ) ;
26112671
26122672 const now = Date . now ( ) ;
26132673 const sessionManager = {
@@ -2694,7 +2754,7 @@ describe("compaction-safeguard double-compaction guard", () => {
26942754
26952755 it ( "keeps source-session sends as inert status history" , async ( ) => {
26962756 mockSummarizeInStages . mockReset ( ) ;
2697- mockSummarizeInStages . mockResolvedValue ( "completed send summary" ) ;
2757+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "completed send summary" ) ) ;
26982758
26992759 const now = Date . now ( ) ;
27002760 const sessionManager = {
@@ -2810,7 +2870,7 @@ describe("compaction-safeguard double-compaction guard", () => {
28102870
28112871 it ( "preserves completed historical inter-session turns outside the active tail" , async ( ) => {
28122872 mockSummarizeInStages . mockReset ( ) ;
2813- mockSummarizeInStages . mockResolvedValue ( "historical branch summary" ) ;
2873+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "historical branch summary" ) ) ;
28142874
28152875 const now = Date . now ( ) ;
28162876 const sessionManager = {
@@ -2902,7 +2962,7 @@ describe("compaction-safeguard double-compaction guard", () => {
29022962
29032963 it ( "recovers user and assistant branch turns when compaction preparation has only tool output" , async ( ) => {
29042964 mockSummarizeInStages . mockReset ( ) ;
2905- mockSummarizeInStages . mockResolvedValue ( "branch summary with visible turns" ) ;
2965+ mockSummarizeInStages . mockResolvedValue ( summaryResult ( "branch summary with visible turns" ) ) ;
29062966
29072967 const now = Date . now ( ) ;
29082968 const sessionManager = {
0 commit comments