webctx 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +97 -0
  2. package/CONTRIBUTORS.md +126 -0
  3. package/LICENSE +202 -0
  4. package/Makefile +69 -0
  5. package/README.md +121 -0
  6. package/bin/webctx.js +28 -0
  7. package/cmd/webctx/main.go +11 -0
  8. package/docs/porting-status.md +173 -0
  9. package/go.mod +3 -0
  10. package/internal/app/app.go +139 -0
  11. package/internal/app/app_test.go +194 -0
  12. package/internal/app/github.go +2084 -0
  13. package/internal/app/github_acceptance_test.go +166 -0
  14. package/internal/app/github_actions.go +1008 -0
  15. package/internal/app/github_actions_test.go +484 -0
  16. package/internal/app/github_activity_deployments.go +503 -0
  17. package/internal/app/github_activity_deployments_test.go +321 -0
  18. package/internal/app/github_commits.go +1004 -0
  19. package/internal/app/github_commits_test.go +503 -0
  20. package/internal/app/github_discussions_gists.go +1156 -0
  21. package/internal/app/github_discussions_gists_test.go +425 -0
  22. package/internal/app/github_issues.go +1616 -0
  23. package/internal/app/github_issues_test.go +731 -0
  24. package/internal/app/github_packages_projects.go +429 -0
  25. package/internal/app/github_packages_projects_test.go +168 -0
  26. package/internal/app/github_phase9_overview_stress_test.go +178 -0
  27. package/internal/app/github_pull_views.go +1141 -0
  28. package/internal/app/github_pull_views_test.go +603 -0
  29. package/internal/app/github_pulls.go +1459 -0
  30. package/internal/app/github_pulls_test.go +715 -0
  31. package/internal/app/github_refs.go +621 -0
  32. package/internal/app/github_refs_test.go +260 -0
  33. package/internal/app/github_search_profiles.go +505 -0
  34. package/internal/app/github_search_profiles_test.go +352 -0
  35. package/internal/app/github_test.go +888 -0
  36. package/internal/app/scrape.go +264 -0
  37. package/internal/app/tools.go +670 -0
  38. package/internal/buildinfo/buildinfo.go +16 -0
  39. package/package.json +66 -0
  40. package/scripts/postinstall.js +137 -0
@@ -0,0 +1,503 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "net/http"
9
+ "net/http/httptest"
10
+ "strings"
11
+ "sync/atomic"
12
+ "testing"
13
+ "unicode/utf8"
14
+ )
15
+
16
+ func TestParseGitHubCommitCompareHistoryBlameTargets(t *testing.T) {
17
+ tests := []struct {
18
+ raw string
19
+ kind GitHubTargetKind
20
+ name string
21
+ tail []string
22
+ }{
23
+ {raw: "https://github.com/o/r/commit/abc123", kind: GitHubTargetCommit, name: "abc123"},
24
+ {raw: "https://github.com/o/r/commit/abc123.diff", kind: GitHubTargetCommitDiff, name: "abc123"},
25
+ {raw: "https://github.com/o/r/commit/abc123.patch", kind: GitHubTargetCommitPatch, name: "abc123"},
26
+ {raw: "https://github.com/o/r/compare/main...feature/foo", kind: GitHubTargetCompare, tail: []string{"main", "feature/foo"}},
27
+ {raw: "https://github.com/o/r/compare/feature/base...feature/head.diff", kind: GitHubTargetCompareDiff, tail: []string{"feature/base", "feature/head"}},
28
+ {raw: "https://github.com/o/r/compare/main...next.patch", kind: GitHubTargetComparePatch, tail: []string{"main", "next"}},
29
+ {raw: "https://github.com/o/r/commits/feature/slash/path.go", kind: GitHubTargetHistory, tail: []string{"feature", "slash", "path.go"}},
30
+ {raw: "https://github.com/o/r/blame/feature/slash/path.go", kind: GitHubTargetBlame, tail: []string{"feature", "slash", "path.go"}},
31
+ }
32
+ for _, tt := range tests {
33
+ t.Run(tt.raw, func(t *testing.T) {
34
+ target := parseGitHubTarget(tt.raw)
35
+ if target == nil || target.Kind != tt.kind || target.Name != tt.name {
36
+ t.Fatalf("unexpected target: %#v", target)
37
+ }
38
+ if strings.Join(target.Tail, "|") != strings.Join(tt.tail, "|") {
39
+ t.Fatalf("tail=%v want %v", target.Tail, tt.tail)
40
+ }
41
+ })
42
+ }
43
+ for _, raw := range []string{
44
+ "https://github.com/o/r/commit/",
45
+ "https://github.com/o/r/compare/main..head",
46
+ "https://github.com/o/r/commits",
47
+ "https://github.com/o/r/blame/main",
48
+ } {
49
+ if target := parseGitHubTarget(raw); target != nil {
50
+ t.Fatalf("expected unsupported target for %s, got %#v", raw, target)
51
+ }
52
+ }
53
+ }
54
+
55
+ func TestReadGitHubCommitOverviewStopsAfterFirstFileAndCommentPages(t *testing.T) {
56
+ var commitPages, commentPages int32
57
+ var server *httptest.Server
58
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59
+ w.Header().Set("Content-Type", "application/json")
60
+ switch r.URL.Path {
61
+ case "/repos/o/r/commits/ref":
62
+ atomic.AddInt32(&commitPages, 1)
63
+ if r.URL.Query().Get("page") == "2" {
64
+ t.Fatal("plain commit overview followed file pagination")
65
+ }
66
+ w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/commits/ref?per_page=100&page=2>; rel="next"`, server.URL))
67
+ _, _ = io.WriteString(w, `{
68
+ "sha":"fullsha","html_url":"https://github.com/o/r/commit/fullsha",
69
+ "author":{"login":"alice"},"committer":{"login":"mergebot"},
70
+ "commit":{"message":"Subject\n\nBody","author":{"name":"Alice","date":"2026-08-01T00:00:00Z"},"committer":{"name":"Bot","date":"2026-08-01T01:00:00Z"},"comment_count":2,"verification":{"verified":true,"reason":"valid","verified_at":"2026-08-01T01:00:01Z"}},
71
+ "stats":{"total":3,"additions":2,"deletions":1},
72
+ "parents":[{"sha":"parentparentparent","html_url":"https://github.com/o/r/commit/parent"}],
73
+ "files":[{"filename":"a.go","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/o/r/blob/fullsha/a.go","raw_url":"https://raw.example/a.go","patch":"@@ -1 +1 @@\n-old\n+new"}]
74
+ }`)
75
+ case "/repos/o/r/commits/fullsha/comments":
76
+ atomic.AddInt32(&commentPages, 1)
77
+ if r.URL.Query().Get("page") == "2" {
78
+ t.Fatal("plain commit overview followed comment pagination")
79
+ }
80
+ w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/commits/fullsha/comments?per_page=100&page=2>; rel="next"`, server.URL))
81
+ _, _ = io.WriteString(w, `[{"id":1,"body":"Visible comment\n<!-- hidden marker -->\nkept","html_url":"https://github.com/o/r/commit/fullsha#commitcomment-1","user":{"login":"reviewer"},"author_association":"MEMBER","path":"a.go","line":1,"created_at":"2026-08-02T00:00:00Z","commit_id":"fullsha"}]`)
82
+ default:
83
+ t.Fatalf("unexpected commit request %s?%s", r.URL.Path, r.URL.RawQuery)
84
+ }
85
+ }))
86
+ defer server.Close()
87
+ out, err := readGitHubCommit(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commit/ref"))
88
+ if err != nil {
89
+ t.Fatal(err)
90
+ }
91
+ for _, want := range []string{
92
+ `sha: "fullsha"`, `author: "@alice"`, `committer: "@mergebot"`, `verified: true`, `verification_reason: "valid"`,
93
+ "overview: true", "files_returned: 1", "files_provider_more_available: true", "comments_reported: 2", "comments_returned: 1", "comments_provider_more_available: true",
94
+ "Subject\n\nBody", "### `a.go`", "Selector: https://github.com/o/r/commit/fullsha#diff-", "Blob: https://github.com/o/r/blob/fullsha/a.go", "Raw: https://raw.example/a.go",
95
+ "Comment `1` by @reviewer", "Location: `a.go` line 1", "Visible comment", "kept", "Selector: https://github.com/o/r/commit/fullsha#commitcomment-1",
96
+ "Diff: https://github.com/o/r/commit/fullsha.diff", "Patch: https://github.com/o/r/commit/fullsha.patch",
97
+ } {
98
+ if !strings.Contains(out, want) {
99
+ t.Fatalf("commit output missing %q:\n%s", want, out)
100
+ }
101
+ }
102
+ if strings.Contains(out, "hidden marker") || strings.Contains(out, "```diff") || strings.Contains(out, "+new") {
103
+ t.Fatalf("commit comment HTML marker leaked:\n%s", out)
104
+ }
105
+ if atomic.LoadInt32(&commitPages) != 1 || atomic.LoadInt32(&commentPages) != 1 {
106
+ t.Fatalf("plain commit overview should fetch one file/comment page: commit=%d comments=%d", commitPages, commentPages)
107
+ }
108
+ }
109
+
110
+ func TestRenderGitHubCommitSurfacesThreeThousandFileCap(t *testing.T) {
111
+ detail := githubCommitDetail{SHA: "sha", HTMLURL: "https://github.com/o/r/commit/sha", Files: make([]githubPullFile, githubCommitFilesMax)}
112
+ for i := range detail.Files {
113
+ detail.Files[i].Filename = fmt.Sprintf("file-%04d.txt", i)
114
+ }
115
+ out := renderGitHubCommit(&GitHubTarget{Owner: "o", Repo: "r"}, detail, nil, githubCommitAvailability{FilesProviderMore: true})
116
+ if !strings.Contains(out, "provider_file_ceiling: 3000") || !strings.Contains(out, "files_provider_more_available: true") || !strings.Contains(out, "files_local_omitted:") {
117
+ t.Fatalf("commit provider cap missing")
118
+ }
119
+ if got := utf8.RuneCountInString(out); got > githubOverviewRunes {
120
+ t.Fatalf("3,000-file commit overview exceeded shared target: %d", got)
121
+ }
122
+ }
123
+
124
+ func TestCommitCommentSelectorUsesSingleCommentEndpointAndValidatesCommitIdentity(t *testing.T) {
125
+ const sha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
126
+ longBody := "Exact commit comment\n\n" + strings.Repeat("selected comment detail stays faithful. ", 220)
127
+ var requests int32
128
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129
+ atomic.AddInt32(&requests, 1)
130
+ if r.URL.Path != "/repos/o/r/comments/99" {
131
+ t.Fatalf("commit-comment selector fetched unrelated endpoint: %s", r.URL.Path)
132
+ }
133
+ w.Header().Set("Content-Type", "application/json")
134
+ _ = json.NewEncoder(w).Encode(map[string]any{
135
+ "id": 99, "body": longBody + "\n<!-- hidden -->", "html_url": "https://github.com/o/r/commit/" + sha + "#commitcomment-99",
136
+ "user": map[string]any{"login": "reviewer"}, "path": "a.go", "line": 12, "created_at": "2026-08-01T00:00:00Z", "commit_id": sha,
137
+ })
138
+ }))
139
+ defer server.Close()
140
+
141
+ out, err := readGitHubCommit(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commit/"+sha+"#commitcomment-99"))
142
+ if err != nil {
143
+ t.Fatal(err)
144
+ }
145
+ if got := atomic.LoadInt32(&requests); got != 1 {
146
+ t.Fatalf("exact commit-comment selector should use one provider request, got %d", got)
147
+ }
148
+ for _, want := range []string{"comment_id: 99", "commit: \"" + sha + "\"", "Comment by @reviewer", "`a.go` line 12", strings.TrimSpace(longBody)} {
149
+ if !strings.Contains(out, want) {
150
+ t.Fatalf("selected commit comment missing %q:\n%s", want, out)
151
+ }
152
+ }
153
+ if strings.Contains(out, "hidden") || strings.Contains(out, "Changed-file index") {
154
+ t.Fatalf("selected commit comment leaked hidden/parent context:\n%s", out)
155
+ }
156
+
157
+ atomic.StoreInt32(&requests, 0)
158
+ wrong := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
159
+ _, err = readGitHubCommit(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commit/"+wrong+"#commitcomment-99"))
160
+ if err == nil || !strings.Contains(err.Error(), "belongs to commit") {
161
+ t.Fatalf("mismatched commit-comment identity was not rejected: %v", err)
162
+ }
163
+ if got := atomic.LoadInt32(&requests); got != 1 {
164
+ t.Fatalf("mismatched canonical commit-comment selector should reject after one provider request, got %d", got)
165
+ }
166
+ }
167
+
168
+ func TestCommitDiffSelectorsTraverseProviderPagesAndSupportLineRanges(t *testing.T) {
169
+ hash := githubDiffPathHash("b.go")
170
+ for _, tt := range []struct {
171
+ name string
172
+ fragment string
173
+ want []string
174
+ forbidden []string
175
+ }{
176
+ {name: "file", fragment: "diff-" + hash, want: []string{"@@ -1 +1 @@", "+new1", "@@ -10 +10 @@", "+new10"}, forbidden: []string{"a.go", "+a"}},
177
+ {name: "line-range", fragment: "diff-" + hash + "R10-R10", want: []string{"@@ -10 +10 @@", "+new10"}, forbidden: []string{"@@ -1 +1 @@", "\n+new1\n", "a.go"}},
178
+ } {
179
+ t.Run(tt.name, func(t *testing.T) {
180
+ var pages int32
181
+ var server *httptest.Server
182
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
183
+ atomic.AddInt32(&pages, 1)
184
+ w.Header().Set("Content-Type", "application/json")
185
+ if r.URL.Path != "/repos/o/r/commits/ref" {
186
+ t.Fatalf("unexpected exact commit diff endpoint: %s", r.URL.Path)
187
+ }
188
+ if r.URL.Query().Get("page") == "2" {
189
+ _, _ = io.WriteString(w, `{"sha":"fullsha","html_url":"https://github.com/o/r/commit/fullsha","files":[{"filename":"b.go","status":"modified","patch":"@@ -1 +1 @@\n-old1\n+new1\n@@ -10 +10 @@\n-old10\n+new10"}]}`)
190
+ return
191
+ }
192
+ w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/commits/ref?per_page=100&page=2>; rel="next"`, server.URL))
193
+ _, _ = io.WriteString(w, `{"sha":"fullsha","html_url":"https://github.com/o/r/commit/fullsha","files":[{"filename":"a.go","status":"modified","patch":"@@ -1 +1 @@\n-a\n+b"}]}`)
194
+ }))
195
+ defer server.Close()
196
+ out, err := readGitHubCommit(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commit/ref#"+tt.fragment))
197
+ if err != nil {
198
+ t.Fatal(err)
199
+ }
200
+ if got := atomic.LoadInt32(&pages); got != 2 {
201
+ t.Fatalf("exact commit diff selector should traverse provider pages, got %d", got)
202
+ }
203
+ for _, want := range append([]string{"file: \"b.go\"", "selector: \"" + tt.fragment + "\"", "Diff: https://github.com/o/r/commit/fullsha.diff", "Patch: https://github.com/o/r/commit/fullsha.patch"}, tt.want...) {
204
+ if !strings.Contains(out, want) {
205
+ t.Fatalf("exact commit diff missing %q:\n%s", want, out)
206
+ }
207
+ }
208
+ for _, forbidden := range tt.forbidden {
209
+ if strings.Contains(out, forbidden) {
210
+ t.Fatalf("exact commit diff leaked %q:\n%s", forbidden, out)
211
+ }
212
+ }
213
+ })
214
+ }
215
+ }
216
+
217
+ func TestLargeCommitOverviewBoundsFileAndCommentIndexesWithoutPatchBodies(t *testing.T) {
218
+ detail := githubCommitDetail{SHA: "fullsha", HTMLURL: "https://github.com/o/r/commit/fullsha"}
219
+ detail.Commit.Message = "Subject\n\nFull commit message body remains faithful."
220
+ detail.Commit.CommentCount = 120
221
+ detail.Stats.Total = 1000
222
+ detail.Stats.Additions = 700
223
+ detail.Stats.Deletions = 300
224
+ for i := 0; i < 100; i++ {
225
+ patch := fmt.Sprintf("@@ -1 +1 @@\n-old %03d\n+new %03d\nTAIL PATCH %03d MUST NOT APPEAR", i, i, i)
226
+ detail.Files = append(detail.Files, githubPullFile{Filename: fmt.Sprintf("pkg/file-%03d.go", i), Status: "modified", Additions: 7, Deletions: 3, Changes: 10, BlobURL: fmt.Sprintf("https://github.com/o/r/blob/fullsha/pkg/file-%03d.go", i), RawURL: fmt.Sprintf("https://raw.example/pkg/file-%03d.go", i), Patch: &patch})
227
+ }
228
+ comments := make([]githubCommitComment, 0, 100)
229
+ for i := 0; i < 100; i++ {
230
+ body := fmt.Sprintf("Comment %03d marker. %s TAIL COMMENT %03d MUST NOT APPEAR", i, strings.Repeat("comment detail. ", 50), i)
231
+ comments = append(comments, githubCommitComment{ID: int64(1000 + i), Body: &body, HTMLURL: fmt.Sprintf("https://github.com/o/r/commit/fullsha#commitcomment-%d", 1000+i), User: githubUser{Login: fmt.Sprintf("user-%03d", i)}, CommitID: "fullsha"})
232
+ }
233
+ out := renderGitHubCommit(&GitHubTarget{Owner: "o", Repo: "r"}, detail, comments, githubCommitAvailability{FilesProviderMore: true, CommentsProviderMore: true})
234
+ if got := utf8.RuneCountInString(out); got > githubOverviewRunes {
235
+ t.Fatalf("large commit overview exceeded shared target: %d runes\n%s", got, out)
236
+ }
237
+ for _, want := range []string{"Full commit message body remains faithful.", "files_local_omitted:", "comments_local_omitted:", "files_provider_more_available: true", "comments_provider_more_available: true", "Selector: https://github.com/o/r/commit/fullsha#diff-", "Selector: https://github.com/o/r/commit/fullsha#commitcomment-1000", "Diff: https://github.com/o/r/commit/fullsha.diff"} {
238
+ if !strings.Contains(out, want) {
239
+ t.Fatalf("large commit overview missing %q:\n%s", want, out)
240
+ }
241
+ }
242
+ if strings.Contains(out, "TAIL PATCH 000 MUST NOT APPEAR") || strings.Contains(out, "TAIL COMMENT 000 MUST NOT APPEAR") || strings.Contains(out, "```diff") {
243
+ t.Fatalf("large commit overview leaked subordinate bodies:\n%s", out)
244
+ }
245
+ }
246
+
247
+ func TestCommitRawDiffPatchPreserveMedia(t *testing.T) {
248
+ for _, tt := range []struct {
249
+ patch bool
250
+ accept string
251
+ body string
252
+ }{
253
+ {accept: "application/vnd.github.v3.diff", body: "diff --git a/a b/a\n-old\n+new\n"},
254
+ {patch: true, accept: "application/vnd.github.v3.patch", body: "From abc Mon Sep 17 00:00:00 2001\nSubject: commit\n"},
255
+ } {
256
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
257
+ if got := r.Header.Get("Accept"); got != tt.accept {
258
+ t.Errorf("Accept=%q want %q", got, tt.accept)
259
+ }
260
+ _, _ = io.WriteString(w, tt.body)
261
+ }))
262
+ got, err := readGitHubCommitRawDiff(context.Background(), testGitHubClient(server.URL, server.URL, ""), &GitHubTarget{Owner: "o", Repo: "r", Name: "sha"}, tt.patch)
263
+ server.Close()
264
+ if err != nil || got != tt.body {
265
+ t.Fatalf("raw commit media got %q err=%v", got, err)
266
+ }
267
+ }
268
+ }
269
+
270
+ func TestReadGitHubCompareOverviewStopsAfterFirstProviderPage(t *testing.T) {
271
+ var requests int32
272
+ var server *httptest.Server
273
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
274
+ atomic.AddInt32(&requests, 1)
275
+ if !strings.Contains(r.RequestURI, "main...feature%2Ffoo") && !strings.Contains(r.RequestURI, "main...feature/foo") {
276
+ t.Errorf("slash-containing compare head was not preserved in endpoint: %s", r.RequestURI)
277
+ }
278
+ w.Header().Set("Content-Type", "application/json")
279
+ if r.URL.Query().Get("page") == "2" {
280
+ t.Fatal("plain compare overview followed provider pagination")
281
+ }
282
+ w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/compare/main...feature%%2Ffoo?per_page=100&page=2>; rel="next"`, server.URL))
283
+ _, _ = io.WriteString(w, `{
284
+ "html_url":"https://github.com/o/r/compare/main...feature/foo","status":"ahead","ahead_by":2,"behind_by":0,"total_commits":2,
285
+ "commits":[{"sha":"a","html_url":"https://github.com/o/r/commit/a","commit":{"message":"First","author":{"date":"2026-08-01T00:00:00Z"}}}],
286
+ "files":[{"filename":"a.go","status":"modified","patch":"@@ -1 +1 @@\n-a\n+b"}]
287
+ }`)
288
+ }))
289
+ defer server.Close()
290
+ out, err := readGitHubCompare(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/compare/main...feature/foo"))
291
+ if err != nil {
292
+ t.Fatal(err)
293
+ }
294
+ for _, want := range []string{
295
+ `base: "main"`, `head: "feature/foo"`, "overview: true", "total_commits: 2", "commits_returned: 1", "commits_provider_complete: false", "commits_provider_more_available: true",
296
+ "files_returned: 1", "files_complete: true", "First", "### `a.go`",
297
+ "Diff: https://github.com/o/r/compare/main...feature/foo.diff", "Patch: https://github.com/o/r/compare/main...feature/foo.patch",
298
+ } {
299
+ if !strings.Contains(out, want) {
300
+ t.Fatalf("compare output missing %q:\n%s", want, out)
301
+ }
302
+ }
303
+ if strings.Contains(out, "Second") || strings.Contains(out, "```diff") || atomic.LoadInt32(&requests) != 1 {
304
+ t.Fatalf("compare overview expanded provider pages/patches (requests=%d):\n%s", requests, out)
305
+ }
306
+ }
307
+
308
+ func TestRenderGitHubCompareSurfacesThreeHundredFileCapAndCommitMismatch(t *testing.T) {
309
+ result := githubCompareResult{Status: "ahead", TotalCommits: 5, Files: make([]githubPullFile, githubCompareFilesMax), Commits: []githubPullCommit{{SHA: "a"}}}
310
+ for i := range result.Files {
311
+ result.Files[i].Filename = fmt.Sprintf("file-%03d.txt", i)
312
+ }
313
+ out := renderGitHubCompare(&GitHubTarget{Owner: "o", Repo: "r"}, "main", "head", result, githubCompareAvailability{}, false)
314
+ if !strings.Contains(out, "commits_provider_complete: false") || !strings.Contains(out, "files_complete: false") || !strings.Contains(out, "provider_file_ceiling: 300") || !strings.Contains(out, "up to 300 files") || !strings.Contains(out, "provider-incomplete") {
315
+ t.Fatalf("compare completeness truth missing")
316
+ }
317
+ if got := utf8.RuneCountInString(out); got > githubOverviewRunes {
318
+ t.Fatalf("300-file compare overview exceeded shared target: %d", got)
319
+ }
320
+ }
321
+
322
+ func TestCompareRawDiffPatchPreserveMedia(t *testing.T) {
323
+ for _, tt := range []struct {
324
+ patch bool
325
+ accept string
326
+ body string
327
+ }{
328
+ {accept: "application/vnd.github.v3.diff", body: "diff compare\n"},
329
+ {patch: true, accept: "application/vnd.github.v3.patch", body: "patch compare\n"},
330
+ } {
331
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
332
+ if got := r.Header.Get("Accept"); got != tt.accept {
333
+ t.Errorf("Accept=%q want %q", got, tt.accept)
334
+ }
335
+ _, _ = io.WriteString(w, tt.body)
336
+ }))
337
+ got, err := readGitHubCompareRawDiff(context.Background(), testGitHubClient(server.URL, server.URL, ""), &GitHubTarget{Owner: "o", Repo: "r", Tail: []string{"base", "head"}}, tt.patch)
338
+ server.Close()
339
+ if err != nil || got != tt.body {
340
+ t.Fatalf("raw compare media got %q err=%v", got, err)
341
+ }
342
+ }
343
+ }
344
+
345
+ func TestHistoryResolverHandlesSlashRefAndBoundedPageNavigation(t *testing.T) {
346
+ var resolverCalls int32
347
+ var server *httptest.Server
348
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
349
+ if r.URL.Path != "/repos/o/r/commits" {
350
+ t.Fatalf("unexpected history path %s", r.URL.Path)
351
+ }
352
+ w.Header().Set("Content-Type", "application/json")
353
+ sha := r.URL.Query().Get("sha")
354
+ path := r.URL.Query().Get("path")
355
+ perPage := r.URL.Query().Get("per_page")
356
+ if perPage == "1" {
357
+ atomic.AddInt32(&resolverCalls, 1)
358
+ if sha == "feature/slash" && path == "path.go" {
359
+ _, _ = io.WriteString(w, `[{"sha":"probe"}]`)
360
+ return
361
+ }
362
+ w.WriteHeader(http.StatusNotFound)
363
+ _, _ = io.WriteString(w, `{"message":"Not Found"}`)
364
+ return
365
+ }
366
+ if sha != "feature/slash" || path != "path.go" || perPage != "8" {
367
+ t.Fatalf("resolved history query incorrect: %s", r.URL.RawQuery)
368
+ }
369
+ w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/commits?sha=feature%%2Fslash&path=path.go&per_page=8&page=2>; rel="next"`, server.URL))
370
+ _, _ = io.WriteString(w, `[{"sha":"abc","html_url":"https://github.com/o/r/commit/abc","author":{"login":"alice"},"commit":{"message":"Change path","author":{"date":"2026-08-01T00:00:00Z"}}}]`)
371
+ }))
372
+ defer server.Close()
373
+ target := parseGitHubTarget("https://github.com/o/r/commits/feature/slash/path.go")
374
+ out, err := readGitHubHistory(context.Background(), testGitHubClient(server.URL, server.URL, ""), target)
375
+ if err != nil {
376
+ t.Fatal(err)
377
+ }
378
+ if !strings.Contains(out, `ref: "feature/slash"`) || !strings.Contains(out, `path: "path.go"`) || !strings.Contains(out, "Change path") || !strings.Contains(out, "Next: https://github.com/o/r/commits/feature/slash/path.go?page=2") {
379
+ t.Fatalf("slash-ref history output incorrect:\n%s", out)
380
+ }
381
+ if atomic.LoadInt32(&resolverCalls) != 3 {
382
+ t.Fatalf("expected three candidate resolution probes, got %d", resolverCalls)
383
+ }
384
+ }
385
+
386
+ func TestHistoryResolverRejectsOverlappingValidRefPathSplits(t *testing.T) {
387
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
388
+ w.Header().Set("Content-Type", "application/json")
389
+ if r.URL.Query().Get("per_page") == "1" {
390
+ _, _ = io.WriteString(w, `[{"sha":"probe"}]`)
391
+ return
392
+ }
393
+ t.Fatalf("ambiguous history should not reach full list request")
394
+ }))
395
+ defer server.Close()
396
+ _, err := readGitHubHistory(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commits/feature/path.go"))
397
+ if err == nil || !strings.Contains(err.Error(), "ambiguous") {
398
+ t.Fatalf("expected history ambiguity, got %v", err)
399
+ }
400
+ }
401
+
402
+ func TestHistoryRejectsInvalidPage(t *testing.T) {
403
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
404
+ w.Header().Set("Content-Type", "application/json")
405
+ if r.URL.Query().Get("per_page") == "1" {
406
+ if r.URL.Query().Get("sha") == "main" && r.URL.Query().Get("path") == "path.go" {
407
+ _, _ = io.WriteString(w, `[{"sha":"probe"}]`)
408
+ return
409
+ }
410
+ w.WriteHeader(http.StatusNotFound)
411
+ _, _ = io.WriteString(w, `{"message":"Not Found"}`)
412
+ return
413
+ }
414
+ t.Fatal("invalid page should fail before full history request")
415
+ }))
416
+ defer server.Close()
417
+ _, err := readGitHubHistory(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/commits/main/path.go?page=nope"))
418
+ if err == nil || !strings.Contains(err.Error(), "invalid GitHub commit-history page") {
419
+ t.Fatalf("expected invalid page error, got %v", err)
420
+ }
421
+ }
422
+
423
+ func TestBlameWithoutTokenFailsBeforeProviderRead(t *testing.T) {
424
+ var calls int32
425
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
426
+ atomic.AddInt32(&calls, 1)
427
+ t.Fatal("no-token blame should not make provider requests")
428
+ }))
429
+ defer server.Close()
430
+ _, err := readGitHubBlame(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/blame/main/a.go"))
431
+ if err == nil || !strings.Contains(err.Error(), "requires authentication") || !strings.Contains(err.Error(), "GH_TOKEN") {
432
+ t.Fatalf("expected concise auth-required blame result, got %v", err)
433
+ }
434
+ if atomic.LoadInt32(&calls) != 0 {
435
+ t.Fatalf("no-token blame made provider calls")
436
+ }
437
+ }
438
+
439
+ func TestAuthenticatedBlameResolvesSlashRefAndRendersRanges(t *testing.T) {
440
+ var graphqlCalls int32
441
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
442
+ w.Header().Set("Content-Type", "application/json")
443
+ switch r.URL.Path {
444
+ case "/repos/o/r/contents/slash/a.go":
445
+ if r.URL.Query().Get("ref") != "feature" {
446
+ t.Fatalf("unexpected first ref %q", r.URL.Query().Get("ref"))
447
+ }
448
+ w.WriteHeader(http.StatusNotFound)
449
+ _, _ = io.WriteString(w, `{"message":"Not Found"}`)
450
+ case "/repos/o/r/contents/a.go":
451
+ if r.URL.Query().Get("ref") != "feature/slash" {
452
+ t.Fatalf("resolved slash ref lost: %q", r.URL.Query().Get("ref"))
453
+ }
454
+ _, _ = io.WriteString(w, `{"type":"file","path":"a.go","sha":"blobsha"}`)
455
+ case "/graphql":
456
+ atomic.AddInt32(&graphqlCalls, 1)
457
+ if r.Header.Get("Authorization") != "Bearer fake-token" {
458
+ t.Errorf("GraphQL auth missing")
459
+ }
460
+ body, _ := io.ReadAll(r.Body)
461
+ if !strings.Contains(string(body), `"expression":"feature/slash"`) || !strings.Contains(string(body), `"path":"a.go"`) || !strings.Contains(string(body), "blame(path:$path)") {
462
+ t.Errorf("blame variables/query wrong: %s", body)
463
+ }
464
+ _, _ = io.WriteString(w, `{"data":{"repository":{"object":{"oid":"commitsha","blame":{"ranges":[
465
+ {"startingLine":1,"endingLine":10,"age":1,"commit":{"oid":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","abbreviatedOid":"aaaaaaa","committedDate":"2026-08-01T00:00:00Z","messageHeadline":"Initial","url":"https://github.com/o/r/commit/a","author":{"name":"Alice","user":{"login":"alice"}}}},
466
+ {"startingLine":11,"endingLine":20,"age":4,"commit":{"oid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","abbreviatedOid":"bbbbbbb","committedDate":"2026-08-02T00:00:00Z","messageHeadline":"Next","url":"https://github.com/o/r/commit/b","author":{"name":"Bob","user":null}}}
467
+ ]}}}}}`)
468
+ default:
469
+ t.Fatalf("unexpected blame request %s", r.URL.Path)
470
+ }
471
+ }))
472
+ defer server.Close()
473
+ out, err := readGitHubBlame(context.Background(), testGitHubClient(server.URL, server.URL, "fake-token"), parseGitHubTarget("https://github.com/o/r/blame/feature/slash/a.go"))
474
+ if err != nil {
475
+ t.Fatal(err)
476
+ }
477
+ for _, want := range []string{`ref: "feature/slash"`, `path: "a.go"`, `commit: "commitsha"`, "Lines 1-10", "@alice", "Initial", "Lines 11-20", "Bob", "Next"} {
478
+ if !strings.Contains(out, want) {
479
+ t.Fatalf("blame output missing %q:\n%s", want, out)
480
+ }
481
+ }
482
+ if atomic.LoadInt32(&graphqlCalls) != 1 {
483
+ t.Fatalf("expected one GraphQL blame call")
484
+ }
485
+ }
486
+
487
+ func TestCommitCompareHistoryBlameFailuresStayNative(t *testing.T) {
488
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
489
+ w.WriteHeader(http.StatusNotFound)
490
+ _, _ = io.WriteString(w, `{"message":"private provider body"}`)
491
+ }))
492
+ defer server.Close()
493
+ for _, raw := range []string{
494
+ "https://github.com/o/r/commit/abc",
495
+ "https://github.com/o/r/compare/main...head",
496
+ "https://github.com/o/r/commits/main/path.go",
497
+ } {
498
+ result := readGitHubNativeWithClient(context.Background(), parseGitHubTarget(raw), testGitHubClient(server.URL, server.URL, ""))
499
+ if result.Outcome != GitHubNativeFailure || result.Err == nil || !strings.Contains(result.Err.Error(), "may be private") || strings.Contains(result.Err.Error(), "private provider body") {
500
+ t.Fatalf("native failure boundary regressed for %s: %#v", raw, result)
501
+ }
502
+ }
503
+ }