// compact_token_gap_test.go - regression guard for the TokenGap precise- // stride path in truncateAndRetryCompact. // // Before this commit, api.APIError.TokenGap was populated at error parse // time but never reached the compactor -- the compactor always shaved one // group at a time regardless of how much excess the API reported. Fix: // CompactTiered(..., WithTokenGap(n)) feeds the excess to // truncateAndRetryCompact, which uses it to take a precise first stride. // This test locks in that the stride branch actually fires (observer // emits compact_token_gap_stride) when a non-zero tokenGap is supplied. // // 本 commit 之前, api.APIError.TokenGap 在错误解析时填充但从未传到 compressor -- // compressor 不管 API 报了多少溢出都是逐组砍. 修复: CompactTiered(..., WithTokenGap(n)) // 把溢出量传给 truncateAndRetryCompact, 后者据此做一次精确跳步. 本 test // 锁定 stride 分支确实在非零 tokenGap 下触发 (observer 会 emit // compact_token_gap_stride). package context import ( "context" "sync" "testing" ) // stridCaptureObs collects Event calls without needing the full engine // MockObserver. Minimal surface -- only what TokenGap stride test needs. // // stridCaptureObs 收集 Event 调用, 不依赖 engine 的 MockObserver. 最小 // 表面, 只覆盖 TokenGap stride test 要的. type stridCaptureObs struct { mu sync.Mutex events map[string][]map[string]any } func newStrideObs() *stridCaptureObs { return &stridCaptureObs{events: make(map[string][]map[string]any)} } func (o *stridCaptureObs) Event(name string, data map[string]any) { o.mu.Lock() defer o.mu.Unlock() o.events[name] = append(o.events[name], data) } func (o *stridCaptureObs) Error(_ error, _ map[string]any) {} func (o *stridCaptureObs) countEvent(name string) int { o.mu.Lock() defer o.mu.Unlock() return len(o.events[name]) } func (o *stridCaptureObs) lastEvent(name string) map[string]any { o.mu.Lock() defer o.mu.Unlock() evs := o.events[name] if len(evs) == 0 { return nil } return evs[len(evs)-1] } // TestTruncateAndRetryCompact_TokenGapStride verifies the precise-stride // path fires when tokenGap > 0 and there are enough groups to drop. // compactFull will fail (no provider), but the stride branch must still // emit its observer event before the failure -- that's the regression // guard. Without the wire, tokenGap would be silently ignored and the // event count would stay at 0. // // TestTruncateAndRetryCompact_TokenGapStride 验证 tokenGap > 0 且有足够 // 组可丢时精确跳步分支触发. 无 provider compactFull 会失败, 但 stride // 分支必须在失败前已经 emit 事件 -- 这是回归锁. 没有 wire 的话 tokenGap // 会被静默忽略, 事件计数保持 0. func TestTruncateAndRetryCompact_TokenGapStride(t *testing.T) { c := NewCompressor(0, nil) obs := newStrideObs() c.SetObserver(obs) // Build 8 rounds of substantial content so we have enough groups to // drop after reserving keepRounds (recentTurnsToKeep) + 1. // 构造 8 轮较多内容的对话, 减去 keepRounds + 1 后仍有足够组可丢. var messages []CompactMessage for i := 0; i < 8; i++ { body := make([]byte, 800) // ~200 tokens per message (EstimateTokens ~= bytes/4) for j := range body { body[j] = 'a' } messages = append(messages, CompactMessage{Role: "user", Content: append([]byte(`"`), append(body, '"')...)}, CompactMessage{Role: "assistant", Content: append([]byte(`"`), append(body, '"')...)}, ) } groups := GroupByAPIRound(messages) if len(groups) < 6 { t.Fatalf("fixture too small: %d groups (want >=6)", len(groups)) } // tokenGap small enough that the first group alone satisfies it -- we // just want to prove the branch runs, not measure the math precisely. // (Precise stride math is covered indirectly: the event payload // includes dropped_groups and dropped_tokens for future assertions.) // // tokenGap 足够小, 第一组就能满足 -- 这里只要证明分支跑过, 不测精确 // 数学. (精确数学间接覆盖: 事件 payload 里 dropped_groups / // dropped_tokens 后续可加断言.) _, _ = c.truncateAndRetryCompact(context.Background(), messages, groups, &DefaultCodePolicy{}, 100) if got := obs.countEvent("compact_token_gap_stride"); got != 1 { t.Errorf("compact_token_gap_stride count: want 1, got %d", got) } ev := obs.lastEvent("compact_token_gap_stride") if ev == nil { t.Fatal("compact_token_gap_stride event missing (stride branch never ran)") } if tg, _ := ev["token_gap"].(int); tg != 100 { t.Errorf("event.token_gap: want 100, got %v", ev["token_gap"]) } if dropped, _ := ev["dropped_groups"].(int); dropped < 1 { t.Errorf("event.dropped_groups: want >=1, got %v", ev["dropped_groups"]) } // Lock the real wire of MessageGroup.Index: the event must carry // dropped_group_indices so operators see *which* API-round groups // were shaved, not just the count. Without this, Index is a silent // write-only field and the event is less actionable in production // triage. // // 锁 MessageGroup.Index 真 wire: 事件必须携 dropped_group_indices, // 运维看到*哪几个* API-round group 被削掉, 不只是数量. 否则 Index // 是静默 write-only 字段, 事件在生产定位时可操作性下降. indices, ok := ev["dropped_group_indices"].([]int) if !ok { t.Fatalf("event.dropped_group_indices: want []int, got %T (%v)", ev["dropped_group_indices"], ev["dropped_group_indices"]) } if len(indices) < 1 { t.Errorf("dropped_group_indices: want at least 1 index, got empty") } // First dropped group is always currentGroups[1] (preamble [0] is // preserved), so index 0 is never in the list. The smallest valid // index is 1. // // 第一个被丢 group 必然是 currentGroups[1] (preamble [0] 保留), // 所以 index 0 永不在列表. 最小合法 index 是 1. for _, idx := range indices { if idx < 1 { t.Errorf("dropped_group_indices contains %d; preamble (Index=0) must never be dropped", idx) } } // Length must match dropped_groups count (same-population invariant). // 长度必须等于 dropped_groups 计数 (同源不变量). if dropped, _ := ev["dropped_groups"].(int); dropped != len(indices) { t.Errorf("dropped_groups=%d vs dropped_group_indices len=%d; must match", dropped, len(indices)) } } // TestTruncateAndRetryCompact_TokenGapZeroSkipsStride verifies that when // tokenGap is 0 (the default, no hint available), the stride branch does // NOT fire -- we fall straight through to the incremental shaving loop. // This locks in the "opt-in" semantics: callers without an APIError // hint get the unchanged pre-2026-04-19 behavior. // // TestTruncateAndRetryCompact_TokenGapZeroSkipsStride 验证 tokenGap 为 0 // (默认, 无 hint) 时 stride 分支不触发 -- 直接 fall through 到逐组 loop. // 锁定 "opt-in" 语义: 没 APIError hint 的调用方看到的是 2026-04-19 之前 // 不变的行为. func TestTruncateAndRetryCompact_TokenGapZeroSkipsStride(t *testing.T) { c := NewCompressor(0, nil) obs := newStrideObs() c.SetObserver(obs) var messages []CompactMessage for i := 0; i < 8; i++ { messages = append(messages, CompactMessage{Role: "user", Content: []byte(`"hi"`)}, CompactMessage{Role: "assistant", Content: []byte(`"ok"`)}, ) } groups := GroupByAPIRound(messages) _, _ = c.truncateAndRetryCompact(context.Background(), messages, groups, &DefaultCodePolicy{}, 0) if got := obs.countEvent("compact_token_gap_stride"); got != 0 { t.Errorf("compact_token_gap_stride count with tokenGap=0: want 0, got %d", got) } }