@bongos/core 1.21.176 → 1.21.178

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.bongos-core.json +186 -496
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +0 -6
  5. package/clients/bongos-client/index.cjs +0 -6
  6. package/clients/bongos-client/index.d.ts +0 -6
  7. package/clients/bongos-client/index.mjs +0 -6
  8. package/config/scheduled-routines.json +3 -8
  9. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
  10. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  11. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  12. package/docs/api/openapi.json +1 -87
  13. package/docs/api-reference.md +1 -8
  14. package/docs/canonical-permissions.md +4 -4
  15. package/docs/copy-inventory.md +25 -39
  16. package/docs/copy-registry.json +73 -200
  17. package/docs/file-map.md +6 -11
  18. package/docs/handoff-template.md +1 -1
  19. package/docs/module-api-changelog.md +4 -0
  20. package/docs/modules-contract.md +2 -2
  21. package/docs/onboarding/diagrams/README.md +3 -7
  22. package/docs/onboarding/diagrams/assertions.json +3 -3
  23. package/docs/page-inventory.json +4 -7
  24. package/docs/page-readings.json +1520 -1569
  25. package/docs/recipes/ops-gotchas.md +3 -1
  26. package/modules/autonomy/db.js +9 -0
  27. package/modules/autonomy/module.json +2 -2
  28. package/modules/autonomy/pollers/daily-checks.js +262 -0
  29. package/modules/autonomy/pollers/routine-timers.js +7 -5
  30. package/modules/autonomy/rank-roles.js +64 -0
  31. package/modules/autonomy/server-schedule.js +37 -0
  32. package/modules/government/catalog.js +10 -7
  33. package/modules/government/decision-board.js +0 -1
  34. package/modules/government/decision-rules.js +0 -1
  35. package/modules/government/docket.js +0 -1
  36. package/modules/government/protected-surfaces.js +1 -1
  37. package/modules/government/protected-surfaces.json +2 -3
  38. package/modules/hall-ui/public/atlas.html +1 -1
  39. package/modules/hall-ui/public/blockers.html +1 -1
  40. package/modules/hall-ui/public/board-room.html +1 -1
  41. package/modules/hall-ui/public/city.html +1 -1
  42. package/modules/hall-ui/public/collab.html +1 -1
  43. package/modules/hall-ui/public/commands.html +1 -1
  44. package/modules/hall-ui/public/copy-desk.html +1 -1
  45. package/modules/hall-ui/public/deploy.html +1 -1
  46. package/modules/hall-ui/public/diagrams.html +1 -1
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.css +4 -26
  50. package/modules/hall-ui/public/gate.html +12 -27
  51. package/modules/hall-ui/public/gate.js +26 -176
  52. package/modules/hall-ui/public/gate.states.json +4 -22
  53. package/modules/hall-ui/public/goal-map.html +1 -1
  54. package/modules/hall-ui/public/goals.html +1 -1
  55. package/modules/hall-ui/public/government.html +1 -1
  56. package/modules/hall-ui/public/idea.html +1 -1
  57. package/modules/hall-ui/public/ideas.html +1 -1
  58. package/modules/hall-ui/public/index.html +1 -1
  59. package/modules/hall-ui/public/modules.html +1 -1
  60. package/modules/hall-ui/public/my-agents.html +1 -1
  61. package/modules/hall-ui/public/primer.html +1 -1
  62. package/modules/hall-ui/public/profile.html +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +1 -1
  69. package/modules/hall-ui/public/shell.js +1 -1
  70. package/modules/hall-ui/public/street-card.js +5 -9
  71. package/modules/hall-ui/public/street-desk.js +2 -2
  72. package/modules/hall-ui/public/street-inside.js +1 -1
  73. package/modules/hall-ui/public/street.html +1 -1
  74. package/modules/hall-ui/public/studio.html +1 -1
  75. package/modules/hall-ui/public/task.html +1 -1
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor.html +1 -1
  78. package/modules/hall-ui/public/watch.html +1 -1
  79. package/modules/hall-ui/public/work.html +1 -1
  80. package/modules/lifecycle/matter-acts.js +0 -6
  81. package/modules/lifecycle/module.json +0 -1
  82. package/package-lock.json +2 -2
  83. package/package.json +1 -1
  84. package/release-notes.json +36 -0
  85. package/scripts/bongos/adopt-preflight.js +3 -3
  86. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  87. package/scripts/bongos/audit-authorship.sh +1 -8
  88. package/scripts/bongos/audit-rank-roles.js +15 -56
  89. package/scripts/bongos/bump-version.js +46 -9
  90. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  91. package/scripts/bongos/diagram-facts.js +1 -1
  92. package/scripts/bongos/do-api.js +1 -1
  93. package/scripts/bongos/doc-cli-guard.js +2 -0
  94. package/scripts/bongos/doc-control-claims.js +2 -2
  95. package/scripts/bongos/fitness-checks-identity.js +4 -15
  96. package/scripts/bongos/gen-diagrams.js +3 -3
  97. package/scripts/bongos/government-walk.js +3 -9
  98. package/scripts/bongos/hard-floor.js +52 -0
  99. package/scripts/bongos/publish-manifest.js +3 -3
  100. package/scripts/bongos/pulse.js +5 -3
  101. package/scripts/bongos/routine-schedule.js +38 -3
  102. package/scripts/bongos/run-unit-tests.js +17 -5
  103. package/scripts/bongos/ship-regen.js +2 -3
  104. package/scripts/gds/{goal-names.js → hard-floor.js} +3 -4
  105. package/scripts/security/dep-audit.js +50 -124
  106. package/src/bongos/module-scope-map.js +2 -2
  107. package/src/bongos/permission-path-check.js +1 -1
  108. package/src/bongos/route-rank-check.js +1 -8
  109. package/src/bongos/routes.js +1 -2
  110. package/src/module-api.js +1 -1
  111. package/src/modules.js +1 -1
  112. package/tests/acclaim_feed_evidence_db.mjs +42 -23
  113. package/tests/adr_0089_scope.mjs +7 -5
  114. package/tests/autonomy_daily_checks.mjs +201 -0
  115. package/tests/bump_version.mjs +52 -18
  116. package/tests/claude_materialize.mjs +12 -12
  117. package/tests/copy_desk_flags_db.mjs +22 -14
  118. package/tests/effective_visibility_predicate.mjs +0 -18
  119. package/tests/gating_review_retired.mjs +1 -1
  120. package/tests/government_act_alone_buildings.mjs +1 -3
  121. package/tests/government_board_membership_db.mjs +35 -22
  122. package/tests/government_protected_surfaces.mjs +29 -13
  123. package/tests/governor_docket.mjs +0 -2
  124. package/tests/governor_matter_acts.mjs +6 -8
  125. package/tests/hall_city.mjs +1 -1
  126. package/tests/hall_matter_card.mjs +2 -2
  127. package/tests/hall_oversight_world.mjs +16 -12
  128. package/tests/hall_street_desk.mjs +4 -3
  129. package/tests/hall_street_inside.mjs +1 -1
  130. package/tests/helpers.mjs +34 -0
  131. package/tests/howto_state.mjs +33 -56
  132. package/tests/howto_state_db.mjs +68 -0
  133. package/tests/idea_credit_streams_db.mjs +11 -3
  134. package/tests/integration_parked_ratchet.mjs +7 -1
  135. package/tests/nav_permission_atoms.mjs +1 -1
  136. package/tests/page_notes_db.mjs +12 -8
  137. package/tests/page_tweak_no_rearm.mjs +50 -0
  138. package/tests/publish_manifest.mjs +64 -0
  139. package/tests/routine_capture_kind.mjs +18 -7
  140. package/tests/routine_schedule.mjs +42 -0
  141. package/tests/search_isolation.mjs +5 -103
  142. package/tests/search_isolation_db.mjs +140 -0
  143. package/tests/session_earnings_mirror_db.mjs +2 -0
  144. package/tests/workflow_exec_paths.mjs +9 -11
  145. package/modules/lifecycle/routes/gate-approvals.js +0 -299
  146. package/scripts/bongos/artifact-staleness.js +0 -267
  147. package/scripts/bongos/ci-grade.js +0 -82
  148. package/scripts/bongos/classify-discipline.js +0 -153
  149. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  150. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  151. package/scripts/bongos/gate-review.js +0 -330
  152. package/scripts/bongos/goal-names.js +0 -102
  153. package/scripts/bongos/rename-goals.js +0 -109
  154. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  155. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  156. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  157. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  158. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  159. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  160. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  161. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  162. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  163. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  164. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  165. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  166. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  167. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  168. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  169. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  170. package/scripts/bongos/seed-governance-tasks.js +0 -340
  171. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  172. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  173. package/scripts/bongos/seed-productization-tasks.js +0 -235
  174. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  175. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  176. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  177. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  178. package/scripts/gds/artifact-staleness.js +0 -7
  179. package/scripts/gds/ci-grade.js +0 -7
  180. package/scripts/gds/classify-discipline.js +0 -7
  181. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  182. package/scripts/gds/coreB-carve-proof.sh +0 -8
  183. package/scripts/gds/gate-review.js +0 -7
  184. package/scripts/gds/rename-goals.js +0 -7
  185. package/scripts/gds/seed-adopt-tasks.js +0 -7
  186. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  187. package/scripts/gds/seed-board-room-tasks.js +0 -6
  188. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  189. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  190. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  191. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  192. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  193. package/scripts/gds/seed-bongos-tasks.js +0 -7
  194. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  195. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  196. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  197. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  198. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  199. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  200. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  201. package/scripts/gds/seed-governance-tasks.js +0 -7
  202. package/scripts/gds/seed-medusa-tasks.js +0 -7
  203. package/scripts/gds/seed-module-upstreaming.js +0 -7
  204. package/scripts/gds/seed-productization-tasks.js +0 -7
  205. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  206. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  207. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  208. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  209. package/scripts/migrate_cost_ledger.sh +0 -178
  210. package/tests/artifact_staleness.mjs +0 -207
  211. package/tests/gate_approvals.mjs +0 -611
  212. package/tests/gate_review.mjs +0 -152
  213. package/tests/goal_names.mjs +0 -120
  214. package/tests/newcomer_generator.mjs +0 -48
@@ -1,611 +0,0 @@
1
- // tests/gate_approvals.mjs
2
- //
3
- // Coverage for Archon gate-surface PR approval from the hall (task 1179, ADR 0058
4
- // amendment) — the path that lets an Archon clear a gate-review-blocked PR from the
5
- // builders' hall instead of leaving a GitHub review.
6
- //
7
- // Three layers, all with NO live DB / server / network:
8
- // 1. src/bongos/github-push.js helpers — listOpenPullRequests / getPullRequestFiles /
9
- // setGateOwnerApproved / recheckPullRequest, through the injectable
10
- // fetchImpl / repoInfo / token seams.
11
- // 2. The route handlers — GET /gate-approvals (filters to gate-review-red PRs and
12
- // reuses the REAL classifyChanges to render why) + POST /gate-approvals/:pr/
13
- // approve (sets the trustless gate-owner-approved status + re-runs the check),
14
- // exercised by stubbing the github-push module the router holds.
15
- // 3. Structural gating — both routes are requireBuilder + classify to archon under
16
- // the route-rank auditor (a silent downgrade of the approve route would let any
17
- // builder clear the hard floor).
18
- //
19
- // Run: node tests/gate_approvals.mjs
20
-
21
- import { strict as assert } from 'node:assert';
22
- import { createRequire } from 'node:module';
23
-
24
- process.env.NODE_ENV = 'test';
25
- const require = createRequire(import.meta.url);
26
-
27
- const gp = require('../modules/lifecycle/github-push.js');
28
- const auth = require('../src/bongos/auth.js');
29
- const buildGateApprovalsRouter = require('../modules/lifecycle/routes/gate-approvals.js');
30
- const { checkRouteRanks } = require('../src/bongos/route-rank-check.js');
31
- const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
32
-
33
- let pass = 0, fail = 0;
34
- const t = async (name, fn) => {
35
- try { await fn(); pass++; console.log(`PASS ${name}`); }
36
- catch (e) { fail++; console.log(`FAIL ${name}\n ${e.stack || e.message}`); }
37
- };
38
-
39
- const REPO = { owner: 'someowner', repo: 'somerepo', branch: 'main' };
40
- const SHA = 'a'.repeat(40);
41
- function jsonRes(status, body) {
42
- return { ok: status >= 200 && status < 300, status, json: async () => body, text: async () => JSON.stringify(body) };
43
- }
44
-
45
- // Call the FINAL handler of a route (after the auth middleware) with a mock req/res.
46
- function routeHandler(router, method, routePath) {
47
- const layer = router.stack.find((l) => l.route && l.route.path === routePath && l.route.methods[method]);
48
- if (!layer) throw new Error(`route ${method} ${routePath} not registered`);
49
- const stack = layer.route.stack;
50
- return stack[stack.length - 1].handle;
51
- }
52
- function fakeRes() {
53
- const res = { statusCode: 200, body: null };
54
- res.status = (c) => { res.statusCode = c; return res; };
55
- res.json = (b) => { res.body = b; return res; };
56
- attachFail({}, res, () => {}); // install res.fail() as the router does (task 1991)
57
- return res;
58
- }
59
- const ARCHON = { id: 1, rank: 'archon', github_login: 'example-owner' };
60
-
61
- // Snapshot/restore the github-push functions a route test stubs, so layers don't bleed.
62
- function withStubs(stubs, fn) {
63
- const keys = Object.keys(stubs);
64
- const saved = keys.map((k) => [k, gp[k]]);
65
- for (const k of keys) gp[k] = stubs[k];
66
- return (async () => { try { return await fn(); } finally { for (const [k, v] of saved) gp[k] = v; } })();
67
- }
68
-
69
- // A PR node as listOpenPullRequests returns it (already flattened).
70
- function pr(over = {}) {
71
- return {
72
- number: 201, title: 'task 1163: x', url: 'http://gh/pr/201', is_draft: false,
73
- head_ref: 'claude/x', head_sha: SHA, author: 'someone', body: '', review_decision: null,
74
- rollup_state: 'FAILURE',
75
- checks: [{ name: 'gate-review', conclusion: 'FAILURE', status: 'COMPLETED', url: 'http://ci' }],
76
- ...over,
77
- };
78
- }
79
- const gateCheck = (conclusion, status = 'COMPLETED') => ({ name: 'gate-review', conclusion, status, url: 'http://ci' });
80
-
81
- // ===========================================================================
82
- // Layer 1 — github-push helpers (injected seams)
83
- // ===========================================================================
84
-
85
- await t('listOpenPullRequests: maps GraphQL nodes → number/head_sha/author/flattened checks', async () => {
86
- const fetchImpl = async (_url, opts) => {
87
- assert.ok(/pullRequests\(states:OPEN/.test(JSON.parse(opts.body).query), 'queries OPEN PRs');
88
- return jsonRes(200, { data: { repository: { pullRequests: { nodes: [{
89
- number: 7, title: 'task 9: t', url: 'http://gh/pr/7', isDraft: false, headRefName: 'claude/y',
90
- bodyText: 'body', author: { login: 'dev1' }, reviewDecision: 'REVIEW_REQUIRED',
91
- commits: { nodes: [{ commit: { oid: SHA, statusCheckRollup: { state: 'FAILURE', contexts: { nodes: [
92
- { __typename: 'CheckRun', name: 'gate-review', conclusion: 'FAILURE', status: 'COMPLETED', detailsUrl: 'http://ci' },
93
- { __typename: 'StatusContext', context: 'gate-author-trust', state: 'SUCCESS', targetUrl: '' },
94
- ] } } } }] },
95
- }] } } } });
96
- };
97
- const out = await gp.listOpenPullRequests({ token: 'T', repoInfo: REPO, fetchImpl });
98
- assert.equal(out.length, 1);
99
- assert.equal(out[0].number, 7);
100
- assert.equal(out[0].head_sha, SHA);
101
- assert.equal(out[0].author, 'dev1');
102
- assert.equal(out[0].checks.find((c) => c.name === 'gate-review').conclusion, 'FAILURE');
103
- assert.equal(out[0].checks.find((c) => c.name === 'gate-author-trust').conclusion, 'SUCCESS');
104
- });
105
-
106
- await t('listOpenPullRequests: tolerates a PARTIAL FORBIDDEN (App lacks Checks:read) — uses data, flags partialErrors', async () => {
107
- // GitHub returns `data` (PR list intact) ALONGSIDE an errors[] when statusCheckRollup.contexts
108
- // is FORBIDDEN. The old code did `if (out.errors) throw` and 502'd; it must now use the data.
109
- const fetchImpl = async () => jsonRes(200, {
110
- data: { repository: { pullRequests: { nodes: [{
111
- number: 167, title: 'task 1234: x', url: 'http://gh/pr/167', isDraft: false, headRefName: 'claude/z',
112
- bodyText: 'b', author: { login: 'dev2' }, reviewDecision: null,
113
- // REAL shape: the FORBIDDEN nulls the individual CheckRun ELEMENTS inside the array
114
- // (a null hole) — NOT the whole `nodes` list. Readable StatusContexts survive.
115
- commits: { nodes: [{ commit: { oid: SHA, statusCheckRollup: { state: 'FAILURE', contexts: { nodes: [
116
- null, // the gate-review CheckRun, FORBIDDEN without Checks:read
117
- { __typename: 'StatusContext', context: 'gate-author-trust', state: 'SUCCESS', targetUrl: '' },
118
- ] } } } }] },
119
- }] } } },
120
- errors: [{ type: 'FORBIDDEN', path: ['repository', 'pullRequests', 'nodes', 0, 'commits', 'nodes', 0, 'commit', 'statusCheckRollup', 'contexts', 'nodes', 0] }],
121
- });
122
- const out = await gp.listOpenPullRequests({ token: 'T', repoInfo: REPO, fetchImpl });
123
- assert.equal(out.length, 1, 'PR list is still returned despite the partial error');
124
- assert.equal(out[0].number, 167);
125
- assert.equal(out[0].head_sha, SHA, 'head sha — all the approve path actually needs — survives');
126
- // The null CheckRun hole is filtered out (would otherwise crash on c.__typename); the
127
- // readable StatusContext survives, so gate-author-trust / gate-owner-approved still show.
128
- assert.deepEqual(out[0].checks.map((c) => c.name), ['gate-author-trust'], 'null CheckRun dropped, StatusContext kept');
129
- assert.deepEqual(out.partialErrors, ['FORBIDDEN'], 'reduced fidelity is signalled, not silent');
130
- });
131
-
132
- await t('listOpenPullRequests: a TOP-LEVEL GraphQL failure (no data) still throws GRAPHQL_FAILED with detail', async () => {
133
- const fetchImpl = async () => jsonRes(200, { data: null, errors: [{ type: 'FORBIDDEN', message: 'bad creds' }] });
134
- await assert.rejects(
135
- () => gp.listOpenPullRequests({ token: 'T', repoInfo: REPO, fetchImpl }),
136
- (e) => e.code === 'GRAPHQL_FAILED' && /FORBIDDEN/.test(e.detail || '')
137
- );
138
- });
139
-
140
- await t('getMergeState: a partial FORBIDDEN on the rollup still yields mergeable/merge_state (merge driver survives)', async () => {
141
- const fetchImpl = async () => jsonRes(200, {
142
- data: { repository: { pullRequests: { nodes: [{
143
- number: 167, id: 'PR_x', state: 'OPEN', merged: false, mergeable: 'MERGEABLE', reviewDecision: null,
144
- mergeStateStatus: 'CLEAN',
145
- // null CheckRun hole (FORBIDDEN) alongside a readable StatusContext.
146
- commits: { nodes: [{ commit: { oid: SHA, statusCheckRollup: { state: 'FAILURE', contexts: { nodes: [
147
- null, { __typename: 'StatusContext', context: 'gate-author-trust', state: 'SUCCESS' },
148
- ] } } } }] },
149
- }] } } },
150
- errors: [{ type: 'FORBIDDEN', path: ['repository', 'pullRequests', 'nodes', 0, 'commits', 'nodes', 0, 'commit', 'statusCheckRollup', 'contexts', 'nodes', 0] }],
151
- });
152
- const s = await gp.getMergeState({ branch: 'claude/z' }, { token: 'T', repoInfo: REPO, fetchImpl });
153
- assert.equal(s.mergeable, 'MERGEABLE');
154
- assert.equal(s.merge_state, 'CLEAN', 'the gate signal mergeIfGreen depends on is intact');
155
- assert.deepEqual(s.checks.map((c) => c.name), ['gate-author-trust'], 'null hole filtered; readable status kept');
156
- });
157
-
158
- await t('getPullRequestFiles: maps REST files → {path,deleted,status}; reflects deletions', async () => {
159
- const fetchImpl = async (url) => {
160
- assert.ok(/\/pulls\/201\/files/.test(url));
161
- return jsonRes(200, [
162
- { filename: 'src/bongos/route-rank-check.js', status: 'modified', additions: 3, deletions: 1 },
163
- { filename: 'old.js', status: 'removed', additions: 0, deletions: 9 },
164
- ]);
165
- };
166
- const files = await gp.getPullRequestFiles({ number: 201 }, { token: 'T', repoInfo: REPO, fetchImpl });
167
- assert.equal(files.length, 2);
168
- assert.equal(files[0].path, 'src/bongos/route-rank-check.js');
169
- assert.equal(files[0].deleted, false);
170
- assert.equal(files[1].deleted, true, 'status removed → deleted');
171
- });
172
-
173
- await t('getPullRequestFiles: rejects a non-positive PR number before any fetch', async () => {
174
- await assert.rejects(
175
- () => gp.getPullRequestFiles({ number: 0 }, { token: 'T', repoInfo: REPO, fetchImpl: async () => { throw new Error('no fetch'); } }),
176
- (e) => e.code === 'BAD_PR'
177
- );
178
- });
179
-
180
- await t('setGateOwnerApproved: POSTs a gate-owner-approved success status on the sha', async () => {
181
- let captured = null;
182
- const fetchImpl = async (url, opts) => { captured = { url, body: JSON.parse(opts.body) }; return { ok: true }; };
183
- const ok = await gp.setGateOwnerApproved({ sha: SHA, login: 'example-owner' }, { token: 'T', repoInfo: REPO, fetchImpl });
184
- assert.equal(ok, true);
185
- assert.ok(captured.url.endsWith(`/statuses/${SHA}`), captured.url);
186
- assert.equal(captured.body.context, gp.GATE_OWNER_APPROVED_CONTEXT);
187
- assert.equal(captured.body.context, 'gate-owner-approved');
188
- assert.equal(captured.body.state, 'success');
189
- assert.ok(/example-owner/.test(captured.body.description), 'records who approved');
190
- });
191
-
192
- await t('recheckPullRequest: PATCHes state=closed then state=open, then re-arms auto-merge', async () => {
193
- const calls = [];
194
- const fetchImpl = async (url, opts) => {
195
- const method = (opts && opts.method) || 'GET';
196
- if (method === 'PATCH') { calls.push(JSON.parse(opts.body).state); return jsonRes(200, { node_id: 'N1', state: JSON.parse(opts.body).state }); }
197
- if (url === 'https://api.github.com/graphql') { calls.push('automerge'); return jsonRes(200, { data: { enablePullRequestAutoMerge: {} } }); }
198
- throw new Error('unexpected ' + url + ' ' + method);
199
- };
200
- const r = await gp.recheckPullRequest({ number: 201 }, { token: 'T', repoInfo: REPO, fetchImpl, reopenDelayMs: 0 });
201
- assert.deepEqual(calls, ['closed', 'open', 'automerge'], 'close → reopen → re-arm auto-merge, in order');
202
- assert.equal(r.reopened, true);
203
- assert.equal(r.auto_merge, true);
204
- });
205
-
206
- await t('recheckPullRequest: waits between close and reopen so GitHub fires a distinct reopened event (task 1241)', async () => {
207
- // Regression for task 1241: a close+reopen issued ~1s apart is coalesced by GitHub
208
- // into no net change and dispatches no `pull_request: reopened` event, so the
209
- // gate-review run never re-fires. recheckPullRequest must sleep BETWEEN the close
210
- // PATCH and the reopen PATCH (deps.reopenDelayMs, default 4s in prod) so the reopen
211
- // lands as a distinct state transition.
212
- //
213
- // task 1002644: this used to sleep a REAL 30ms and assert wall-clock elapsed >=
214
- // 30 — Date.now() truncation + NTP slew on CI runners can read a punctual timer
215
- // as 29ms, which flaked the unit lane (the ONLY required merge check, ADR 0162).
216
- // Inject the sleep and assert its PLACE in the event sequence instead — strictly
217
- // stronger (total elapsed never proved the wait sat BETWEEN the two PATCHes) and
218
- // fully deterministic.
219
- const events = [];
220
- const delayMs = 4000; // the prod-default shape — never actually slept (injected)
221
- const fetchImpl = async (url, opts) => {
222
- const method = (opts && opts.method) || 'GET';
223
- if (method === 'PATCH') { events.push('patch:' + JSON.parse(opts.body).state); return jsonRes(200, { node_id: 'N1', state: JSON.parse(opts.body).state }); }
224
- if (url === 'https://api.github.com/graphql') { events.push('automerge'); return jsonRes(200, { data: { enablePullRequestAutoMerge: {} } }); }
225
- throw new Error('unexpected ' + url + ' ' + method);
226
- };
227
- const sleepImpl = async (ms) => { events.push(`sleep:${ms}`); };
228
- await gp.recheckPullRequest({ number: 202 }, { token: 'T', repoInfo: REPO, fetchImpl, sleepImpl, reopenDelayMs: delayMs });
229
- assert.deepEqual(events, ['patch:closed', `sleep:${delayMs}`, 'patch:open', 'automerge'],
230
- 'the wait happens BETWEEN the close and the reopen — a coalesced close+reopen dispatches no reopened event');
231
- });
232
-
233
- // ===========================================================================
234
- // Layer 2 — GET /gate-approvals handler logic (stub the github-push module)
235
- // ===========================================================================
236
-
237
- await t('GET /gate-approvals: lists ONLY gate-review-red PRs, with real classifier verdict + reasons', async () => {
238
- const prs = [
239
- pr({ number: 201, title: 'task 1163: harbor', checks: [gateCheck('FAILURE')] }), // escalate (route-rank-check)
240
- pr({ number: 202, checks: [gateCheck('SUCCESS')] }), // green → excluded
241
- pr({ number: 203, is_draft: true, checks: [gateCheck('FAILURE')] }), // draft → excluded
242
- pr({ number: 204, checks: [gateCheck(null, 'IN_PROGRESS')] }), // still checking
243
- pr({ number: 205, title: 'task 50: mig', checks: [gateCheck('FAILURE')] }), // needs_trust (migration)
244
- pr({ number: 206, checks: [] }), // no gate-review → excluded
245
- ];
246
- const filesByPr = {
247
- 201: [{ path: 'src/bongos/route-rank-check.js', status: 'modified' }],
248
- 205: [{ path: 'migrations/200_x.sql', status: 'added' }],
249
- };
250
- await withStubs({
251
- isConfigured: () => true,
252
- listOpenPullRequests: async () => prs,
253
- getPullRequestFiles: async ({ number }) => filesByPr[number] || [],
254
- }, async () => {
255
- const handler = routeHandler(buildGateApprovalsRouter(), 'get', '/gate-approvals');
256
- const res = fakeRes();
257
- await handler({ builder: ARCHON, params: {}, body: {} }, res);
258
- assert.equal(res.statusCode, 200);
259
- const nums = res.body.awaiting.map((a) => a.number).sort();
260
- assert.deepEqual(nums, [201, 205], 'only the two completed-FAILURE non-draft gate PRs');
261
- assert.equal(res.body.checking, 1, 'the in-progress PR is counted as checking');
262
- const a201 = res.body.awaiting.find((a) => a.number === 201);
263
- assert.equal(a201.verdict, 'escalate');
264
- assert.equal(a201.task, 1163, 'task id parsed from the title');
265
- assert.ok(a201.reasons.some((r) => r.hard_floor === true), 'route-rank-check flagged as hard floor');
266
- const a205 = res.body.awaiting.find((a) => a.number === 205);
267
- assert.equal(a205.verdict, 'needs_trust');
268
- assert.ok(a205.reasons.length >= 1 && a205.reasons.every((r) => r.hard_floor === false));
269
- });
270
- });
271
-
272
- await t('GET /gate-approvals: marks already_approved when a gate-owner-approved status is present', async () => {
273
- await withStubs({
274
- isConfigured: () => true,
275
- listOpenPullRequests: async () => [pr({
276
- number: 210,
277
- checks: [gateCheck('FAILURE'), { name: 'gate-owner-approved', conclusion: 'SUCCESS', status: 'COMPLETED' }],
278
- })],
279
- getPullRequestFiles: async () => [{ path: 'src/bongos/route-rank-check.js', status: 'modified' }],
280
- }, async () => {
281
- const handler = routeHandler(buildGateApprovalsRouter(), 'get', '/gate-approvals');
282
- const res = fakeRes();
283
- await handler({ builder: ARCHON, params: {}, body: {} }, res);
284
- assert.equal(res.body.awaiting[0].already_approved, true);
285
- });
286
- });
287
-
288
- await t('GET /gate-approvals: 503 when no GitHub credential is configured', async () => {
289
- await withStubs({ isConfigured: () => false }, async () => {
290
- const handler = routeHandler(buildGateApprovalsRouter(), 'get', '/gate-approvals');
291
- const res = fakeRes();
292
- await handler({ builder: ARCHON, params: {}, body: {} }, res);
293
- assert.equal(res.statusCode, 503);
294
- assert.equal(res.body.error.code, 'publish_unconfigured');
295
- });
296
- });
297
-
298
- await t('GET /gate-approvals: surfaces checks_readable=false + a note when the check rollup is unreadable', async () => {
299
- // listOpenPullRequests returned PRs but flagged a partial error → checks unreadable.
300
- const prs = [pr({ number: 167, checks: [] })];
301
- prs.partialErrors = ['FORBIDDEN'];
302
- await withStubs({
303
- isConfigured: () => true,
304
- listOpenPullRequests: async () => prs,
305
- getPullRequestFiles: async () => [],
306
- }, async () => {
307
- const handler = routeHandler(buildGateApprovalsRouter(), 'get', '/gate-approvals');
308
- const res = fakeRes();
309
- await handler({ builder: ARCHON, params: {}, body: {} }, res);
310
- assert.equal(res.statusCode, 200);
311
- assert.equal(res.body.checks_readable, false);
312
- assert.ok(/Checks:read/.test(res.body.note), 'tells the operator exactly what to grant');
313
- assert.equal(res.body.count, 0, 'no PRs are listed as awaiting when checks are unreadable (honest empty)');
314
- });
315
- });
316
-
317
- // ===========================================================================
318
- // Layer 2 — POST /gate-approvals/:pr/approve handler logic
319
- // ===========================================================================
320
-
321
- await t('POST approve: sets the owner-approval status on the head sha + re-runs the check', async () => {
322
- let approved = null, rechecked = null;
323
- await withStubs({
324
- isConfigured: () => true,
325
- listOpenPullRequests: async () => [pr({ number: 201, head_sha: SHA, checks: [gateCheck('FAILURE')] })],
326
- setGateOwnerApproved: async ({ sha, login }) => { approved = { sha, login }; return true; },
327
- recheckPullRequest: async ({ number }) => { rechecked = number; return { reopened: true, auto_merge: true }; },
328
- }, async () => {
329
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
330
- const res = fakeRes();
331
- await handler({ builder: ARCHON, params: { pr: '201' }, body: {} }, res);
332
- assert.equal(res.statusCode, 200);
333
- assert.equal(res.body.ok, true);
334
- assert.equal(res.body.status_set, true);
335
- assert.deepEqual(approved, { sha: SHA, login: 'example-owner' }, 'approval is sha-pinned to head + records the Archon');
336
- assert.equal(rechecked, 201, 'the gate check is re-run for the PR');
337
- });
338
- });
339
-
340
- await t('POST approve: still works when checks are unreadable (no gate-review check visible) — sets status + reopens', async () => {
341
- // App lacks Checks:read → listOpenPullRequests returns the PR but with empty checks. The
342
- // approve path must not depend on reading the gate-review conclusion: it resolves head_sha,
343
- // sets the owner-approval status, and reopens. (This is the reported-bug path post-fix.)
344
- let approved = null, rechecked = null;
345
- await withStubs({
346
- isConfigured: () => true,
347
- listOpenPullRequests: async () => [pr({ number: 167, head_sha: SHA, checks: [] })],
348
- setGateOwnerApproved: async ({ sha, login }) => { approved = { sha, login }; return true; },
349
- recheckPullRequest: async ({ number }) => { rechecked = number; return { reopened: true, auto_merge: true }; },
350
- }, async () => {
351
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
352
- const res = fakeRes();
353
- await handler({ builder: ARCHON, params: { pr: '167' }, body: {} }, res);
354
- assert.equal(res.statusCode, 200);
355
- assert.equal(res.body.status_set, true);
356
- assert.deepEqual(approved, { sha: SHA, login: 'example-owner' });
357
- assert.equal(rechecked, 167);
358
- });
359
- });
360
-
361
- await t('POST approve: a green gate-review is a no-op (already_clear; no status set)', async () => {
362
- let approvedCalled = false;
363
- await withStubs({
364
- isConfigured: () => true,
365
- listOpenPullRequests: async () => [pr({ number: 201, checks: [gateCheck('SUCCESS')] })],
366
- setGateOwnerApproved: async () => { approvedCalled = true; return true; },
367
- recheckPullRequest: async () => ({ reopened: true }),
368
- }, async () => {
369
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
370
- const res = fakeRes();
371
- await handler({ builder: ARCHON, params: { pr: '201' }, body: {} }, res);
372
- assert.equal(res.body.already_clear, true);
373
- assert.equal(approvedCalled, false, 'must not set an approval status when nothing is blocked');
374
- });
375
- });
376
-
377
- await t('POST approve: 404 when the PR is not open', async () => {
378
- await withStubs({
379
- isConfigured: () => true,
380
- listOpenPullRequests: async () => [],
381
- }, async () => {
382
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
383
- const res = fakeRes();
384
- await handler({ builder: ARCHON, params: { pr: '999' }, body: {} }, res);
385
- assert.equal(res.statusCode, 404);
386
- assert.equal(res.body.error.code, 'pr_not_open');
387
- });
388
- });
389
-
390
- await t('POST approve: 500 (not 502) when the status write fails — Cloudflare masks an origin 502', async () => {
391
- await withStubs({
392
- isConfigured: () => true,
393
- listOpenPullRequests: async () => [pr({ number: 201, checks: [gateCheck('FAILURE')] })],
394
- setGateOwnerApproved: async () => false,
395
- }, async () => {
396
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
397
- const res = fakeRes();
398
- await handler({ builder: ARCHON, params: { pr: '201' }, body: {} }, res);
399
- assert.equal(res.statusCode, 500, 'never a bare 502 — Cloudflare would replace its body');
400
- assert.equal(res.body.error.code, 'status_set_failed');
401
- });
402
- });
403
-
404
- await t('POST approve: a thrown github-push error becomes a structured 500 with detail (never a bare 502)', async () => {
405
- await withStubs({
406
- isConfigured: () => true,
407
- listOpenPullRequests: async () => { throw Object.assign(new Error('GRAPHQL_FAILED'), { code: 'GRAPHQL_FAILED', detail: '[{"type":"FORBIDDEN"}]' }); },
408
- }, async () => {
409
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
410
- const res = fakeRes();
411
- await handler({ builder: ARCHON, params: { pr: '201' }, body: {} }, res);
412
- assert.equal(res.statusCode, 500, 'a failed upstream read is a 500, not a Cloudflare-masked 502');
413
- assert.equal(res.body.error.code, 'gate_approve_failed');
414
- assert.equal(res.body.error.message, 'GRAPHQL_FAILED');
415
- assert.ok(/FORBIDDEN/.test(res.body.error.details.detail), 'the GraphQL detail is surfaced loudly, not swallowed');
416
- });
417
- });
418
-
419
- await t('POST approve: a non-positive :pr is rejected 400 before any GitHub call', async () => {
420
- await withStubs({
421
- isConfigured: () => { throw new Error('should not reach isConfigured'); },
422
- }, async () => {
423
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
424
- const res = fakeRes();
425
- await handler({ builder: ARCHON, params: { pr: 'abc' }, body: {} }, res);
426
- assert.equal(res.statusCode, 400);
427
- assert.equal(res.body.error.code, 'bad_pr');
428
- });
429
- });
430
-
431
- // ===========================================================================
432
- // Layer 3 — structural gating: requireBuilder present + archon under the auditor
433
- // ===========================================================================
434
-
435
- await t('both routes are requireBuilder-gated and registered', async () => {
436
- const router = buildGateApprovalsRouter();
437
- for (const [method, p] of [['get', '/gate-approvals'], ['post', '/gate-approvals/:pr/approve']]) {
438
- const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
439
- assert.ok(layer, `${method.toUpperCase()} ${p} registered`);
440
- const handles = layer.route.stack.map((s) => s.handle);
441
- assert.ok(handles.includes(auth.requireBuilder), `${p} must be requireBuilder-gated`);
442
- }
443
- });
444
-
445
- // ADR 0157 SPLIT these two apart: reading the held-PR list is `gate_approval.read`
446
- // (metic — the Gate page opens to the working rank, and a page whose only data endpoint
447
- // 403s is a page that renders an error), while APPROVING still clears the gate-review
448
- // hard floor and stays `gate_approval.decide` (archon, system:true). Asserting the split
449
- // explicitly is the point — a future edit that re-merged them onto one atom would either
450
- // close the page again or hand out the approval, and both are caught here.
451
- await t('route-rank auditor: gate-approval READ is metic+, APPROVE stays archon, no findings', async () => {
452
- const res = checkRouteRanks();
453
- const mine = res.routes.filter((r) => r.route.includes('gate-approvals'));
454
- assert.equal(mine.length, 2, 'both routes seen by the auditor');
455
- const byKey = new Map(mine.map((r) => [r.key, r]));
456
- const read = byKey.get('GET /gate-approvals');
457
- const approve = byKey.get('POST /gate-approvals/:pr/approve');
458
- assert.ok(read && approve, 'both the list and the approve route must be present');
459
- assert.deepEqual(read.perms, ['gate_approval.read'], 'the list route gates on the read atom');
460
- assert.equal(read.rank, 'metic+archon', 'the list route is metic+ (ADR 0157)');
461
- assert.deepEqual(approve.perms, ['gate_approval.decide'], 'approve gates on the decide atom');
462
- assert.equal(approve.rank, 'archon', 'approving a held PR must stay archon');
463
- const myFindings = res.findings.filter((f) => (f.route || '').includes('gate-approvals'));
464
- assert.deepEqual(myFindings, [], 'no auditor findings on the gate-approval routes');
465
- });
466
-
467
-
468
- // ===========================================================================
469
- // Layer 3 — the page-tweak hold (task 1004323 / BV2.TW12, ADR 0341 D7). The
470
- // approve re-arms auto-merge on the reopened PR; a page tweak held for its
471
- // artist must be reopened (the check re-runs) but NEVER armed from here.
472
- // ===========================================================================
473
-
474
- const pageTweakReads = require('../modules/lifecycle/page-tweak-reads.js');
475
- async function withHold(fnHeld, body) {
476
- const saved = pageTweakReads.heldForArtistFailClosed;
477
- pageTweakReads.heldForArtistFailClosed = fnHeld;
478
- try { return await body(); } finally { pageTweakReads.heldForArtistFailClosed = saved; }
479
- }
480
- async function approveWith(prNode) {
481
- let rechecked = null;
482
- const res = fakeRes();
483
- await withStubs({
484
- isConfigured: () => true,
485
- listOpenPullRequests: async () => [prNode],
486
- setGateOwnerApproved: async () => true,
487
- recheckPullRequest: async (args) => { rechecked = args; return { reopened: true, auto_merge: args.arm !== false }; },
488
- }, async () => {
489
- const handler = routeHandler(buildGateApprovalsRouter(), 'post', '/gate-approvals/:pr/approve');
490
- await handler({ builder: ARCHON, params: { pr: String(prNode.number) }, body: {} }, res);
491
- });
492
- return { res, rechecked };
493
- }
494
-
495
- await t('POST approve: a page tweak HELD for its artist is reopened but not re-armed', async () => {
496
- const asked = [];
497
- await withHold(async (id) => { asked.push(id); return true; }, async () => {
498
- const { res, rechecked } = await approveWith(pr({ number: 301, title: 'task 5001: Tweak: Home (landing:index)', head_sha: SHA, checks: [gateCheck('FAILURE')] }));
499
- assert.equal(res.statusCode, 200);
500
- assert.deepEqual(asked, [5001], 'the hold is asked of the PR\'s own task');
501
- assert.equal(rechecked.number, 301);
502
- assert.equal(rechecked.arm, false, 'the recheck must not arm auto-merge');
503
- assert.match(rechecked.heldReason, /held for the artist/);
504
- assert.equal(res.body.held_for_artist, true);
505
- assert.match(res.body.message, /waiting for its artist/);
506
- });
507
- });
508
-
509
- await t('POST approve: an ordinary task is re-armed exactly as before (no arm flag sent)', async () => {
510
- await withHold(async () => false, async () => {
511
- const { rechecked, res } = await approveWith(pr({ number: 302, title: 'task 1163: x', head_sha: SHA, checks: [gateCheck('FAILURE')] }));
512
- assert.deepEqual(rechecked, { number: 302 }, 'the pre-change call, byte for byte');
513
- assert.equal(res.body.held_for_artist, false);
514
- });
515
- });
516
-
517
- await t('POST approve: a PR naming no task is never asked about a hold', async () => {
518
- let asked = 0;
519
- await withHold(async () => { asked++; return true; }, async () => {
520
- const { rechecked } = await approveWith(pr({ number: 303, title: 'chore: no task here', head_sha: SHA, checks: [gateCheck('FAILURE')] }));
521
- assert.equal(asked, 0);
522
- assert.deepEqual(rechecked, { number: 303 });
523
- });
524
- });
525
-
526
- await t('POST approve: the REAL fail-closed read answers held when it cannot read the ledger', async () => {
527
- // No stub: page-tweak-reads.heldForArtistFailClosed runs, its read throws
528
- // (no pool in this process), and the route must then withhold the arm.
529
- const { rechecked } = await approveWith(pr({ number: 304, title: 'task 5002: y', head_sha: SHA, checks: [gateCheck('FAILURE')] }));
530
- assert.equal(rechecked.arm, false, 'an unreadable hold is treated as held');
531
- });
532
-
533
- await t('recheckPullRequest({ arm: false }): closes and reopens, and never calls the auto-merge mutation', async () => {
534
- const calls = [];
535
- const fetchImpl = async (url, opts) => {
536
- const method = (opts && opts.method) || 'GET';
537
- if (method === 'PATCH') { calls.push(JSON.parse(opts.body).state); return jsonRes(200, { node_id: 'N1' }); }
538
- calls.push(`unexpected ${url}`);
539
- return jsonRes(200, {});
540
- };
541
- const r = await gp.recheckPullRequest({ number: 205, arm: false, heldReason: 'held for the artist' }, { token: 'T', repoInfo: REPO, fetchImpl, reopenDelayMs: 0 });
542
- assert.deepEqual(calls, ['closed', 'open']);
543
- assert.equal(r.reopened, true);
544
- assert.equal(r.auto_merge, false);
545
- assert.equal(r.auto_merge_reason, 'held for the artist');
546
- });
547
-
548
- await t('closePullRequest (github-pr-close.js): one PATCH to state=closed, and a refusal is thrown by name', async () => {
549
- const prClose = require('../modules/lifecycle/github-pr-close.js');
550
- const calls = [];
551
- const fetchImpl = async (url, opts) => { calls.push([url, opts.method, JSON.parse(opts.body)]); return jsonRes(200, {}); };
552
- const r = await prClose.closePullRequest({ number: 77 }, { token: 'T', repoInfo: REPO, fetchImpl });
553
- assert.deepEqual(r, { closed: true, number: 77 });
554
- assert.deepEqual(calls, [['https://api.github.com/repos/someowner/somerepo/pulls/77', 'PATCH', { state: 'closed' }]]);
555
- await assert.rejects(
556
- prClose.closePullRequest({ number: 77 }, { token: 'T', repoInfo: REPO, fetchImpl: async () => jsonRes(422, { message: 'no' }) }),
557
- (e) => e.code === 'PR_PATCH_FAILED'
558
- );
559
- });
560
- // ===========================================================================
561
- // task 1004829 — viewer_may_approve: the hall draws the Approve card only on true,
562
- // so the list says whether THIS viewer's approval would be let through — the same
563
- // two checks the approve route mounts (gate_approval.decide, then act-alone at
564
- // release), fail-closed.
565
- // ===========================================================================
566
- {
567
- const seams = require('../src/module-seams.js');
568
- const gov = { held: true, actAlone: { ok: true }, throws: false };
569
- if (!seams.hasProvider('government')) {
570
- seams.registerProvider('government', {
571
- builderHasPermissions: async (id, keys) => {
572
- if (gov.throws) throw new Error('resolver down');
573
- return gov.held && keys.every((k) => k === 'gate_approval.decide');
574
- },
575
- checkActAlone: async (area) => {
576
- assert.equal(area, 'release', 'the approve route\'s building');
577
- return gov.actAlone;
578
- },
579
- });
580
- }
581
- const listFor = async () => withStubs({
582
- isConfigured: () => true,
583
- listOpenPullRequests: async () => [pr({ number: 301 })],
584
- getPullRequestFiles: async () => [{ path: 'src/bongos/route-rank-check.js', status: 'modified' }],
585
- }, async () => {
586
- const res = fakeRes();
587
- await routeHandler(buildGateApprovalsRouter(), 'get', '/gate-approvals')({ builder: ARCHON, params: {}, body: {} }, res);
588
- return res.body;
589
- });
590
- await t('GET /gate-approvals: viewer_may_approve is true for a holder of gate_approval.decide who may act alone', async () => {
591
- Object.assign(gov, { held: true, actAlone: { ok: true }, throws: false });
592
- assert.equal((await listFor()).viewer_may_approve, true);
593
- });
594
- await t('GET /gate-approvals: viewer_may_approve is false without gate_approval.decide', async () => {
595
- Object.assign(gov, { held: false, actAlone: { ok: true }, throws: false });
596
- assert.equal((await listFor()).viewer_may_approve, false);
597
- });
598
- await t('GET /gate-approvals: viewer_may_approve is false when the board decides at release', async () => {
599
- Object.assign(gov, { held: true, actAlone: { ok: false, reason: 'decided_by_sitting' }, throws: false });
600
- assert.equal((await listFor()).viewer_may_approve, false);
601
- });
602
- await t('GET /gate-approvals: viewer_may_approve is false (and the list still answers) when authority cannot be read', async () => {
603
- Object.assign(gov, { held: true, actAlone: { ok: true }, throws: true });
604
- const body = await listFor();
605
- assert.equal(body.viewer_may_approve, false);
606
- assert.equal(body.awaiting.length, 1, 'the read itself is unharmed');
607
- });
608
- }
609
-
610
- console.log(`\n${pass} passed, ${fail} failed`);
611
- process.exit(fail === 0 ? 0 : 1);