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,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
+ }