@@ -3,7 +3,7 @@ import test from "node:test"
33import { createSessionState } from "../lib/state"
44import type { WithParts } from "../lib/state"
55import { assignMessageRefs } from "../lib/message-ids"
6- import { countMessageCharacters } from "../lib/token-utils"
6+ import { COMPACTED_TOOL_OUTPUT_PLACEHOLDER , countMessageCharacters } from "../lib/token-utils"
77import {
88 buildCompressibleRanges ,
99 filterRecommendedRanges ,
@@ -232,6 +232,49 @@ test("#359 deeply nested JSON object output: rec == exec counter", () => {
232232 )
233233} )
234234
235+ // ---------------------------------------------------------------------------
236+ // Fixture shape 5: compacted tool output (placeholder path)
237+ // ---------------------------------------------------------------------------
238+
239+ test ( "#359 compacted tool output: exec counts placeholder, rec matches" , ( ) => {
240+ const bigOutput = "x" . repeat ( 5000 )
241+ const toolMsg = makeMsg ( "m2" , "assistant" , "" , [
242+ {
243+ type : "tool" ,
244+ callID : "t1" ,
245+ tool : "read" ,
246+ state : {
247+ status : "completed" ,
248+ input : { filePath : "/tmp/big.log" } ,
249+ output : bigOutput ,
250+ time : { compacted : true } ,
251+ } ,
252+ } ,
253+ ] )
254+ const userMsg = makeMsg ( "m1" , "user" , "read the log" )
255+ const state = buildSession ( [ userMsg , toolMsg ] )
256+
257+ const { compressible } = buildCompressibleRanges ( [ userMsg , toolMsg ] , state )
258+ const execChars = countMessageCharacters ( toolMsg )
259+ const expected =
260+ JSON . stringify ( { filePath : "/tmp/big.log" } ) . length +
261+ COMPACTED_TOOL_OUTPUT_PLACEHOLDER . length
262+ assert . equal (
263+ execChars ,
264+ expected ,
265+ "exec counter = stringified input + compacted placeholder (not full output)" ,
266+ )
267+ assert . equal (
268+ compressible [ 0 ] . effectiveTokens ,
269+ Math . round ( execChars / 4 ) ,
270+ "rec-side uses same counter" ,
271+ )
272+ assert . ok (
273+ legacyMessageTokens ( toolMsg ) > compressible [ 0 ] . effectiveTokens ,
274+ "pre-fix estimator counted the full pre-compaction output" ,
275+ )
276+ } )
277+
235278// ---------------------------------------------------------------------------
236279// Incident shape: tool-heavy range below the exec min-size threshold but
237280// above the floor under the pre-fix inflated estimator.
@@ -241,6 +284,12 @@ test("#359 deeply nested JSON object output: rec == exec counter", () => {
241284
242285const INCIDENT_MIN_COMPRESS_RANGE = 3000
243286
287+ // Triple constraint on this fixture (asserted dynamically inside the test):
288+ // execChars < 3000 AND legacyEffective >= floor(750) AND post-fix
289+ // effectiveTokens < 750. Current margins: exec 2866 (-134), legacy 780 (+30),
290+ // post-fix 716 (-34). Legacy inflation is ~constant (~256 chars) for this
291+ // shape, so keep future text edits within exec ∈ [~2744, 3000); the dynamic
292+ // asserts fail loudly if an edit breaks any side.
244293function buildIncidentMessages ( ) : WithParts [ ] {
245294 const stack =
246295 "Error: fetch failed\n" +
@@ -285,8 +334,9 @@ test("#359 incident shape: sub-floor tool-heavy range is NOT recommended (was re
285334 assert . equal ( compressible . length , 1 , "single range covering the whole span" )
286335 const range = compressible [ 0 ]
287336
288- // Execution-side truth: the pipeline sums countMessageCharacters over the
289- // plan's surviving messages (last user message soft-filtered out).
337+ // Min-size-check counter over last-user-filtered survivors (the other soft
338+ // filters — protected tools / recent zone — are out of scope for this
339+ // shared-counter pin).
290340 const execChars = messages . slice ( 1 ) . reduce ( ( sum , m ) => sum + countMessageCharacters ( m ) , 0 )
291341 assert . ok (
292342 execChars < INCIDENT_MIN_COMPRESS_RANGE ,
0 commit comments