| 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 |