返回 DeepSeek-Reasonix
compact_manual_deadlock_test.go
根目录 / internal / agent / compact_manual_deadlock_test.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "testing"
8
9 "reasonix/internal/provider"
10 )
11
12 // recordingSharedWindowProvider logs every request before delegating to the
13 // shared-window fake, so tests can assert each summary request stayed
14 // admissible.
15 type recordingSharedWindowProvider struct {
16 sharedWindowTestProvider
17 requests []provider.Request
18 }
19
20 func (p *recordingSharedWindowProvider) Stream(ctx context.Context, req provider.Request) (<-chan provider.Chunk, error) {
21 p.requests = append(p.requests, req)
22 return p.sharedWindowTestProvider.Stream(ctx, req)
23 }
24
25 // TestManualCompactEscapesOverCeilingDeadlock reproduces #9059: once the
26 // model-visible view sits at or above the hard input ceiling, a manual compact
27 // must still issue an admissible summary request and fold the view back under
28 // the ceiling instead of failing forever before the provider call.
29 func TestManualCompactEscapesOverCeilingDeadlock(t *testing.T) {
30 window := 50_000
31 prov := &recordingSharedWindowProvider{sharedWindowTestProvider: sharedWindowTestProvider{budget: 128 * 1024, shared: true}}
32 sess := foldableSessionOverForce(6)
33 a := agentOverForceWindow(t, prov, sess, window)
34
35 big := strings.Repeat("word ", 400)
36 for i := 0; i < 300 && a.estimatedVisibleRequestTokens(a.modelVisibleMessages()) < a.hardInputCeiling(); i++ {
37 sess.Add(provider.Message{Role: provider.RoleAssistant, Content: big})
38 sess.Add(provider.Message{Role: provider.RoleUser, Content: "continue"})
39 }
40 if est, hard := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()), a.hardInputCeiling(); est < hard {
41 t.Fatalf("fixture did not reach hard ceiling: %d < %d", est, hard)
42 }
43
44 if err := a.CompactNow(context.Background(), ""); err != nil {
45 t.Fatalf("manual compact over ceiling deadlocked: %v", err)
46 }
47 if est, hard := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()), a.hardInputCeiling(); est >= hard {
48 t.Fatalf("manual compact left view at or above hard ceiling: %d >= %d", est, hard)
49 }
50 if len(prov.requests) == 0 {
51 t.Fatal("manual compact never reached the summarizer")
52 }
53 maxPrompt := window - a.summaryOutputBudget() - protocolReserveTokens
54 for i, req := range prov.requests {
55 if got := a.estimatedRequestTokens(req); got > maxPrompt {
56 t.Fatalf("summary request %d estimated %d tokens, exceeds admissible %d", i, got, maxPrompt)
57 }
58 }
59 summaries := 0
60 for _, m := range a.modelVisibleMessages() {
61 if isCompactionSummary(m) {
62 summaries++
63 }
64 }
65 if summaries != 1 {
66 t.Fatalf("projection summaries = %d, want exactly 1", summaries)
67 }
68 }
69
70 // TestManualCompactOverCeilingPrunesBeforeSummary pins the rescue order: the
71 // free view-side prune runs before the first summarizer call, so oversized
72 // tool results in the never-folded tail shrink without a model round-trip.
73 func TestManualCompactOverCeilingPrunesBeforeSummary(t *testing.T) {
74 window := 50_000
75 prov := &recordingSharedWindowProvider{sharedWindowTestProvider: sharedWindowTestProvider{budget: 128 * 1024, shared: true}}
76 msgs := []provider.Message{
77 {Role: provider.RoleSystem, Content: "sys"},
78 {Role: provider.RoleUser, Content: "dump everything"},
79 }
80 for i := range 10 {
81 callID := "call-dump"
82 if i > 0 {
83 callID = fmt.Sprintf("call-dump-%d", i)
84 }
85 msgs = append(msgs,
86 provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: callID, Name: "dump", Arguments: `{}`}}},
87 provider.Message{Role: provider.RoleTool, ToolCallID: callID, Name: "dump", Content: strings.Repeat("x", 24_000)},
88 provider.Message{Role: provider.RoleUser, Content: "again"},
89 )
90 }
91 sess := &Session{Messages: msgs}
92 a := agentOverForceWindow(t, prov, sess, window)
93 if est, hard := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()), a.hardInputCeiling(); est < hard {
94 t.Fatalf("fixture did not reach hard ceiling: %d < %d", est, hard)
95 }
96
97 if err := a.CompactNow(context.Background(), ""); err != nil {
98 t.Fatalf("manual compact over tool-heavy ceiling: %v", err)
99 }
100 if len(prov.requests) == 0 {
101 t.Fatal("manual compact never reached the summarizer")
102 }
103 prunedSeen := false
104 for _, m := range prov.requests[0].Messages {
105 if m.Role != provider.RoleTool {
106 continue
107 }
108 if len(m.Content) < 24_000 && strings.Contains(m.Content, toolPruneMarker) {
109 prunedSeen = true
110 }
111 }
112 if !prunedSeen {
113 t.Fatal("first summary request carried unpruned tool results; prune did not run before the summarizer")
114 }
115 if est, hard := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()), a.hardInputCeiling(); est >= hard {
116 t.Fatalf("manual compact left view at or above hard ceiling: %d >= %d", est, hard)
117 }
118 }
119
120 // TestManualCompactMultiBatchWhenSingleFoldInsufficient drives a view several
121 // times the window: one admissible batch cannot free enough, so the rescue
122 // loop folds again with the prior digest merged into the next input.
123 func TestManualCompactMultiBatchWhenSingleFoldInsufficient(t *testing.T) {
124 window := 50_000
125 prov := &recordingSharedWindowProvider{sharedWindowTestProvider: sharedWindowTestProvider{budget: 128 * 1024, shared: true}}
126 sess := foldableSessionOverForce(6)
127 a := agentOverForceWindow(t, prov, sess, window)
128
129 big := strings.Repeat("word ", 400)
130 hard := a.hardInputCeiling()
131 for i := 0; i < 500 && a.estimatedVisibleRequestTokens(a.modelVisibleMessages()) < 2*hard+10_000; i++ {
132 sess.Add(provider.Message{Role: provider.RoleAssistant, Content: big})
133 sess.Add(provider.Message{Role: provider.RoleUser, Content: "continue"})
134 }
135 if est := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()); est < 2*hard {
136 t.Fatalf("fixture did not reach double ceiling: %d < %d", est, 2*hard)
137 }
138
139 if err := a.CompactNow(context.Background(), ""); err != nil {
140 t.Fatalf("multi-batch manual compact: %v", err)
141 }
142 if len(prov.requests) < 2 {
143 t.Fatalf("summary calls = %d, want at least 2 rescue batches", len(prov.requests))
144 }
145 maxPrompt := window - a.summaryOutputBudget() - protocolReserveTokens
146 for i, req := range prov.requests {
147 if got := a.estimatedRequestTokens(req); got > maxPrompt {
148 t.Fatalf("summary request %d estimated %d tokens, exceeds admissible %d", i, got, maxPrompt)
149 }
150 }
151 var second strings.Builder
152 for _, m := range prov.requests[1].Messages {
153 second.WriteString(m.Content)
154 }
155 if !strings.Contains(second.String(), summaryTagOpen) {
156 t.Fatal("second rescue batch did not merge the prior digest into its input")
157 }
158 if est := a.estimatedVisibleRequestTokens(a.modelVisibleMessages()); est >= hard {
159 t.Fatalf("multi-batch manual compact left view over ceiling: %d >= %d", est, hard)
160 }
161 summaries := 0
162 for _, m := range a.modelVisibleMessages() {
163 if isCompactionSummary(m) {
164 summaries++
165 }
166 }
167 if summaries != 1 {
168 t.Fatalf("projection summaries = %d, want exactly 1", summaries)
169 }
170 }
171
172 // TestManualCompactBelowCeilingKeepsSingleFold pins the ordinary manual
173 // contract: below the hard ceiling a manual compact still prunes nothing,
174 // makes exactly one summarizer call, and installs exactly one projection.
175 func TestManualCompactBelowCeilingKeepsSingleFold(t *testing.T) {
176 prov := &recordingSharedWindowProvider{sharedWindowTestProvider: sharedWindowTestProvider{budget: 128 * 1024, shared: true}}
177 sess := foldableSessionOverForce(60)
178 a := agentOverForceWindow(t, prov, sess, 50_000)
179 before := a.estimatedVisibleRequestTokens(a.modelVisibleMessages())
180 if before >= a.hardInputCeiling() || before < a.compactTrigger() {
181 t.Fatalf("fixture est = %d, want between trigger %d and hard %d", before, a.compactTrigger(), a.hardInputCeiling())
182 }
183
184 if err := a.CompactNow(context.Background(), ""); err != nil {
185 t.Fatalf("manual compact below ceiling: %v", err)
186 }
187 if prov.calls != 1 {
188 t.Fatalf("summary calls = %d, want exactly 1", prov.calls)
189 }
190 if got := a.currentProjectionVersion(); got != 1 {
191 t.Fatalf("projection version = %d, want exactly 1 (single fold, no prune)", got)
192 }
193 visible := a.modelVisibleMessages()
194 if last := visible[len(visible)-1]; last.Role != provider.RoleUser || last.Content != "continue" {
195 t.Fatalf("verbatim tail not retained: %+v", last)
196 }
197 }
198
198 lines GO