返回 DeepSeek-Reasonix
coordinator_test.go
根目录 / internal / readcoord / coordinator_test.go
1 package readcoord
2
3 import (
4 "testing"
5 "time"
6
7 "reasonix/internal/tool"
8 )
9
10 func envelope(readID, path, version string, intent tool.ReadIntent, requested *tool.ReadRange, delivered []tool.ReadRange, eof bool) tool.ReadResultEnvelope {
11 env := tool.ReadResultEnvelope{
12 ProtocolVersion: tool.ReadResultProtocolVersion,
13 ReadID: readID,
14 Intent: intent,
15 Source: tool.ReadResultSource{CanonicalPath: path, Snapshot: version},
16 DeliveredRanges: delivered,
17 EOF: eof,
18 HasMore: !eof,
19 }
20 if len(delivered) > 0 {
21 next := tool.ReadCursor{Path: path, ReadID: readID, Snapshot: version, NextStart: delivered[len(delivered)-1].End}
22 env.NextCursor = tool.EncodeReadCursor(next)
23 }
24 if eof {
25 end := 0
26 for _, r := range delivered {
27 end = max(end, r.End)
28 }
29 env.SourceEnd = &end
30 }
31 if requested != nil {
32 env.RequestedRange = requested
33 }
34 return env
35 }
36
37 func TestInspectObligationEndsAfterOnePage(t *testing.T) {
38 c := New()
39 env := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentInspect, nil, ranges(0, 2000), false)
40 tr, ok := c.Observe(env, 0)
41 if !ok {
42 t.Fatal("inspect delivery must be folded")
43 }
44 if tr.To != StateSatisfied || !tr.Progress || len(tr.Missing) != 0 {
45 t.Fatalf("transition = %+v, want satisfied with no missing coverage", tr)
46 }
47 if _, ok := c.Observe(env, 0); ok {
48 t.Fatal("a satisfied obligation must ignore a late delivery")
49 }
50 }
51
52 func TestRangeObligationPagesUntilCovered(t *testing.T) {
53 c := New()
54 scope := Scope{WorkspaceID: "ws", CanonicalPath: "/w/a.go"}
55 req := Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 20)}
56 c.Begin("ir-1", scope, req)
57
58 first := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(0, 10), false)
59 tr, ok := c.Observe(first, 0)
60 if !ok || tr.To != StateNeedsMore {
61 t.Fatalf("first page transition = %+v (ok=%v), want needs_more", tr, ok)
62 }
63 if !sameRanges(tr.Missing, ranges(10, 20)) {
64 t.Fatalf("Missing = %+v, want 10-20", tr.Missing)
65 }
66
67 second := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(10, 20), false)
68 tr, ok = c.Observe(second, 0)
69 if !ok || tr.To != StateSatisfied || len(tr.Missing) != 0 {
70 t.Fatalf("second page transition = %+v (ok=%v), want satisfied", tr, ok)
71 }
72 ob, _ := c.Get("ir-1")
73 if !sameRanges(ob.Covered, ranges(0, 20)) || ob.Pages != 2 {
74 t.Fatalf("obligation = %+v, want covered 0-20 over 2 pages", ob)
75 }
76 }
77
78 func TestRangeObligationIsSatisfiedByEOFShortOfWindow(t *testing.T) {
79 c := New()
80 req := Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 20)}
81 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, req)
82 tr, ok := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(0, 5), true), 0)
83 if !ok || tr.To != StateSatisfied {
84 t.Fatalf("EOF short of the window must satisfy the range: %+v (ok=%v)", tr, ok)
85 }
86 }
87
88 func TestWholeFileRequiresContiguousCoverageFromLineZero(t *testing.T) {
89 c := New()
90 req := Requirement{Intent: tool.ReadIntentFull, WholeFile: true}
91 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, req)
92
93 tr, _ := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(10, 20), true), 0)
94 if tr.To != StateNeedsMore {
95 t.Fatalf("tail-only delivery must not satisfy a whole-file read: %+v", tr)
96 }
97 if !sameRanges(tr.Missing, ranges(0, 10)) {
98 t.Fatalf("Missing = %+v, want 0-10", tr.Missing)
99 }
100 tr, _ = c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), false), 0)
101 if tr.To != StateSatisfied {
102 t.Fatalf("contiguous coverage from line 0 after EOF must satisfy: %+v", tr)
103 }
104 }
105
106 func TestWholeFileWithoutEOFStaysNeedsMore(t *testing.T) {
107 c := New()
108 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
109 tr, _ := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 100), false), 0)
110 if tr.To != StateNeedsMore {
111 t.Fatalf("coverage without EOF cannot prove the whole file: %+v", tr)
112 }
113 }
114
115 func TestVersionChangeResetsCoverageAndBumpsGeneration(t *testing.T) {
116 c := New()
117 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
118 c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), false), 0)
119
120 tr, ok := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v2", tool.ReadIntentFull, nil, ranges(10, 20), true), 0)
121 if !ok || !tr.Stale || tr.Generation != 1 {
122 t.Fatalf("version change must be reported stale with a new generation: %+v (ok=%v)", tr, ok)
123 }
124 ob, _ := c.Get("ir-1")
125 if !sameRanges(ob.Covered, ranges(10, 20)) {
126 t.Fatalf("Covered = %+v, want only the v2 delivery", ob.Covered)
127 }
128 if tr.To != StateNeedsMore || !sameRanges(tr.Missing, ranges(0, 10)) {
129 t.Fatalf("after a version change the missing prefix must be reported: %+v", tr)
130 }
131 }
132
133 func TestOutOfOrderPagesStillSatisfyAWholeFileRead(t *testing.T) {
134 c := New()
135 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
136 for _, r := range [][]tool.ReadRange{ranges(20, 30), ranges(0, 10), ranges(10, 20)} {
137 eof := r[0].Start == 20
138 c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, r, eof), 0)
139 }
140 ob, _ := c.Get("ir-1")
141 if ob.State != StateSatisfied {
142 t.Fatalf("state = %s, want satisfied regardless of delivery order", ob.State)
143 }
144 if !sameRanges(ob.Covered, ranges(0, 30)) {
145 t.Fatalf("Covered = %+v, want 0-30", ob.Covered)
146 }
147 }
148
149 func TestRepeatedPageIsNotProgress(t *testing.T) {
150 c := New()
151 req := Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 40)}
152 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, req)
153 page := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 40}, ranges(0, 10), false)
154 if tr, _ := c.Observe(page, 0); !tr.Progress {
155 t.Fatal("first delivery is progress")
156 }
157 tr, _ := c.Observe(page, 0)
158 if tr.Progress || len(tr.Added) != 0 {
159 t.Fatalf("a repeated page must not count as progress: %+v", tr)
160 }
161 ob, _ := c.Get("ir-1")
162 if ob.Stagnant != 1 {
163 t.Fatalf("Stagnant = %d, want 1", ob.Stagnant)
164 }
165 }
166
167 func TestCancelledObligationIgnoresLateDelivery(t *testing.T) {
168 c := New()
169 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
170 tr, ok := c.Cancel("ir-1")
171 if !ok || tr.To != StateCancelled {
172 t.Fatalf("cancel transition = %+v (ok=%v)", tr, ok)
173 }
174 if _, ok := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), true), 0); ok {
175 t.Fatal("a cancelled obligation must not accept a late delivery")
176 }
177 ob, _ := c.Get("ir-1")
178 if ob.State != StateCancelled {
179 t.Fatalf("state = %s, want cancelled", ob.State)
180 }
181 }
182
183 func TestStopReasonsAreReportedAndClearedByADelivery(t *testing.T) {
184 c := New()
185 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
186 tr, ok := c.Fail("ir-1", Block{Code: "read_error", Detail: "permission denied", Recovery: "fix permissions"})
187 if !ok || tr.To != StateBlocked || tr.Stop == nil || tr.Stop.Code != "read_error" {
188 t.Fatalf("fail transition = %+v (ok=%v)", tr, ok)
189 }
190 tr, ok = c.Narrow("ir-1", Block{Code: "budget", Detail: "context window unknown", Recovery: "read a narrower window"})
191 if !ok || tr.To != StateNeedsScope || tr.Stop == nil || tr.Stop.Code != "budget" {
192 t.Fatalf("narrow transition = %+v (ok=%v)", tr, ok)
193 }
194 ob, _ := c.Get("ir-1")
195 if !ob.Requirement.WholeFile {
196 t.Fatal("narrowing must not silently downgrade a whole-file requirement")
197 }
198 tr, ok = c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), true), 0)
199 if !ok || tr.To != StateSatisfied || tr.Stop != nil {
200 t.Fatalf("a delivery must clear the stop reason: %+v (ok=%v)", tr, ok)
201 }
202 }
203
204 func TestBeginRefreshesRequirementAndKeepsCoverage(t *testing.T) {
205 c := New()
206 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 10)})
207 c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 10}, ranges(0, 10), false), 0)
208 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 20)})
209 ob, _ := c.Get("ir-1")
210 if !sameRanges(ob.Covered, ranges(0, 10)) {
211 t.Fatalf("Covered = %+v, want the earlier delivery kept", ob.Covered)
212 }
213 tr, _ := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 10, End: 20}, ranges(10, 20), false), 0)
214 if tr.To != StateSatisfied {
215 t.Fatalf("refreshed requirement transition = %+v, want satisfied", tr)
216 }
217 }
218
219 func TestObserveIgnoresEnvelopesWithoutIdentity(t *testing.T) {
220 c := New()
221 if _, ok := c.Observe(tool.ReadResultEnvelope{Intent: tool.ReadIntentInspect}, 0); ok {
222 t.Fatal("an envelope without a read id or path must be ignored")
223 }
224 }
225
226 func TestObserveWithoutBeginDerivesTheRequirementFromIntent(t *testing.T) {
227 c := New()
228 if _, ok := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), true), 0); !ok {
229 t.Fatal("an unregistered full read must still be folded")
230 }
231 ob, _ := c.Get("ir-1")
232 if ob.Requirement.Intent != tool.ReadIntentFull || !ob.Requirement.WholeFile {
233 t.Fatalf("requirement = %+v, want a whole-file full read", ob.Requirement)
234 }
235 if ob.State != StateSatisfied {
236 t.Fatalf("state = %s, want satisfied after a complete full read", ob.State)
237 }
238
239 inspect, ok := c.Observe(envelope("ir-2", "/w/b.go", "rw1:v1", tool.ReadIntentInspect, nil, ranges(0, 200), false), 0)
240 if !ok || inspect.To != StateSatisfied {
241 t.Fatalf("inspect transition = %+v (ok=%v)", inspect, ok)
242 }
243 if got, _ := c.Get("ir-2"); got.Requirement.WholeFile || len(got.Requirement.Ranges) != 0 {
244 t.Fatalf("inspect requirement = %+v, want no coverage debt", got.Requirement)
245 }
246 }
247
248 func TestSnapshotIsOrderedByKey(t *testing.T) {
249 c := New()
250 for _, key := range []string{"ir-b", "ir-a"} {
251 c.Begin(key, Scope{CanonicalPath: "/w/" + key}, Requirement{Intent: tool.ReadIntentInspect})
252 }
253 snap := c.Snapshot()
254 if len(snap) != 2 || snap[0].Key != "ir-a" || snap[1].Key != "ir-b" {
255 t.Fatalf("Snapshot = %+v, want key order", snap)
256 }
257 }
258
259 func TestReturnedObligationsAreDeepCopies(t *testing.T) {
260 c := New()
261 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 20)})
262 c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(0, 10), false), 0)
263
264 got, _ := c.Get("ir-1")
265 got.Covered[0] = tool.ReadRange{Start: 99, End: 100}
266 got.Requirement.Ranges[0] = tool.ReadRange{Start: 99, End: 100}
267
268 again, _ := c.Get("ir-1")
269 if !sameRanges(again.Covered, ranges(0, 10)) || !sameRanges(again.Requirement.Ranges, ranges(0, 20)) {
270 t.Fatalf("mutating a returned obligation changed coordinator state: %+v", again)
271 }
272 snap := c.Snapshot()
273 snap[0].Covered[0] = tool.ReadRange{Start: 1, End: 2}
274 third, _ := c.Get("ir-1")
275 if !sameRanges(third.Covered, ranges(0, 10)) {
276 t.Fatal("mutating a snapshot changed coordinator state")
277 }
278 }
279
280 func TestRangeCompletionNeedsATrustworthySourceEnd(t *testing.T) {
281 c := New()
282 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 20)})
283
284 // EOF without a source end proves nothing: the reader may have stopped early.
285 unvouched := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(0, 5), true)
286 unvouched.SourceEnd = nil
287 if tr, _ := c.Observe(unvouched, 0); tr.To != StateNeedsMore {
288 t.Fatalf("bare EOF must not complete a range: %+v", tr)
289 }
290
291 // A source end inside the requested window does complete it.
292 shortFile := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 20}, ranges(0, 5), true)
293 shortFile.SourceEnd = new(int)
294 *shortFile.SourceEnd = 5
295 if tr, _ := c.Observe(shortFile, 0); tr.To != StateSatisfied {
296 t.Fatalf("a source end inside the window completes the range: %+v", tr)
297 }
298 }
299
300 func TestStalledPagesPivotOnceThenPause(t *testing.T) {
301 c := NewWithPolicy(Policy{MaxPages: 64, PivotAfter: 2, PauseAfter: 2})
302 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 40)})
303 page := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 40}, ranges(0, 10), false)
304 c.Observe(page, 0)
305
306 if tr, _ := c.Observe(page, 0); tr.Advice != "" {
307 t.Fatalf("one stalled page must not pivot yet: %+v", tr)
308 }
309 tr, _ := c.Observe(page, 0)
310 if tr.Advice != AdvicePivot {
311 t.Fatalf("the second stalled page must pivot once: %+v", tr)
312 }
313 c.Observe(page, 0)
314 tr, _ = c.Observe(page, 0)
315 if tr.To != StateBlocked || tr.Stop == nil || tr.Stop.Code != "no_progress" {
316 t.Fatalf("two stalled pages after the pivot must pause the read: %+v", tr)
317 }
318 }
319
320 func TestPageBudgetStopsContinuation(t *testing.T) {
321 c := NewWithPolicy(Policy{MaxPages: 2})
322 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 100)})
323 for _, r := range [][]tool.ReadRange{ranges(0, 10), ranges(10, 20), ranges(20, 30)} {
324 env := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 100}, r, false)
325 tr, _ := c.Observe(env, 0)
326 if r[0].Start == 20 {
327 if tr.To != StateBlocked || tr.Stop == nil || tr.Stop.Code != "page_budget" {
328 t.Fatalf("the page budget must stop continuation: %+v", tr)
329 }
330 }
331 }
332 }
333
334 func TestActiveTimeBudgetStopsContinuation(t *testing.T) {
335 c := NewWithPolicy(Policy{MaxActiveTime: 100 * time.Millisecond})
336 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentRange, Ranges: ranges(0, 100)})
337 env := envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentRange, &tool.ReadRange{Start: 0, End: 100}, ranges(0, 10), false)
338 tr, _ := c.Observe(env, 200)
339 if tr.To != StateBlocked || tr.Stop == nil || tr.Stop.Code != "time_budget" {
340 t.Fatalf("the active-time budget must stop continuation: %+v", tr)
341 }
342 }
343
344 func TestContentChangeDoesNotResetTheBudget(t *testing.T) {
345 c := NewWithPolicy(Policy{MaxPages: 1})
346 c.Begin("ir-1", Scope{CanonicalPath: "/w/a.go"}, Requirement{Intent: tool.ReadIntentFull, WholeFile: true})
347 c.Observe(envelope("ir-1", "/w/a.go", "rw1:v1", tool.ReadIntentFull, nil, ranges(0, 10), false), 0)
348
349 tr, _ := c.Observe(envelope("ir-1", "/w/a.go", "rw1:v2", tool.ReadIntentFull, nil, ranges(10, 20), false), 0)
350 if tr.To != StateBlocked || tr.Stop == nil || tr.Stop.Code != "page_budget" {
351 t.Fatalf("a content change must not reset the hard budget: %+v", tr)
352 }
353 }
354
354 lines GO