返回 DeepSeek-Reasonix
readresult_test.go
根目录 / internal / tool / readresult_test.go
1 package tool
2
3 import (
4 "fmt"
5 "strings"
6 "testing"
7 )
8
9 func TestParseReadWindowRequiresContiguousNumberedLines(t *testing.T) {
10 cases := []struct {
11 name string
12 output string
13 wantOK bool
14 wantStart int
15 wantLines []string
16 }{
17 {name: "contiguous", output: " 1→a\n 2→b\n", wantOK: true, wantStart: 1, wantLines: []string{"a", "b"}},
18 {name: "offset window", output: " 10→x\n 11→y\n", wantOK: true, wantStart: 10, wantLines: []string{"x", "y"}},
19 {name: "gap fails closed", output: " 1→a\n 5→b\n"},
20 {name: "unnumbered", output: "plain text\n"},
21 {name: "empty", output: ""},
22 {name: "trailer and blank lines ignored", output: " 1→a\n\n[more lines below; pass offset=1 to continue]\n", wantOK: true, wantStart: 1, wantLines: []string{"a"}},
23 {name: "arrow inside content", output: " 1→a→b\n", wantOK: true, wantStart: 1, wantLines: []string{"a→b"}},
24 {name: "non-numeric prefix skipped", output: "abc→def\n"},
25 {name: "zero line number skipped", output: " 0→a\n"},
26 }
27 for _, tc := range cases {
28 t.Run(tc.name, func(t *testing.T) {
29 got, ok := ParseReadWindow(tc.output)
30 if ok != tc.wantOK {
31 t.Fatalf("ok = %v, want %v (window %+v)", ok, tc.wantOK, got)
32 }
33 if !tc.wantOK {
34 return
35 }
36 if got.StartLine != tc.wantStart {
37 t.Fatalf("StartLine = %d, want %d", got.StartLine, tc.wantStart)
38 }
39 if len(got.Lines) != len(tc.wantLines) {
40 t.Fatalf("Lines = %q, want %q", got.Lines, tc.wantLines)
41 }
42 for i := range tc.wantLines {
43 if got.Lines[i] != tc.wantLines[i] {
44 t.Fatalf("line %d = %q, want %q", i, got.Lines[i], tc.wantLines[i])
45 }
46 }
47 })
48 }
49 }
50
51 func TestReadWindowRangeIsZeroBasedHalfOpen(t *testing.T) {
52 w := ReadWindow{StartLine: 120, Lines: []string{"a", "b"}}
53 if got, want := w.Range(), (ReadRange{Start: 119, End: 121}); got != want {
54 t.Fatalf("Range() = %+v, want %+v", got, want)
55 }
56 if !(ReadRange{Start: 5, End: 5}).Empty() {
57 t.Fatal("equal bounds must be empty")
58 }
59 if got := (ReadRange{Start: 3, End: 8}).Lines(); got != 5 {
60 t.Fatalf("Lines() = %d, want 5", got)
61 }
62 }
63
64 func TestWindowDigestBindsPathAndContent(t *testing.T) {
65 base := ReadWindow{StartLine: 1, Lines: []string{"alpha", "beta"}}
66 other := ReadWindow{StartLine: 1, Lines: []string{"alpha", "gamma"}}
67 shifted := ReadWindow{StartLine: 2, Lines: []string{"alpha", "beta"}}
68
69 token := WindowDigest("/w/a.go", base)
70 if token == "" {
71 t.Fatal("token must not be empty")
72 }
73 if again := WindowDigest("/w/a.go", base); again != token {
74 t.Fatalf("same content produced different tokens: %q vs %q", token, again)
75 }
76 if changed := WindowDigest("/w/a.go", other); changed == token {
77 t.Fatal("edited line must change the version token")
78 }
79 if moved := WindowDigest("/w/a.go", shifted); moved == token {
80 t.Fatal("shifted window must change the version token")
81 }
82 if elsewhere := WindowDigest("/w/b.go", base); elsewhere == token {
83 t.Fatal("different path must change the version token")
84 }
85 }
86
87 func TestReadCursorRoundTripAndMatching(t *testing.T) {
88 env := ReadResultEnvelope{
89 ReadID: "ir-1",
90 Source: ReadResultSource{CanonicalPath: "/w/a.go", Snapshot: "ss2:abc"},
91 DeliveredRanges: []ReadRange{{Start: 0, End: 40}},
92 }
93 token := EncodeReadCursor(ReadCursor{Path: "/w/a.go", ReadID: "ir-1", Snapshot: "ss2:abc", NextStart: 40})
94 if token == "" {
95 t.Fatal("cursor must encode")
96 }
97 cursor, ok := DecodeReadCursor(token)
98 if !ok {
99 t.Fatalf("decode failed for %q", token)
100 }
101 if !cursor.Matches(env) {
102 t.Fatal("cursor must match its own envelope")
103 }
104 if (ReadCursor{Path: "/w/other.go", ReadID: "ir-1", Snapshot: "ss2:abc", NextStart: 40}).Matches(env) {
105 t.Fatal("cursor must not match a different path")
106 }
107 if (ReadCursor{Path: "/w/a.go", ReadID: "ir-1", Snapshot: "ss2:def", NextStart: 40}).Matches(env) {
108 t.Fatal("cursor must not match a different content version")
109 }
110 if (ReadCursor{Path: "/w/a.go", ReadID: "ir-1", Snapshot: "ss2:abc", NextStart: 41}).Matches(env) {
111 t.Fatal("cursor must not match a start outside the delivered range")
112 }
113 for _, bad := range []string{"", "rc2:", "rc2:!!!", "other:abc", "rc2:" + "eyJwYXRoIjoiIn0"} {
114 if _, ok := DecodeReadCursor(bad); ok {
115 t.Fatalf("decoded malformed cursor %q", bad)
116 }
117 }
118 if EncodeReadCursor(ReadCursor{Path: "", ReadID: "ir-1", Snapshot: "ss2:abc", NextStart: 0}) != "" {
119 t.Fatal("cursor without a path must not encode")
120 }
121 }
122
123 func TestClipToNarrowsDeliveredRangeToVisibleBytes(t *testing.T) {
124 env := ReadResultEnvelope{
125 Source: ReadResultSource{CanonicalPath: "/w/a.go", Snapshot: "ss2:abc"},
126 ReadID: "ir-1",
127 DeliveredRanges: []ReadRange{{Start: 0, End: 100}},
128 HasMore: false,
129 EOF: true,
130 }
131 var visible strings.Builder
132 for i := 1; i <= 40; i++ {
133 fmt.Fprintf(&visible, " %d→line\n", i)
134 }
135
136 clipped := env.ClipTo(visible.String())
137 if got, want := clipped.DeliveredRanges, []ReadRange{{Start: 0, End: 40}}; len(got) != 1 || got[0] != want[0] {
138 t.Fatalf("DeliveredRanges = %+v, want %+v", got, want)
139 }
140 if clipped.TransportCut != ReadCutToolOutput {
141 t.Fatalf("TransportCut = %q, want %q", clipped.TransportCut, ReadCutToolOutput)
142 }
143 if !clipped.HasMore || clipped.EOF {
144 t.Fatalf("a truncated delivery must be resumable: has_more=%v eof=%v", clipped.HasMore, clipped.EOF)
145 }
146 cursor, ok := DecodeReadCursor(clipped.NextCursor)
147 if !ok || cursor.NextStart != 40 {
148 t.Fatalf("NextCursor = %q (cursor %+v, ok=%v), want next_start 40", clipped.NextCursor, cursor, ok)
149 }
150 if !cursor.Matches(clipped) {
151 t.Fatal("clipped envelope must accept its own cursor")
152 }
153 if env.TransportCut != ReadCutNone || !env.EOF {
154 t.Fatal("ClipTo must not mutate the receiver")
155 }
156 }
157
158 func TestClipToKeepsCompleteDelivery(t *testing.T) {
159 env := ReadResultEnvelope{
160 Source: ReadResultSource{CanonicalPath: "/w/a.go", Snapshot: "ss2:abc"},
161 ReadID: "ir-1",
162 DeliveredRanges: []ReadRange{{Start: 0, End: 2}},
163 EOF: true,
164 }
165 clipped := env.ClipTo(" 1→a\n 2→b\n")
166 if clipped.TransportCut != ReadCutNone || clipped.HasMore {
167 t.Fatalf("complete delivery must stay complete: cut=%q has_more=%v", clipped.TransportCut, clipped.HasMore)
168 }
169 if len(clipped.DeliveredRanges) != 1 || clipped.DeliveredRanges[0] != (ReadRange{Start: 0, End: 2}) {
170 t.Fatalf("DeliveredRanges = %+v", clipped.DeliveredRanges)
171 }
172 }
173
174 func TestParseReadTrailerRecognizesBothPagingForms(t *testing.T) {
175 more := ParseReadTrailer(" 1→a\n\n[more lines below; pass offset=7 to continue]\n")
176 if !more.HasMore || more.NextOffset != 7 || more.LocalSafety {
177 t.Fatalf("more-lines trailer = %+v", more)
178 }
179 safety := ParseReadTrailer(" 1→a\n\n[read_file local safety page; next_offset=9 requested_end=20]\n")
180 if !safety.HasMore || !safety.LocalSafety || safety.NextOffset != 9 || safety.RequestedEnd != 20 {
181 t.Fatalf("safety trailer = %+v", safety)
182 }
183 if absent := ParseReadTrailer(" 1→a\n"); absent.HasMore || absent.LocalSafety {
184 t.Fatalf("absent trailer = %+v", absent)
185 }
186 }
187
187 lines GO