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.
- package/AGENTS.md +97 -0
- package/CONTRIBUTORS.md +126 -0
- package/LICENSE +202 -0
- package/Makefile +69 -0
- package/README.md +121 -0
- package/bin/webctx.js +28 -0
- package/cmd/webctx/main.go +11 -0
- package/docs/porting-status.md +173 -0
- package/go.mod +3 -0
- package/internal/app/app.go +139 -0
- package/internal/app/app_test.go +194 -0
- package/internal/app/github.go +2084 -0
- package/internal/app/github_acceptance_test.go +166 -0
- package/internal/app/github_actions.go +1008 -0
- package/internal/app/github_actions_test.go +484 -0
- package/internal/app/github_activity_deployments.go +503 -0
- package/internal/app/github_activity_deployments_test.go +321 -0
- package/internal/app/github_commits.go +1004 -0
- package/internal/app/github_commits_test.go +503 -0
- package/internal/app/github_discussions_gists.go +1156 -0
- package/internal/app/github_discussions_gists_test.go +425 -0
- package/internal/app/github_issues.go +1616 -0
- package/internal/app/github_issues_test.go +731 -0
- package/internal/app/github_packages_projects.go +429 -0
- package/internal/app/github_packages_projects_test.go +168 -0
- package/internal/app/github_phase9_overview_stress_test.go +178 -0
- package/internal/app/github_pull_views.go +1141 -0
- package/internal/app/github_pull_views_test.go +603 -0
- package/internal/app/github_pulls.go +1459 -0
- package/internal/app/github_pulls_test.go +715 -0
- package/internal/app/github_refs.go +621 -0
- package/internal/app/github_refs_test.go +260 -0
- package/internal/app/github_search_profiles.go +505 -0
- package/internal/app/github_search_profiles_test.go +352 -0
- package/internal/app/github_test.go +888 -0
- package/internal/app/scrape.go +264 -0
- package/internal/app/tools.go +670 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/package.json +66 -0
- 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
|
+
}
|