@volter/twin-github 0.1.0

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.
@@ -0,0 +1,2144 @@
1
+ // GitHub capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
2
+ // TOP-DOWN from what GitHub actually does — NOT from what this twin has built. This is the
3
+ // honest denominator: most entries start as `todo` and coverage reads LOW until the twin
4
+ // truly reaches 100% of GitHub. `verify()` (required to count as done) is ground truth;
5
+ // `expected: 'done'` only on capabilities we genuinely claim, so a broken one surfaces as a
6
+ // regression. Grow this toward GitHub's *full* surface every cycle — a missing entry is a
7
+ // hidden gap, and a high % against a thin list is a misleading metric (the bug this fixes).
8
+ //
9
+ // GitHub is enormous: PRs, Issues, Actions, Checks, Projects v2, Discussions, Releases, Git
10
+ // Data API, Repos/Orgs/Teams/Users, Gists, Search, Notifications, Packages, Pages, code
11
+ // scanning, GraphQL, Apps/OAuth, Webhooks, plus the web UI screens. The twin currently
12
+ // models the PR/Issue/Checks/Actions core (REST objects) + a UI mirror; everything else is
13
+ // an honest `todo`. Two explicit out-of-scope carve-outs (outOfScope): real repo CODE/FILE
14
+ // BYTES (git blob content — large+redundant; API objects are covered) and running real CI
15
+ // COMPUTE (executing Actions — infra, not API; the run/job OBJECTS are covered and modeled).
16
+ import { mkdtempSync, rmSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { checkCapabilities, uiDataCoupled, type CapabilityReport, type CapabilitySpec } from '@volter/twin-tooling';
20
+ import { githubMirrorState } from './github-mirror-ui.ts';
21
+ import { applyGithubWrite, handleGithubRequest } from './github-twin.ts';
22
+
23
+ // ── API predicate: run a real verify() against a fresh temp root (mkdtemp), exercising the
24
+ // twin's handleGithubRequest (GET) + applyGithubWrite (POST/PATCH/PUT). Each verify creates
25
+ // its OWN isolated root so order never matters and nothing leaks between checks. Catch-parity
26
+ // with the other packs (see stripe-capabilities.ts's withRoot): a throwing verify reads as an
27
+ // honest false (todo/regression in the capability report), never a harness crash. Exported
28
+ // (unlike the other packs' private withRoot) so github-capabilities.test.ts can prove the
29
+ // catch behavior directly, independent of checkCapabilities' own outer try/catch.
30
+ export async function withRoot(fn: (root: string) => Promise<boolean>): Promise<boolean> {
31
+ const root = mkdtempSync(join(tmpdir(), 'gh-cap-'));
32
+ try {
33
+ return await fn(root);
34
+ } catch {
35
+ return false;
36
+ } finally {
37
+ rmSync(root, { recursive: true, force: true });
38
+ }
39
+ }
40
+ const ok = (status: number) => status >= 200 && status < 300;
41
+ // GET returns the expected status (and optional body-shape check passes).
42
+ const getOk = (path: string, shape?: (body: unknown) => boolean) => () =>
43
+ withRoot(async (root) => {
44
+ const res = handleGithubRequest({ method: 'GET', path, root });
45
+ return ok(res.status) && (shape ? shape(res.body) : true);
46
+ });
47
+
48
+ // ── shorthands ───────────────────────────────────────────────────────────────────────────
49
+ const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'done', verify });
50
+ const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'todo' });
51
+ const outOfScope = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], reason: string): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'todo', outOfScope: reason });
52
+
53
+ const REPO = 'octo/demo';
54
+ const isArr = (b: unknown): b is unknown[] => Array.isArray(b);
55
+
56
+ // The real GitHub surface. Anything not here is still a gap (keep growing it). Entries with
57
+ // verify() + expected:'done' are what we currently claim; everything else is todo/out-of-scope.
58
+ export const GITHUB_CAPABILITIES: CapabilitySpec[] = [
59
+ // ── Pull Requests ──────────────────────────────────────────────────────────────────────
60
+ done('github.pulls.create', 'pulls', 'PRs: create (POST .../pulls → 201)', 'api', 'core', () =>
61
+ withRoot(async (root) => {
62
+ const res = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'PR', head: 'feat', base: 'main' }), root });
63
+ return res.response.status === 201 && (res.response.body as { number?: number }).number === 1;
64
+ })),
65
+ done('github.pulls.head_sha_resolves', 'pulls', 'PRs: head.sha resolves to the head branch tip (not the branch name)', 'api', 'core', () =>
66
+ withRoot(async (root) => {
67
+ // Register the head branch's tip (as `git push` + a git-ref create would), then open a PR
68
+ // with head=<branch>. Real GitHub reports PR.head.sha as the 40-hex branch tip — assert the
69
+ // twin resolves it, so statuses posted on the pushed sha line up with the PR's head.
70
+ const sha = 'a'.repeat(40);
71
+ const mk = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/heads/feat', sha }), root });
72
+ if (mk.response.status !== 201) return false;
73
+ const pr = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'PR', head: 'feat', base: 'main' }), root });
74
+ const head = (pr.response.body as { head?: { sha?: string; ref?: string; label?: string } }).head;
75
+ // head.sha resolves to the tip; head.ref keeps the branch; head.label is owner:ref.
76
+ if (head?.sha !== sha || head?.ref !== 'feat' || head?.label !== 'octo:feat') return false;
77
+ // A literal sha head stays as-is; an unregistered branch falls back to the literal value.
78
+ const direct = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'PR2', head: 'b'.repeat(40), base: 'main' }), root });
79
+ return (direct.response.body as { head?: { sha?: string } }).head?.sha === 'b'.repeat(40);
80
+ })),
81
+ done('github.pulls.list_filters', 'pulls', 'PRs: list filters (state / head / base query params)', 'api', 'core', () =>
82
+ withRoot(async (root) => {
83
+ // open PR on feat→main, plus a second on fix→main that we then close.
84
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'A', head: 'feat', base: 'main' }), root });
85
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'B', head: 'fix', base: 'main' }), root });
86
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/pulls/2`, body: JSON.stringify({ state: 'closed' }), root });
87
+ const open = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls?state=open`, root });
88
+ const byHead = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls?head=octo:feat`, root });
89
+ const closed = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls?state=closed`, root });
90
+ const all = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls?state=all`, root });
91
+ return isArr(open.body) && open.body.length === 1 && (open.body[0] as { number?: number }).number === 1
92
+ && isArr(byHead.body) && byHead.body.length === 1 && (byHead.body[0] as { number?: number }).number === 1
93
+ && isArr(closed.body) && closed.body.length === 1 && (closed.body[0] as { number?: number }).number === 2
94
+ && isArr(all.body) && all.body.length === 2;
95
+ })),
96
+ done('github.pulls.get', 'pulls', 'PRs: get one (GET .../pulls/:n)', 'api', 'core', () =>
97
+ withRoot(async (root) => {
98
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'PR', head: 'feat', base: 'main' }), root });
99
+ const res = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1`, root });
100
+ return ok(res.status) && (res.body as { number?: number }).number === 1;
101
+ })),
102
+ done('github.pulls.list', 'pulls', 'PRs: list (GET .../pulls, paginated)', 'api', 'core', () =>
103
+ withRoot(async (root) => {
104
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'A', head: 'a', base: 'main' }), root });
105
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'B', head: 'b', base: 'main' }), root });
106
+ const res = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls`, root });
107
+ return ok(res.status) && isArr(res.body) && res.body.length === 2;
108
+ })),
109
+ done('github.pulls.edit', 'pulls', 'PRs: edit title/body/state (PATCH .../pulls/:n)', 'api', 'core', () =>
110
+ withRoot(async (root) => {
111
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'old', head: 'a', base: 'main' }), root });
112
+ const res = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/pulls/1`, body: JSON.stringify({ title: 'new' }), root });
113
+ return ok(res.response.status) && (res.response.body as { title?: string }).title === 'new';
114
+ })),
115
+ done('github.pulls.merge', 'pulls', 'PRs: merge (PUT .../pulls/:n/merge → 200 merged)', 'api', 'core', () =>
116
+ withRoot(async (root) => {
117
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'm', head: 'a', base: 'main' }), root });
118
+ const res = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: '{}', root });
119
+ const merged = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/merge`, root });
120
+ return ok(res.response.status) && (res.response.body as { merged?: boolean }).merged === true && merged.status === 204;
121
+ })),
122
+ done('github.pulls.reviews', 'pulls', 'PRs: reviews submit + list (APPROVE/COMMENT/REQUEST_CHANGES)', 'api', 'core', () =>
123
+ withRoot(async (root) => {
124
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'r', head: 'a', base: 'main' }), root });
125
+ const sub = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/reviews`, body: JSON.stringify({ event: 'APPROVE', body: 'lgtm' }), root });
126
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/reviews`, root });
127
+ return ok(sub.response.status) && ok(list.status) && isArr(list.body) && list.body.length === 1;
128
+ })),
129
+ done('github.pulls.review_comments', 'pulls', 'PRs: diff-anchored review comments (path/line/side)', 'api', 'core', () =>
130
+ withRoot(async (root) => {
131
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'c', head: 'a', base: 'main' }), root });
132
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/comments`, body: JSON.stringify({ body: 'nit', path: 'a.ts', line: 10, side: 'RIGHT' }), root });
133
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/comments`, root });
134
+ return c.response.status === 201 && ok(list.status) && isArr(list.body) && (list.body[0] as { path?: string }).path === 'a.ts';
135
+ })),
136
+ done('github.pulls.requested_reviewers', 'pulls', 'PRs: request/remove reviewers', 'api', 'core', () =>
137
+ withRoot(async (root) => {
138
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'rr', head: 'a', base: 'main' }), root });
139
+ const add = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/requested_reviewers`, body: JSON.stringify({ reviewers: ['alice'] }), root });
140
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/requested_reviewers`, root });
141
+ return add.response.status === 201 && ok(get.status) && (get.body as { users?: unknown[] }).users?.length === 1;
142
+ })),
143
+ done('github.pulls.files', 'pulls', 'PRs: files changed (per-file diff list)', 'api', 'core', () =>
144
+ withRoot(async (root) => {
145
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'f', head: 'a', base: 'main', files: [{ filename: 'x.ts', additions: 3, deletions: 1 }] }), root });
146
+ const res = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/files`, root });
147
+ return ok(res.status) && isArr(res.body) && (res.body[0] as { filename?: string }).filename === 'x.ts';
148
+ })),
149
+ done('github.pulls.commits', 'pulls', 'PRs: commits list (per-commit detail)', 'api', 'core', () =>
150
+ withRoot(async (root) => {
151
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'cm', head: 'a', base: 'main', commits: [{ sha: 'deadbeef', message: 'init' }] }), root });
152
+ const res = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/commits`, root });
153
+ return ok(res.status) && isArr(res.body) && res.body.length === 1;
154
+ })),
155
+ done('github.pulls.labels_assignees_milestone', 'pulls', 'PRs: labels/assignees/milestone via PATCH', 'api', 'core', () =>
156
+ withRoot(async (root) => {
157
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'la', head: 'a', base: 'main' }), root });
158
+ const res = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/pulls/1`, body: JSON.stringify({ labels: ['bug'], assignees: ['bob'] }), root });
159
+ return ok(res.response.status) && (res.response.body as { labels?: unknown[] }).labels?.length === 1;
160
+ })),
161
+ done('github.pulls.draft', 'pulls', 'PRs: draft state + ready_for_review', 'api', 'common', () =>
162
+ withRoot(async (root) => {
163
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'd', head: 'a', base: 'main', draft: true }), root });
164
+ if ((c.response.body as { draft?: boolean }).draft !== true) return false;
165
+ // a draft cannot merge → 405; ready_for_review (draft:false) then merges.
166
+ const blocked = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: '{}', root });
167
+ if (blocked.response.status !== 405) return false;
168
+ const ready = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/pulls/1`, body: JSON.stringify({ draft: false }), root });
169
+ if ((ready.response.body as { draft?: boolean }).draft !== false) return false;
170
+ const merged = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: '{}', root });
171
+ return merged.response.status === 200;
172
+ })),
173
+ done('github.pulls.review_threads', 'pulls', 'PRs: review threads (resolve/unresolve, replies)', 'api', 'core', () =>
174
+ withRoot(async (root) => {
175
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'rt', head: 'a', base: 'main' }), root });
176
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/comments`, body: JSON.stringify({ body: 'nit', path: 'a.ts', line: 1, side: 'RIGHT' }), root });
177
+ const cid = (cm.response.body as { id?: number }).id!;
178
+ // a threaded reply attaches to the root review comment; resolve then unresolve the thread.
179
+ const reply = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/comments`, body: JSON.stringify({ body: 'fixed', in_reply_to: cid }), root });
180
+ if (reply.response.status !== 201) return false;
181
+ const resolve = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/comments/${cid}/resolve`, body: '{}', root });
182
+ if (!(ok(resolve.response.status) && (resolve.response.body as { resolved?: boolean }).resolved === true)) return false;
183
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/threads`, root });
184
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { is_resolved?: boolean }).is_resolved === true)) return false;
185
+ const unresolve = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/pulls/1/comments/${cid}/resolve`, root });
186
+ return ok(unresolve.response.status) && (unresolve.response.body as { resolved?: boolean }).resolved === false;
187
+ })),
188
+ done('github.pulls.merge_methods', 'pulls', 'PRs: merge methods (squash/rebase) + auto-merge', 'api', 'common', () =>
189
+ withRoot(async (root) => {
190
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'mm', head: 'a', base: 'main' }), root });
191
+ // enable auto-merge (squash) → PR.auto_merge reflects the method; invalid method 422s.
192
+ const am = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/auto-merge`, body: JSON.stringify({ merge_method: 'squash' }), root });
193
+ if ((am.response.body as { auto_merge?: { merge_method?: string } }).auto_merge?.merge_method !== 'squash') return false;
194
+ const bad = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: JSON.stringify({ merge_method: 'nope' }), root });
195
+ if (bad.response.status !== 422) return false;
196
+ const m = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: JSON.stringify({ merge_method: 'rebase' }), root });
197
+ return m.response.status === 200 && (m.response.body as { merged?: boolean }).merged === true;
198
+ })),
199
+ done('github.pulls.update_branch', 'pulls', 'PRs: update branch / mergeability + conflicts', 'api', 'common', () =>
200
+ withRoot(async (root) => {
201
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'ub', head: 'a', base: 'main' }), root });
202
+ // a dirty PR reports mergeable:false; update-branch advances head + marks it clean.
203
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/pulls/1`, body: JSON.stringify({ mergeable_state: 'dirty' }), root });
204
+ const dirty = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1`, root });
205
+ if ((dirty.body as { mergeable?: boolean | null }).mergeable !== false) return false;
206
+ const before = (dirty.body as { head?: { sha?: string } }).head?.sha;
207
+ const upd = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/update-branch`, body: '{}', root });
208
+ if (upd.response.status !== 202) return false;
209
+ const clean = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1`, root });
210
+ return (clean.body as { mergeable?: boolean }).mergeable === true && (clean.body as { mergeable_state?: string }).mergeable_state === 'clean' && (clean.body as { head?: { sha?: string } }).head?.sha !== before;
211
+ })),
212
+
213
+ // ── Issues ─────────────────────────────────────────────────────────────────────────────
214
+ done('github.issues.create', 'issues', 'Issues: create (POST .../issues → 201)', 'api', 'core', () =>
215
+ withRoot(async (root) => {
216
+ const res = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'bug' }), root });
217
+ return res.response.status === 201 && (res.response.body as { number?: number }).number === 1;
218
+ })),
219
+ done('github.issues.get_list', 'issues', 'Issues: get + list', 'api', 'core', () =>
220
+ withRoot(async (root) => {
221
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'i1' }), root });
222
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues`, root });
223
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
224
+ return ok(list.status) && isArr(list.body) && list.body.length === 1 && ok(one.status);
225
+ })),
226
+ done('github.issues.edit_state', 'issues', 'Issues: edit + close/reopen (state)', 'api', 'core', () =>
227
+ withRoot(async (root) => {
228
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'e' }), root });
229
+ const res = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ state: 'closed' }), root });
230
+ return ok(res.response.status) && (res.response.body as { state?: string }).state === 'closed';
231
+ })),
232
+ done('github.issues.comments', 'issues', 'Issues: comments create + list', 'api', 'core', () =>
233
+ withRoot(async (root) => {
234
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'c' }), root });
235
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/comments`, body: JSON.stringify({ body: 'me too' }), root });
236
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/comments`, root });
237
+ return c.response.status === 201 && ok(list.status) && isArr(list.body) && list.body.length === 1;
238
+ })),
239
+ done('github.issues.labels_assignees_milestone', 'issues', 'Issues: labels/assignees/milestone', 'api', 'core', () =>
240
+ withRoot(async (root) => {
241
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'la' }), root });
242
+ const res = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ labels: ['p1'], assignees: ['ann'] }), root });
243
+ return ok(res.response.status) && (res.response.body as { assignees?: unknown[] }).assignees?.length === 1;
244
+ })),
245
+ done('github.issues.timeline_crossref', 'issues', 'Issues: timeline/events (cross-reference links)', 'api', 'common', () =>
246
+ withRoot(async (root) => {
247
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'link' }), root });
248
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ _twin_linked: [{ type: 'pull_request', number: 2 }] }), root });
249
+ const res = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/timeline`, root });
250
+ return ok(res.status) && isArr(res.body) && res.body.length === 1;
251
+ })),
252
+ done('github.issues.state_reasons', 'issues', 'Issues: state reasons (completed/not_planned/duplicate)', 'api', 'common', () =>
253
+ withRoot(async (root) => {
254
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'sr' }), root });
255
+ const r = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ state: 'closed', state_reason: 'not_planned' }), root });
256
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
257
+ return ok(r.response.status) && (r.response.body as { state_reason?: string }).state_reason === 'not_planned' && (get.body as { state?: string }).state === 'closed';
258
+ })),
259
+ done('github.issues.types', 'issues', 'Issues: issue types (org-level)', 'api', 'niche', () =>
260
+ withRoot(async (root) => {
261
+ // a missing name 422s; create → listed; duplicate name 422s; edit; delete → gone.
262
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/issue-types`, body: '{}', root })).response.status !== 422) return false;
263
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/issue-types`, body: JSON.stringify({ name: 'Bug', color: 'red', description: 'A defect' }), root });
264
+ if (!(c.response.status === 201 && (c.response.body as { name?: string }).name === 'Bug')) return false;
265
+ const id = (c.response.body as { id?: number }).id!;
266
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/issue-types`, body: JSON.stringify({ name: 'Bug' }), root })).response.status !== 422) return false;
267
+ const list = handleGithubRequest({ method: 'GET', path: `/orgs/octo/issue-types`, root });
268
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
269
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/orgs/octo/issue-types/${id}`, body: JSON.stringify({ description: 'updated' }), root });
270
+ if ((e.response.body as { description?: string }).description !== 'updated') return false;
271
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/issue-types/${id}`, root });
272
+ const after = handleGithubRequest({ method: 'GET', path: `/orgs/octo/issue-types`, root });
273
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 0;
274
+ })),
275
+ done('github.issues.subissues', 'issues', 'Issues: sub-issues (parent/child hierarchy)', 'api', 'common', () =>
276
+ withRoot(async (root) => {
277
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'parent' }), root });
278
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'child' }), root });
279
+ const add = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/sub_issues`, body: JSON.stringify({ sub_issue_number: 2 }), root });
280
+ if (add.response.status !== 201) return false;
281
+ const subs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/sub_issues`, root });
282
+ if (!(ok(subs.status) && isArr(subs.body) && subs.body.length === 1 && (subs.body[0] as { number?: number }).number === 2)) return false;
283
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/issues/1/sub_issue`, body: JSON.stringify({ sub_issue_number: 2 }), root });
284
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/sub_issues`, root });
285
+ return ok(del.response.status) && isArr(after.body) && after.body.length === 0;
286
+ })),
287
+ done('github.issues.reactions', 'issues', 'Issues/comments: reactions (emoji)', 'api', 'common', () =>
288
+ withRoot(async (root) => {
289
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'react' }), root });
290
+ // invalid content → 422; a valid one → 201 and appears in the list.
291
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/reactions`, body: JSON.stringify({ content: 'nope' }), root });
292
+ if (bad.response.status !== 422) return false;
293
+ const r = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/reactions`, body: JSON.stringify({ content: 'heart' }), root });
294
+ if (r.response.status !== 201) return false;
295
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/reactions`, root });
296
+ // comment reactions too: create a comment, react to it.
297
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/comments`, body: JSON.stringify({ body: 'hi' }), root });
298
+ const cid = (cm.response.body as { id?: number }).id!;
299
+ const cr = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/comments/${cid}/reactions`, body: JSON.stringify({ content: 'rocket' }), root });
300
+ const clist = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/comments/${cid}/reactions`, root });
301
+ return ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { content?: string }).content === 'heart'
302
+ && cr.response.status === 201 && isArr(clist.body) && clist.body.length === 1;
303
+ })),
304
+ done('github.issues.lock', 'issues', 'Issues: lock/unlock conversation', 'api', 'common', () =>
305
+ withRoot(async (root) => {
306
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'lock' }), root });
307
+ const lock = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/issues/1/lock`, body: JSON.stringify({ lock_reason: 'off-topic' }), root });
308
+ if (lock.response.status !== 204) return false;
309
+ const locked = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
310
+ if (!((locked.body as { locked?: boolean }).locked === true && (locked.body as { active_lock_reason?: string }).active_lock_reason === 'off-topic')) return false;
311
+ const unlock = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/issues/1/lock`, root });
312
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
313
+ return unlock.response.status === 204 && (after.body as { locked?: boolean }).locked === false;
314
+ })),
315
+ done('github.issues.transfer', 'issues', 'Issues: transfer to another repo', 'api', 'niche', () =>
316
+ withRoot(async (root) => {
317
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'moves', body: 'context' }), root });
318
+ // transfer to a sibling repo → 201 NEW issue carrying title; source closes as transferred.
319
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/transfer`, body: JSON.stringify({ new_repository: 'nope' }), root });
320
+ if (bad.response.status !== 422) return false;
321
+ const t = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/1/transfer`, body: JSON.stringify({ new_repository: 'octo/other' }), root });
322
+ if (!(t.response.status === 201 && (t.response.body as { title?: string }).title === 'moves')) return false;
323
+ const newNum = (t.response.body as { number?: number }).number!;
324
+ const moved = handleGithubRequest({ method: 'GET', path: `/repos/octo/other/issues/${newNum}`, root });
325
+ if (!(ok(moved.status) && (moved.body as { title?: string }).title === 'moves')) return false;
326
+ const src = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
327
+ return (src.body as { state?: string }).state === 'closed' && (src.body as { state_reason?: string }).state_reason === 'transferred';
328
+ })),
329
+ done('github.issues.pin', 'issues', 'Issues: pin/unpin', 'api', 'niche', () =>
330
+ withRoot(async (root) => {
331
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'important' }), root });
332
+ // pinning a missing issue → 404; a real one → 204 and the flag round-trips.
333
+ if ((await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/issues/99/pin`, body: '{}', root })).response.status !== 404) return false;
334
+ const pin = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/issues/1/pin`, body: '{}', root });
335
+ if (pin.response.status !== 204) return false;
336
+ const pinned = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
337
+ if ((pinned.body as { pinned?: boolean }).pinned !== true) return false;
338
+ const unpin = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/issues/1/pin`, root });
339
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1`, root });
340
+ return unpin.response.status === 204 && (after.body as { pinned?: boolean }).pinned === false;
341
+ })),
342
+ done('github.issues.full_timeline', 'issues', 'Issues: full timeline events (labeled/assigned/renamed/…)', 'api', 'common', () =>
343
+ withRoot(async (root) => {
344
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'tl' }), root });
345
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ labels: ['bug'], assignees: ['bob'], state: 'closed', state_reason: 'completed' }), root });
346
+ const tl = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/timeline`, root });
347
+ if (!(ok(tl.status) && isArr(tl.body))) return false;
348
+ const events = (tl.body as Array<{ event?: string }>).map((e) => e.event);
349
+ return events.includes('labeled') && events.includes('assigned') && events.includes('closed');
350
+ })),
351
+
352
+ // ── Labels / Milestones ──────────────────────────────────────────────────────────────────
353
+ done('github.milestones.crud', 'fields', 'Milestones: create/edit/get/list', 'api', 'common', () =>
354
+ withRoot(async (root) => {
355
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/milestones`, body: JSON.stringify({ title: 'v1' }), root });
356
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/milestones/1`, body: JSON.stringify({ state: 'closed' }), root });
357
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/milestones`, root });
358
+ return c.response.status === 201 && ok(e.response.status) && ok(list.status) && isArr(list.body) && list.body.length === 1;
359
+ })),
360
+ done('github.labels.crud', 'fields', 'Labels: repo label CRUD (color/description)', 'api', 'core', () =>
361
+ withRoot(async (root) => {
362
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/labels`, body: JSON.stringify({ name: 'bug', color: 'd73a4a', description: 'Something broken' }), root });
363
+ if (!(c.response.status === 201 && (c.response.body as { color?: string }).color === 'd73a4a')) return false;
364
+ // duplicate name → 422; edit color + rename; get/list; delete then 404.
365
+ const dup = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/labels`, body: JSON.stringify({ name: 'bug' }), root });
366
+ if (dup.response.status !== 422) return false;
367
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/labels/bug`, body: JSON.stringify({ new_name: 'defect', color: '000000' }), root });
368
+ if (!(ok(e.response.status) && (e.response.body as { name?: string }).name === 'defect')) return false;
369
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/labels`, root });
370
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/labels/defect`, root });
371
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && ok(one.status))) return false;
372
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/labels/defect`, root });
373
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/labels/defect`, root });
374
+ return del.response.status === 204 && gone.status === 404;
375
+ })),
376
+
377
+ // ── Commit statuses + Checks ─────────────────────────────────────────────────────────────
378
+ done('github.statuses.create_combined', 'checks', 'Commit statuses: create + combined + list', 'api', 'core', () =>
379
+ withRoot(async (root) => {
380
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/statuses/abc123`, body: JSON.stringify({ state: 'success', context: 'ci' }), root });
381
+ const combined = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/commits/abc123/status`, root });
382
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/commits/abc123/statuses`, root });
383
+ return c.response.status === 201 && ok(combined.status) && ok(list.status) && isArr(list.body) && list.body.length === 1;
384
+ })),
385
+ done('github.checks.runs', 'checks', 'Check runs: create + list for a ref', 'api', 'core', () =>
386
+ withRoot(async (root) => {
387
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/check-runs`, body: JSON.stringify({ name: 'build', head_sha: 'sha9', conclusion: 'success' }), root });
388
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/commits/sha9/check-runs`, root });
389
+ return c.response.status === 201 && ok(list.status) && (list.body as { total_count?: number }).total_count === 1;
390
+ })),
391
+ done('github.checks.suites', 'checks', 'Check suites: list/get/rerequest + GET .../check-runs/:id', 'api', 'common', () =>
392
+ withRoot(async (root) => {
393
+ const cr = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/check-runs`, body: JSON.stringify({ name: 'build', head_sha: 'shaS', conclusion: 'success' }), root });
394
+ const runId = (cr.response.body as { id?: number }).id!;
395
+ const suiteId = (cr.response.body as { check_suite?: { id?: number } }).check_suite?.id!;
396
+ // GET a single check run; list/get the suite; suite rolls up to success; rerequest 201s.
397
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/check-runs/${runId}`, root });
398
+ if (!(ok(one.status) && (one.body as { id?: number }).id === runId)) return false;
399
+ const suites = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/commits/shaS/check-suites`, root });
400
+ if (!(ok(suites.status) && (suites.body as { total_count?: number }).total_count === 1)) return false;
401
+ const suite = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/check-suites/${suiteId}`, root });
402
+ if (!(ok(suite.status) && (suite.body as { conclusion?: string }).conclusion === 'success')) return false;
403
+ const re = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/check-suites/${suiteId}/rerequest`, body: '{}', root });
404
+ return re.response.status === 201;
405
+ })),
406
+ done('github.checks.annotations', 'checks', 'Check runs: annotations + output details', 'api', 'common', () =>
407
+ withRoot(async (root) => {
408
+ const cr = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/check-runs`, body: JSON.stringify({ name: 'lint', head_sha: 'shaA', status: 'in_progress' }), root });
409
+ const id = (cr.response.body as { id?: number }).id!;
410
+ const upd = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/check-runs/${id}`, body: JSON.stringify({ conclusion: 'failure', output: { title: 'Lint', summary: '1 issue', annotations: [{ path: 'a.ts', start_line: 3, end_line: 3, annotation_level: 'warning', message: 'unused var' }] } }), root });
411
+ if (!(ok(upd.response.status) && (upd.response.body as { output?: { annotations_count?: number } }).output?.annotations_count === 1)) return false;
412
+ const anns = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/check-runs/${id}/annotations`, root });
413
+ return ok(anns.status) && isArr(anns.body) && anns.body.length === 1 && (anns.body[0] as { annotation_level?: string }).annotation_level === 'warning';
414
+ })),
415
+
416
+ // ── Actions ──────────────────────────────────────────────────────────────────────────────
417
+ done('github.actions.workflows', 'actions', 'Actions: register + list/get workflows', 'api', 'niche', () =>
418
+ withRoot(async (root) => {
419
+ const reg = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/workflows/ci.yml`, body: JSON.stringify({ name: 'CI', path: '.github/workflows/ci.yml' }), root });
420
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/workflows`, root });
421
+ return ok(reg.response.status) && ok(list.status) && (list.body as { total_count?: number }).total_count === 1;
422
+ })),
423
+ done('github.actions.dispatch_runs', 'actions', 'Actions: dispatch workflow → queued run; list runs', 'api', 'niche', () =>
424
+ withRoot(async (root) => {
425
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/workflows/ci.yml`, body: JSON.stringify({ name: 'CI', path: '.github/workflows/ci.yml' }), root });
426
+ const d = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/workflows/ci.yml/dispatches`, body: JSON.stringify({ ref: 'main' }), root });
427
+ const runs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs`, root });
428
+ return d.response.status === 204 && ok(runs.status) && (runs.body as { total_count?: number }).total_count === 1;
429
+ })),
430
+ done('github.actions.run_jobs', 'actions', 'Actions: run jobs + get run + rerun/cancel transitions', 'api', 'niche', () =>
431
+ withRoot(async (root) => {
432
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/workflows/ci.yml`, body: JSON.stringify({ name: 'CI', path: '.github/workflows/ci.yml' }), root });
433
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/workflows/ci.yml/dispatches`, body: JSON.stringify({ ref: 'main' }), root });
434
+ const runs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs`, root });
435
+ const runId = (runs.body as { workflow_runs: { id: number }[] }).workflow_runs[0]!.id;
436
+ const jobs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs/${runId}/jobs`, root });
437
+ const cancel = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/runs/${runId}/cancel`, body: '{}', root });
438
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs/${runId}`, root });
439
+ return ok(jobs.status) && (jobs.body as { total_count?: number }).total_count === 1 && cancel.response.status === 202 && (after.body as { conclusion?: string }).conclusion === 'cancelled';
440
+ })),
441
+ done('github.actions.logs', 'actions', 'Actions: download run logs (302 redirect to signed URL)', 'api', 'niche', () =>
442
+ withRoot(async (root) => {
443
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/workflows/ci.yml`, body: JSON.stringify({ name: 'CI', path: '.github/workflows/ci.yml' }), root });
444
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/workflows/ci.yml/dispatches`, body: JSON.stringify({ ref: 'main' }), root });
445
+ const runs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs`, root });
446
+ const runId = (runs.body as { workflow_runs: { id: number }[] }).workflow_runs[0]!.id;
447
+ // a real run's logs 302-redirect to a download URL; an unknown run → 404.
448
+ const logs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs/${runId}/logs`, root });
449
+ if (!(logs.status === 302 && typeof (logs.body as { location?: string }).location === 'string')) return false;
450
+ return handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/runs/999999/logs`, root }).status === 404;
451
+ })),
452
+ done('github.actions.artifacts', 'actions', 'Actions: artifacts register/list/get/delete (metadata)', 'api', 'niche', () =>
453
+ withRoot(async (root) => {
454
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/artifacts`, body: '{}', root });
455
+ if (bad.response.status !== 422) return false;
456
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/artifacts`, body: JSON.stringify({ name: 'build-output', size_in_bytes: 2048, run_id: 5 }), root });
457
+ if (!(c.response.status === 201 && (c.response.body as { name?: string }).name === 'build-output')) return false;
458
+ const id = (c.response.body as { id?: number }).id!;
459
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/artifacts`, root });
460
+ if (!(ok(list.status) && (list.body as { total_count?: number }).total_count === 1)) return false;
461
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/artifacts/${id}`, root });
462
+ if (!(ok(one.status) && (one.body as { size_in_bytes?: number }).size_in_bytes === 2048)) return false;
463
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/actions/artifacts/${id}`, root });
464
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/artifacts/${id}`, root });
465
+ return del.response.status === 204 && gone.status === 404;
466
+ })),
467
+ done('github.actions.secrets', 'actions', 'Actions: secrets (repo + org; value never returned)', 'api', 'niche', () =>
468
+ withRoot(async (root) => {
469
+ // PUT a repo secret → 201 (first time); the value is NOT stored/returned (only name).
470
+ const c = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/secrets/API_KEY`, body: JSON.stringify({ encrypted_value: 'xxx', key_id: '1' }), root });
471
+ if (c.response.status !== 201) return false;
472
+ const upd = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/secrets/API_KEY`, body: JSON.stringify({ encrypted_value: 'yyy', key_id: '1' }), root });
473
+ if (upd.response.status !== 204) return false;
474
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/secrets`, root });
475
+ const secrets = (list.body as { secrets?: Array<Record<string, unknown>> }).secrets ?? [];
476
+ if (!(ok(list.status) && secrets.length === 1 && secrets[0]!.name === 'API_KEY' && !('value' in secrets[0]!))) return false;
477
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/secrets/API_KEY`, root });
478
+ if (!(ok(get.status) && (get.body as { name?: string }).name === 'API_KEY')) return false;
479
+ // org-scope secret is a separate namespace.
480
+ const org = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/actions/secrets/ORG_TOKEN`, body: JSON.stringify({ encrypted_value: 'z' }), root });
481
+ if (org.response.status !== 201) return false;
482
+ const orgList = handleGithubRequest({ method: 'GET', path: `/orgs/octo/actions/secrets`, root });
483
+ if (!(ok(orgList.status) && (orgList.body as { total_count?: number }).total_count === 1)) return false;
484
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/actions/secrets/API_KEY`, root });
485
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/secrets/API_KEY`, root }).status === 404;
486
+ })),
487
+ done('github.actions.variables', 'actions', 'Actions: variables (repo + org; value returned)', 'api', 'niche', () =>
488
+ withRoot(async (root) => {
489
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/variables`, body: JSON.stringify({ name: 'X' }), root });
490
+ if (bad.response.status !== 422) return false;
491
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/variables`, body: JSON.stringify({ name: 'NODE_ENV', value: 'production' }), root });
492
+ if (c.response.status !== 201) return false;
493
+ // duplicate → 409.
494
+ const dup = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/variables`, body: JSON.stringify({ name: 'NODE_ENV', value: 'x' }), root });
495
+ if (dup.response.status !== 409) return false;
496
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/variables/NODE_ENV`, root });
497
+ if (!(ok(get.status) && (get.body as { value?: string }).value === 'production')) return false;
498
+ // PATCH the value; re-read reflects it.
499
+ const upd = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/actions/variables/NODE_ENV`, body: JSON.stringify({ value: 'staging' }), root });
500
+ if (upd.response.status !== 204) return false;
501
+ const get2 = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/variables/NODE_ENV`, root });
502
+ if ((get2.body as { value?: string }).value !== 'staging') return false;
503
+ // org-scope variable.
504
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/actions/variables`, body: JSON.stringify({ name: 'REGION', value: 'us' }), root });
505
+ const orgGet = handleGithubRequest({ method: 'GET', path: `/orgs/octo/actions/variables/REGION`, root });
506
+ if ((orgGet.body as { value?: string }).value !== 'us') return false;
507
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/actions/variables/NODE_ENV`, root });
508
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/variables/NODE_ENV`, root }).status === 404;
509
+ })),
510
+ done('github.actions.caches', 'actions', 'Actions: caches list/delete', 'api', 'niche', () =>
511
+ withRoot(async (root) => {
512
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/caches`, body: '{}', root });
513
+ if (bad.response.status !== 422) return false;
514
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/caches`, body: JSON.stringify({ key: 'node-modules-abc', ref: 'refs/heads/main', size_in_bytes: 1024 }), root });
515
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/caches`, body: JSON.stringify({ key: 'node-modules-def', size_in_bytes: 2048 }), root });
516
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/caches`, root });
517
+ if (!(ok(list.status) && (list.body as { total_count?: number }).total_count === 2)) return false;
518
+ // delete by key → removes one; list shrinks.
519
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/actions/caches?key=node-modules-abc`, root });
520
+ if (!(del.response.status === 200 && (del.response.body as { total_count?: number }).total_count === 1)) return false;
521
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/actions/caches`, root });
522
+ return (after.body as { total_count?: number }).total_count === 1;
523
+ })),
524
+ done('github.actions.environments', 'actions', 'Actions: deployment environments (alias of environments.crud)', 'api', 'niche', () =>
525
+ withRoot(async (root) => {
526
+ // environments are reachable under the repo; create + list round-trips.
527
+ const c = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/environments/staging`, body: JSON.stringify({ wait_timer: 5 }), root });
528
+ if (c.response.status !== 200) return false;
529
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/environments`, root });
530
+ return ok(list.status) && (list.body as { total_count?: number }).total_count === 1 && (list.body as { environments?: Array<{ name?: string }> }).environments?.[0]?.name === 'staging';
531
+ })),
532
+ outOfScope('github.actions.compute', 'actions', 'Actions: real CI COMPUTE (executing the workflow)', 'api', 'niche',
533
+ 'Out of scope: running real CI compute is infra, not API. The run/job OBJECTS (queued/rerun/cancel) are modeled (covered).'),
534
+
535
+ // ── Repos / settings / branches ──────────────────────────────────────────────────────────
536
+ done('github.repos.crud', 'repos', 'Repos: create/get/update/delete + settings', 'api', 'common', () =>
537
+ withRoot(async (root) => {
538
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo', description: 'a repo', private: true }), root });
539
+ if (!(c.response.status === 201 && (c.response.body as { full_name?: string }).full_name === 'octo/demo')) return false;
540
+ const dup = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
541
+ if (dup.response.status !== 422) return false;
542
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo`, root });
543
+ if (!(ok(get.status) && (get.body as { private?: boolean }).private === true)) return false;
544
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/octo/demo`, body: JSON.stringify({ description: 'updated', has_wiki: false }), root });
545
+ if (!(ok(e.response.status) && (e.response.body as { description?: string }).description === 'updated')) return false;
546
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/octo/demo`, root });
547
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo`, root });
548
+ return del.response.status === 204 && gone.status === 404;
549
+ })),
550
+ done('github.repos.branches', 'repos', 'Repos: branches list/get + default branch', 'api', 'common', () =>
551
+ withRoot(async (root) => {
552
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo', default_branch: 'main' }), root });
553
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/branches`, root });
554
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { name?: string }).name === 'main')) return false;
555
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/branches/main`, root });
556
+ return ok(one.status) && (one.body as { name?: string }).name === 'main' && typeof (one.body as { commit?: { sha?: string } }).commit?.sha === 'string';
557
+ })),
558
+ done('github.repos.branch_protection', 'repos', 'Repos: branch protection rules + rulesets', 'api', 'niche', () =>
559
+ withRoot(async (root) => {
560
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
561
+ const p = await applyGithubWrite({ method: 'PUT', path: `/repos/octo/demo/branches/main/protection`, body: JSON.stringify({ required_status_checks: null }), root });
562
+ if (!(ok(p.response.status) && (p.response.body as { enabled?: boolean }).enabled === true)) return false;
563
+ const b = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/branches/main`, root });
564
+ if ((b.body as { protected?: boolean }).protected !== true) return false;
565
+ const d = await applyGithubWrite({ method: 'DELETE', path: `/repos/octo/demo/branches/main/protection`, root });
566
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/branches/main`, root });
567
+ return d.response.status === 204 && (after.body as { protected?: boolean }).protected === false;
568
+ })),
569
+ done('github.repos.collaborators', 'repos', 'Repos: collaborators + permissions', 'api', 'common', () =>
570
+ withRoot(async (root) => {
571
+ const add = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/collaborators/alice`, body: JSON.stringify({ permission: 'maintain' }), root });
572
+ if (add.response.status !== 201) return false;
573
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/collaborators`, root });
574
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
575
+ const perm = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/collaborators/alice/permission`, root });
576
+ if ((perm.body as { permission?: string }).permission !== 'maintain') return false;
577
+ const check = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/collaborators/alice`, root });
578
+ if (check.status !== 204) return false;
579
+ const rm = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/collaborators/alice`, root });
580
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/collaborators/alice`, root });
581
+ return rm.response.status === 204 && gone.status === 404;
582
+ })),
583
+ done('github.repos.webhooks', 'repos', 'Repos: webhook config CRUD', 'api', 'common', () =>
584
+ withRoot(async (root) => {
585
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/hooks`, body: JSON.stringify({ events: ['push', 'pull_request'], config: { url: 'https://e.x/hook', content_type: 'json' } }), root });
586
+ if (!(c.response.status === 201 && (c.response.body as { events?: string[] }).events?.length === 2)) return false;
587
+ const id = (c.response.body as { id?: number }).id!;
588
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/hooks/${id}`, body: JSON.stringify({ active: false }), root });
589
+ if (!(ok(e.response.status) && (e.response.body as { active?: boolean }).active === false)) return false;
590
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/hooks`, root });
591
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
592
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/hooks/${id}`, root });
593
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/hooks/${id}`, root });
594
+ return del.response.status === 204 && gone.status === 404;
595
+ })),
596
+ done('github.repos.topics', 'repos', 'Repos: topics', 'api', 'common', () =>
597
+ withRoot(async (root) => {
598
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
599
+ const set = await applyGithubWrite({ method: 'PUT', path: `/repos/octo/demo/topics`, body: JSON.stringify({ names: ['ai', 'twin'] }), root });
600
+ if (!(ok(set.response.status) && (set.response.body as { names?: string[] }).names?.length === 2)) return false;
601
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/topics`, root });
602
+ const onRepo = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo`, root });
603
+ return ok(get.status) && (get.body as { names?: string[] }).names?.includes('twin') === true && (onRepo.body as { topics?: string[] }).topics?.length === 2;
604
+ })),
605
+ done('github.repos.forks', 'repos', 'Repos: forks list/create', 'api', 'common', () =>
606
+ withRoot(async (root) => {
607
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
608
+ const f = await applyGithubWrite({ method: 'POST', path: `/repos/octo/demo/forks`, body: JSON.stringify({ organization: 'me' }), root });
609
+ if (!(f.response.status === 202 && (f.response.body as { fork?: boolean }).fork === true && (f.response.body as { full_name?: string }).full_name === 'me/demo')) return false;
610
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/forks`, root });
611
+ return ok(list.status) && isArr(list.body) && list.body.length === 1;
612
+ })),
613
+ done('github.repos.contents', 'repos', 'Repos: contents API (get/put/delete file objects)', 'api', 'common', () =>
614
+ withRoot(async (root) => {
615
+ const content = Buffer.from('hello world').toString('base64');
616
+ const put = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/README.md`, body: JSON.stringify({ message: 'add', content }), root });
617
+ if (!(put.response.status === 201 && (put.response.body as { content?: { path?: string } }).content?.path === 'README.md')) return false;
618
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/contents/README.md`, root });
619
+ if (!(ok(get.status) && (get.body as { content?: string }).content === content && (get.body as { encoding?: string }).encoding === 'base64')) return false;
620
+ const upd = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/README.md`, body: JSON.stringify({ message: 'upd', content: Buffer.from('v2').toString('base64') }), root });
621
+ if (upd.response.status !== 200) return false;
622
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/contents/README.md`, body: JSON.stringify({ message: 'rm' }), root });
623
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/contents/README.md`, root });
624
+ return ok(del.response.status) && gone.status === 404;
625
+ })),
626
+ outOfScope('github.repos.blob_bytes', 'repos', 'Repos: raw file/blob BYTES (git object contents)', 'api', 'niche',
627
+ 'Out of scope: actual repository code/file contents are large + redundant; the metadata/API objects are modeled; the bytes are out of scope.'),
628
+
629
+ // ── Git Data API ─────────────────────────────────────────────────────────────────────────
630
+ done('github.git.refs', 'git', 'Git Data: refs (create/get/update/delete branches+tags)', 'api', 'niche', () =>
631
+ withRoot(async (root) => {
632
+ const sha = 'a'.repeat(40);
633
+ // missing sha → 422; non-refs/ prefix → 422.
634
+ const noSha = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/heads/feat' }), root });
635
+ if (noSha.response.status !== 422) return false;
636
+ const badPrefix = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'heads/feat', sha }), root });
637
+ if (badPrefix.response.status !== 422) return false;
638
+ // create a branch ref → 201 with ref+object.sha; duplicate → 422.
639
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/heads/feat', sha }), root });
640
+ if (!(c.response.status === 201 && (c.response.body as { ref?: string }).ref === 'refs/heads/feat' && (c.response.body as { object?: { sha?: string } }).object?.sha === sha)) return false;
641
+ const dup = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/heads/feat', sha }), root });
642
+ if (dup.response.status !== 422) return false;
643
+ // get one via .../git/ref/:ref; update its sha (PATCH); delete (DELETE) then 404.
644
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/ref/heads/feat`, root });
645
+ if (!(ok(get.status) && (get.body as { object?: { sha?: string } }).object?.sha === sha)) return false;
646
+ const sha2 = 'b'.repeat(40);
647
+ const upd = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/git/refs/heads/feat`, body: JSON.stringify({ sha: sha2 }), root });
648
+ if (!(upd.response.status === 200 && (upd.response.body as { object?: { sha?: string } }).object?.sha === sha2)) return false;
649
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/refs/heads`, root });
650
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
651
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/git/refs/heads/feat`, root });
652
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/ref/heads/feat`, root });
653
+ return del.response.status === 204 && gone.status === 404;
654
+ })),
655
+ done('github.git.commits', 'git', 'Git Data: commit objects (get/create)', 'api', 'niche', () =>
656
+ withRoot(async (root) => {
657
+ const tree = 't'.repeat(40);
658
+ // missing required fields → 422.
659
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/commits`, body: JSON.stringify({ message: 'x' }), root });
660
+ if (bad.response.status !== 422) return false;
661
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/commits`, body: JSON.stringify({ message: 'init', tree, parents: ['p'.repeat(40)], author: { name: 'Ann', email: 'a@x' } }), root });
662
+ if (!(c.response.status === 201 && (c.response.body as { message?: string }).message === 'init')) return false;
663
+ const sha = (c.response.body as { sha?: string }).sha!;
664
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/commits/${sha}`, root });
665
+ const b = get.body as { tree?: { sha?: string }; parents?: unknown[]; author?: { name?: string } };
666
+ return ok(get.status) && b.tree?.sha === tree && b.parents?.length === 1 && b.author?.name === 'Ann';
667
+ })),
668
+ done('github.git.trees', 'git', 'Git Data: trees (get/create)', 'api', 'niche', () =>
669
+ withRoot(async (root) => {
670
+ const blobSha = 'd'.repeat(40);
671
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/trees`, body: '{}', root });
672
+ if (bad.response.status !== 422) return false;
673
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/trees`, body: JSON.stringify({ tree: [{ path: 'a.ts', mode: '100644', type: 'blob', sha: blobSha }] }), root });
674
+ if (c.response.status !== 201) return false;
675
+ const sha = (c.response.body as { sha?: string }).sha!;
676
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/trees/${sha}`, root });
677
+ const entries = (get.body as { tree?: Array<{ path?: string; type?: string }> }).tree ?? [];
678
+ return ok(get.status) && entries.length === 1 && entries[0]!.path === 'a.ts' && entries[0]!.type === 'blob';
679
+ })),
680
+ done('github.git.blobs', 'git', 'Git Data: blob objects (create/get)', 'api', 'niche', () =>
681
+ withRoot(async (root) => {
682
+ const content = Buffer.from('hello blob').toString('base64');
683
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/blobs`, body: '{}', root });
684
+ if (bad.response.status !== 422) return false;
685
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/blobs`, body: JSON.stringify({ content, encoding: 'base64' }), root });
686
+ if (c.response.status !== 201) return false;
687
+ const sha = (c.response.body as { sha?: string }).sha!;
688
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/blobs/${sha}`, root });
689
+ const b = get.body as { content?: string; encoding?: string; size?: number };
690
+ return ok(get.status) && b.content === content && b.encoding === 'base64' && b.size === Buffer.from('hello blob').length;
691
+ })),
692
+ done('github.git.tags', 'git', 'Git Data: annotated tags', 'api', 'niche', () =>
693
+ withRoot(async (root) => {
694
+ const objSha = 'c'.repeat(40);
695
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/tags`, body: JSON.stringify({ tag: 'v1' }), root });
696
+ if (bad.response.status !== 422) return false;
697
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/tags`, body: JSON.stringify({ tag: 'v1.0.0', message: 'release', object: objSha, type: 'commit', tagger: { name: 'Ann' } }), root });
698
+ if (!(c.response.status === 201 && (c.response.body as { tag?: string }).tag === 'v1.0.0')) return false;
699
+ const sha = (c.response.body as { sha?: string }).sha!;
700
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/tags/${sha}`, root });
701
+ const b = get.body as { object?: { sha?: string; type?: string }; message?: string };
702
+ if (!(ok(get.status) && b.object?.sha === objSha && b.message === 'release')) return false;
703
+ // creating a refs/tags ref pointing at the annotated tag → object.type 'tag'.
704
+ const ref = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/tags/v1.0.0', sha }), root });
705
+ return ref.response.status === 201 && (ref.response.body as { object?: { type?: string } }).object?.type === 'tag';
706
+ })),
707
+
708
+ // ── Releases / Tags ──────────────────────────────────────────────────────────────────────
709
+ done('github.releases.crud', 'releases', 'Releases: create/edit/list/get + latest + by-tag', 'api', 'common', () =>
710
+ withRoot(async (root) => {
711
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v1.0.0', name: 'First', body: 'notes' }), root });
712
+ if (c.response.status !== 201) return false;
713
+ const id = (c.response.body as { id?: number; tag_name?: string }).id!;
714
+ if ((c.response.body as { html_url?: string }).html_url?.includes('releases/tag/v1.0.0') !== true) return false;
715
+ // GET one, by id, asserting tag_name/name round-trip.
716
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/${id}`, root });
717
+ if (!(ok(one.status) && (one.body as { tag_name?: string }).tag_name === 'v1.0.0' && (one.body as { name?: string }).name === 'First')) return false;
718
+ // GET latest (published) + GET by tag.
719
+ const latest = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/latest`, root });
720
+ const byTag = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/tags/v1.0.0`, root });
721
+ if (!(ok(latest.status) && (latest.body as { tag_name?: string }).tag_name === 'v1.0.0' && ok(byTag.status) && (byTag.body as { id?: number }).id === id)) return false;
722
+ // PATCH (edit name) round-trips; list returns the release; DELETE 204s then 404s.
723
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/releases/${id}`, body: JSON.stringify({ name: 'Renamed' }), root });
724
+ if (!(ok(e.response.status) && (e.response.body as { name?: string }).name === 'Renamed')) return false;
725
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases`, root });
726
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
727
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/releases/${id}`, root });
728
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/${id}`, root });
729
+ return del.response.status === 204 && gone.status === 404;
730
+ })),
731
+ done('github.releases.assets', 'releases', 'Releases: assets register/list/get (metadata; bytes out of scope)', 'api', 'common', () =>
732
+ withRoot(async (root) => {
733
+ const r = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v2', name: 'Two' }), root });
734
+ const relId = (r.response.body as { id?: number }).id!;
735
+ const a = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases/${relId}/assets`, body: JSON.stringify({ name: 'app.zip', content_type: 'application/zip', size: 1024 }), root });
736
+ if (!(a.response.status === 201 && (a.response.body as { name?: string }).name === 'app.zip' && (a.response.body as { size?: number }).size === 1024)) return false;
737
+ const assetId = (a.response.body as { id?: number }).id!;
738
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/${relId}/assets`, root });
739
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/assets/${assetId}`, root });
740
+ // The asset rides on the release object too (assets:[...]).
741
+ const rel = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/releases/${relId}`, root });
742
+ return ok(list.status) && isArr(list.body) && list.body.length === 1 && ok(one.status) && (one.body as { name?: string }).name === 'app.zip'
743
+ && ok(rel.status) && ((rel.body as { assets?: unknown[] }).assets?.length === 1);
744
+ })),
745
+ done('github.tags.list', 'releases', 'Tags: list (GET /tags) + git refs (GET /git/refs/tags)', 'api', 'common', () =>
746
+ withRoot(async (root) => {
747
+ // A published release creates its tag; a draft does NOT (real GitHub behavior).
748
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v3.0.0', name: 'Three' }), root });
749
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v4.0.0-draft', name: 'Draft', draft: true }), root });
750
+ const tags = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/tags`, root });
751
+ if (!(ok(tags.status) && isArr(tags.body) && tags.body.length === 1 && (tags.body[0] as { name?: string }).name === 'v3.0.0')) return false;
752
+ const ref = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/git/refs/tags/v3.0.0`, root });
753
+ return ok(ref.status) && (ref.body as { ref?: string }).ref === 'refs/tags/v3.0.0' && (ref.body as { object?: { type?: string } }).object?.type === 'commit';
754
+ })),
755
+
756
+ // ── Projects v2 (modeled as stateful CRUD over the kernel; GraphQL graph not modeled) ──────
757
+ done('github.projects.crud', 'projects', 'Projects v2: project CRUD', 'api', 'niche', () =>
758
+ withRoot(async (root) => {
759
+ // missing title 422s; create → 201 with number 1; get by number; PATCH closes; DELETE → gone.
760
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/projectsV2`, body: '{}', root })).response.status !== 422) return false;
761
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/projectsV2`, body: JSON.stringify({ title: 'Roadmap', short_description: 'Q3' }), root });
762
+ if (!(c.response.status === 201 && (c.response.body as { number?: number }).number === 1)) return false;
763
+ const pid = (c.response.body as { id?: number }).id!;
764
+ const byNum = handleGithubRequest({ method: 'GET', path: `/orgs/octo/projectsV2/1`, root });
765
+ if (!(ok(byNum.status) && (byNum.body as { title?: string }).title === 'Roadmap')) return false;
766
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/projectsV2/${pid}`, body: JSON.stringify({ closed: true, title: 'Done' }), root });
767
+ if (!(ok(e.response.status) && (e.response.body as { closed?: boolean }).closed === true && (e.response.body as { title?: string }).title === 'Done')) return false;
768
+ const list = handleGithubRequest({ method: 'GET', path: `/orgs/octo/projectsV2`, root });
769
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
770
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/projectsV2/${pid}`, root });
771
+ const gone = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}`, root });
772
+ return del.response.status === 204 && gone.status === 404;
773
+ })),
774
+ done('github.projects.items', 'projects', 'Projects v2: items add/remove + field values', 'api', 'niche', () =>
775
+ withRoot(async (root) => {
776
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/projectsV2`, body: JSON.stringify({ title: 'P' }), root });
777
+ const pid = (c.response.body as { id?: number }).id!;
778
+ // a draft item needs a title (422 without); add → 201; a status field value sets via PATCH.
779
+ if ((await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/items`, body: '{}', root })).response.status !== 422) return false;
780
+ const add = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/items`, body: JSON.stringify({ content_type: 'DraftIssue', title: 'Ship it' }), root });
781
+ if (!(add.response.status === 201 && (add.response.body as { content_type?: string }).content_type === 'DraftIssue')) return false;
782
+ const iid = (add.response.body as { id?: number }).id!;
783
+ const linked = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/items`, body: JSON.stringify({ content_type: 'Issue', content_id: 42, content_repository: 'octo/demo' }), root });
784
+ if (linked.response.status !== 201) return false;
785
+ const setv = await applyGithubWrite({ method: 'PATCH', path: `/projectsV2/${pid}/items/${iid}`, body: JSON.stringify({ field_id: 'status', value: 'In Progress' }), root });
786
+ if (!(ok(setv.response.status) && (setv.response.body as { field_values?: Record<string, unknown> }).field_values?.status === 'In Progress')) return false;
787
+ const list = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/items`, root });
788
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 2)) return false;
789
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/projectsV2/${pid}/items/${iid}`, root });
790
+ const after = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/items`, root });
791
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 1;
792
+ })),
793
+ done('github.projects.fields', 'projects', 'Projects v2: custom fields (status/iteration/single-select)', 'api', 'niche', () =>
794
+ withRoot(async (root) => {
795
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/projectsV2`, body: JSON.stringify({ title: 'P' }), root });
796
+ const pid = (c.response.body as { id?: number }).id!;
797
+ // missing data_type 422s; an invalid type 422s; single-select carries options; iteration carries config.
798
+ if ((await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/fields`, body: JSON.stringify({ name: 'X' }), root })).response.status !== 422) return false;
799
+ if ((await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/fields`, body: JSON.stringify({ name: 'X', data_type: 'nope' }), root })).response.status !== 422) return false;
800
+ const ss = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/fields`, body: JSON.stringify({ name: 'Status', data_type: 'single_select', single_select_options: [{ name: 'Todo' }, { name: 'Done', color: 'GREEN' }] }), root });
801
+ if (!(ss.response.status === 201 && (ss.response.body as { options?: unknown[] }).options?.length === 2)) return false;
802
+ const it = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/fields`, body: JSON.stringify({ name: 'Sprint', data_type: 'iteration', iterations: [{ title: 'S1', duration: 14 }] }), root });
803
+ if (!(it.response.status === 201 && (((it.response.body as { configuration?: { iterations?: unknown[] } }).configuration?.iterations?.length) === 1))) return false;
804
+ const fid = (ss.response.body as { id?: number }).id!;
805
+ const list = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/fields`, root });
806
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 2)) return false;
807
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/projectsV2/${pid}/fields/${fid}`, root });
808
+ const after = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/fields`, root });
809
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 1;
810
+ })),
811
+ done('github.projects.views', 'projects', 'Projects v2: views (board/table/roadmap)', 'api', 'niche', () =>
812
+ withRoot(async (root) => {
813
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/projectsV2`, body: JSON.stringify({ title: 'P' }), root });
814
+ const pid = (c.response.body as { id?: number }).id!;
815
+ // an invalid layout 422s; board/table/roadmap each create; PATCH relayouts; DELETE removes.
816
+ if ((await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/views`, body: JSON.stringify({ name: 'V', layout: 'pie' }), root })).response.status !== 422) return false;
817
+ const board = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/views`, body: JSON.stringify({ name: 'Board', layout: 'board' }), root });
818
+ if (!(board.response.status === 201 && (board.response.body as { number?: number }).number === 1 && (board.response.body as { layout?: string }).layout === 'board')) return false;
819
+ const table = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/views`, body: JSON.stringify({ name: 'Table', layout: 'table' }), root });
820
+ if (table.response.status !== 201) return false;
821
+ const upd = await applyGithubWrite({ method: 'PATCH', path: `/projectsV2/${pid}/views/1`, body: JSON.stringify({ layout: 'roadmap' }), root });
822
+ if (!(ok(upd.response.status) && (upd.response.body as { layout?: string }).layout === 'roadmap')) return false;
823
+ const list = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/views`, root });
824
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 2)) return false;
825
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/projectsV2/${pid}/views/2`, root });
826
+ const after = handleGithubRequest({ method: 'GET', path: `/projectsV2/${pid}/views`, root });
827
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 1;
828
+ })),
829
+
830
+ // ── Discussions (whole product: discussions + categories + comments/replies + answers +
831
+ // a UI view). Modeled over the twin's REST handler (real GitHub Discussions are GraphQL
832
+ // — a declared deviation), consistent with the rest of the twin. LOCAL constructs. ─────
833
+ done('github.discussions.crud', 'discussions', 'Discussions: create/list/get/edit/delete', 'api', 'niche', () =>
834
+ withRoot(async (root) => {
835
+ // create → 201 with number + html_url + resolved category.
836
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'Welcome', body: 'hi', category: 'general' }), root });
837
+ if (c.response.status !== 201) return false;
838
+ const n = (c.response.body as { number?: number; category?: { slug?: string }; html_url?: string }).number!;
839
+ if (n !== 1 || (c.response.body as { category?: { slug?: string } }).category?.slug !== 'general') return false;
840
+ if (!(c.response.body as { html_url?: string }).html_url?.includes('/discussions/1')) return false;
841
+ // missing title → 422; unknown category → 422 (vendor-faithful validation).
842
+ const noTitle = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ category: 'general' }), root });
843
+ const badCat = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'x', category: 'nope' }), root });
844
+ if (noTitle.response.status !== 422 || badCat.response.status !== 422) return false;
845
+ // GET one + list round-trips title/category.
846
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1`, root });
847
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions`, root });
848
+ if (!(ok(one.status) && (one.body as { title?: string }).title === 'Welcome' && isArr(list.body) && list.body.length === 1)) return false;
849
+ // edit (title + close) round-trips; delete 204s then 404s.
850
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/discussions/1`, body: JSON.stringify({ title: 'Renamed', state: 'closed' }), root });
851
+ if (!(ok(e.response.status) && (e.response.body as { title?: string; state?: string }).title === 'Renamed' && (e.response.body as { state?: string }).state === 'closed')) return false;
852
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/discussions/1`, root });
853
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1`, root });
854
+ return del.response.status === 204 && gone.status === 404;
855
+ })),
856
+ done('github.discussions.categories', 'discussions', 'Discussions: categories (seeded defaults + custom + answerable Q&A)', 'api', 'niche', () =>
857
+ withRoot(async (root) => {
858
+ // the repo's category list seeds GitHub's defaults; Q&A is the answerable one.
859
+ const cats = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/categories`, root });
860
+ if (!(ok(cats.status) && isArr(cats.body))) return false;
861
+ const qa = (cats.body as Array<{ slug?: string; is_answerable?: boolean }>).find((c) => c.slug === 'q-a');
862
+ const general = (cats.body as Array<{ slug?: string; is_answerable?: boolean }>).find((c) => c.slug === 'general');
863
+ if (!(qa?.is_answerable === true && general?.is_answerable === false && (cats.body as unknown[]).length >= 5)) return false;
864
+ // a custom category is creatable + appears in the list with its slug.
865
+ const made = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/categories`, body: JSON.stringify({ name: 'Polls', is_answerable: false }), root });
866
+ if (!(made.response.status === 201 && (made.response.body as { slug?: string }).slug === 'polls')) return false;
867
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/categories`, root });
868
+ return isArr(after.body) && (after.body as Array<{ slug?: string }>).some((c) => c.slug === 'polls');
869
+ })),
870
+ done('github.discussions.comments', 'discussions', 'Discussions: comments + threaded replies', 'api', 'niche', () =>
871
+ withRoot(async (root) => {
872
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'Q', body: '?', category: 'q-a' }), root });
873
+ // top-level comment → 201; missing body → 422.
874
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/1/comments`, body: JSON.stringify({ body: 'an answer' }), root });
875
+ if (cm.response.status !== 201) return false;
876
+ const cid = (cm.response.body as { id?: number }).id!;
877
+ const noBody = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/1/comments`, body: '{}', root });
878
+ if (noBody.response.status !== 422) return false;
879
+ // a threaded reply pins to its parent via parent_id.
880
+ const reply = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/1/comments`, body: JSON.stringify({ body: 'thanks', parent_id: cid }), root });
881
+ if (!(reply.response.status === 201 && (reply.response.body as { parent_id?: number }).parent_id === cid)) return false;
882
+ // list returns both, and the discussion's comment count reflects them.
883
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1/comments`, root });
884
+ const disc = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1`, root });
885
+ return ok(list.status) && isArr(list.body) && list.body.length === 2 && (disc.body as { comments?: number }).comments === 2;
886
+ })),
887
+ done('github.discussions.answers', 'discussions', 'Discussions: mark/unmark accepted answer (Q&A only)', 'api', 'niche', () =>
888
+ withRoot(async (root) => {
889
+ // a Q&A (answerable) discussion + a comment.
890
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'Q', body: '?', category: 'q-a' }), root });
891
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/1/comments`, body: JSON.stringify({ body: 'this is the answer' }), root });
892
+ const cid = (cm.response.body as { id?: number }).id!;
893
+ // mark → comment.is_answer true + the discussion points at it.
894
+ const mark = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/discussions/1/comments/${cid}/answer`, body: '{}', root });
895
+ if (!(ok(mark.response.status) && (mark.response.body as { is_answer?: boolean }).is_answer === true)) return false;
896
+ const disc = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1`, root });
897
+ if ((disc.body as { answer_comment_id?: number }).answer_comment_id !== cid) return false;
898
+ // unmark clears both.
899
+ const unmark = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/discussions/1/comments/${cid}/answer`, root });
900
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/discussions/1`, root });
901
+ if (!(ok(unmark.response.status) && (unmark.response.body as { is_answer?: boolean }).is_answer === false && (after.body as { answer_comment_id?: number | null }).answer_comment_id === null)) return false;
902
+ // a NON-answerable (General) discussion rejects an answer mark → 422 (vendor-faithful).
903
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'chat', category: 'general' }), root });
904
+ const cm2 = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/2/comments`, body: JSON.stringify({ body: 'hi' }), root });
905
+ const bad = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/discussions/2/comments/${(cm2.response.body as { id?: number }).id}/answer`, body: '{}', root });
906
+ return bad.response.status === 422;
907
+ })),
908
+
909
+ // ── Orgs / Teams / Members / Users ───────────────────────────────────────────────────────
910
+ done('github.orgs.crud', 'orgs', 'Orgs: get/update + members', 'api', 'niche', () =>
911
+ withRoot(async (root) => {
912
+ // an unknown org 404s; PATCH auto-vivifies + round-trips the profile fields.
913
+ if (handleGithubRequest({ method: 'GET', path: `/orgs/octo`, root }).status !== 404) return false;
914
+ const upd = await applyGithubWrite({ method: 'PATCH', path: `/orgs/octo`, body: JSON.stringify({ description: 'The Octo org', billing_email: 'pay@octo.dev' }), root });
915
+ if (!(ok(upd.response.status) && (upd.response.body as { description?: string }).description === 'The Octo org')) return false;
916
+ const get = handleGithubRequest({ method: 'GET', path: `/orgs/octo`, root });
917
+ if (!(ok(get.status) && (get.body as { type?: string }).type === 'Organization' && (get.body as { description?: string }).description === 'The Octo org')) return false;
918
+ // members: invite via membership, then the member appears + the 204 membership check passes.
919
+ await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/memberships/alice`, body: JSON.stringify({ role: 'admin' }), root });
920
+ const members = handleGithubRequest({ method: 'GET', path: `/orgs/octo/members`, root });
921
+ return ok(members.status) && isArr(members.body) && members.body.length === 1 && (members.body[0] as { login?: string }).login === 'alice'
922
+ && handleGithubRequest({ method: 'GET', path: `/orgs/octo/members/alice`, root }).status === 204;
923
+ })),
924
+ done('github.orgs.membership', 'orgs', 'Orgs: membership (invite/role/remove) + outside collaborators', 'api', 'niche', () =>
925
+ withRoot(async (root) => {
926
+ // invite → role round-trips via GET memberships/:login; PATCH-style re-PUT changes role.
927
+ const inv = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/memberships/bob`, body: JSON.stringify({ role: 'member' }), root });
928
+ if (!(ok(inv.response.status) && (inv.response.body as { role?: string }).role === 'member')) return false;
929
+ const promote = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/memberships/bob`, body: JSON.stringify({ role: 'admin' }), root });
930
+ if ((promote.response.body as { role?: string }).role !== 'admin') return false;
931
+ const get = handleGithubRequest({ method: 'GET', path: `/orgs/octo/memberships/bob`, root });
932
+ if ((get.body as { role?: string }).role !== 'admin') return false;
933
+ // remove → member gone (404 check + delete idempotency: second delete 404s).
934
+ const rm = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/members/bob`, root });
935
+ if (rm.response.status !== 204) return false;
936
+ if (handleGithubRequest({ method: 'GET', path: `/orgs/octo/members/bob`, root }).status !== 404) return false;
937
+ if ((await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/members/bob`, root })).response.status !== 404) return false;
938
+ // outside collaborators: add → listed → remove → gone.
939
+ const oc = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/outside_collaborators/carol`, body: '{}', root });
940
+ if (oc.response.status !== 204) return false;
941
+ const ocList = handleGithubRequest({ method: 'GET', path: `/orgs/octo/outside_collaborators`, root });
942
+ if (!(isArr(ocList.body) && ocList.body.length === 1 && (ocList.body[0] as { login?: string }).login === 'carol')) return false;
943
+ const ocRm = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/outside_collaborators/carol`, root });
944
+ const after = handleGithubRequest({ method: 'GET', path: `/orgs/octo/outside_collaborators`, root });
945
+ return ocRm.response.status === 204 && isArr(after.body) && after.body.length === 0;
946
+ })),
947
+ done('github.orgs.repos', 'orgs', 'Orgs: list org repositories', 'api', 'common', () =>
948
+ withRoot(async (root) => {
949
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'pub' }), root });
950
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'sec', private: true }), root });
951
+ await applyGithubWrite({ method: 'POST', path: `/orgs/other/repos`, body: JSON.stringify({ name: 'x' }), root });
952
+ // list scopes to the org (not the unrelated 'other/x'); ?type=private filters.
953
+ const all = handleGithubRequest({ method: 'GET', path: `/orgs/octo/repos`, root });
954
+ if (!(ok(all.status) && isArr(all.body) && all.body.length === 2 && (all.body as Array<{ full_name?: string }>).every((r) => r.full_name!.startsWith('octo/')))) return false;
955
+ const priv = handleGithubRequest({ method: 'GET', path: `/orgs/octo/repos?type=private`, root });
956
+ return isArr(priv.body) && priv.body.length === 1 && (priv.body[0] as { name?: string }).name === 'sec';
957
+ })),
958
+ done('github.orgs.webhooks', 'orgs', 'Orgs: org-level webhooks CRUD', 'api', 'niche', () =>
959
+ withRoot(async (root) => {
960
+ // a config.url is required (422 without it); create → edit active → list → delete → 404.
961
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/hooks`, body: JSON.stringify({ events: ['push'] }), root })).response.status !== 422) return false;
962
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/hooks`, body: JSON.stringify({ events: ['push', 'repository'], config: { url: 'https://e.x/h', content_type: 'json' } }), root });
963
+ if (!(c.response.status === 201 && (c.response.body as { events?: string[] }).events?.length === 2)) return false;
964
+ const id = (c.response.body as { id?: number }).id!;
965
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/orgs/octo/hooks/${id}`, body: JSON.stringify({ active: false }), root });
966
+ if (!(ok(e.response.status) && (e.response.body as { active?: boolean }).active === false)) return false;
967
+ const list = handleGithubRequest({ method: 'GET', path: `/orgs/octo/hooks`, root });
968
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
969
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/hooks/${id}`, root });
970
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/orgs/octo/hooks/${id}`, root }).status === 404;
971
+ })),
972
+ done('github.teams.crud', 'orgs', 'Teams: CRUD + membership + repo access', 'api', 'niche', () =>
973
+ withRoot(async (root) => {
974
+ // create → slug derived from name; get by slug; PATCH description; membership; repo grant.
975
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams`, body: JSON.stringify({ name: 'Core Team', privacy: 'closed' }), root });
976
+ if (!(c.response.status === 201 && (c.response.body as { slug?: string }).slug === 'core-team')) return false;
977
+ // duplicate name (same slug) → 422.
978
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams`, body: JSON.stringify({ name: 'core-team' }), root })).response.status !== 422) return false;
979
+ const get = handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/core-team`, root });
980
+ if (!(ok(get.status) && (get.body as { privacy?: string }).privacy === 'closed')) return false;
981
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/orgs/octo/teams/core-team`, body: JSON.stringify({ description: 'the core team' }), root });
982
+ if ((e.response.body as { description?: string }).description !== 'the core team') return false;
983
+ // membership: add maintainer → appears in members + membership get shows role.
984
+ const m = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/teams/core-team/memberships/dana`, body: JSON.stringify({ role: 'maintainer' }), root });
985
+ if ((m.response.body as { role?: string }).role !== 'maintainer') return false;
986
+ const mem = handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/core-team/members`, root });
987
+ if (!(isArr(mem.body) && mem.body.length === 1 && (mem.body[0] as { login?: string }).login === 'dana')) return false;
988
+ // repo access: grant push → 204 check passes + repo appears in team repos.
989
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'svc' }), root });
990
+ const g = await applyGithubWrite({ method: 'PUT', path: `/orgs/octo/teams/core-team/repos/octo/svc`, body: JSON.stringify({ permission: 'push' }), root });
991
+ if (g.response.status !== 204) return false;
992
+ if (handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/core-team/repos/octo/svc`, root }).status !== 204) return false;
993
+ const repos = handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/core-team/repos`, root });
994
+ if (!(isArr(repos.body) && repos.body.length === 1 && (repos.body[0] as { full_name?: string }).full_name === 'octo/svc')) return false;
995
+ // delete team → 404 thereafter.
996
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/teams/core-team`, root });
997
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/core-team`, root }).status === 404;
998
+ })),
999
+ done('github.teams.discussions', 'orgs', 'Teams: team discussions + comments', 'api', 'niche', () =>
1000
+ withRoot(async (root) => {
1001
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams`, body: JSON.stringify({ name: 'eng' }), root });
1002
+ // a missing title 422s; create → per-team number 1; edit body; pin.
1003
+ if ((await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams/eng/discussions`, body: '{}', root })).response.status !== 422) return false;
1004
+ const d = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams/eng/discussions`, body: JSON.stringify({ title: 'Roadmap', body: 'Q3 plan' }), root });
1005
+ if (!(d.response.status === 201 && (d.response.body as { number?: number }).number === 1)) return false;
1006
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/orgs/octo/teams/eng/discussions/1`, body: JSON.stringify({ pinned: true }), root });
1007
+ if ((e.response.body as { pinned?: boolean }).pinned !== true) return false;
1008
+ // comments: create → per-discussion number 1; edit; list; delete → list shrinks.
1009
+ const c = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/teams/eng/discussions/1/comments`, body: JSON.stringify({ body: '+1' }), root });
1010
+ if (!(c.response.status === 201 && (c.response.body as { number?: number }).number === 1)) return false;
1011
+ const cl = handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/eng/discussions/1/comments`, root });
1012
+ if (!(isArr(cl.body) && cl.body.length === 1 && (cl.body[0] as { body?: string }).body === '+1')) return false;
1013
+ const cdel = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/teams/eng/discussions/1/comments/1`, root });
1014
+ if (cdel.response.status !== 204) return false;
1015
+ const cl2 = handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/eng/discussions/1/comments`, root });
1016
+ if (!(isArr(cl2.body) && cl2.body.length === 0)) return false;
1017
+ // delete the discussion → 404 thereafter (and its comments orphan-pruned).
1018
+ const ddel = await applyGithubWrite({ method: 'DELETE', path: `/orgs/octo/teams/eng/discussions/1`, root });
1019
+ return ddel.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/orgs/octo/teams/eng/discussions/1`, root }).status === 404;
1020
+ })),
1021
+ done('github.users.profile', 'users', 'Users: get profile + authenticated user', 'api', 'common', () =>
1022
+ withRoot(async (root) => {
1023
+ const u = handleGithubRequest({ method: 'GET', path: `/users/octocat`, root });
1024
+ if (!(ok(u.status) && (u.body as { login?: string }).login === 'octocat' && typeof (u.body as { id?: number }).id === 'number')) return false;
1025
+ const me = handleGithubRequest({ method: 'GET', path: `/user`, root });
1026
+ return ok(me.status) && (me.body as { login?: string }).login === 'octocat';
1027
+ })),
1028
+ done('github.users.followers', 'users', 'Users: followers/following', 'api', 'niche', () =>
1029
+ withRoot(async (root) => {
1030
+ // not following → 404 check; follow → 204 check + appears in /user/following.
1031
+ if (handleGithubRequest({ method: 'GET', path: `/user/following/ann`, root }).status !== 404) return false;
1032
+ const f = await applyGithubWrite({ method: 'PUT', path: `/user/following/ann`, body: '{}', root });
1033
+ if (f.response.status !== 204) return false;
1034
+ if (handleGithubRequest({ method: 'GET', path: `/user/following/ann`, root }).status !== 204) return false;
1035
+ const following = handleGithubRequest({ method: 'GET', path: `/user/following`, root });
1036
+ if (!(isArr(following.body) && following.body.length === 1 && (following.body[0] as { login?: string }).login === 'ann')) return false;
1037
+ // ann's followers now include octocat (the only modeled actor).
1038
+ const followers = handleGithubRequest({ method: 'GET', path: `/users/ann/followers`, root });
1039
+ if (!(isArr(followers.body) && followers.body.length === 1 && (followers.body[0] as { login?: string }).login === 'octocat')) return false;
1040
+ // unfollow → check 404 again + list empties.
1041
+ const uf = await applyGithubWrite({ method: 'DELETE', path: `/user/following/ann`, root });
1042
+ const after = handleGithubRequest({ method: 'GET', path: `/user/following`, root });
1043
+ return uf.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/user/following/ann`, root }).status === 404 && isArr(after.body) && after.body.length === 0;
1044
+ })),
1045
+
1046
+ // ── Gists ────────────────────────────────────────────────────────────────────────────────
1047
+ done('github.gists.crud', 'gists', 'Gists: create/edit/list/get + comments', 'api', 'common', () =>
1048
+ withRoot(async (root) => {
1049
+ const c = await applyGithubWrite({ method: 'POST', path: `/gists`, body: JSON.stringify({ description: 'snip', public: true, files: { 'a.ts': { content: 'export const a=1' } } }), root });
1050
+ if (c.response.status !== 201) return false;
1051
+ const id = (c.response.body as { id?: string }).id!;
1052
+ const get = handleGithubRequest({ method: 'GET', path: `/gists/${id}`, root });
1053
+ if (!(ok(get.status) && (get.body as { files?: Record<string, unknown> }).files?.['a.ts'])) return false;
1054
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/gists/${id}`, body: JSON.stringify({ description: 'renamed' }), root });
1055
+ if (!(ok(e.response.status) && (e.response.body as { description?: string }).description === 'renamed')) return false;
1056
+ const list = handleGithubRequest({ method: 'GET', path: `/gists`, root });
1057
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1058
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/gists/${id}`, root });
1059
+ const gone = handleGithubRequest({ method: 'GET', path: `/gists/${id}`, root });
1060
+ return del.response.status === 204 && gone.status === 404;
1061
+ })),
1062
+
1063
+ // ── Search ───────────────────────────────────────────────────────────────────────────────
1064
+ done('github.search.issues', 'search', 'Search: issues/PRs', 'api', 'common', () =>
1065
+ withRoot(async (root) => {
1066
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'fix login bug' }), root });
1067
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'add login', head: 'a', base: 'main' }), root });
1068
+ const all = handleGithubRequest({ method: 'GET', path: `/search/issues?q=${encodeURIComponent('login')}`, root });
1069
+ if (!(ok(all.status) && (all.body as { total_count?: number }).total_count === 2)) return false;
1070
+ const prsOnly = handleGithubRequest({ method: 'GET', path: `/search/issues?q=${encodeURIComponent('login is:pr')}`, root });
1071
+ const items = (prsOnly.body as { items?: Array<{ pull_request?: unknown }> }).items ?? [];
1072
+ return (prsOnly.body as { total_count?: number }).total_count === 1 && items.every((i) => 'pull_request' in i);
1073
+ })),
1074
+ done('github.search.code', 'search', 'Search: code', 'api', 'common', () =>
1075
+ withRoot(async (root) => {
1076
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/src/util.ts`, body: JSON.stringify({ message: 'm', content: Buffer.from('x').toString('base64') }), root });
1077
+ const r = handleGithubRequest({ method: 'GET', path: `/search/code?q=${encodeURIComponent('util')}`, root });
1078
+ return ok(r.status) && (r.body as { total_count?: number }).total_count === 1 && ((r.body as { items?: Array<{ path?: string }> }).items?.[0]?.path === 'src/util.ts');
1079
+ })),
1080
+ done('github.search.repos', 'search', 'Search: repos', 'api', 'common', () =>
1081
+ withRoot(async (root) => {
1082
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'searchme', description: 'a findable repo' }), root });
1083
+ const r = handleGithubRequest({ method: 'GET', path: `/search/repositories?q=${encodeURIComponent('searchme')}`, root });
1084
+ return ok(r.status) && (r.body as { total_count?: number }).total_count === 1;
1085
+ })),
1086
+ done('github.search.users', 'search', 'Search: users', 'api', 'common', () =>
1087
+ withRoot(async (root) => {
1088
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/collaborators/alice`, body: JSON.stringify({ permission: 'push' }), root });
1089
+ const r = handleGithubRequest({ method: 'GET', path: `/search/users?q=${encodeURIComponent('alice')}`, root });
1090
+ return ok(r.status) && (r.body as { total_count?: number }).total_count === 1 && ((r.body as { items?: Array<{ login?: string }> }).items?.[0]?.login === 'alice');
1091
+ })),
1092
+ done('github.search.commits', 'search', 'Search: commits', 'api', 'common', () =>
1093
+ withRoot(async (root) => {
1094
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'p', head: 'a', base: 'main', commits: [{ sha: 'abcd', message: 'initial commit' }] }), root });
1095
+ const r = handleGithubRequest({ method: 'GET', path: `/search/commits?q=${encodeURIComponent('initial')}`, root });
1096
+ return ok(r.status) && (r.body as { total_count?: number }).total_count === 1;
1097
+ })),
1098
+
1099
+ // ── Notifications ────────────────────────────────────────────────────────────────────────
1100
+ done('github.notifications.list', 'notifications', 'Notifications: list/mark-read + thread subscriptions', 'api', 'common', () =>
1101
+ withRoot(async (root) => {
1102
+ const n = await applyGithubWrite({ method: 'POST', path: `/notifications`, body: JSON.stringify({ repository: REPO, subject_title: 'New PR', subject_type: 'PullRequest', reason: 'review_requested' }), root });
1103
+ if (n.response.status !== 201) return false;
1104
+ const id = (n.response.body as { id?: string }).id!;
1105
+ const list = handleGithubRequest({ method: 'GET', path: `/notifications`, root });
1106
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { unread?: boolean }).unread === true)) return false;
1107
+ const read = await applyGithubWrite({ method: 'PATCH', path: `/notifications/threads/${id}`, body: '{}', root });
1108
+ if (read.response.status !== 205) return false;
1109
+ const after = handleGithubRequest({ method: 'GET', path: `/notifications`, root });
1110
+ const allList = handleGithubRequest({ method: 'GET', path: `/notifications?all=true`, root });
1111
+ return isArr(after.body) && after.body.length === 0 && isArr(allList.body) && allList.body.length === 1;
1112
+ })),
1113
+
1114
+ // ── Platform meta endpoints (stateless reference catalogs) ───────────────────────────────
1115
+ done('github.meta.rate_limit', 'meta', 'Rate limit: GET /rate_limit envelope (resources + rate)', 'api', 'core', () =>
1116
+ withRoot(async (root) => {
1117
+ const r = handleGithubRequest({ method: 'GET', path: `/rate_limit`, root });
1118
+ const b = r.body as { resources?: { core?: { limit?: number; remaining?: number } }; rate?: { limit?: number } };
1119
+ return ok(r.status) && b.resources?.core?.limit === 5000 && b.resources?.core?.remaining === 5000 && b.rate?.limit === 5000;
1120
+ })),
1121
+ done('github.meta.markdown', 'meta', 'Markdown: POST /markdown → rendered GFM HTML', 'api', 'common', () =>
1122
+ withRoot(async (root) => {
1123
+ const r = await applyGithubWrite({ method: 'POST', path: `/markdown`, body: JSON.stringify({ text: '# Title\n**b** `c` [x](http://e.x)' }), root });
1124
+ const html = String(r.response.body ?? '');
1125
+ if (!(ok(r.response.status) && html.includes('<h1>Title</h1>') && html.includes('<strong>b</strong>') && html.includes('<code>c</code>') && html.includes('<a href="http://e.x">x</a>'))) return false;
1126
+ // missing required `text` → 422 like GitHub.
1127
+ const bad = await applyGithubWrite({ method: 'POST', path: `/markdown`, body: '{}', root });
1128
+ return bad.response.status === 422;
1129
+ })),
1130
+ done('github.meta.gitignore', 'meta', 'gitignore templates: list + GET one (name/source)', 'api', 'niche', () =>
1131
+ withRoot(async (root) => {
1132
+ const list = handleGithubRequest({ method: 'GET', path: `/gitignore/templates`, root });
1133
+ if (!(ok(list.status) && isArr(list.body) && (list.body as string[]).includes('Node'))) return false;
1134
+ const one = handleGithubRequest({ method: 'GET', path: `/gitignore/templates/Node`, root });
1135
+ const b = one.body as { name?: string; source?: string };
1136
+ if (!(ok(one.status) && b.name === 'Node' && typeof b.source === 'string' && b.source.length > 0)) return false;
1137
+ return handleGithubRequest({ method: 'GET', path: `/gitignore/templates/Nope`, root }).status === 404;
1138
+ })),
1139
+ done('github.meta.licenses', 'meta', 'Licenses: list + GET one (key/spdx_id)', 'api', 'niche', () =>
1140
+ withRoot(async (root) => {
1141
+ const list = handleGithubRequest({ method: 'GET', path: `/licenses`, root });
1142
+ if (!(ok(list.status) && isArr(list.body) && (list.body as Array<{ key?: string }>).some((l) => l.key === 'mit'))) return false;
1143
+ const one = handleGithubRequest({ method: 'GET', path: `/licenses/mit`, root });
1144
+ const b = one.body as { spdx_id?: string; key?: string };
1145
+ if (!(ok(one.status) && b.key === 'mit' && b.spdx_id === 'MIT')) return false;
1146
+ return handleGithubRequest({ method: 'GET', path: `/licenses/nope`, root }).status === 404;
1147
+ })),
1148
+ done('github.meta.emojis', 'meta', 'Emojis: GET /emojis name→asset-url map', 'api', 'niche', () =>
1149
+ withRoot(async (root) => {
1150
+ const r = handleGithubRequest({ method: 'GET', path: `/emojis`, root });
1151
+ const b = r.body as Record<string, string>;
1152
+ return ok(r.status) && typeof b['+1'] === 'string' && b['rocket']!.startsWith('https://');
1153
+ })),
1154
+ done('github.meta.service_meta', 'meta', 'Meta: GET /meta service metadata envelope', 'api', 'niche', () =>
1155
+ withRoot(async (root) => {
1156
+ const r = handleGithubRequest({ method: 'GET', path: `/meta`, root });
1157
+ const b = r.body as { verifiable_password_authentication?: boolean; hooks?: unknown[] };
1158
+ return ok(r.status) && typeof b.verifiable_password_authentication === 'boolean' && Array.isArray(b.hooks);
1159
+ })),
1160
+
1161
+ // ── Activity (starring / watching) ───────────────────────────────────────────────────────
1162
+ done('github.activity.starring', 'activity', 'Starring: PUT/DELETE /user/starred/:o/:r + stargazers + check', 'api', 'common', () =>
1163
+ withRoot(async (root) => {
1164
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1165
+ // not starred yet → 404 on the membership check.
1166
+ if (handleGithubRequest({ method: 'GET', path: `/user/starred/octo/demo`, root }).status !== 404) return false;
1167
+ const star = await applyGithubWrite({ method: 'PUT', path: `/user/starred/octo/demo`, root });
1168
+ if (!(star.response.status === 204 && star.webhook?.event === 'star' && star.webhook.action === 'created')) return false;
1169
+ if (handleGithubRequest({ method: 'GET', path: `/user/starred/octo/demo`, root }).status !== 204) return false;
1170
+ const gz = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/stargazers`, root });
1171
+ if (!(ok(gz.status) && isArr(gz.body) && gz.body.length === 1 && (gz.body[0] as { login?: string }).login === 'octocat')) return false;
1172
+ const mine = handleGithubRequest({ method: 'GET', path: `/user/starred`, root });
1173
+ if (!(ok(mine.status) && isArr(mine.body) && mine.body.length === 1)) return false;
1174
+ const un = await applyGithubWrite({ method: 'DELETE', path: `/user/starred/octo/demo`, root });
1175
+ return un.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/user/starred/octo/demo`, root }).status === 404;
1176
+ })),
1177
+ done('github.activity.watching', 'activity', 'Watching: PUT/DELETE/GET .../subscription + subscribers', 'api', 'common', () =>
1178
+ withRoot(async (root) => {
1179
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1180
+ // not watching yet → 404.
1181
+ if (handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/subscription`, root }).status !== 404) return false;
1182
+ const w = await applyGithubWrite({ method: 'PUT', path: `/repos/octo/demo/subscription`, root });
1183
+ const wb = w.response.body as { subscribed?: boolean };
1184
+ if (!(ok(w.response.status) && wb.subscribed === true)) return false;
1185
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/subscription`, root });
1186
+ if (!(ok(get.status) && (get.body as { subscribed?: boolean }).subscribed === true)) return false;
1187
+ const subs = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/subscribers`, root });
1188
+ if (!(ok(subs.status) && isArr(subs.body) && subs.body.length === 1)) return false;
1189
+ const un = await applyGithubWrite({ method: 'DELETE', path: `/repos/octo/demo/subscription`, root });
1190
+ return un.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/subscription`, root }).status === 404;
1191
+ })),
1192
+ done('github.activity.feeds', 'activity', 'Activity: feeds + received/public events streams', 'api', 'niche', () =>
1193
+ withRoot(async (root) => {
1194
+ // GET /feeds returns the feed-URL catalog (with timeline + _links). The event streams
1195
+ // (public timeline, per-user events, received_events) are DERIVED from modeled objects:
1196
+ // a merged PR + an issue produce PullRequestEvent + IssuesEvent in the stream.
1197
+ const feeds = handleGithubRequest({ method: 'GET', path: '/feeds', root });
1198
+ if (!(ok(feeds.status) && typeof (feeds.body as { timeline_url?: string }).timeline_url === 'string' && (feeds.body as { _links?: object })._links)) return false;
1199
+ await applyGithubWrite({ method: 'POST', path: `/repos/octo/demo/pulls`, body: JSON.stringify({ title: 'p', head: 'a', base: 'main' }), root });
1200
+ await applyGithubWrite({ method: 'PUT', path: `/repos/octo/demo/pulls/1/merge`, body: '{}', root });
1201
+ await applyGithubWrite({ method: 'POST', path: `/repos/octo/demo/issues`, body: JSON.stringify({ title: 'i' }), root });
1202
+ const pub = handleGithubRequest({ method: 'GET', path: '/events', root });
1203
+ if (!(ok(pub.status) && isArr(pub.body) && pub.body.length === 2)) return false;
1204
+ const types = new Set((pub.body as Array<{ type?: string }>).map((e) => e.type));
1205
+ if (!(types.has('PullRequestEvent') && types.has('IssuesEvent'))) return false;
1206
+ // octo OWNS octo/demo → its received_events stream carries both; an unknown user's is empty.
1207
+ const received = handleGithubRequest({ method: 'GET', path: '/users/octo/received_events', root });
1208
+ const userEv = handleGithubRequest({ method: 'GET', path: '/users/octo/events', root });
1209
+ const empty = handleGithubRequest({ method: 'GET', path: '/users/nobody/events', root });
1210
+ const repoEv = handleGithubRequest({ method: 'GET', path: '/repos/octo/demo/events', root });
1211
+ return ok(received.status) && isArr(received.body) && received.body.length === 2
1212
+ && ok(userEv.status) && isArr(userEv.body) && userEv.body.length === 2
1213
+ && ok(empty.status) && isArr(empty.body) && empty.body.length === 0
1214
+ && ok(repoEv.status) && isArr(repoEv.body) && repoEv.body.length === 2;
1215
+ })),
1216
+ done('github.activity.notifications_repo', 'activity', 'Activity: per-repo notifications + thread subscriptions', 'api', 'niche', () =>
1217
+ withRoot(async (root) => {
1218
+ // Seed two notifications in different repos; the per-repo inbox returns only that repo's.
1219
+ const n = await applyGithubWrite({ method: 'POST', path: '/notifications', body: JSON.stringify({ repository: 'octo/a', subject_title: 'PR in a', subject_type: 'PullRequest' }), root });
1220
+ await applyGithubWrite({ method: 'POST', path: '/notifications', body: JSON.stringify({ repository: 'octo/b', subject_title: 'Issue in b', subject_type: 'Issue' }), root });
1221
+ const nid = (n.response.body as { id?: string }).id!;
1222
+ const inbox = handleGithubRequest({ method: 'GET', path: '/repos/octo/a/notifications', root });
1223
+ if (!(ok(inbox.status) && isArr(inbox.body) && inbox.body.length === 1 && (inbox.body[0] as { repository?: { full_name?: string } }).repository?.full_name === 'octo/a')) return false;
1224
+ // thread subscription: a subscribed thread reads subscribed:true; DELETE clears it (and
1225
+ // marks the thread read, so it drops out of the unread per-repo inbox).
1226
+ const sub = handleGithubRequest({ method: 'GET', path: `/notifications/threads/${nid}/subscription`, root });
1227
+ if (!(ok(sub.status) && (sub.body as { subscribed?: boolean }).subscribed === true)) return false;
1228
+ const ignore = await applyGithubWrite({ method: 'PUT', path: `/notifications/threads/${nid}/subscription`, body: JSON.stringify({ ignored: true }), root });
1229
+ if (!(ok(ignore.response.status) && (ignore.response.body as { ignored?: boolean }).ignored === true)) return false;
1230
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/notifications/threads/${nid}/subscription`, root });
1231
+ const after = handleGithubRequest({ method: 'GET', path: '/repos/octo/a/notifications', root });
1232
+ return del.response.status === 204 && ok(after.status) && isArr(after.body) && after.body.length === 0;
1233
+ })),
1234
+
1235
+ // ── Repo metadata (derived read endpoints) ───────────────────────────────────────────────
1236
+ done('github.repos.languages', 'repos', 'Repos: language byte-breakdown (GET .../languages)', 'api', 'common', () =>
1237
+ withRoot(async (root) => {
1238
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/src/a.ts`, body: JSON.stringify({ message: 'm', content: Buffer.from('x'.repeat(40)).toString('base64') }), root });
1239
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/main.py`, body: JSON.stringify({ message: 'm', content: Buffer.from('y'.repeat(10)).toString('base64') }), root });
1240
+ const r = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/languages`, root });
1241
+ const b = r.body as Record<string, number>;
1242
+ return ok(r.status) && b.TypeScript === 40 && b.Python === 10;
1243
+ })),
1244
+ done('github.repos.contributors', 'repos', 'Repos: contributors (GET .../contributors, commit-author tallies)', 'api', 'common', () =>
1245
+ withRoot(async (root) => {
1246
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'p', head: 'a', base: 'main', commits: [{ sha: 'a1', message: 'c1', author_name: 'alice' }, { sha: 'a2', message: 'c2', author_name: 'alice' }, { sha: 'b1', message: 'c3', author_name: 'bob' }] }), root });
1247
+ const r = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/contributors`, root });
1248
+ const arr = r.body as Array<{ login?: string; contributions?: number }>;
1249
+ return ok(r.status) && arr.length === 2 && arr[0]!.login === 'alice' && arr[0]!.contributions === 2 && arr[1]!.login === 'bob';
1250
+ })),
1251
+ done('github.repos.compare', 'repos', 'Repos: compare commits (GET .../compare/:base...:head)', 'api', 'common', () =>
1252
+ withRoot(async (root) => {
1253
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'p', head: 'feat-sha', base: 'main', commits: [{ sha: 'c1', message: 'one' }], files: [{ filename: 'x.ts', additions: 2, deletions: 0 }] }), root });
1254
+ const r = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/compare/main...feat-sha`, root });
1255
+ const b = r.body as { total_commits?: number; commits?: unknown[]; files?: Array<{ filename?: string }>; status?: string };
1256
+ return ok(r.status) && b.total_commits === 1 && b.status === 'ahead' && (b.files?.[0]?.filename === 'x.ts');
1257
+ })),
1258
+ done('github.repos.commit_comments', 'repos', 'Repos: commit comments (on a commit sha)', 'api', 'niche', () =>
1259
+ withRoot(async (root) => {
1260
+ // a missing body 422s; create on a sha → listed under the commit + the repo-wide list.
1261
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/commits/abc123/comments`, body: '{}', root })).response.status !== 422) return false;
1262
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/commits/abc123/comments`, body: JSON.stringify({ body: 'nice fix', path: 'a.ts', line: 5 }), root });
1263
+ if (!(c.response.status === 201 && (c.response.body as { commit_id?: string }).commit_id === 'abc123' && (c.response.body as { path?: string }).path === 'a.ts')) return false;
1264
+ const id = (c.response.body as { id?: number }).id!;
1265
+ const onCommit = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/commits/abc123/comments`, root });
1266
+ if (!(ok(onCommit.status) && isArr(onCommit.body) && onCommit.body.length === 1)) return false;
1267
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/comments/${id}`, root });
1268
+ if (!(ok(one.status) && (one.body as { body?: string }).body === 'nice fix')) return false;
1269
+ // edit body; delete → repo-wide list empties.
1270
+ const e = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/comments/${id}`, body: JSON.stringify({ body: 'edited' }), root });
1271
+ if ((e.response.body as { body?: string }).body !== 'edited') return false;
1272
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/comments/${id}`, root });
1273
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/comments`, root });
1274
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 0
1275
+ && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/comments/${id}`, root }).status === 404;
1276
+ })),
1277
+ done('github.repos.readme', 'repos', 'Repos: GET .../readme (rendered + raw)', 'api', 'niche', () =>
1278
+ withRoot(async (root) => {
1279
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1280
+ // no README yet → 404; write README.md then it returns the base64 content object.
1281
+ if (handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/readme`, root }).status !== 404) return false;
1282
+ const b64 = Buffer.from('# Demo\nhello').toString('base64');
1283
+ const put = await applyGithubWrite({ method: 'PUT', path: `/repos/octo/demo/contents/README.md`, body: JSON.stringify({ message: 'add readme', content: b64 }), root });
1284
+ if (put.response.status !== 201) return false;
1285
+ const r = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/readme`, root });
1286
+ return ok(r.status) && (r.body as { name?: string }).name === 'README.md' && (r.body as { encoding?: string }).encoding === 'base64'
1287
+ && Buffer.from(String((r.body as { content?: string }).content ?? ''), 'base64').toString() === '# Demo\nhello';
1288
+ })),
1289
+ done('github.repos.traffic', 'repos', 'Repos: traffic (views/clones/referrers/paths)', 'api', 'niche', () =>
1290
+ withRoot(async (root) => {
1291
+ // traffic on a missing repo → 404; on a real repo → vendor-shaped zero-history payloads
1292
+ // (per-account traffic is not observable offline — honestly empty, never fabricated).
1293
+ if (handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/traffic/views`, root }).status !== 404) return false;
1294
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1295
+ const views = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/traffic/views`, root });
1296
+ if (!(ok(views.status) && (views.body as { count?: number }).count === 0 && isArr((views.body as { views?: unknown[] }).views))) return false;
1297
+ const clones = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/traffic/clones`, root });
1298
+ if (!(ok(clones.status) && isArr((clones.body as { clones?: unknown[] }).clones))) return false;
1299
+ const refs = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/traffic/popular/referrers`, root });
1300
+ const paths = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/traffic/popular/paths`, root });
1301
+ return ok(refs.status) && isArr(refs.body) && ok(paths.status) && isArr(paths.body);
1302
+ })),
1303
+ done('github.repos.deploy_keys', 'repos', 'Repos: deploy keys CRUD', 'api', 'niche', () =>
1304
+ withRoot(async (root) => {
1305
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1306
+ // missing key 422s; create → 201; get/list; delete → 404.
1307
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/keys`, body: JSON.stringify({ title: 'ci' }), root })).response.status !== 422) return false;
1308
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/keys`, body: JSON.stringify({ title: 'deploy', key: 'ssh-ed25519 AAAA', read_only: false }), root });
1309
+ if (!(c.response.status === 201 && (c.response.body as { read_only?: boolean }).read_only === false)) return false;
1310
+ const id = (c.response.body as { id?: number }).id!;
1311
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/keys/${id}`, root });
1312
+ if (!(ok(one.status) && (one.body as { title?: string }).title === 'deploy')) return false;
1313
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/keys`, root });
1314
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1315
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/keys/${id}`, root });
1316
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/keys/${id}`, root }).status === 404;
1317
+ })),
1318
+ done('github.repos.autolinks', 'repos', 'Repos: autolink references CRUD', 'api', 'niche', () =>
1319
+ withRoot(async (root) => {
1320
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1321
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/autolinks`, body: JSON.stringify({ key_prefix: 'TICKET-' }), root })).response.status !== 422) return false;
1322
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/autolinks`, body: JSON.stringify({ key_prefix: 'TICKET-', url_template: 'https://jira/<num>', is_alphanumeric: false }), root });
1323
+ if (!(c.response.status === 201 && (c.response.body as { is_alphanumeric?: boolean }).is_alphanumeric === false)) return false;
1324
+ const id = (c.response.body as { id?: number }).id!;
1325
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/autolinks/${id}`, root });
1326
+ if (!(ok(one.status) && (one.body as { key_prefix?: string }).key_prefix === 'TICKET-')) return false;
1327
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/autolinks`, root });
1328
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1329
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/autolinks/${id}`, root });
1330
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/autolinks/${id}`, root }).status === 404;
1331
+ })),
1332
+ done('github.repos.tarball', 'repos', 'Repos: tar/zipball archive download', 'api', 'niche', () =>
1333
+ withRoot(async (root) => {
1334
+ // archive of a missing repo → 404; a real repo → 302 redirect to codeload (bytes out of scope).
1335
+ if (handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/tarball`, root }).status !== 404) return false;
1336
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo', default_branch: 'main' }), root });
1337
+ const tar = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/tarball`, root });
1338
+ if (!(tar.status === 302 && String((tar.body as { location?: string }).location).includes('legacy.tar.gz'))) return false;
1339
+ const zip = handleGithubRequest({ method: 'GET', path: `/repos/octo/demo/zipball/v1.0.0`, root });
1340
+ return zip.status === 302 && String((zip.body as { location?: string }).location).includes('legacy.zip/v1.0.0');
1341
+ })),
1342
+ done('github.repos.rulesets', 'repos', 'Repos: rulesets (the newer branch/tag rule engine)', 'api', 'niche', () =>
1343
+ withRoot(async (root) => {
1344
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1345
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/rulesets`, body: JSON.stringify({ target: 'branch' }), root })).response.status !== 422) return false;
1346
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/rulesets`, body: JSON.stringify({ name: 'main-protection', target: 'branch', enforcement: 'active', rules: [{ type: 'pull_request' }] }), root });
1347
+ if (!(c.response.status === 201 && (c.response.body as { enforcement?: string }).enforcement === 'active' && ((c.response.body as { rules?: unknown[] }).rules?.length === 1))) return false;
1348
+ const id = (c.response.body as { id?: number }).id!;
1349
+ const upd = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/rulesets/${id}`, body: JSON.stringify({ enforcement: 'disabled' }), root });
1350
+ if (!(ok(upd.response.status) && (upd.response.body as { enforcement?: string }).enforcement === 'disabled')) return false;
1351
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/rulesets/${id}`, root });
1352
+ if (!(ok(one.status) && (one.body as { name?: string }).name === 'main-protection')) return false;
1353
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/rulesets`, root });
1354
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1355
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/rulesets/${id}`, root });
1356
+ return del.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/rulesets/${id}`, root }).status === 404;
1357
+ })),
1358
+
1359
+ // ── Deployments / Environments ───────────────────────────────────────────────────────────
1360
+ done('github.deployments.crud', 'deployments', 'Deployments: create/list + deployment statuses', 'api', 'common', () =>
1361
+ withRoot(async (root) => {
1362
+ // create → 201 with id + environment; missing ref → 422.
1363
+ const bad = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments`, body: '{}', root });
1364
+ if (bad.response.status !== 422) return false;
1365
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments`, body: JSON.stringify({ ref: 'main', environment: 'production', description: 'deploy' }), root });
1366
+ if (!(c.response.status === 201 && (c.response.body as { environment?: string }).environment === 'production')) return false;
1367
+ const id = (c.response.body as { id?: number }).id!;
1368
+ // get one + list (newest-first) + ?environment filter.
1369
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/deployments/${id}`, root });
1370
+ if (!(ok(one.status) && (one.body as { id?: number }).id === id)) return false;
1371
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/deployments`, root });
1372
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1373
+ const filtered = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/deployments?environment=staging`, root });
1374
+ if (!(isArr(filtered.body) && filtered.body.length === 0)) return false;
1375
+ // statuses: invalid state → 422; status on a missing deployment → 404.
1376
+ const badState = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments/${id}/statuses`, body: JSON.stringify({ state: 'nope' }), root });
1377
+ if (badState.response.status !== 422) return false;
1378
+ const noDep = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments/999/statuses`, body: JSON.stringify({ state: 'success' }), root });
1379
+ if (noDep.response.status !== 404) return false;
1380
+ const s1 = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments/${id}/statuses`, body: JSON.stringify({ state: 'in_progress' }), root });
1381
+ const s2 = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments/${id}/statuses`, body: JSON.stringify({ state: 'success', environment_url: 'https://app.x' }), root });
1382
+ if (!(s1.response.status === 201 && s2.response.status === 201)) return false;
1383
+ const statuses = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/deployments/${id}/statuses`, root });
1384
+ // newest-first → the latest (success) leads.
1385
+ return ok(statuses.status) && isArr(statuses.body) && statuses.body.length === 2 && (statuses.body[0] as { state?: string }).state === 'success';
1386
+ })),
1387
+ done('github.environments.crud', 'deployments', 'Environments: CRUD + protection rules', 'api', 'niche', () =>
1388
+ withRoot(async (root) => {
1389
+ // create-or-update (PUT) → 200 with the env + protection rules from wait_timer/reviewers.
1390
+ const c = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/environments/production`, body: JSON.stringify({ wait_timer: 30, reviewers: [{ type: 'User', reviewer: { login: 'alice', type: 'User' } }] }), root });
1391
+ if (!(c.response.status === 200 && (c.response.body as { name?: string }).name === 'production')) return false;
1392
+ const rules = (c.response.body as { protection_rules?: Array<{ type?: string }> }).protection_rules ?? [];
1393
+ if (!(rules.some((r) => r.type === 'wait_timer') && rules.some((r) => r.type === 'required_reviewers'))) return false;
1394
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/environments/production`, root });
1395
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/environments`, root });
1396
+ if (!(ok(get.status) && ok(list.status) && (list.body as { total_count?: number }).total_count === 1)) return false;
1397
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/environments/production`, root });
1398
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/environments/production`, root });
1399
+ return del.response.status === 204 && gone.status === 404;
1400
+ })),
1401
+
1402
+ // ── User account (the authenticated user) ────────────────────────────────────────────────
1403
+ done('github.user.emails', 'users', 'User: emails (list/add/delete) + visibility', 'api', 'niche', () =>
1404
+ withRoot(async (root) => {
1405
+ // empty add 422s; first add becomes primary+public; second is private; public_emails filters;
1406
+ // delete removes one.
1407
+ if ((await applyGithubWrite({ method: 'POST', path: `/user/emails`, body: '{}', root })).response.status !== 422) return false;
1408
+ const a1 = await applyGithubWrite({ method: 'POST', path: `/user/emails`, body: JSON.stringify({ emails: ['me@x.io'] }), root });
1409
+ if (!(a1.response.status === 201 && isArr(a1.response.body) && (a1.response.body[0] as { primary?: boolean }).primary === true)) return false;
1410
+ await applyGithubWrite({ method: 'POST', path: `/user/emails`, body: JSON.stringify({ emails: ['alt@x.io'] }), root });
1411
+ const list = handleGithubRequest({ method: 'GET', path: `/user/emails`, root });
1412
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 2)) return false;
1413
+ const pub = handleGithubRequest({ method: 'GET', path: `/user/public_emails`, root });
1414
+ if (!(ok(pub.status) && isArr(pub.body) && pub.body.length === 1 && (pub.body[0] as { email?: string }).email === 'me@x.io')) return false;
1415
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/user/emails`, body: JSON.stringify({ emails: ['alt@x.io'] }), root });
1416
+ const after = handleGithubRequest({ method: 'GET', path: `/user/emails`, root });
1417
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 1;
1418
+ })),
1419
+ done('github.user.keys', 'users', 'User: SSH + GPG keys CRUD', 'api', 'niche', () =>
1420
+ withRoot(async (root) => {
1421
+ // SSH: missing key 422s; create → 201; get/list; delete → 404. GPG: armored key required.
1422
+ if ((await applyGithubWrite({ method: 'POST', path: `/user/keys`, body: JSON.stringify({ title: 'x' }), root })).response.status !== 422) return false;
1423
+ const ssh = await applyGithubWrite({ method: 'POST', path: `/user/keys`, body: JSON.stringify({ title: 'laptop', key: 'ssh-ed25519 AAAA' }), root });
1424
+ if (!(ssh.response.status === 201 && (ssh.response.body as { title?: string }).title === 'laptop')) return false;
1425
+ const sid = (ssh.response.body as { id?: number }).id!;
1426
+ if (!(ok(handleGithubRequest({ method: 'GET', path: `/user/keys/${sid}`, root }).status))) return false;
1427
+ const sshList = handleGithubRequest({ method: 'GET', path: `/user/keys`, root });
1428
+ if (!(isArr(sshList.body) && sshList.body.length === 1)) return false;
1429
+ const sdel = await applyGithubWrite({ method: 'DELETE', path: `/user/keys/${sid}`, root });
1430
+ if (!(sdel.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/user/keys/${sid}`, root }).status === 404)) return false;
1431
+ if ((await applyGithubWrite({ method: 'POST', path: `/user/gpg_keys`, body: '{}', root })).response.status !== 422) return false;
1432
+ const gpg = await applyGithubWrite({ method: 'POST', path: `/user/gpg_keys`, body: JSON.stringify({ armored_public_key: '-----BEGIN PGP-----\nabc\n-----END PGP-----' }), root });
1433
+ if (!(gpg.response.status === 201 && typeof (gpg.response.body as { key_id?: string }).key_id === 'string')) return false;
1434
+ const gid = (gpg.response.body as { id?: number }).id!;
1435
+ const gpgList = handleGithubRequest({ method: 'GET', path: `/user/gpg_keys`, root });
1436
+ if (!(isArr(gpgList.body) && gpgList.body.length === 1)) return false;
1437
+ const gdel = await applyGithubWrite({ method: 'DELETE', path: `/user/gpg_keys/${gid}`, root });
1438
+ return gdel.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/user/gpg_keys/${gid}`, root }).status === 404;
1439
+ })),
1440
+ done('github.user.update', 'users', 'User: PATCH /user (update authenticated profile)', 'api', 'niche', () =>
1441
+ withRoot(async (root) => {
1442
+ const r = await applyGithubWrite({ method: 'PATCH', path: `/user`, body: JSON.stringify({ name: 'Octo Cat', bio: 'builder', company: '@octo' }), root });
1443
+ if (!(ok(r.response.status) && (r.response.body as { name?: string }).name === 'Octo Cat' && (r.response.body as { bio?: string }).bio === 'builder')) return false;
1444
+ const get = handleGithubRequest({ method: 'GET', path: `/user`, root });
1445
+ return ok(get.status) && (get.body as { name?: string }).name === 'Octo Cat' && (get.body as { company?: string }).company === '@octo' && (get.body as { login?: string }).login === 'octocat';
1446
+ })),
1447
+ done('github.user.blocking', 'users', 'User: block/unblock users', 'api', 'niche', () =>
1448
+ withRoot(async (root) => {
1449
+ // not blocked → check 404; block → 204; appears in list + check 204; unblock → check 404.
1450
+ if (handleGithubRequest({ method: 'GET', path: `/user/blocks/spammer`, root }).status !== 404) return false;
1451
+ const b = await applyGithubWrite({ method: 'PUT', path: `/user/blocks/spammer`, body: '{}', root });
1452
+ if (b.response.status !== 204) return false;
1453
+ if (handleGithubRequest({ method: 'GET', path: `/user/blocks/spammer`, root }).status !== 204) return false;
1454
+ const list = handleGithubRequest({ method: 'GET', path: `/user/blocks`, root });
1455
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { login?: string }).login === 'spammer')) return false;
1456
+ const u = await applyGithubWrite({ method: 'DELETE', path: `/user/blocks/spammer`, root });
1457
+ return u.response.status === 204 && handleGithubRequest({ method: 'GET', path: `/user/blocks/spammer`, root }).status === 404;
1458
+ })),
1459
+
1460
+ // ── Issues (remaining surface) ───────────────────────────────────────────────────────────
1461
+ done('github.issues.events', 'issues', 'Issues: events list (the typed issue-events endpoint)', 'api', 'niche', () =>
1462
+ withRoot(async (root) => {
1463
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'ev' }), root });
1464
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/1`, body: JSON.stringify({ labels: ['bug'], assignees: ['bob'], state: 'closed', state_reason: 'completed' }), root });
1465
+ // per-issue typed events carry numeric ids; repo-wide list aggregates them; get-by-id works.
1466
+ const perIssue = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/1/events`, root });
1467
+ if (!(ok(perIssue.status) && isArr(perIssue.body) && perIssue.body.length >= 3)) return false;
1468
+ const repoWide = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/events`, root });
1469
+ if (!(ok(repoWide.status) && isArr(repoWide.body) && repoWide.body.length >= 3 && (repoWide.body[0] as { issue?: { number?: number } }).issue?.number === 1)) return false;
1470
+ const eid = (perIssue.body[0] as { id?: number }).id!;
1471
+ const one = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/events/${eid}`, root });
1472
+ if (!(ok(one.status) && (one.body as { id?: number }).id === eid && typeof (one.body as { event?: string }).event === 'string')) return false;
1473
+ return handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/events/999999`, root }).status === 404;
1474
+ })),
1475
+ done('github.issues.assignees_check', 'issues', 'Issues: assignable users list + check', 'api', 'niche', () =>
1476
+ withRoot(async (root) => {
1477
+ await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'demo' }), root });
1478
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/collaborators/alice`, body: JSON.stringify({ permission: 'push' }), root });
1479
+ // an assignable collaborator → check 204; a non-collaborator → 404; the list includes alice.
1480
+ if (handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/assignees/alice`, root }).status !== 204) return false;
1481
+ if (handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/assignees/nobody`, root }).status !== 404) return false;
1482
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/assignees`, root });
1483
+ return ok(list.status) && isArr(list.body) && list.body.some((u) => (u as { login?: string }).login === 'alice');
1484
+ })),
1485
+
1486
+ // ── Pulls (remaining surface) ────────────────────────────────────────────────────────────
1487
+ done('github.pulls.review_comment_reactions', 'pulls', 'PRs: reactions on review comments', 'api', 'niche', () =>
1488
+ withRoot(async (root) => {
1489
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'p', head: 'a', base: 'main' }), root });
1490
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/comments`, body: JSON.stringify({ body: 'nit', path: 'a.ts', line: 1, side: 'RIGHT' }), root });
1491
+ const cid = (cm.response.body as { id?: number }).id!;
1492
+ // invalid content 422s; a valid reaction on the REVIEW comment → 201 and lists; delete → gone.
1493
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/comments/${cid}/reactions`, body: JSON.stringify({ content: 'nope' }), root })).response.status !== 422) return false;
1494
+ const r = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/comments/${cid}/reactions`, body: JSON.stringify({ content: 'rocket' }), root });
1495
+ if (r.response.status !== 201) return false;
1496
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/comments/${cid}/reactions`, root });
1497
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { content?: string }).content === 'rocket')) return false;
1498
+ const rid = (r.response.body as { id?: number }).id!;
1499
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/pulls/comments/${cid}/reactions/${rid}`, root });
1500
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/comments/${cid}/reactions`, root });
1501
+ return del.response.status === 204 && isArr(after.body) && after.body.length === 0;
1502
+ })),
1503
+ done('github.pulls.review_dismiss', 'pulls', 'PRs: dismiss a review + update a review body', 'api', 'niche', () =>
1504
+ withRoot(async (root) => {
1505
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'r', head: 'a', base: 'main' }), root });
1506
+ const sub = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/reviews`, body: JSON.stringify({ event: 'CHANGES_REQUESTED', body: 'please fix' }), root });
1507
+ const rid = (sub.response.body as { id?: number }).id!;
1508
+ // dismiss requires a message (422 without); after dismiss the review reads back DISMISSED.
1509
+ if ((await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/reviews/${rid}/dismissals`, body: '{}', root })).response.status !== 422) return false;
1510
+ const dm = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/reviews/${rid}/dismissals`, body: JSON.stringify({ message: 'stale' }), root });
1511
+ if (!(ok(dm.response.status) && (dm.response.body as { state?: string }).state === 'DISMISSED' && dm.webhook?.action === 'dismissed')) return false;
1512
+ const listed = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/reviews`, root });
1513
+ if (!(ok(listed.status) && isArr(listed.body) && (listed.body[0] as { state?: string }).state === 'DISMISSED')) return false;
1514
+ // update the review body (PUT .../reviews/:rid); 422 without a body; read-back reflects it.
1515
+ if ((await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/reviews/${rid}`, body: '{}', root })).response.status !== 422) return false;
1516
+ const up = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/reviews/${rid}`, body: JSON.stringify({ body: 'edited note' }), root });
1517
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/1/reviews`, root });
1518
+ // a dismiss on an unknown review id 404s.
1519
+ const missing = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/reviews/9999/dismissals`, body: JSON.stringify({ message: 'x' }), root });
1520
+ return ok(up.response.status) && (after.body as Array<{ body?: string }>)[0]?.body === 'edited note' && missing.response.status === 404;
1521
+ })),
1522
+
1523
+ // ── Packages / Pages ─────────────────────────────────────────────────────────────────────
1524
+ done('github.packages.list', 'packages', 'Packages: list/get/delete versions', 'api', 'niche', () =>
1525
+ withRoot(async (root) => {
1526
+ // seed a package + two versions, list/get, delete one version then the package.
1527
+ const c = await applyGithubWrite({ method: 'POST', path: '/user/packages/npm/my-lib', body: JSON.stringify({ visibility: 'public', versions: ['1.0.0', '1.1.0'] }), root });
1528
+ if (!(c.response.status === 201 && (c.response.body as { version_count?: number }).version_count === 2)) return false;
1529
+ const list = handleGithubRequest({ method: 'GET', path: '/user/packages?package_type=npm', root });
1530
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1 && (list.body[0] as { name?: string }).name === 'my-lib')) return false;
1531
+ const versions = handleGithubRequest({ method: 'GET', path: '/user/packages/npm/my-lib/versions', root });
1532
+ if (!(ok(versions.status) && isArr(versions.body) && versions.body.length === 2)) return false;
1533
+ const vid = (versions.body as Array<{ id?: number }>)[0]!.id!;
1534
+ const one = handleGithubRequest({ method: 'GET', path: `/user/packages/npm/my-lib/versions/${vid}`, root });
1535
+ if (!(ok(one.status) && (one.body as { id?: number }).id === vid)) return false;
1536
+ const delV = await applyGithubWrite({ method: 'DELETE', path: `/user/packages/npm/my-lib/versions/${vid}`, root });
1537
+ const after = handleGithubRequest({ method: 'GET', path: '/user/packages/npm/my-lib/versions', root });
1538
+ if (!(delV.response.status === 204 && isArr(after.body) && after.body.length === 1)) return false;
1539
+ const delP = await applyGithubWrite({ method: 'DELETE', path: '/user/packages/npm/my-lib', root });
1540
+ const gone = handleGithubRequest({ method: 'GET', path: '/user/packages/npm/my-lib', root });
1541
+ // a delete of an unknown package 404s.
1542
+ const missing = await applyGithubWrite({ method: 'DELETE', path: '/user/packages/npm/nope', root });
1543
+ return delP.response.status === 204 && gone.status === 404 && missing.response.status === 404;
1544
+ })),
1545
+ done('github.pages.get', 'pages', 'Pages: site config + builds', 'api', 'niche', () =>
1546
+ withRoot(async (root) => {
1547
+ // no site → 404; create (POST) → 201 building; update source (PUT) → 204; request a build.
1548
+ if (handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pages`, root }).status !== 404) return false;
1549
+ const create = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pages`, body: JSON.stringify({ source: { branch: 'gh-pages', path: '/' } }), root });
1550
+ if (!(create.response.status === 201 && (create.response.body as { source?: { branch?: string } }).source?.branch === 'gh-pages')) return false;
1551
+ // creating twice 409s.
1552
+ if ((await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pages`, body: '{}', root })).response.status !== 409) return false;
1553
+ const upd = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pages`, body: JSON.stringify({ cname: 'docs.example.com', source: { branch: 'main', path: '/docs' } }), root });
1554
+ if (upd.response.status !== 204) return false;
1555
+ const site = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pages`, root });
1556
+ if (!(ok(site.status) && (site.body as { cname?: string }).cname === 'docs.example.com' && (site.body as { source?: { path?: string } }).source?.path === '/docs')) return false;
1557
+ const build = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pages/builds`, body: '{}', root });
1558
+ if (build.response.status !== 202) return false;
1559
+ const builds = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pages/builds`, root });
1560
+ const latest = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pages/builds/latest`, root });
1561
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/pages`, root });
1562
+ const gone = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pages`, root });
1563
+ return ok(builds.status) && isArr(builds.body) && builds.body.length === 1 && ok(latest.status) && (latest.body as { status?: string }).status === 'built' && del.response.status === 204 && gone.status === 404;
1564
+ })),
1565
+ done('github.codespaces.crud', 'codespaces', 'Codespaces: list/create/start/stop/delete', 'api', 'niche', () =>
1566
+ withRoot(async (root) => {
1567
+ // create requires a repo (422 without); create → 201 Available; stop → Shutdown; start →
1568
+ // Available; delete → 202 then gone. (The VM COMPUTE is out-of-scope; the RESOURCE
1569
+ // lifecycle/state machine IS modeled and verified here.)
1570
+ if ((await applyGithubWrite({ method: 'POST', path: '/user/codespaces', body: '{}', root })).response.status !== 422) return false;
1571
+ const c = await applyGithubWrite({ method: 'POST', path: '/user/codespaces', body: JSON.stringify({ repository: REPO, machine: 'standardLinux32gb', ref: 'feature' }), root });
1572
+ if (!(c.response.status === 201 && (c.response.body as { state?: string }).state === 'Available')) return false;
1573
+ const name = (c.response.body as { name?: string }).name!;
1574
+ const list = handleGithubRequest({ method: 'GET', path: '/user/codespaces', root });
1575
+ if (!(ok(list.status) && (list.body as { total_count?: number }).total_count === 1)) return false;
1576
+ const stop = await applyGithubWrite({ method: 'POST', path: `/user/codespaces/${name}/stop`, body: '{}', root });
1577
+ if (!(ok(stop.response.status) && (stop.response.body as { state?: string }).state === 'Shutdown')) return false;
1578
+ const start = await applyGithubWrite({ method: 'POST', path: `/user/codespaces/${name}/start`, body: '{}', root });
1579
+ if (!(ok(start.response.status) && (start.response.body as { state?: string }).state === 'Available')) return false;
1580
+ const get = handleGithubRequest({ method: 'GET', path: `/user/codespaces/${name}`, root });
1581
+ if (!(ok(get.status) && (get.body as { git_status?: { ref?: string } }).git_status?.ref === 'feature')) return false;
1582
+ const del = await applyGithubWrite({ method: 'DELETE', path: `/user/codespaces/${name}`, root });
1583
+ const gone = handleGithubRequest({ method: 'GET', path: `/user/codespaces/${name}`, root });
1584
+ return del.response.status === 202 && gone.status === 404;
1585
+ })),
1586
+
1587
+ // ── Security (code scanning / dependabot / secret scanning) ──────────────────────────────
1588
+ // The alert RESOURCES + their state machines (open→dismissed/fixed/resolved) are modeled as
1589
+ // kernel-folded CRUD + state transitions; the actual SCAN that generates them is infra the
1590
+ // offline twin can't run (a simulator seeds the alert objects, like the rest of the twin).
1591
+ done('github.security.code_scanning', 'security', 'Code scanning: alerts + analyses', 'api', 'niche', () =>
1592
+ withRoot(async (root) => {
1593
+ const a = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/code-scanning/alerts`, body: JSON.stringify({ rule_id: 'js/sql-injection', rule_severity: 'error' }), root });
1594
+ if (!(a.response.status === 201 && (a.response.body as { number?: number }).number === 1 && (a.response.body as { state?: string }).state === 'open')) return false;
1595
+ const n = (a.response.body as { number?: number }).number!;
1596
+ // dismiss requires a valid reason (422 without / invalid); dismissing reads back dismissed.
1597
+ if ((await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/code-scanning/alerts/${n}`, body: JSON.stringify({ state: 'dismissed' }), root })).response.status !== 422) return false;
1598
+ if ((await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/code-scanning/alerts/${n}`, body: JSON.stringify({ state: 'dismissed', dismissed_reason: 'bogus' }), root })).response.status !== 422) return false;
1599
+ const dm = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/code-scanning/alerts/${n}`, body: JSON.stringify({ state: 'dismissed', dismissed_reason: 'false positive' }), root });
1600
+ if (!(ok(dm.response.status) && (dm.response.body as { state?: string }).state === 'dismissed' && (dm.response.body as { dismissed_reason?: string }).dismissed_reason === 'false positive')) return false;
1601
+ // filter by state; the dismissed alert is excluded from ?state=open.
1602
+ const open = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/code-scanning/alerts?state=open`, root });
1603
+ if (!(ok(open.status) && isArr(open.body) && open.body.length === 0)) return false;
1604
+ // analyses: create → list → delete.
1605
+ const an = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/code-scanning/analyses`, body: JSON.stringify({ ref: 'refs/heads/main', results_count: 3, rules_count: 50 }), root });
1606
+ if (!(an.response.status === 201 && (an.response.body as { results_count?: number }).results_count === 3)) return false;
1607
+ const aid = (an.response.body as { id?: number }).id!;
1608
+ const list = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/code-scanning/analyses`, root });
1609
+ const delA = await applyGithubWrite({ method: 'DELETE', path: `/repos/${REPO}/code-scanning/analyses/${aid}`, root });
1610
+ const after = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/code-scanning/analyses`, root });
1611
+ // an unknown alert PATCH 404s.
1612
+ const missing = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/code-scanning/alerts/999`, body: JSON.stringify({ state: 'open' }), root });
1613
+ return ok(list.status) && isArr(list.body) && list.body.length === 1 && delA.response.status === 200 && isArr(after.body) && after.body.length === 0 && missing.response.status === 404;
1614
+ })),
1615
+ done('github.security.dependabot', 'security', 'Dependabot: alerts + security updates', 'api', 'niche', () =>
1616
+ withRoot(async (root) => {
1617
+ const a = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/dependabot/alerts`, body: JSON.stringify({ package_name: 'lodash', ecosystem: 'npm', severity: 'critical', ghsa_id: 'GHSA-x' }), root });
1618
+ if (!(a.response.status === 201 && (a.response.body as { state?: string }).state === 'open' && (a.response.body as { security_advisory?: { ghsa_id?: string } }).security_advisory?.ghsa_id === 'GHSA-x')) return false;
1619
+ const n = (a.response.body as { number?: number }).number!;
1620
+ if ((await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/dependabot/alerts/${n}`, body: JSON.stringify({ state: 'dismissed' }), root })).response.status !== 422) return false;
1621
+ const dm = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/dependabot/alerts/${n}`, body: JSON.stringify({ state: 'dismissed', dismissed_reason: 'tolerable_risk' }), root });
1622
+ if (!(ok(dm.response.status) && (dm.response.body as { state?: string }).state === 'dismissed')) return false;
1623
+ // reopen → state open + dismissal cleared.
1624
+ const re = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/dependabot/alerts/${n}`, body: JSON.stringify({ state: 'open' }), root });
1625
+ if (!(ok(re.response.status) && (re.response.body as { state?: string }).state === 'open' && (re.response.body as { dismissed_reason?: unknown }).dismissed_reason === null)) return false;
1626
+ const dismissed = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/dependabot/alerts?state=dismissed`, root });
1627
+ const get = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/dependabot/alerts/${n}`, root });
1628
+ return ok(dismissed.status) && isArr(dismissed.body) && dismissed.body.length === 0 && ok(get.status) && (get.body as { severity?: unknown; security_vulnerability?: { severity?: string } }).security_vulnerability?.severity === 'critical';
1629
+ })),
1630
+ done('github.security.secret_scanning', 'security', 'Secret scanning: alerts', 'api', 'niche', () =>
1631
+ withRoot(async (root) => {
1632
+ const a = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/secret-scanning/alerts`, body: JSON.stringify({ secret_type: 'github_pat' }), root });
1633
+ if (!(a.response.status === 201 && (a.response.body as { state?: string }).state === 'open')) return false;
1634
+ const n = (a.response.body as { number?: number }).number!;
1635
+ // resolving requires a valid resolution (422 without/invalid); resolved reads back resolved.
1636
+ if ((await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/secret-scanning/alerts/${n}`, body: JSON.stringify({ state: 'resolved' }), root })).response.status !== 422) return false;
1637
+ if ((await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/secret-scanning/alerts/${n}`, body: JSON.stringify({ state: 'resolved', resolution: 'bogus' }), root })).response.status !== 422) return false;
1638
+ const rs = await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/secret-scanning/alerts/${n}`, body: JSON.stringify({ state: 'resolved', resolution: 'revoked' }), root });
1639
+ if (!(ok(rs.response.status) && (rs.response.body as { state?: string }).state === 'resolved' && (rs.response.body as { resolution?: string }).resolution === 'revoked')) return false;
1640
+ const locs = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/secret-scanning/alerts/${n}/locations`, root });
1641
+ const open = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/secret-scanning/alerts?state=open`, root });
1642
+ return ok(locs.status) && isArr(locs.body) && locs.body.length === 1 && ok(open.status) && isArr(open.body) && open.body.length === 0;
1643
+ })),
1644
+
1645
+ // ── GraphQL / Apps / OAuth ───────────────────────────────────────────────────────────────
1646
+ done('github.graphql.api', 'api', 'GraphQL API (v4) — faithful SUBSET (viewer/repository/node + addComment) over the same projection; unmodeled fields error honestly', 'api', 'niche', () =>
1647
+ withRoot(async (root) => {
1648
+ const gql = (query: string, variables?: Record<string, unknown>) => applyGithubWrite({ method: 'POST', path: '/graphql', body: JSON.stringify({ query, variables }), root });
1649
+ // seed a repo + PR + issue via REST, then read them back through GraphQL (SAME projection).
1650
+ await applyGithubWrite({ method: 'POST', path: '/orgs/octo/repos', body: JSON.stringify({ name: 'demo', private: true }), root });
1651
+ await applyGithubWrite({ method: 'POST', path: '/repos/octo/demo/pulls', body: JSON.stringify({ title: 'Add X', head: 'a', base: 'main' }), root });
1652
+ await applyGithubWrite({ method: 'POST', path: '/repos/octo/demo/issues', body: JSON.stringify({ title: 'A bug' }), root });
1653
+ // viewer
1654
+ const v = await gql('query { viewer { login name } }');
1655
+ if ((v.response.body as any).data?.viewer?.login !== 'octocat') return false;
1656
+ // repository(owner,name){ pullRequests/issues totalCount+nodes } — reflects seeded state.
1657
+ const r = await gql('query($o:String!,$n:String!){ repository(owner:$o,name:$n){ nameWithOwner isPrivate pullRequests(first:10){ totalCount nodes { number title state } } issues(first:10){ totalCount nodes { number title } } } }', { o: 'octo', n: 'demo' });
1658
+ const repo = (r.response.body as any).data?.repository;
1659
+ if (!(repo?.nameWithOwner === 'octo/demo' && repo.isPrivate === true && repo.pullRequests.totalCount === 1 && repo.pullRequests.nodes[0].title === 'Add X' && repo.pullRequests.nodes[0].state === 'OPEN' && repo.issues.totalCount === 1)) return false;
1660
+ // node(id) by global id
1661
+ const n = await gql('query($id:ID!){ node(id:$id){ ... on PullRequest { number title } } }', { id: 'PR_octo/demo#1' });
1662
+ if ((n.response.body as any).data?.node?.number !== 1) return false;
1663
+ // addComment mutation maps onto the SAME write path the REST twin uses; the comment then
1664
+ // appears in the REST issue-comments list (consistency across surfaces).
1665
+ const mut = await gql('mutation($s:ID!,$b:String!){ addComment(input:{subjectId:$s,body:$b}){ commentEdge { node { id body } } } }', { s: 'I_octo/demo#2', b: 'from graphql' });
1666
+ if ((mut.response.body as any).data?.addComment?.commentEdge?.node?.body !== 'from graphql') return false;
1667
+ const restComments = handleGithubRequest({ method: 'GET', path: '/repos/octo/demo/issues/2/comments', root });
1668
+ if (!(ok(restComments.status) && isArr(restComments.body) && restComments.body.length === 1 && (restComments.body[0] as { body?: string }).body === 'from graphql')) return false;
1669
+ // HONESTY: an UNMODELED field raises a typed GraphQL error (not a fabricated value).
1670
+ const bad = await gql('query { repository(owner:"octo",name:"demo"){ stargazerCount } }');
1671
+ const errs = (bad.response.body as any).errors;
1672
+ if (!(Array.isArray(errs) && errs.length && errs[0].type === 'undefinedField')) return false;
1673
+ // an unknown node id resolves to a NOT_FOUND error (honest).
1674
+ const nf = await gql('query { node(id:"PR_octo/demo#999"){ ... on PullRequest { number } } }');
1675
+ return (nf.response.body as any).errors?.[0]?.type === 'NOT_FOUND';
1676
+ })),
1677
+ done('github.graphql.repository_info', 'api', 'GraphQL RepositoryInfo (the repo fragment gh CLI sends: id/owner/defaultBranchRef/viewerPermission/merge flags), incl. object-arg parsing', 'api', 'niche', () =>
1678
+ withRoot(async (root) => {
1679
+ const gql = (query: string, variables?: Record<string, unknown>) => applyGithubWrite({ method: 'POST', path: '/graphql', body: JSON.stringify({ query, variables }), root });
1680
+ await applyGithubWrite({ method: 'POST', path: '/orgs/octo/repos', body: JSON.stringify({ name: 'demo', description: 'd' }), root });
1681
+ // The exact RepositoryInfo shape gh pr create sends: a `repo` fragment spread + parent + merge
1682
+ // flags. Asserts the gh-CLI fields resolve from the SAME repo projection REST serves.
1683
+ const q = `fragment repo on Repository { id name owner { login } hasIssuesEnabled description hasWikiEnabled viewerPermission defaultBranchRef { name } }
1684
+ query RepositoryInfo($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ...repo parent { ...repo } mergeCommitAllowed rebaseMergeAllowed squashMergeAllowed } }`;
1685
+ const r = (await gql(q, { owner: 'octo', name: 'demo' })).response.body as any;
1686
+ const repo = r.data?.repository;
1687
+ if (!(repo?.id === 'R_octo/demo' && repo.owner?.login === 'octo' && repo.defaultBranchRef?.name === 'main'
1688
+ && repo.viewerPermission === 'ADMIN' && repo.squashMergeAllowed === true && repo.parent === null)) return false;
1689
+ // Object args (orderBy:{field:…}) must not be mis-parsed as a selection set — pullRequests
1690
+ // with an orderBy object resolves rather than erroring on a phantom `field`.
1691
+ const pq = `query($o:String!,$n:String!){ repository(owner:$o,name:$n){ pullRequests(first:30, orderBy:{ field: CREATED_AT, direction: DESC }){ totalCount nodes { number } } } }`;
1692
+ const p = (await gql(pq, { o: 'octo', n: 'demo' })).response.body as any;
1693
+ return p.data?.repository?.pullRequests?.totalCount === 0 && !p.errors;
1694
+ })),
1695
+ done('github.graphql.create_pull_request', 'api', 'GraphQL createPullRequest mutation + pullRequests(headRefName,states) connection (gh pr create/view/list)', 'api', 'niche', () =>
1696
+ withRoot(async (root) => {
1697
+ const gql = (query: string, variables?: Record<string, unknown>) => applyGithubWrite({ method: 'POST', path: '/graphql', body: JSON.stringify({ query, variables }), root });
1698
+ await applyGithubWrite({ method: 'POST', path: '/orgs/octo/repos', body: JSON.stringify({ name: 'demo' }), root });
1699
+ await applyGithubWrite({ method: 'POST', path: '/repos/octo/demo/git/refs', body: JSON.stringify({ ref: 'refs/heads/feat', sha: 'a'.repeat(40) }), root });
1700
+ // createPullRequest maps onto the SAME REST create path; returns the new PR node id+url.
1701
+ const mutation = 'mutation($input: CreatePullRequestInput!){ createPullRequest(input:$input){ pullRequest { id url } } }';
1702
+ const m = (await gql(mutation, { input: { repositoryId: 'R_octo/demo', baseRefName: 'main', headRefName: 'feat', title: 'Add X', body: 'B', draft: false } })).response.body as any;
1703
+ if (m.data?.createPullRequest?.pullRequest?.id !== 'PR_octo/demo#1') return false;
1704
+ // the PR is now real on the REST surface too (cross-surface consistency).
1705
+ const rest = handleGithubRequest({ method: 'GET', path: '/repos/octo/demo/pulls/1', root });
1706
+ if (!(ok(rest.status) && (rest.body as any).head?.ref === 'feat')) return false;
1707
+ // pullRequests(headRefName, states) filters: matching branch returns the node; a miss is empty.
1708
+ const byBranch = 'query($o:String!,$n:String!,$h:String!,$s:[PullRequestState!]){ repository(owner:$o,name:$n){ pullRequests(headRefName:$h, states:$s, first:30, orderBy:{field:CREATED_AT,direction:DESC}){ totalCount nodes { number headRefName baseRefName url state } pageInfo { hasNextPage } } } }';
1709
+ const hit = (await gql(byBranch, { o: 'octo', n: 'demo', h: 'feat', s: ['OPEN'] })).response.body as any;
1710
+ const node = hit.data?.repository?.pullRequests?.nodes?.[0];
1711
+ if (!(hit.data?.repository?.pullRequests?.totalCount === 1 && node?.headRefName === 'feat' && node?.baseRefName === 'main' && node?.number === 1)) return false;
1712
+ const miss = (await gql(byBranch, { o: 'octo', n: 'demo', h: 'nope', s: ['OPEN'] })).response.body as any;
1713
+ return miss.data?.repository?.pullRequests?.totalCount === 0 && !miss.errors;
1714
+ })),
1715
+ done('github.apps.installations', 'api', 'GitHub Apps: installations + installation tokens', 'api', 'niche', () =>
1716
+ withRoot(async (root) => {
1717
+ // seed an installation (twin construct — real installs can't be observed offline), list,
1718
+ // get one, then mint an installation access token bound to its permissions.
1719
+ const inst = await applyGithubWrite({ method: 'POST', path: '/app/installations', body: JSON.stringify({ account_login: 'octo-org', app_id: 7, app_slug: 'ci-bot', permissions: { contents: 'read', issues: 'write' }, events: ['push'] }), root });
1720
+ if (!(inst.response.status === 201 && (inst.response.body as { account?: { login?: string } }).account?.login === 'octo-org')) return false;
1721
+ const id = (inst.response.body as { id?: number }).id!;
1722
+ const list = handleGithubRequest({ method: 'GET', path: '/app/installations', root });
1723
+ if (!(ok(list.status) && isArr(list.body) && list.body.length === 1)) return false;
1724
+ const get = handleGithubRequest({ method: 'GET', path: `/app/installations/${id}`, root });
1725
+ if (!(ok(get.status) && (get.body as { app_slug?: string }).app_slug === 'ci-bot')) return false;
1726
+ const tok = await applyGithubWrite({ method: 'POST', path: `/app/installations/${id}/access_tokens`, body: JSON.stringify({ permissions: { contents: 'read' } }), root });
1727
+ if (!(tok.response.status === 201 && typeof (tok.response.body as { token?: string }).token === 'string' && (tok.response.body as { token?: string }).token!.startsWith('ghs_') && (tok.response.body as { permissions?: { contents?: string } }).permissions?.contents === 'read')) return false;
1728
+ // a token for an unknown installation 404s.
1729
+ const missing = await applyGithubWrite({ method: 'POST', path: '/app/installations/9999/access_tokens', body: '{}', root });
1730
+ return missing.response.status === 404;
1731
+ })),
1732
+ done('github.oauth.flow', 'auth', 'OAuth app authorization + token scopes', 'api', 'niche', () =>
1733
+ withRoot(async (root) => {
1734
+ // The API half of the OAuth flow: exchange a code for a token bound to scopes, then a
1735
+ // scope-gated call returns 401 (no token), 403 (missing scope), 200 (has scope); revoke.
1736
+ // The hosted CONSENT SCREEN (a browser UI, not an API) is out-of-scope (carved below).
1737
+ if ((await applyGithubWrite({ method: 'POST', path: '/login/oauth/access_token', body: JSON.stringify({ client_id: 'cid' }), root })).response.status !== 422) return false;
1738
+ const ex = await applyGithubWrite({ method: 'POST', path: '/login/oauth/access_token', body: JSON.stringify({ client_id: 'cid', code: 'abc', scope: 'repo read:org' }), root });
1739
+ if (!(ok(ex.response.status) && (ex.response.body as { token_type?: string }).token_type === 'bearer' && (ex.response.body as { scope?: string }).scope === 'repo,read:org')) return false;
1740
+ const token = (ex.response.body as { access_token?: string }).access_token!;
1741
+ // no token → 401; valid token lacking scope → 403; with scope → 200.
1742
+ if ((await applyGithubWrite({ method: 'POST', path: '/_twin/oauth/call', body: JSON.stringify({ token: 'bad', required_scope: 'repo' }), root })).response.status !== 401) return false;
1743
+ if ((await applyGithubWrite({ method: 'POST', path: '/_twin/oauth/call', body: JSON.stringify({ token, required_scope: 'admin:org' }), root })).response.status !== 403) return false;
1744
+ const okCall = await applyGithubWrite({ method: 'POST', path: '/_twin/oauth/call', body: JSON.stringify({ token, required_scope: 'repo' }), root });
1745
+ if (!(ok(okCall.response.status) && (okCall.response.body as { ok?: boolean }).ok === true)) return false;
1746
+ // revoke the token → a subsequent gated call 401s.
1747
+ const rev = await applyGithubWrite({ method: 'DELETE', path: '/applications/token', body: JSON.stringify({ access_token: token }), root });
1748
+ const afterRevoke = await applyGithubWrite({ method: 'POST', path: '/_twin/oauth/call', body: JSON.stringify({ token, required_scope: 'repo' }), root });
1749
+ return rev.response.status === 204 && afterRevoke.response.status === 401;
1750
+ })),
1751
+
1752
+ // ── Next honest gaps (top-down GitHub surface NOT yet modeled — keeps the denominator
1753
+ // honest; a high % against a thin list would be misleading). Each is a real product
1754
+ // area still to close in a future cycle. ─────────────────────────────────────────────────
1755
+ todo('github.graphql.connections', 'api', 'GraphQL: cursor pagination (pageInfo/edges + after) on connections', 'api', 'niche'),
1756
+ todo('github.graphql.search', 'api', 'GraphQL: the search() connection (typed SearchResultItem nodes)', 'api', 'niche'),
1757
+ todo('github.security.advisories', 'security', 'Repository security advisories (GHSA drafts + CVE request + credits)', 'api', 'niche'),
1758
+ todo('github.dependency_graph.sbom', 'security', 'Dependency graph: SBOM export + dependency review (compare base...head)', 'api', 'niche'),
1759
+ todo('github.codespaces.machines_secrets', 'codespaces', 'Codespaces: machine types + user secrets + repo defaults', 'api', 'niche'),
1760
+ todo('github.pages.deployments', 'pages', 'Pages: build-type:workflow deployments (create/cancel/status)', 'api', 'niche'),
1761
+ todo('github.activity.starring_feed', 'activity', 'Activity: per-user public timeline ATOM feed + organization feed', 'api', 'niche'),
1762
+ todo('github.apps.webhook_config', 'api', 'GitHub Apps: app webhook config + deliveries + manifest conversion', 'api', 'niche'),
1763
+
1764
+ // ── Webhooks (event delivery) ────────────────────────────────────────────────────────────
1765
+ done('github.webhooks.core_events', 'webhooks', 'Webhooks: core write events (pull_request/issues/comment/review)', 'connector', 'common', () =>
1766
+ withRoot(async (root) => {
1767
+ const a = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'w', head: 'a', base: 'main' }), root });
1768
+ const b = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'w' }), root });
1769
+ return a.webhook?.event === 'pull_request' && a.webhook.action === 'opened' && b.webhook?.event === 'issues' && b.webhook.action === 'opened';
1770
+ })),
1771
+ done('github.webhooks.full_events', 'webhooks', 'Webhooks: full event-type coverage (push/release/check_run/workflow_run/…)', 'connector', 'common', () =>
1772
+ withRoot(async (root) => {
1773
+ // Writes across many resources each emit the matching webhook envelope (event+action),
1774
+ // mirroring the spread of events real GitHub fires beyond the PR/issue core.
1775
+ const checks: Array<[string, string]> = [];
1776
+ const rel = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v1' }), root });
1777
+ checks.push([rel.webhook?.event ?? '', rel.webhook?.action ?? '']);
1778
+ const repo = await applyGithubWrite({ method: 'POST', path: `/orgs/octo/repos`, body: JSON.stringify({ name: 'wh' }), root });
1779
+ checks.push([repo.webhook?.event ?? '', repo.webhook?.action ?? '']);
1780
+ const fork = await applyGithubWrite({ method: 'POST', path: `/repos/octo/wh/forks`, body: JSON.stringify({ organization: 'me' }), root });
1781
+ checks.push([fork.webhook?.event ?? '', fork.webhook?.action ?? '']);
1782
+ const mem = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/collaborators/bob`, body: JSON.stringify({ permission: 'push' }), root });
1783
+ checks.push([mem.webhook?.event ?? '', mem.webhook?.action ?? '']);
1784
+ const push = await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/f.txt`, body: JSON.stringify({ message: 'm', content: 'eA==' }), root });
1785
+ checks.push([push.webhook?.event ?? '', '']);
1786
+ const expected: Record<string, string> = { release: 'published', repository: 'created', fork: 'created', member: 'added', push: '' };
1787
+ return checks.every(([e, a]) => e in expected && (expected[e] === '' || expected[e] === a)) && new Set(checks.map((c) => c[0])).size === 5;
1788
+ })),
1789
+ done('github.webhooks.delivery', 'webhooks', 'Webhooks: deliveries log + redelivery + signatures', 'connector', 'common', () =>
1790
+ withRoot(async (root) => {
1791
+ const { clearGithubWebhooks, registerGithubWebhook, emitGithubEvent, listGithubDeliveries, redeliverGithubDelivery, signGithubPayload } = await import('./github-events.ts');
1792
+ clearGithubWebhooks();
1793
+ registerGithubWebhook('https://e.x/hook', 's3cr3t');
1794
+ const captured: Array<{ headers: Record<string, string>; body: string }> = [];
1795
+ const deliver = (_url: string, headerEvent: string, payload: unknown, headers: Record<string, string>) => { captured.push({ headers, body: JSON.stringify(payload) }); };
1796
+ await emitGithubEvent({ event: 'pull_request', action: 'opened', repository: REPO, number: 1 }, { deliver });
1797
+ // a delivery is logged with an id + signature; the signature verifies against the secret.
1798
+ const deliveries = listGithubDeliveries();
1799
+ if (deliveries.length !== 1) return false;
1800
+ const d = deliveries[0]!;
1801
+ if (d.event !== 'pull_request' || typeof d.id !== 'string') return false;
1802
+ const expectedSig = signGithubPayload('s3cr3t', d.payloadJson);
1803
+ if (captured[0]?.headers['x-hub-signature-256'] !== expectedSig) return false;
1804
+ // redelivery replays the SAME payload to the endpoint (a second captured delivery).
1805
+ const re = await redeliverGithubDelivery(d.id, { deliver });
1806
+ clearGithubWebhooks();
1807
+ return re === true && captured.length === 2 && captured[1]!.body === captured[0]!.body;
1808
+ })),
1809
+
1810
+ // ── Connector (pull/push) ────────────────────────────────────────────────────────────────
1811
+ done('github.connector.pull', 'connector', 'Connector: pull full repo graph (PRs/issues/checks/actions)', 'connector', 'common', () =>
1812
+ withRoot(async (root) => {
1813
+ const { syncGithubFromReal } = await import('./github-connector.ts');
1814
+ // A fake octokit-like executor returns observed PRs + issues; the connector folds them
1815
+ // into the twin offline (the SOLE credentialed boundary is the injected executor).
1816
+ const execute = {
1817
+ async request(route: string, params: Record<string, unknown> = {}) {
1818
+ if (route === 'GET /repos/{owner}/{repo}/pulls') return { status: 200, data: [{ number: 5, title: 'Real title', body: 'Real body', state: 'open', base: { ref: 'main' }, head: { sha: 'deadbeef' }, changed_files: 7, commits: 3 }] };
1819
+ if (route === 'GET /repos/{owner}/{repo}/issues') return { status: 200, data: [{ number: 9, title: 'Bug', body: 'broke', state: 'open' }] };
1820
+ if (route.endsWith('/reviews') && route.startsWith('GET')) return { status: 200, data: [{ id: 1, state: 'APPROVED', body: 'LGTM' }] };
1821
+ if (route.endsWith('/comments') && route.startsWith('GET')) return { status: 200, data: [] };
1822
+ return { status: 404, data: { message: 'not found' } };
1823
+ },
1824
+ };
1825
+ const res = await syncGithubFromReal(execute, { owner: 'octo', repo: 'demo', root, occurredAt: '2026-01-01T00:00:00Z' });
1826
+ if (res.observed < 2 || res.issues !== 1) return false;
1827
+ // Read the folded PR + issue back through the twin with their REAL content.
1828
+ const pr = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/pulls/5`, root });
1829
+ const iss = handleGithubRequest({ method: 'GET', path: `/repos/${REPO}/issues/9`, root });
1830
+ return ok(pr.status) && (pr.body as { title?: string }).title === 'Real title'
1831
+ && ok(iss.status) && (iss.body as { title?: string }).title === 'Bug';
1832
+ })),
1833
+ done('github.connector.push', 'connector', 'Connector: push reconcile (apply desired-state writes)', 'connector', 'common', () =>
1834
+ withRoot(async (root) => {
1835
+ const { pushPendingGithubActions } = await import('./github-connector.ts');
1836
+ // Author LOCAL writes in the twin (a PR + an issue), then push them to the real vendor
1837
+ // via a fake executor — the connector maps each pending action to its REST call.
1838
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'push me', head: 'a', base: 'main' }), root });
1839
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'push issue' }), root });
1840
+ const calls: string[] = [];
1841
+ const execute = {
1842
+ async request(route: string) {
1843
+ calls.push(route);
1844
+ if (route === 'POST /repos/{owner}/{repo}/pulls') return { status: 201, data: { number: 42 } };
1845
+ if (route === 'POST /repos/{owner}/{repo}/issues') return { status: 201, data: { number: 77 } };
1846
+ return { status: 201, data: { id: 1 } };
1847
+ },
1848
+ };
1849
+ const res = await pushPendingGithubActions(execute, { root, occurredAt: '2026-01-02T00:00:00Z' });
1850
+ return res.pushed >= 2 && calls.includes('POST /repos/{owner}/{repo}/pulls') && calls.includes('POST /repos/{owner}/{repo}/issues');
1851
+ })),
1852
+
1853
+ // ── UI screens (the real GitHub web app) ─────────────────────────────────────────────────
1854
+ // PR list table: seed a PR whose head resolves to a REAL branch tip + a commit status
1855
+ // on that sha — the fetched row's ciStatus must reflect it (moves with real CI state).
1856
+ done('github.ui.pr_table', 'ui', 'PR list table (per-repo, state/CI) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1857
+ withRoot,
1858
+ seed: async (root) => {
1859
+ const sha = 'd'.repeat(40);
1860
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/git/refs`, body: JSON.stringify({ ref: 'refs/heads/pt-probe', sha }), root });
1861
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'PR Table Probe', head: 'pt-probe', base: 'main' }), root });
1862
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/statuses/${sha}`, body: JSON.stringify({ state: 'success', context: 'ci' }), root });
1863
+ },
1864
+ fetch: async (root) => githubMirrorState(root),
1865
+ assert: (m) => (m.github as any).pullRequests.some((p: any) => p.title === 'PR Table Probe' && p.ciStatus === 'success'),
1866
+ })),
1867
+ // PR conversation: seed a review, a requested reviewer, and a diff-anchored review
1868
+ // comment — assert all three counts/lists show up on the fetched PR.
1869
+ done('github.ui.pr_conversation', 'ui', 'PR conversation/detail (reviews, comments, reviewers) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1870
+ withRoot,
1871
+ seed: async (root) => {
1872
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'Conversation Probe', head: 'convo', base: 'main' }), root });
1873
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/reviews`, body: JSON.stringify({ event: 'APPROVE', body: 'lgtm' }), root });
1874
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/requested_reviewers`, body: JSON.stringify({ reviewers: ['carol'] }), root });
1875
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls/1/comments`, body: JSON.stringify({ body: 'nit', path: 'a.ts', line: 1, side: 'RIGHT' }), root });
1876
+ },
1877
+ fetch: async (root) => githubMirrorState(root),
1878
+ assert: (m) => {
1879
+ const pr = (m.github as any).pullRequests.find((p: any) => p.title === 'Conversation Probe');
1880
+ return !!pr && pr.reviewCount === 1 && pr.reviewCommentsCount === 1 && (pr.requestedReviewers as string[]).includes('carol');
1881
+ },
1882
+ })),
1883
+ // Issues list: seed an issue with a distinct label + assignee — both must appear on
1884
+ // the fetched row (the list's badges + assignee avatars are not static chrome).
1885
+ done('github.ui.issues_view', 'ui', 'Issues list view (per-repo) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1886
+ withRoot,
1887
+ seed: async (root) => {
1888
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'Issues View Probe' }), root });
1889
+ const n = (c.response.body as { number?: number }).number;
1890
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/${n}`, body: JSON.stringify({ labels: ['triage'], assignees: ['dave'] }), root });
1891
+ },
1892
+ fetch: async (root) => githubMirrorState(root),
1893
+ assert: (m) => {
1894
+ const i = (m.github as any).issues.find((x: any) => x.title === 'Issues View Probe');
1895
+ return !!i && (i.labels as Array<{ name: string }>).some((l) => l.name === 'triage') && (i.assignees as string[]).includes('dave');
1896
+ },
1897
+ })),
1898
+ // Actions runs view: register a workflow + dispatch it (real run + job) — assert the
1899
+ // fetched run's workflowName resolves + it carries the real jobCount.
1900
+ done('github.ui.actions_view', 'ui', 'Actions runs view (runs + jobs + status badges) (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
1901
+ withRoot,
1902
+ seed: async (root) => {
1903
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/actions/workflows/probe.yml`, body: JSON.stringify({ name: 'Probe CI', path: '.github/workflows/probe.yml' }), root });
1904
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/actions/workflows/probe.yml/dispatches`, body: JSON.stringify({ ref: 'main' }), root });
1905
+ },
1906
+ fetch: async (root) => githubMirrorState(root),
1907
+ assert: (m) => (m.github as any).runs.some((r: any) => r.workflowName === 'Probe CI' && r.jobCount === 1),
1908
+ })),
1909
+ // Repo chrome (tabs + per-repo counters): seed a PR + an issue in ONE repo — the
1910
+ // "All repositories" pill total AND that repo's own tab count must both move with
1911
+ // the seeded objects (proves the chrome's counts read real state, not a static shell).
1912
+ done('github.ui.repo_chrome', 'ui', 'Repo chrome (tabs: Code/PRs/Issues/Actions/…) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1913
+ withRoot,
1914
+ seed: async (root) => {
1915
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'Chrome Probe PR', head: 'chrome', base: 'main' }), root });
1916
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'Chrome Probe Issue' }), root });
1917
+ },
1918
+ fetch: async (root) => githubMirrorState(root),
1919
+ assert: (m) => {
1920
+ const g = m.github as any;
1921
+ const repoPrs = g.pullRequests.filter((p: any) => p.repo === REPO);
1922
+ return repoPrs.length === 1 && g.pullRequests.length === repoPrs.length && g.issues.some((i: any) => i.title === 'Chrome Probe Issue' && i.repo === REPO);
1923
+ },
1924
+ })),
1925
+ // PR Files-changed diff: seed a file with a real unified-diff PATCH — assert the
1926
+ // fetched file carries the exact seeded patch text (the diff hunk the view renders).
1927
+ done('github.ui.pr_files_diff', 'ui', 'PR Files-changed diff view (rendered hunks) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1928
+ withRoot,
1929
+ seed: (root) => applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'Diff Probe', head: 'diffprobe', base: 'main', files: [{ filename: 'diff-probe.ts', additions: 2, deletions: 1, patch: '@@ -1,1 +1,2 @@\n-old\n+new\n+line2' }] }), root }),
1930
+ fetch: async (root) => githubMirrorState(root),
1931
+ assert: (m) => (m.github as any).files.some((f: any) => f.filename === 'diff-probe.ts' && String(f.patch).includes('+new')),
1932
+ })),
1933
+ // Issue detail: seed a comment + a label/assignee/close (a REAL derived timeline) —
1934
+ // assert the fetched issue's comments + timeline both carry the seeded activity.
1935
+ done('github.ui.issue_detail', 'ui', 'Issue detail page (timeline + comments) (data-coupled)', 'ui', 'core', uiDataCoupled<string, Record<string, unknown>>({
1936
+ withRoot,
1937
+ seed: async (root) => {
1938
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'Detail Probe' }), root });
1939
+ const n = (c.response.body as { number?: number }).number;
1940
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues/${n}/comments`, body: JSON.stringify({ body: 'Issue detail probe comment' }), root });
1941
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/${n}`, body: JSON.stringify({ labels: ['bug'], state: 'closed', state_reason: 'completed' }), root });
1942
+ },
1943
+ fetch: async (root) => githubMirrorState(root),
1944
+ assert: (m) => {
1945
+ const i = (m.github as any).issues.find((x: any) => x.title === 'Detail Probe');
1946
+ return !!i && i.comments.some((c: any) => c.body === 'Issue detail probe comment') && (i.timeline as Array<{ event: string }>).some((t) => t.event === 'labeled') && (i.timeline as Array<{ event: string }>).some((t) => t.event === 'closed');
1947
+ },
1948
+ })),
1949
+ // Projects v2 board (converged onto the SHARED tooling uiDataCoupled — was the local
1950
+ // bundle-marker-grep + assert helper; the marker-grep half is dropped, the real
1951
+ // data-coupled seed/assert half is kept verbatim): seed a project + a single-select
1952
+ // Status field + two items, set each item's Status value, so the board's columns +
1953
+ // card counts are driven by the modeled item state.
1954
+ done('github.ui.project_board', 'ui', 'Projects v2 board view (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
1955
+ withRoot,
1956
+ seed: async (root) => {
1957
+ const c = await applyGithubWrite({ method: 'POST', path: '/orgs/octo/projectsV2', body: JSON.stringify({ title: 'Roadmap' }), root });
1958
+ const pid = (c.response.body as { id?: number }).id!;
1959
+ await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/fields`, body: JSON.stringify({ name: 'Status', data_type: 'single_select', single_select_options: [{ name: 'Todo' }, { name: 'Done' }] }), root });
1960
+ await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/views`, body: JSON.stringify({ name: 'Board', layout: 'board' }), root });
1961
+ const i1 = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/items`, body: JSON.stringify({ content_type: 'DraftIssue', title: 'Task A' }), root });
1962
+ const i2 = await applyGithubWrite({ method: 'POST', path: `/projectsV2/${pid}/items`, body: JSON.stringify({ content_type: 'DraftIssue', title: 'Task B' }), root });
1963
+ await applyGithubWrite({ method: 'PATCH', path: `/projectsV2/${pid}/items/${(i1.response.body as { id?: number }).id}`, body: JSON.stringify({ field_id: 'Status', value: 'Todo' }), root });
1964
+ await applyGithubWrite({ method: 'PATCH', path: `/projectsV2/${pid}/items/${(i2.response.body as { id?: number }).id}`, body: JSON.stringify({ field_id: 'Status', value: 'Done' }), root });
1965
+ },
1966
+ fetch: async (root) => githubMirrorState(root),
1967
+ assert: (m) => {
1968
+ const board = (m.github as any).projectBoards?.[0];
1969
+ if (!board || board.itemCount !== 2 || board.columns.length !== 2) return false;
1970
+ const todo = board.columns.find((c: any) => c.name === 'Todo');
1971
+ const doneCol = board.columns.find((c: any) => c.name === 'Done');
1972
+ // counts MOVE with the seeded items: exactly one card in each column, none unstatused.
1973
+ return todo?.cards.length === 1 && todo.cards[0].title === 'Task A' && doneCol?.cards.length === 1 && doneCol.cards[0].title === 'Task B' && board.noStatus.cards.length === 0;
1974
+ },
1975
+ })),
1976
+ // Discussions screen: seed a Q&A discussion + a comment marked as the accepted
1977
+ // answer — assert the fetched discussion carries the answer flag + category.
1978
+ done('github.ui.discussions', 'ui', 'Discussions screen (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
1979
+ withRoot,
1980
+ seed: async (root) => {
1981
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions`, body: JSON.stringify({ title: 'Discussion Probe', body: '?', category: 'q-a' }), root });
1982
+ const cm = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/discussions/1/comments`, body: JSON.stringify({ body: 'Discussion probe answer' }), root });
1983
+ const cid = (cm.response.body as { id?: number }).id;
1984
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/discussions/1/comments/${cid}/answer`, body: '{}', root });
1985
+ },
1986
+ fetch: async (root) => githubMirrorState(root),
1987
+ assert: (m) => {
1988
+ const d = (m.github as any).discussions.find((x: any) => x.title === 'Discussion Probe');
1989
+ return !!d && d.categorySlug === 'q-a' && d.isAnswered === true && d.comments.some((c: any) => c.body === 'Discussion probe answer' && c.isAnswer === true);
1990
+ },
1991
+ })),
1992
+ // Releases page: seed a release with a distinct asset — assert both the release's
1993
+ // fields AND the asset catalog entry appear on the fetched row.
1994
+ done('github.ui.releases', 'ui', 'Releases page (releases list + asset catalog) (data-coupled)', 'ui', 'common', uiDataCoupled<string, Record<string, unknown>>({
1995
+ withRoot,
1996
+ seed: async (root) => {
1997
+ const r = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases`, body: JSON.stringify({ tag_name: 'v-probe', name: 'Release Probe', body: 'notes' }), root });
1998
+ const id = (r.response.body as { id?: number }).id;
1999
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/releases/${id}/assets`, body: JSON.stringify({ name: 'probe.zip', content_type: 'application/zip', size: 2048 }), root });
2000
+ },
2001
+ fetch: async (root) => githubMirrorState(root),
2002
+ assert: (m) => {
2003
+ const r = (m.github as any).releases.find((x: any) => x.tagName === 'v-probe');
2004
+ return !!r && r.name === 'Release Probe' && r.assets.some((a: any) => a.name === 'probe.zip' && a.size === 2048);
2005
+ },
2006
+ })),
2007
+ // Code/file browser: seed a real committed file at a distinct path — assert it
2008
+ // appears in the fetched repo contents (the directory tree the browser renders).
2009
+ done('github.ui.code_browser', 'ui', 'Code/file browser tree (data-coupled)', 'ui', 'common', uiDataCoupled<string, Record<string, unknown>>({
2010
+ withRoot,
2011
+ seed: (root) => applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/src/probe.ts`, body: JSON.stringify({ message: 'add probe file', content: Buffer.from('export const probe = 1;').toString('base64') }), root }),
2012
+ fetch: async (root) => githubMirrorState(root),
2013
+ assert: (m) => (m.github as any).contents.some((c: any) => c.path === 'src/probe.ts' && c.repo === REPO),
2014
+ })),
2015
+ // Insights/Pulse/Contributors (converged onto the SHARED tooling uiDataCoupled — same
2016
+ // note as project_board above): seed a merged PR (two commits by alice) + an open
2017
+ // issue + a closed issue, so Pulse counts + the contributor leaderboard are derived
2018
+ // from real objects.
2019
+ done('github.ui.insights', 'ui', 'Insights/Pulse/Contributors (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
2020
+ withRoot,
2021
+ seed: async (root) => {
2022
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'feat', head: 'a', base: 'main', commits: [{ sha: 'a'.repeat(40), message: 'c1', author_name: 'alice' }, { sha: 'b'.repeat(40), message: 'c2', author_name: 'alice' }] }), root });
2023
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/pulls/1/merge`, body: '{}', root });
2024
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'open one' }), root });
2025
+ const closed = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'close me' }), root });
2026
+ await applyGithubWrite({ method: 'PATCH', path: `/repos/${REPO}/issues/${(closed.response.body as { number?: number }).number}`, body: JSON.stringify({ state: 'closed' }), root });
2027
+ },
2028
+ fetch: async (root) => githubMirrorState(root),
2029
+ assert: (m) => {
2030
+ const ins = (m.github as any).insights?.find((x: any) => x.repo === REPO);
2031
+ if (!ins) return false;
2032
+ // Pulse counts MOVE with the seeded activity; alice leads the contributor board with 2 commits.
2033
+ return ins.pulse.mergedPrs === 1 && ins.pulse.openedIssues === 1 && ins.pulse.closedIssues === 1
2034
+ && ins.contributors[0]?.login === 'alice' && ins.contributors[0]?.commits === 2;
2035
+ },
2036
+ })),
2037
+ // Repo/org settings screens (converged onto the SHARED tooling uiDataCoupled — same
2038
+ // note as above): seed a repo + a collaborator + a webhook, so the Settings screen's
2039
+ // rows + counts are driven by the modeled repo config / collaborators / webhooks.
2040
+ done('github.ui.settings', 'ui', 'Repo/org settings screens (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
2041
+ withRoot,
2042
+ seed: async (root) => {
2043
+ await applyGithubWrite({ method: 'POST', path: '/orgs/octo/repos', body: JSON.stringify({ name: 'demo', private: true }), root });
2044
+ await applyGithubWrite({ method: 'PUT', path: '/repos/octo/demo/collaborators/bob', body: JSON.stringify({ permission: 'push' }), root });
2045
+ await applyGithubWrite({ method: 'POST', path: '/repos/octo/demo/hooks', body: JSON.stringify({ config: { url: 'https://e.x/h' }, events: ['push'] }), root });
2046
+ },
2047
+ fetch: async (root) => githubMirrorState(root),
2048
+ assert: (m) => {
2049
+ const s = (m.github as any).settings?.find((x: any) => x.repo === 'octo/demo');
2050
+ if (!s) return false;
2051
+ // counts MOVE with the seeded objects: one collaborator (bob/push), one webhook, private flag set.
2052
+ return s.general.private === true && s.collaborators.length === 1 && s.collaborators[0].login === 'bob' && s.collaborators[0].permission === 'push'
2053
+ && s.webhooks.length === 1 && s.webhooks[0].events.includes('push');
2054
+ },
2055
+ })),
2056
+ // Global search: the search screen filters client-side over the SAME pullRequests/
2057
+ // issues/contents the mirror fetches — seed one of each with a shared distinctive
2058
+ // term and assert all three surface in the fetched payload the search view scans.
2059
+ done('github.ui.search', 'ui', 'Global search results screen (data-coupled)', 'ui', 'common', uiDataCoupled<string, Record<string, unknown>>({
2060
+ withRoot,
2061
+ seed: async (root) => {
2062
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/pulls`, body: JSON.stringify({ title: 'Zephyrus search term', head: 'searchpr', base: 'main' }), root });
2063
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/issues`, body: JSON.stringify({ title: 'Zephyrus search term issue' }), root });
2064
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/contents/zephyrus-search-term.ts`, body: JSON.stringify({ message: 'm', content: Buffer.from('x').toString('base64') }), root });
2065
+ },
2066
+ fetch: async (root) => githubMirrorState(root),
2067
+ assert: (m) => {
2068
+ const g = m.github as any;
2069
+ return g.pullRequests.some((p: any) => String(p.title).includes('Zephyrus search term'))
2070
+ && g.issues.some((i: any) => String(i.title).includes('Zephyrus search term'))
2071
+ && g.contents.some((c: any) => String(c.path).includes('zephyrus-search-term'));
2072
+ },
2073
+ })),
2074
+ // Notifications inbox: seed a real notification thread — assert it appears unread
2075
+ // with the seeded subject title (an unseeded root has zero notification threads).
2076
+ done('github.ui.notifications', 'ui', 'Notifications inbox screen (data-coupled)', 'ui', 'common', uiDataCoupled<string, Record<string, unknown>>({
2077
+ withRoot,
2078
+ seed: (root) => applyGithubWrite({ method: 'POST', path: `/notifications`, body: JSON.stringify({ repository: REPO, subject_title: 'Notification Probe', subject_type: 'PullRequest', reason: 'review_requested' }), root }),
2079
+ fetch: async (root) => githubMirrorState(root),
2080
+ assert: (m) => (m.github as any).notifications.some((n: any) => n.title === 'Notification Probe' && n.unread === true),
2081
+ })),
2082
+ // Deployments/Environments: seed a deployment + a status transition + an
2083
+ // environment's protection rule — assert the fetched row carries the LATEST state.
2084
+ done('github.ui.deployments', 'ui', 'Deployments/Environments screen (deployments list + state + environments) (data-coupled)', 'ui', 'niche', uiDataCoupled<string, Record<string, unknown>>({
2085
+ withRoot,
2086
+ seed: async (root) => {
2087
+ const c = await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments`, body: JSON.stringify({ ref: 'main', environment: 'deploy-probe', description: 'probe deploy' }), root });
2088
+ const id = (c.response.body as { id?: number }).id;
2089
+ await applyGithubWrite({ method: 'POST', path: `/repos/${REPO}/deployments/${id}/statuses`, body: JSON.stringify({ state: 'success', environment_url: 'https://probe.x' }), root });
2090
+ await applyGithubWrite({ method: 'PUT', path: `/repos/${REPO}/environments/deploy-probe`, body: JSON.stringify({ wait_timer: 5 }), root });
2091
+ },
2092
+ fetch: async (root) => githubMirrorState(root),
2093
+ assert: (m) => {
2094
+ const g = m.github as any;
2095
+ const d = g.deployments.find((x: any) => x.environment === 'deploy-probe');
2096
+ const e = g.environments.find((x: any) => x.name === 'deploy-probe');
2097
+ return !!d && d.state === 'success' && d.environmentUrl === 'https://probe.x' && !!e && e.protected === true;
2098
+ },
2099
+ })),
2100
+ // ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos so
2101
+ // scripts/seed-conformance-issues.ts (A1) turns them into real backlog work. See
2102
+ // pull-audit.json (repo root) for the per-pack pull-vs-read-surface evidence.
2103
+ todo('github.connector.pull_pr_files', 'connector', 'Connector: pull PR files (per-file diff list) from the real vendor', 'connector', 'core'),
2104
+ todo('github.connector.pull_repo_metadata', 'connector', 'Connector: pull repo metadata/settings (GET /repos/:owner/:repo) from the real vendor', 'connector', 'common'),
2105
+
2106
+ // ── Missing-area sweep (TWIN-87 / F1) ────────────────────────────────────────────────────
2107
+ // These whole vendor product areas had NO manifest entry of any status before this pass —
2108
+ // not todo'd, not carved out — so they never surfaced in checkCapabilities gaps and the
2109
+ // coverage denominator structurally couldn't include them. Filed as honest todos (all
2110
+ // genuinely buildable; none is an impossibility warranting outOfScope). See GITHUB_AREAS
2111
+ // below + github-capabilities.test.ts's area-census meta-test, which is what makes a future
2112
+ // whole-area omission fail the gate instead of waiting for another review pass.
2113
+ todo('github.copilot.seats', 'copilot', 'Copilot: manage Business/Enterprise seat assignments (list/add/remove billed seats)', 'api', 'niche'),
2114
+ todo('github.copilot.usage', 'copilot', 'Copilot: usage/metrics API (org-level Copilot usage breakdown)', 'api', 'niche'),
2115
+ todo('github.billing.actions_usage', 'billing', 'Billing: Actions minutes usage (org/enterprise billing API)', 'api', 'niche'),
2116
+ todo('github.billing.packages_storage', 'billing', 'Billing: shared storage/packages billing usage', 'api', 'niche'),
2117
+ todo('github.migrations.org_export', 'migrations', 'Migrations: start + poll an org migration (data export archive)', 'api', 'niche'),
2118
+ todo('github.migrations.repo_import', 'migrations', 'Migrations: source import status (GitHub Importer API)', 'api', 'niche'),
2119
+ todo('github.actions.runners_crud', 'actions', 'Actions: self-hosted runners CRUD (repo/org) + registration/removal tokens', 'api', 'niche'),
2120
+ todo('github.actions.runner_groups', 'actions', 'Actions: runner groups (org) CRUD + repo/runner assignment', 'api', 'niche'),
2121
+ todo('github.interactions.limits', 'interactions', 'Interactions: repo/org temporary interaction limits (collaborators-only restriction)', 'api', 'niche'),
2122
+ todo('github.marketplace.listing', 'marketplace', 'Marketplace: listing plans for a GitHub App', 'api', 'niche'),
2123
+ todo('github.marketplace.purchase_webhook', 'marketplace', 'Marketplace: purchase/change/cancellation webhook event shapes', 'api', 'niche'),
2124
+ todo('github.scim.provisioning', 'scim', 'SCIM: enterprise/org user + group provisioning (SCIM 2.0 endpoints)', 'api', 'niche'),
2125
+
2126
+ ];
2127
+
2128
+ // TWIN-87 committed area census — the vendor's top-level API product areas (docs nav /
2129
+ // OpenAPI tags), authored top-down independent of what a manifest entry happens to already
2130
+ // exist for. github-capabilities.test.ts's area-census meta-test (assertAreaCensus) fails the
2131
+ // gate if a declared area has zero manifest entries and no named exclusion, OR if a manifest
2132
+ // entry's `area` drifts outside this list — so a whole missing area (Copilot/billing/
2133
+ // migrations/runners/interactions/marketplace/SCIM were the concrete F1 leak) can never again
2134
+ // hide invisibly behind a denominator that structurally couldn't include it.
2135
+ export const GITHUB_AREAS = [
2136
+ 'actions', 'activity', 'api', 'auth', 'billing', 'checks', 'codespaces', 'connector',
2137
+ 'copilot', 'deployments', 'discussions', 'fields', 'gists', 'git', 'interactions', 'issues',
2138
+ 'marketplace', 'meta', 'migrations', 'notifications', 'orgs', 'packages', 'pages', 'projects',
2139
+ 'pulls', 'releases', 'repos', 'scim', 'search', 'security', 'ui', 'users', 'webhooks',
2140
+ ] as const;
2141
+
2142
+ export function githubCapabilities(): Promise<CapabilityReport> {
2143
+ return checkCapabilities('github', GITHUB_CAPABILITIES);
2144
+ }