@@ -135,6 +135,67 @@ func TestReclaim_UnderBudgetNoChange(t *testing.T) {
135135 }
136136}
137137
138+ func TestReclaim_SmallOldOutputsKept (t * testing.T ) {
139+ // 较新的几条大 Read 把 keptTokens 撑过预算;更旧的都是小 Bash 输出。
140+ // 大的旧 Read 应被回收,小的旧 Bash 不该(占位≈原文,回收无意义)。
141+ big := strings .Repeat ("x" , 8000 )
142+ convo := []ChatMessage {{Role : "system" , Content : "s" }, {Role : "user" , Content : "t" }}
143+ for k := 0 ; k < 6 ; k ++ { // 更旧:小 Bash
144+ id := fmt .Sprintf ("old%d" , k )
145+ convo = append (convo , asstCall (id , "Bash" , `{"command":"echo"}` ))
146+ convo = append (convo , toolMsg (id , "Bash" , "小结果" ))
147+ }
148+ for k := 0 ; k < 5 ; k ++ { // 较新:大 Read
149+ id := fmt .Sprintf ("new%d" , k )
150+ convo = append (convo , asstCall (id , "Read" , fmt .Sprintf (`{"path":"f%d.go"}` , k )))
151+ convo = append (convo , toolMsg (id , "Read" , big ))
152+ }
153+ reclaimToolOutputs (convo , 2048 ) // keepBudget≈409;5 条大 Read 远超,更旧的进入回收判定
154+
155+ reclaimedRead := false
156+ for _ , m := range convo {
157+ if m .Role != "tool" || ! strings .HasPrefix (m .Content , reclaimMarkerPrefix ) {
158+ continue
159+ }
160+ if m .Name == "Bash" {
161+ t .Fatal ("小的旧 Bash 输出不应被回收(低于 reclaimMinMsgTokens)" )
162+ }
163+ if m .Name == "Read" {
164+ reclaimedRead = true
165+ }
166+ }
167+ if ! reclaimedRead {
168+ t .Fatal ("大的旧 Read 输出应被回收(sanity)" )
169+ }
170+ }
171+
172+ func TestReclaim_BelowTotalFloorNoChange (t * testing.T ) {
173+ // 总回收量低于聚合下限(reclaimMinTotalPct 窗口)时,整趟不动 —— 护缓存。
174+ ctxWin := 100000 // minTotal = 5000, keepBudget = 20000
175+ huge := strings .Repeat ("x" , 40000 ) // ~10k token,4 条就撑爆 keepBudget
176+ medium := strings .Repeat ("y" , 1200 ) // > reclaimMinMsgTokens,但两条合计仍 < minTotal
177+ convo := []ChatMessage {{Role : "system" , Content : "s" }, {Role : "user" , Content : "t" }}
178+ for k := 0 ; k < 2 ; k ++ { // 更旧:2 条 medium(会成为候选,但总量小)
179+ id := fmt .Sprintf ("old%d" , k )
180+ convo = append (convo , asstCall (id , "Read" , `{"path":"o.go"}` ))
181+ convo = append (convo , toolMsg (id , "Read" , medium ))
182+ }
183+ for k := 0 ; k < reclaimMinTailToolMsgs ; k ++ { // 较新:huge 占满 tail + 撑爆预算
184+ id := fmt .Sprintf ("new%d" , k )
185+ convo = append (convo , asstCall (id , "Read" , `{"path":"n.go"}` ))
186+ convo = append (convo , toolMsg (id , "Read" , huge ))
187+ }
188+
189+ if reclaimToolOutputs (convo , ctxWin ) {
190+ t .Fatal ("总回收量低于聚合下限时不应回收(护缓存)" )
191+ }
192+ for _ , m := range convo {
193+ if m .Role == "tool" && strings .HasPrefix (m .Content , reclaimMarkerPrefix ) {
194+ t .Fatal ("聚合下限未过,任何工具输出都不应被回收" )
195+ }
196+ }
197+ }
198+
138199func TestReclaim_PairingPreserved (t * testing.T ) {
139200 // 回收后每条 tool 消息的 ToolCallID / Name 不变,配对完整。
140201 convo := buildConvo (10 , "Read" , strings .Repeat ("w" , 6000 ))
0 commit comments