返回 DeepSeek-Reasonix
compact_truncate_test.go
根目录 / internal / agent / compact_truncate_test.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 // A conversation rewind that only removes tail messages must keep the fold
15 // usable: the covered prefix is byte-identical, so the projection stays valid
16 // for the shorter transcript and the model-visible view keeps the compacted
17 // size instead of ballooning back to the pre-compaction transcript and
18 // re-paying a full summary.
19 func TestTailOnlyRewindKeepsCompactedView(t *testing.T) {
20 const window = 10_000
21 big := strings.Repeat("line\n", 200)
22 msgs := []provider.Message{
23 {Role: provider.RoleSystem, Content: "system"},
24 {Role: provider.RoleUser, Content: "first task"},
25 }
26 for i := range 40 {
27 id := fmt.Sprintf("old-%d", i)
28 msgs = append(msgs,
29 provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: "{}"}}},
30 provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "read_file", Content: big},
31 )
32 }
33 msgs = append(msgs,
34 provider.Message{Role: provider.RoleUser, Content: "recent question"},
35 provider.Message{Role: provider.RoleAssistant, Content: "recent answer"},
36 )
37 sess := &Session{Messages: msgs}
38 a := New(&fakeProvider{reply: "structured digest"}, tool.NewRegistry(), sess, Options{
39 ContextWindow: window,
40 CompactRatio: 0.80,
41 RecentKeep: 2,
42 ArchiveDir: t.TempDir(),
43 }, event.Discard)
44
45 fold := a.compactTrigger()
46 before := estimateMessagesTokens(provider.ModelMessages(sess.Messages))
47 if before < fold {
48 t.Fatalf("fixture estimates %d tokens, below the fold trigger %d", before, fold)
49 }
50 if err := a.CompactNow(context.Background(), ""); err != nil {
51 t.Fatalf("compact: %v", err)
52 }
53 if after := projectionTokens(a); after >= fold {
54 t.Fatalf("compacted view %d tokens still at or above fold %d", after, fold)
55 }
56
57 a.sess.compactionMu.Lock()
58 saved := a.sess.compactionState
59 a.sess.compactionMu.Unlock()
60 canonical, _ := a.sess.conversation.snapshotMessagesVersion()
61
62 // Rewind past the final exchange — a tail-only truncation.
63 boundary := len(canonical) - 2
64 sess.Rewrite(canonical[:boundary], "rewind_truncate")
65 truncated, _ := a.sess.conversation.snapshotMessagesVersion()
66
67 if !projectionContentValid(saved, truncated) {
68 t.Fatal("tail-only truncation must keep the projection valid")
69 }
70 view := provider.ModelMessages(modelVisibleFromProjection(saved.Projection, truncated))
71 if after := estimateMessagesTokens(view); after >= fold {
72 t.Fatalf("spliced view %d tokens ballooned past the fold trigger %d", after, fold)
73 }
74 for _, m := range view {
75 if m.Content == "recent answer" || m.Content == "recent question" {
76 t.Fatal("rewound message still visible in the spliced view")
77 }
78 }
79 }
80
80 lines GO