返回 DeepSeek-Reasonix
positional_compression_test.go
根目录 / internal / agent / positional_compression_test.go
1 package agent
2
3 import (
4 "context"
5 "reflect"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 func TestSummarizeFromPreservesLocalOnlyOutsideModelAndArchive(t *testing.T) {
15 local := provider.Message{
16 Role: provider.RoleTool, ToolCallID: provider.LocalOnlyToolID, Name: provider.LocalOnlyToolName,
17 LocalOnly: true, Content: "visible interrupted output", ReasoningContent: "private interrupted reasoning",
18 InterruptedTurn: &provider.InterruptedTurnRecovery{Pending: true, InterruptedTools: []string{"bash"}},
19 }
20 prov := &fakeProvider{reply: "later summary"}
21 // Enough foldable assistant work so the projection candidate reduces tokens.
22 sess := &Session{Messages: []provider.Message{
23 {Role: provider.RoleSystem, Content: "sys"},
24 {Role: provider.RoleUser, Content: "task"},
25 local,
26 {Role: provider.RoleAssistant, Content: "safe answer " + strings.Repeat("work ", 400)},
27 {Role: provider.RoleUser, Content: "more"},
28 {Role: provider.RoleAssistant, Content: strings.Repeat("more work ", 400)},
29 }}
30 a := New(prov, tool.NewRegistry(), sess, Options{
31 ContextWindow: 50_000, CompactRatio: 0.85, RecentKeep: 2,
32 }, event.Discard)
33 before := sess.Snapshot()
34
35 if err := a.SummarizeFrom(context.Background(), 1); err != nil {
36 t.Fatalf("SummarizeFrom: %v", err)
37 }
38 if !reflect.DeepEqual(sess.Snapshot(), before) {
39 t.Fatalf("canonical transcript changed: before=%+v after=%+v", before, sess.Snapshot())
40 }
41 if len(a.sess.compactionState.Projection.Messages) == 0 {
42 t.Fatal("expected summarize-from projection")
43 }
44 assertLocalOnlyAbsentFromSummary(t, prov, a, local)
45 }
46
47 func TestSummarizeUpToPreservesLocalOnlyOutsideModelAndArchive(t *testing.T) {
48 local := provider.Message{
49 Role: provider.RoleTool, ToolCallID: provider.LocalOnlyToolID, Name: provider.LocalOnlyToolName,
50 LocalOnly: true, Content: "visible earlier interruption", ReasoningContent: "private earlier reasoning",
51 InterruptedTurn: &provider.InterruptedTurnRecovery{Pending: true, InterruptedTools: []string{"read_file"}},
52 }
53 prov := &fakeProvider{reply: "earlier summary"}
54 sess := &Session{Messages: []provider.Message{
55 {Role: provider.RoleSystem, Content: "sys"},
56 {Role: provider.RoleUser, Content: "old task"},
57 local,
58 {Role: provider.RoleAssistant, Content: "old answer " + strings.Repeat("work ", 400)},
59 {Role: provider.RoleUser, Content: "new task"},
60 {Role: provider.RoleAssistant, Content: "new answer"},
61 }}
62 a := New(prov, tool.NewRegistry(), sess, Options{
63 ContextWindow: 50_000, CompactRatio: 0.85, RecentKeep: 2,
64 }, event.Discard)
65 before := sess.Snapshot()
66
67 if err := a.SummarizeUpTo(context.Background(), 4); err != nil {
68 t.Fatalf("SummarizeUpTo: %v", err)
69 }
70 if !reflect.DeepEqual(sess.Snapshot(), before) {
71 t.Fatalf("canonical transcript changed: before=%+v after=%+v", before, sess.Snapshot())
72 }
73 if len(a.sess.compactionState.Projection.Messages) == 0 {
74 t.Fatal("expected summarize-up-to projection")
75 }
76 assertLocalOnlyAbsentFromSummary(t, prov, a, local)
77 }
78
79 func TestSummarizeAtProjectionBoundaryReportsFoldedAnchor(t *testing.T) {
80 sess := &Session{Messages: []provider.Message{
81 {Role: provider.RoleSystem, Content: "sys"},
82 {Role: provider.RoleUser, Content: "old boundary"},
83 {Role: provider.RoleAssistant, Content: strings.Repeat("old work ", 200)},
84 {Role: provider.RoleUser, Content: "retained boundary"},
85 {Role: provider.RoleAssistant, Content: "retained answer"},
86 }}
87 before := sess.Snapshot()
88 a := New(&fakeProvider{reply: "old work summarized"}, tool.NewRegistry(), sess, Options{}, event.Discard)
89
90 if err := a.SummarizeUpTo(context.Background(), 3); err != nil {
91 t.Fatalf("initial SummarizeUpTo: %v", err)
92 }
93 version := a.sess.compactionState.Projection.ProjectionVersion
94 err := a.SummarizeFrom(context.Background(), 1)
95 if err == nil || !strings.Contains(err.Error(), "no longer present in the model context") {
96 t.Fatalf("folded-boundary error = %v", err)
97 }
98 if a.sess.compactionState.Projection.ProjectionVersion != version {
99 t.Fatal("folded-boundary retry replaced the projection")
100 }
101 if !reflect.DeepEqual(sess.Snapshot(), before) {
102 t.Fatal("folded-boundary retry changed canonical history")
103 }
104 }
105
106 func TestSummarizeAtProjectionBoundaryReportsNoSavings(t *testing.T) {
107 sess := &Session{Messages: []provider.Message{
108 {Role: provider.RoleSystem, Content: "sys"},
109 {Role: provider.RoleUser, Content: "tiny"},
110 {Role: provider.RoleUser, Content: "keep boundary"},
111 }}
112 a := New(&fakeProvider{reply: strings.Repeat("long summary ", 30)}, tool.NewRegistry(), sess, Options{}, event.Discard)
113
114 err := a.SummarizeUpTo(context.Background(), 2)
115 if err == nil || !strings.Contains(err.Error(), "compressed context would not be smaller") {
116 t.Fatalf("no-savings error = %v", err)
117 }
118 if len(a.sess.compactionState.Projection.Messages) != 0 {
119 t.Fatal("no-savings positional compression installed a projection")
120 }
121 }
122
123 // assertLocalOnlyAbsentFromSummary checks local-only content stays out of the
124 // summarizer request and the installed projection. Archives are no longer written.
125 func assertLocalOnlyAbsentFromSummary(t *testing.T, prov *fakeProvider, a *Agent, local provider.Message) {
126 t.Helper()
127 if len(prov.got) < 2 || strings.Contains(prov.got[1].Content, local.Content) || strings.Contains(prov.got[1].Content, local.ReasoningContent) {
128 t.Fatalf("local-only output leaked into summarizer prompt: %+v", prov.got)
129 }
130 for _, m := range a.sess.compactionState.Projection.Messages {
131 if m.LocalOnly || strings.Contains(m.Content, local.Content) || strings.Contains(m.Content, local.ReasoningContent) {
132 t.Fatalf("local-only output entered projection: %+v", m)
133 }
134 }
135 if a.sess.compactionState.LastReceipt != nil && a.sess.compactionState.LastReceipt.Archive != "" {
136 t.Fatalf("checkpoint must not create archives, got %q", a.sess.compactionState.LastReceipt.Archive)
137 }
138 }
139
139 lines GO