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,260 @@
|
|
|
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 TestParseGitHubRefReleaseSocialTargets(t *testing.T) {
|
|
17
|
+
tests := []struct {
|
|
18
|
+
raw string
|
|
19
|
+
kind GitHubTargetKind
|
|
20
|
+
name string
|
|
21
|
+
}{
|
|
22
|
+
{raw: "https://github.com/o/r/branches", kind: GitHubTargetBranches},
|
|
23
|
+
{raw: "https://github.com/o/r/tags", kind: GitHubTargetTags},
|
|
24
|
+
{raw: "https://github.com/o/r/releases", kind: GitHubTargetReleases},
|
|
25
|
+
{raw: "https://github.com/o/r/releases/latest", kind: GitHubTargetReleaseLatest},
|
|
26
|
+
{raw: "https://github.com/o/r/releases/tag/v1.2.3", kind: GitHubTargetRelease, name: "v1.2.3"},
|
|
27
|
+
{raw: "https://github.com/o/r/releases/tag/release/foo", kind: GitHubTargetRelease, name: "release/foo"},
|
|
28
|
+
{raw: "https://github.com/o/r/forks", kind: GitHubTargetForks},
|
|
29
|
+
{raw: "https://github.com/o/r/stargazers", kind: GitHubTargetStargazers},
|
|
30
|
+
{raw: "https://github.com/o/r/watchers", kind: GitHubTargetWatchers},
|
|
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
|
+
})
|
|
39
|
+
}
|
|
40
|
+
for _, raw := range []string{
|
|
41
|
+
"https://github.com/o/r/branches/main",
|
|
42
|
+
"https://github.com/o/r/tags/v1",
|
|
43
|
+
"https://github.com/o/r/releases/123",
|
|
44
|
+
"https://github.com/o/r/stargazers/extra",
|
|
45
|
+
} {
|
|
46
|
+
if target := parseGitHubTarget(raw); target != nil {
|
|
47
|
+
t.Fatalf("unstable/synthetic route should stay unsupported: %s => %#v", raw, target)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func TestBranchesBoundedPaginationAndProtection(t *testing.T) {
|
|
53
|
+
var server *httptest.Server
|
|
54
|
+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
55
|
+
if r.URL.Path != "/repos/o/r/branches" {
|
|
56
|
+
t.Fatalf("unexpected branch path %s", r.URL.Path)
|
|
57
|
+
}
|
|
58
|
+
if r.URL.Query().Get("per_page") != "8" || r.URL.Query().Get("page") != "2" || r.URL.Query().Get("protected") != "true" {
|
|
59
|
+
t.Fatalf("branch query lost: %s", r.URL.RawQuery)
|
|
60
|
+
}
|
|
61
|
+
w.Header().Set("Content-Type", "application/json")
|
|
62
|
+
w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/branches?protected=true&per_page=8&page=1>; rel="prev", <%s/repos/o/r/branches?protected=true&per_page=8&page=3>; rel="next"`, server.URL, server.URL))
|
|
63
|
+
_, _ = io.WriteString(w, `[{"name":"release/next","protected":true,"commit":{"sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}]`)
|
|
64
|
+
}))
|
|
65
|
+
defer server.Close()
|
|
66
|
+
out, err := readGitHubBranches(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/branches?protected=true&page=2"))
|
|
67
|
+
if err != nil {
|
|
68
|
+
t.Fatal(err)
|
|
69
|
+
}
|
|
70
|
+
for _, want := range []string{"view: \"branches\"", "page: 2", "[release/next](https://github.com/o/r/tree/release/next)", "`aaaaaaaaaaaa`", "protected", "Previous", "page=1", "Next", "page=3"} {
|
|
71
|
+
if !strings.Contains(out, want) {
|
|
72
|
+
t.Fatalf("branches output missing %q:\n%s", want, out)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestTagsBoundedAndLinkToTreeRef(t *testing.T) {
|
|
78
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
79
|
+
if r.Header.Get("Accept") != "application/vnd.github+json" {
|
|
80
|
+
t.Errorf("unexpected Accept %q", r.Header.Get("Accept"))
|
|
81
|
+
}
|
|
82
|
+
w.Header().Set("Content-Type", "application/json")
|
|
83
|
+
_, _ = io.WriteString(w, `[{"name":"release/v1","commit":{"sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}]`)
|
|
84
|
+
}))
|
|
85
|
+
defer server.Close()
|
|
86
|
+
out, err := readGitHubTags(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/tags"))
|
|
87
|
+
if err != nil {
|
|
88
|
+
t.Fatal(err)
|
|
89
|
+
}
|
|
90
|
+
if !strings.Contains(out, "[release/v1](https://github.com/o/r/tree/release/v1)") || !strings.Contains(out, "`bbbbbbbbbbbb`") {
|
|
91
|
+
t.Fatalf("tags output incorrect:\n%s", out)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestReleaseListDoesNotExpandBodies(t *testing.T) {
|
|
96
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
97
|
+
w.Header().Set("Content-Type", "application/json")
|
|
98
|
+
_, _ = io.WriteString(w, `[
|
|
99
|
+
{"id":1,"tag_name":"v2","name":"Version 2","body":"MUST NOT EXPAND","html_url":"https://github.com/o/r/releases/tag/v2","published_at":"2026-08-01T00:00:00Z"},
|
|
100
|
+
{"id":2,"tag_name":"v1-rc","name":"","body":"MUST NOT EXPAND EITHER","html_url":"https://github.com/o/r/releases/tag/v1-rc","prerelease":true,"published_at":"2026-07-01T00:00:00Z"}
|
|
101
|
+
]`)
|
|
102
|
+
}))
|
|
103
|
+
defer server.Close()
|
|
104
|
+
out, err := readGitHubReleases(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/releases"))
|
|
105
|
+
if err != nil {
|
|
106
|
+
t.Fatal(err)
|
|
107
|
+
}
|
|
108
|
+
if !strings.Contains(out, "[Version 2](https://github.com/o/r/releases/tag/v2)") || !strings.Contains(out, "[v1-rc](https://github.com/o/r/releases/tag/v1-rc)") || !strings.Contains(out, "prerelease") || strings.Contains(out, "MUST NOT EXPAND") {
|
|
109
|
+
t.Fatalf("release list expanded body or lost metadata:\n%s", out)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func TestReleaseDetailBoundsLongBodyAndStopsAssetPagination(t *testing.T) {
|
|
114
|
+
longBody := strings.Repeat("release-notes-line\n", 500)
|
|
115
|
+
assets := make([]githubReleaseAsset, 100)
|
|
116
|
+
for i := range assets {
|
|
117
|
+
assets[i] = githubReleaseAsset{
|
|
118
|
+
ID: int64(i + 1),
|
|
119
|
+
Name: fmt.Sprintf("asset-%03d.zip", i),
|
|
120
|
+
Size: int64(1024 + i),
|
|
121
|
+
ContentType: "application/zip",
|
|
122
|
+
DownloadCount: i,
|
|
123
|
+
BrowserDownloadURL: fmt.Sprintf("https://github.com/o/r/releases/download/release/v2/asset-%03d.zip", i),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var assetPages int32
|
|
127
|
+
var server *httptest.Server
|
|
128
|
+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
129
|
+
w.Header().Set("Content-Type", "application/json")
|
|
130
|
+
switch r.URL.Path {
|
|
131
|
+
case "/repos/o/r/releases/tags/release%2Fv2", "/repos/o/r/releases/tags/release/v2":
|
|
132
|
+
_, _ = io.WriteString(w, fmt.Sprintf(`{"id":10,"tag_name":"release/v2","target_commitish":"main","name":"Big Release","body":%q,"html_url":"https://github.com/o/r/releases/tag/release/v2","author":{"login":"maintainer"},"published_at":"2026-08-01T00:00:00Z"}`, longBody+"<!-- hidden -->\nvisible-tail"))
|
|
133
|
+
case "/repos/o/r/releases/10/assets":
|
|
134
|
+
atomic.AddInt32(&assetPages, 1)
|
|
135
|
+
if r.URL.Query().Get("page") != "" || r.URL.Query().Get("per_page") != "100" {
|
|
136
|
+
t.Fatalf("release asset overview followed/changed pagination: %s", r.URL.RawQuery)
|
|
137
|
+
}
|
|
138
|
+
w.Header().Set("Link", fmt.Sprintf(`<%s/repos/o/r/releases/10/assets?per_page=100&page=2>; rel="next"`, server.URL))
|
|
139
|
+
_ = json.NewEncoder(w).Encode(assets)
|
|
140
|
+
default:
|
|
141
|
+
t.Fatalf("unexpected release request %s", r.URL.Path)
|
|
142
|
+
}
|
|
143
|
+
}))
|
|
144
|
+
defer server.Close()
|
|
145
|
+
out, err := readGitHubRelease(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/releases/tag/release/v2"), false)
|
|
146
|
+
if err != nil {
|
|
147
|
+
t.Fatal(err)
|
|
148
|
+
}
|
|
149
|
+
if got := utf8.RuneCountInString(out); got > githubOverviewRunes {
|
|
150
|
+
t.Fatalf("release overview exceeded shared target: %d runes\n%s", got, out)
|
|
151
|
+
}
|
|
152
|
+
for _, want := range []string{
|
|
153
|
+
"overview: true", "assets_returned: 100", "assets_provider_more_available: true", "assets_local_omitted:",
|
|
154
|
+
"Release notes preview locally truncated", "Canonical GitHub release page (complete notes in the browser): https://github.com/o/r/releases/tag/release/v2",
|
|
155
|
+
"asset-000.zip", "https://github.com/o/r/releases/download/release/v2/asset-000.zip",
|
|
156
|
+
"locally omitted from this overview", "more release assets beyond the provider page",
|
|
157
|
+
} {
|
|
158
|
+
if !strings.Contains(out, want) {
|
|
159
|
+
t.Fatalf("release overview missing %q:\n%s", want, out)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if strings.Contains(out, "assets_local_omitted: 0") || strings.Contains(out, "asset-099.zip") || strings.Contains(out, "visible-tail") || strings.Contains(out, "hidden") || atomic.LoadInt32(&assetPages) != 1 {
|
|
163
|
+
t.Fatalf("release overview expanded subordinate content/provider pages; pages=%d\n%s", assetPages, out)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func TestLatestReleaseUsesLatestEndpoint(t *testing.T) {
|
|
168
|
+
var gotPath string
|
|
169
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
170
|
+
gotPath = r.URL.Path
|
|
171
|
+
w.Header().Set("Content-Type", "application/json")
|
|
172
|
+
if strings.HasSuffix(r.URL.Path, "/releases/latest") {
|
|
173
|
+
_, _ = io.WriteString(w, `{"id":10,"tag_name":"v2","name":"Latest"}`)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if strings.HasSuffix(r.URL.Path, "/releases/10/assets") {
|
|
177
|
+
_, _ = io.WriteString(w, `[]`)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
t.Fatalf("unexpected latest endpoint %s", r.URL.Path)
|
|
181
|
+
}))
|
|
182
|
+
defer server.Close()
|
|
183
|
+
_, err := readGitHubRelease(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/releases/latest"), true)
|
|
184
|
+
if err != nil {
|
|
185
|
+
t.Fatal(err)
|
|
186
|
+
}
|
|
187
|
+
if !strings.HasSuffix(gotPath, "/releases/10/assets") {
|
|
188
|
+
t.Fatalf("latest release did not proceed through expected endpoints; final=%s", gotPath)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func TestForksUseStargazersCountNotWatchersAlias(t *testing.T) {
|
|
193
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
194
|
+
w.Header().Set("Content-Type", "application/json")
|
|
195
|
+
_, _ = io.WriteString(w, `[{"full_name":"u/r","html_url":"https://github.com/u/r","stargazers_count":12,"watchers_count":999,"language":"Go","updated_at":"2026-08-01T00:00:00Z","owner":{"login":"u"}}]`)
|
|
196
|
+
}))
|
|
197
|
+
defer server.Close()
|
|
198
|
+
out, err := readGitHubForks(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/forks?sort=stargazers"))
|
|
199
|
+
if err != nil {
|
|
200
|
+
t.Fatal(err)
|
|
201
|
+
}
|
|
202
|
+
if !strings.Contains(out, "12 stars") || strings.Contains(out, "999") || strings.Contains(out, "999 watchers") {
|
|
203
|
+
t.Fatalf("fork star/watcher semantics wrong:\n%s", out)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func TestStargazersUseStarMediaTypeAndDates(t *testing.T) {
|
|
208
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
209
|
+
if got := r.Header.Get("Accept"); got != "application/vnd.github.star+json" {
|
|
210
|
+
t.Errorf("stargazer media type=%q", got)
|
|
211
|
+
}
|
|
212
|
+
w.Header().Set("Content-Type", "application/json")
|
|
213
|
+
_, _ = io.WriteString(w, `[{"starred_at":"2026-08-01T00:00:00Z","user":{"login":"star-user"}}]`)
|
|
214
|
+
}))
|
|
215
|
+
defer server.Close()
|
|
216
|
+
out, err := readGitHubStargazers(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/stargazers"))
|
|
217
|
+
if err != nil {
|
|
218
|
+
t.Fatal(err)
|
|
219
|
+
}
|
|
220
|
+
if !strings.Contains(out, "[@star-user](https://github.com/star-user)") || !strings.Contains(out, "starred 2026-08-01") || !strings.Contains(out, "not the subscriber count") {
|
|
221
|
+
t.Fatalf("stargazer output incorrect:\n%s", out)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func TestWatchersUseSubscribersEndpointAndNaming(t *testing.T) {
|
|
226
|
+
var gotPath string
|
|
227
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
228
|
+
gotPath = r.URL.Path
|
|
229
|
+
w.Header().Set("Content-Type", "application/json")
|
|
230
|
+
_, _ = io.WriteString(w, `[{"login":"actual-watcher"}]`)
|
|
231
|
+
}))
|
|
232
|
+
defer server.Close()
|
|
233
|
+
out, err := readGitHubWatchers(context.Background(), testGitHubClient(server.URL, server.URL, ""), parseGitHubTarget("https://github.com/o/r/watchers"))
|
|
234
|
+
if err != nil {
|
|
235
|
+
t.Fatal(err)
|
|
236
|
+
}
|
|
237
|
+
if !strings.HasSuffix(gotPath, "/subscribers") || !strings.Contains(out, "# Watchers / subscribers") || !strings.Contains(out, "actual repository watchers/subscribers, not stars") || !strings.Contains(out, "@actual-watcher") {
|
|
238
|
+
t.Fatalf("watchers/subscribers output wrong path=%s:\n%s", gotPath, out)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func TestRefReleaseSocialQueriesRejectUnmappedParameters(t *testing.T) {
|
|
243
|
+
var calls int32
|
|
244
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&calls, 1) }))
|
|
245
|
+
defer server.Close()
|
|
246
|
+
client := testGitHubClient(server.URL, server.URL, "")
|
|
247
|
+
for _, target := range []*GitHubTarget{
|
|
248
|
+
parseGitHubTarget("https://github.com/o/r/branches?query=foo"),
|
|
249
|
+
parseGitHubTarget("https://github.com/o/r/tags?sort=newest"),
|
|
250
|
+
parseGitHubTarget("https://github.com/o/r/stargazers?foo=bar"),
|
|
251
|
+
} {
|
|
252
|
+
result := readGitHubNativeWithClient(context.Background(), target, client)
|
|
253
|
+
if result.Outcome != GitHubNativeFailure || result.Err == nil || !strings.Contains(result.Err.Error(), "not supported") {
|
|
254
|
+
t.Fatalf("unmapped list query not rejected: %#v", result)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if atomic.LoadInt32(&calls) != 0 {
|
|
258
|
+
t.Fatalf("unmapped queries made provider calls")
|
|
259
|
+
}
|
|
260
|
+
}
|