@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
@@ -0,0 +1,201 @@
1
+ // tests/autonomy_daily_checks.mjs — the server's daily checks run once a day, file
2
+ // each distinct finding once, and never record "could not check" as clean
3
+ // (task 1004878).
4
+ //
5
+ // DB-free and network-free: the run ledger, the roster, npm and the idea inbox are
6
+ // all injected. The one thing these tests exist to stop is the failure every one of
7
+ // these routines already had — declared daily, run never — so the scheduling half
8
+ // (isDue against the ledger, not the boot) is pinned as hard as the filing half.
9
+ //
10
+ // Run: node --test tests/autonomy_daily_checks.mjs
11
+
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const dc = require('../modules/autonomy/pollers/daily-checks.js');
18
+ const { SERVER_ROUTINES, DAY_MS, WEEK_MS } = require('../modules/autonomy/server-schedule.js');
19
+ const rt = require('../modules/autonomy/pollers/routine-timers.js');
20
+
21
+ const NOW = Date.parse('2026-10-26T12:00:00Z');
22
+ const hoursAgo = (h) => new Date(NOW - h * 3600_000).toISOString();
23
+ const LADDER = ['xenos', 'thetes', 'metic', 'archon'];
24
+
25
+ // A fake ledger: rows newest-first, plus a list of what got recorded.
26
+ function ledger(rows = []) {
27
+ const recorded = [];
28
+ return {
29
+ recorded,
30
+ db: {
31
+ readAutonomyRuns: async () => rows,
32
+ recordAutonomyRun: async (r) => { recorded.push(r); return {}; },
33
+ readRankRoster: async () => [
34
+ { id: 1, github_login: 'ok', rank: 'archon', status: 'active' },
35
+ { id: 2, github_login: 'old', rank: 'ephor', status: 'inactive' },
36
+ ],
37
+ },
38
+ };
39
+ }
40
+ const quiet = { log: () => {}, error: () => {} };
41
+ const capturing = () => {
42
+ const filed = [];
43
+ return { filed, captureIdea: async (i) => { filed.push(i); return { id: 42 }; } };
44
+ };
45
+
46
+ // ------------------------------------------------------------- the schedule table
47
+
48
+ test('the daily poller runs exactly the server-schedule rows that name it', () => {
49
+ assert.deepEqual(dc.ROUTINES, SERVER_ROUTINES.filter((r) => r.poller === 'daily-checks').map((r) => r.routine));
50
+ assert.deepEqual(dc.ROUTINES.sort(), ['dep-audit-nightly', 'rank-roles-audit']);
51
+ });
52
+
53
+ test('peer-vote-tally takes its period from the same table (weekly)', () => {
54
+ assert.equal(rt.PEER_VOTE_INTERVAL_MS, WEEK_MS);
55
+ });
56
+
57
+ // ------------------------------------------------------------------ isDue
58
+
59
+ test('due when it has never run', () => {
60
+ assert.equal(dc.isDue([], 'rank-roles-audit', NOW), true);
61
+ });
62
+
63
+ test('not due 20 hours after the last run, due 23.5 hours after — daily, not per boot', () => {
64
+ assert.equal(dc.isDue([{ created_at: hoursAgo(20) }], 'rank-roles-audit', NOW), false);
65
+ assert.equal(dc.isDue([{ created_at: hoursAgo(23.5) }], 'rank-roles-audit', NOW), true);
66
+ assert.equal(dc.DUE_SLACK_MS < DAY_MS / 24, true, 'slack must stay under one hourly tick or runs drift');
67
+ });
68
+
69
+ test('an ERROR row counts as a run too, so a broken check is retried daily, not hourly', async () => {
70
+ const L = ledger([{ decision: 'no-go', reason: 'error: x', created_at: hoursAgo(2) }]);
71
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, ...quiet });
72
+ assert.equal(out.skipped, 'not_due');
73
+ assert.equal(L.recorded.length, 0);
74
+ });
75
+
76
+ // --------------------------------------------------------------- filing
77
+
78
+ test('a finding is filed as one idea and the run records its fingerprint', async () => {
79
+ const L = ledger();
80
+ const C = capturing();
81
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
82
+ assert.equal(out.outcome, 'filed');
83
+ assert.equal(C.filed.length, 1);
84
+ assert.match(C.filed[0].title, /^\[rank-roles\] 1 builder/);
85
+ assert.match(C.filed[0].bodyMd, /builder 2 \(old\) rank=ephor status=inactive/);
86
+ assert.equal(C.filed[0].kind, 'bug');
87
+ assert.equal(L.recorded[0].decision, 'go');
88
+ assert.match(L.recorded[0].reason, /^findings n=1 fp=[0-9a-f]{12} idea=42$/);
89
+ });
90
+
91
+ test('the SAME finding the next day is recorded unchanged and NOT filed again', async () => {
92
+ const fp = dc.fingerprint(['2:ephor']);
93
+ const L = ledger([{ decision: 'go', reason: `findings n=1 fp=${fp} idea=42`, created_at: hoursAgo(24) }]);
94
+ const C = capturing();
95
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
96
+ assert.equal(out.outcome, 'unchanged');
97
+ assert.equal(C.filed.length, 0);
98
+ assert.match(L.recorded[0].reason, /unchanged$/);
99
+ });
100
+
101
+ test('an error between two identical findings does not cause a duplicate filing', async () => {
102
+ const fp = dc.fingerprint(['2:ephor']);
103
+ const L = ledger([
104
+ { decision: 'no-go', reason: 'error: db blip', created_at: hoursAgo(24) },
105
+ { decision: 'go', reason: `findings n=1 fp=${fp} idea=42`, created_at: hoursAgo(48) },
106
+ ]);
107
+ const C = capturing();
108
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
109
+ assert.equal(out.outcome, 'unchanged');
110
+ assert.equal(C.filed.length, 0);
111
+ });
112
+
113
+ test('a finding that comes back after a clean day is filed again', async () => {
114
+ const L = ledger([{ decision: 'go', reason: 'clean', created_at: hoursAgo(24) }]);
115
+ const C = capturing();
116
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
117
+ assert.equal(out.outcome, 'filed');
118
+ });
119
+
120
+ test('the fingerprint ignores order and changes when the set changes', () => {
121
+ assert.equal(dc.fingerprint(['a', 'b']), dc.fingerprint(['b', 'a']));
122
+ assert.notEqual(dc.fingerprint(['a']), dc.fingerprint(['a', 'b']));
123
+ });
124
+
125
+ test('a clean run files nothing and records clean', async () => {
126
+ const L = ledger();
127
+ L.db.readRankRoster = async () => [{ id: 1, rank: 'archon' }];
128
+ const C = capturing();
129
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
130
+ assert.equal(out.outcome, 'clean');
131
+ assert.equal(C.filed.length, 0);
132
+ assert.deepEqual(L.recorded[0], { routine: 'rank-roles-audit', decision: 'go', reason: 'clean' });
133
+ });
134
+
135
+ test('no idea inbox (ideas module off) is recorded as undelivered, and retried next time', async () => {
136
+ const L = ledger();
137
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: 'not a function', ...quiet });
138
+ assert.equal(out.outcome, 'undelivered');
139
+ assert.equal(L.recorded[0].decision, 'no-go', 'a no-go row is never read as delivered');
140
+ assert.equal(dc.lastFiledFingerprint(L.recorded.map((r) => ({ ...r }))), null);
141
+ });
142
+
143
+ test('a capture that throws is undelivered, never a crash', async () => {
144
+ const L = ledger();
145
+ const out = await dc.runCheck('rank-roles-audit', {
146
+ db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: async () => { throw { code: 'BAD_KIND' }; }, ...quiet,
147
+ });
148
+ assert.equal(out.outcome, 'undelivered');
149
+ assert.match(L.recorded[0].reason, /undelivered: BAD_KIND/);
150
+ });
151
+
152
+ // ------------------------------------------------------- could not check ≠ clean
153
+
154
+ test('npm missing is an ERROR row, not clean', async () => {
155
+ const L = ledger();
156
+ const out = await dc.runCheck('dep-audit-nightly', {
157
+ db: L.db, now: () => NOW, cwd: '/i', npmAudit: async () => ({ error: 'npm not found' }), ...quiet,
158
+ });
159
+ assert.equal(out.outcome, 'error');
160
+ assert.deepEqual(L.recorded[0], { routine: 'dep-audit-nightly', decision: 'no-go', reason: 'error: npm not found' });
161
+ });
162
+
163
+ test("npm's own JSON error (no lockfile) is an ERROR row, not clean", async () => {
164
+ const L = ledger();
165
+ const out = await dc.runCheck('dep-audit-nightly', {
166
+ db: L.db, now: () => NOW, cwd: '/i',
167
+ npmAudit: async () => ({ stdout: JSON.stringify({ error: { code: 'ENOLOCK', summary: 'This command requires an existing lockfile.' } }), stderr: '' }),
168
+ ...quiet,
169
+ });
170
+ assert.equal(out.outcome, 'error');
171
+ assert.match(L.recorded[0].reason, /lockfile/);
172
+ });
173
+
174
+ test('dep-audit audits the cwd it is handed and files high/critical findings only', async () => {
175
+ const L = ledger();
176
+ const C = capturing();
177
+ let auditedIn = null;
178
+ const out = await dc.runCheck('dep-audit-nightly', {
179
+ db: L.db, now: () => NOW, cwd: '/srv/instance', captureIdea: C.captureIdea, ...quiet,
180
+ npmAudit: async (cwd) => {
181
+ auditedIn = cwd;
182
+ return { stdout: JSON.stringify({ vulnerabilities: {
183
+ bad: { severity: 'critical', via: [{ severity: 'critical', title: 'RCE', url: 'https://x/1', source: 1 }], fixAvailable: true },
184
+ meh: { severity: 'moderate', via: [], fixAvailable: true },
185
+ } }), stderr: '' };
186
+ },
187
+ });
188
+ assert.equal(auditedIn, '/srv/instance');
189
+ assert.equal(out.outcome, 'filed');
190
+ assert.match(C.filed[0].title, /^\[dep-audit\] 1 high\/critical vulnerability in/);
191
+ assert.match(C.filed[0].bodyMd, /\*\*bad\*\* \(critical\) — RCE/);
192
+ assert.doesNotMatch(C.filed[0].bodyMd, /meh/);
193
+ });
194
+
195
+ // --------------------------------------------------------------- the tick
196
+
197
+ test('an unreachable database is a quiet skip for every routine (a builder laptop)', async () => {
198
+ const db = { readAutonomyRuns: async () => { throw new Error('connect ENOENT /var/run/postgresql'); } };
199
+ const out = await dc.tick({ db, ...quiet });
200
+ assert.deepEqual(out.map((r) => r.skipped), dc.ROUTINES.map(() => 'db_unreachable'));
201
+ });
@@ -22,7 +22,7 @@ const require = createRequire(import.meta.url);
22
22
  const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
23
23
 
24
24
  const bump = require(path.join(ROOT, 'scripts', 'bongos', 'bump-version.js'));
25
- const { bumpPatch, CONST_RE } = bump;
25
+ const { bumpPatch, nextFreePatch, registryVersions, CONST_RE } = bump;
26
26
  // The LIVE test below reads this repo's real release history (task 1003202).
27
27
  const { execFileSync } = require('node:child_process');
28
28
 
@@ -217,25 +217,59 @@ test('the push to main happens exactly once, and not inside the bump step', () =
217
217
  assert.ok(bumpStep.includes('git commit -m'), 'the bump step still commits locally — package-core stamps the version found AT the ref');
218
218
  });
219
219
 
220
- test('an already-published version is success, not a wedge', () => {
221
- // Publish-then-failed-push leaves a version on npm with no release commit.
222
- // The next run reads it as free (laneReleasedVersions is git-based), bumps to
223
- // it, and npm refuses the duplicate. Without this branch that run fails, and
224
- // so does every run after it — the lane wedges with no way out but a human.
220
+ test('an already-published version FAILS the publish and records nothing (task 1004886)', () => {
221
+ // Task 1003914 made a duplicate-version refusal count as success and fell through to
222
+ // the record step. But a version is left unrecorded only when its record push lost a
223
+ // race to a MOVED main, so the registry's copy is never this run's tree: on
224
+ // 2026-10-25 that recorded 1.21.175 on main for a tree npm's 1.21.175 (2026-10-23)
225
+ // never held. The refusal must now fail the step, which skips the record step.
225
226
  const pubAt = PUBLISH_YML.indexOf('id: pub');
226
227
  assert.ok(pubAt !== -1, 'the publish step must carry id: pub');
227
- const pubStep = PUBLISH_YML.slice(pubAt, PUBLISH_YML.indexOf('\n # PUBLISH VERDICT'));
228
- assert.match(
229
- pubStep,
230
- /cannot publish over\|previously published\|EPUBLISHCONFLICT/,
231
- 'the publish step must recognise npm refusing a duplicate version',
232
- );
233
- assert.match(pubStep, /already=true/, 'and must record that it took the already-present branch');
234
- assert.match(
235
- pubStep,
236
- /exit "\$RC"/,
237
- 'every OTHER publish failure must still fail the step — the tolerance is narrow on purpose',
238
- );
228
+ const pubStep = PUBLISH_YML.slice(pubAt, PUBLISH_YML.indexOf('\n # THE ONLY IRREVERSIBLE STEP'));
229
+ assert.match(pubStep, /cannot publish over\|previously published\|EPUBLISHCONFLICT/,
230
+ 'the publish step still recognises npm refusing a duplicate — to say so plainly');
231
+ assert.doesNotMatch(pubStep, /already=true/, 'no already-present success branch');
232
+ // After a non-zero npm publish, the only way out is `exit "$RC"`: the duplicate
233
+ // branch must not be able to reach the "published" line.
234
+ const failAt = pubStep.indexOf('if [ "$RC" -ne 0 ]; then');
235
+ const failBlock = pubStep.slice(failAt, pubStep.indexOf('\n fi\n echo "published', failAt));
236
+ assert.ok(failAt !== -1 && failBlock.trimEnd().endsWith('exit "$RC"'), 'every publish failure, the duplicate included, exits non-zero');
237
+ const recAt = PUBLISH_YML.indexOf('- name: Record the release on main');
238
+ const recIf = PUBLISH_YML.slice(recAt, PUBLISH_YML.indexOf('\n run:', recAt));
239
+ assert.doesNotMatch(recIf, /always\(\)|failure\(\)/, 'the record step must not run after a failed publish');
240
+ const verdict = PUBLISH_YML.slice(PUBLISH_YML.indexOf('# PUBLISH VERDICT'));
241
+ assert.doesNotMatch(verdict.slice(0, 1500), /ALREADY PRESENT/, 'the verdict no longer reports a recorded duplicate');
242
+ });
243
+
244
+ test('the registry listing is read and every listed version is skipped by the bump (task 1004886)', () => {
245
+ const verAt = PUBLISH_YML.indexOf('id: ver');
246
+ const verStep = PUBLISH_YML.slice(verAt, PUBLISH_YML.indexOf('\n - name:', verAt));
247
+ assert.match(verStep, /npm view @bongos\/core versions --json > "\$TAKEN_FILE"/, 'the resolve step reads the registry listing');
248
+ assert.match(verStep, /registryVersions\(.*\)\.has\(process\.argv\[2\]\) \? 0 : 1\)" "\$TAKEN_FILE" "\$HEAD_V"; then\s+TAKEN=true/,
249
+ 'a HEAD version already on the registry counts as taken, so it is bumped past');
250
+ assert.match(verStep, /echo '\[\]' > "\$TAKEN_FILE"/, 'an unreadable listing degrades to git-only, it does not stop the lane');
251
+ assert.match(verStep, /taken_file=/, 'the listing is handed on');
252
+ const bumpAt = PUBLISH_YML.indexOf('id: bump');
253
+ const bumpStep = PUBLISH_YML.slice(bumpAt, PUBLISH_YML.indexOf('\n - name:', bumpAt));
254
+ assert.match(bumpStep, /bump-version\.js --apply .*--taken-file "\$\{\{ steps\.ver\.outputs\.taken_file \}\}"/, 'the bump skips the listed versions');
255
+ });
256
+
257
+ test('bumpPatch skips every taken version (task 1004886)', () => {
258
+ const src = "const CORE_VERSION = '1.21.174'; // x\n";
259
+ assert.equal(bumpPatch(src).to, '1.21.175', 'nothing taken → the next patch, as before');
260
+ assert.equal(bumpPatch(src, { taken: new Set(['1.21.175']) }).to, '1.21.176', 'the 2026-10-25 shape: 1.21.175 is on npm with an older tree');
261
+ assert.equal(bumpPatch(src, { taken: new Set(['1.21.175', '1.21.176', '1.21.178']) }).to, '1.21.177', 'the first free patch, not the highest');
262
+ assert.equal(bumpPatch(src, { taken: new Set(['1.22.0', '1.21.174']) }).to, '1.21.175', 'other minors and the current version do not matter');
263
+ assert.match(bumpPatch(src, { taken: new Set(['1.21.175']) }).text, /^const CORE_VERSION = '1\.21\.176';/m, 'the literal carries the skipped-to version');
264
+ assert.throws(() => nextFreePatch(1, 0, 0, { has: () => true }), /no free patch/, 'a taken-list that covers everything throws, never spins');
265
+ });
266
+
267
+ test('registryVersions parses npm view output and drops anything that is not x.y.z', () => {
268
+ assert.deepEqual([...registryVersions('["1.21.174","1.21.175"]')], ['1.21.174', '1.21.175']);
269
+ assert.deepEqual([...registryVersions('"1.0.0"')], ['1.0.0'], 'npm prints a bare string when one version exists');
270
+ assert.deepEqual([...registryVersions('["1.2.3","1.2.4-rc.1","x; rm -rf",7]')], ['1.2.3']);
271
+ assert.equal(registryVersions('not json').size, 0);
272
+ assert.equal(registryVersions('').size, 0);
239
273
  });
240
274
 
241
275
  test('the record step gates on a bump actually having been made', () => {
@@ -177,7 +177,7 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
177
177
  assert.ok(existsSync(rt), '.claude/scheduled-tasks created in the instance');
178
178
  // Every materialized routine's core invocation is bongosified — asserted over
179
179
  // WHATEVER routines the tree carries (the neutral core carries redteam-weekly;
180
- // the founder core also carries stale-claim-release/diagram-drift). No routine
180
+ // the founder core also carries rank-roles-audit/peer-vote-tally). No routine
181
181
  // body may keep a bare `node scripts/bongos/…` core invocation.
182
182
  let sawBongosified = false;
183
183
  for (const name of readdirSync(rt)) {
@@ -191,17 +191,17 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
191
191
  // Founder-specific fixtures — assert their finer behavior only when present, so
192
192
  // this coverage stays live on the founder core without demanding those routines
193
193
  // exist in the neutral core (which carries a different routine set).
194
- const stalePath = join(rt, 'stale-claim-release', 'SKILL.md');
195
- if (existsSync(stalePath)) {
196
- assert.ok(readFileSync(stalePath, 'utf8').includes('bongos exec scripts/bongos/pulse.js'), 'body core-invocation rewritten to bongos exec');
197
- }
198
- const ddCheck = join(rt, 'diagram-drift', 'check.js');
199
- if (existsSync(ddCheck)) {
200
- const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
201
- assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
202
- const check = readFileSync(ddCheck, 'utf8');
203
- assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
204
- assert.ok(!check.includes("require('../../../scripts/bongos/"), 'no bare ../../../scripts/bongos require left in the companion');
194
+ // The companion-script case (task 1004878 moved this off the retired
195
+ // diagram-drift routine): a routine whose step runs a .js beside its SKILL.md.
196
+ const pvTally = join(rt, 'peer-vote-tally', 'tally.js');
197
+ if (existsSync(pvTally)) {
198
+ const pv = readFileSync(join(rt, 'peer-vote-tally', 'SKILL.md'), 'utf8');
199
+ assert.ok(pv.includes('node .claude/scheduled-tasks/peer-vote-tally/tally.js'), 'companion-path step command preserved');
200
+ const tally = readFileSync(pvTally, 'utf8');
201
+ assert.ok(tally.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
202
+ // Code lines only: tally.js's own header QUOTES the bare form to explain why it is gone.
203
+ const code = tally.split('\n').filter((l) => !/^\s*\/\//.test(l)).join('\n');
204
+ assert.ok(!code.includes("require('../../../scripts/bongos/"), 'no bare ../../../scripts/bongos require left in the companion');
205
205
  }
206
206
  rmSync(dir, { recursive: true, force: true });
207
207
  });
@@ -26,15 +26,17 @@
26
26
  //
27
27
  // Run: DATABASE_URL=postgres://... node tests/copy_desk_flags_db.mjs
28
28
  //
29
- // It creates its OWN minimal `builders`/`tasks` stand-ins if they are absent, so
30
- // it works against a throwaway container as well as a full migrated database, and
31
- // it cleans up every row it writes.
29
+ // It needs a MIGRATED database (scripts/migrate.sh), the one the integration lane
30
+ // builds. It once created its own minimal `builders`/`tasks` stand-ins; those drifted
31
+ // from the real schema (builders.github_id became NOT NULL) and hid it, so they are
32
+ // gone (task 1004863). It cleans up every row it writes.
32
33
 
33
34
  import { createRequire } from 'node:module';
34
35
  import { strict as assert } from 'node:assert';
35
36
  import fs from 'node:fs';
36
37
  import path from 'node:path';
37
38
  import url from 'node:url';
39
+ import { insertTestBuilders } from './helpers.mjs';
38
40
 
39
41
  const require = createRequire(import.meta.url);
40
42
  const { Pool } = require('pg');
@@ -58,21 +60,19 @@ async function test(name, fn) {
58
60
  }
59
61
 
60
62
  // --- fixtures -------------------------------------------------------------
61
- // Minimal stand-ins ONLY if the real tables are absent (throwaway container).
62
- // Against a fully migrated DB these are no-ops and the real tables are used.
63
- await pool.query(`
64
- CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
65
- CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text, display_name text);
66
- CREATE TABLE IF NOT EXISTS tasks (id bigserial PRIMARY KEY);
67
- `);
68
-
69
63
  // Two throwaway builders + one task, at ids far above any real row.
70
64
  const A = 900000001;
71
65
  const B = 900000002;
72
66
  const T = 900000003;
73
- await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES ($1,$2,$3),($4,$5,$6) ON CONFLICT (id) DO NOTHING',
74
- [A, 'cdtest-a', 'Copy Desk Test A', B, 'cdtest-b', 'Copy Desk Test B']);
75
- await pool.query('INSERT INTO tasks (id) VALUES ($1) ON CONFLICT (id) DO NOTHING', [T]);
67
+ await insertTestBuilders(pool, [
68
+ { id: A, login: 'cdtest-a', display_name: 'Copy Desk Test A' },
69
+ { id: B, login: 'cdtest-b', display_name: 'Copy Desk Test B' },
70
+ ]);
71
+ // The task needs a version (tasks.version_id is NOT NULL). Its own row, at status
72
+ // 'building': the seeds own the one 'planning' version the partial index allows.
73
+ const VER = 'ZZZ-CDF';
74
+ await pool.query("INSERT INTO versions (id, name, status) VALUES ($1, 'copy desk fixture', 'building') ON CONFLICT (id) DO NOTHING", [VER]);
75
+ await pool.query("INSERT INTO tasks (id, version_id, title, discipline) VALUES ($1, $2, 'copy desk fixture', 'engineer') ON CONFLICT (id) DO NOTHING", [T, VER]);
76
76
 
77
77
  // Apply the migration verbatim — this also proves it is idempotent, since a
78
78
  // migrated DB has already run it.
@@ -248,6 +248,12 @@ await test('closeFlag returns null on a second call — the route answers 409 fr
248
248
  // --- page asks (task 1004317 / TW06, copy_desk_002) -----------------------
249
249
  // A page ask's surface IS its page id, so these use SURF as the page id and the
250
250
  // cleanup above still catches every row.
251
+ //
252
+ // The section starts from no rows (task 1004863): the string-flag cases above leave
253
+ // open flags on SURF (some with a null string_id), and the one-open-per-target index
254
+ // keys on (flagged_by, surface, string_id) — so a page ask by the same builder on the
255
+ // same surface collided with them rather than testing anything about page asks.
256
+ await clean();
251
257
  console.log('copy_desk_flags — page asks (copy_desk_002)');
252
258
 
253
259
  const pageIns = (o = {}) => pool.query(
@@ -299,6 +305,8 @@ await test('pageAskSummary groups open asks by page: distinct askers, the oldest
299
305
 
300
306
  // --- cleanup --------------------------------------------------------------
301
307
  await clean();
308
+ await pool.query('DELETE FROM tasks WHERE id = $1', [T]).catch(() => {});
309
+ await pool.query('DELETE FROM versions WHERE id = $1', [VER]).catch(() => {});
302
310
  await pool.query('DELETE FROM builders WHERE id = ANY($1)', [[A, B]]).catch(() => {});
303
311
 
304
312
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -145,24 +145,6 @@ const SPELLING_EXEMPTIONS = [
145
145
  obligation: (code) => !/\.query\s*\(/.test(code) && !/require\(\s*'pg'\s*\)/.test(code),
146
146
  demands: 'issues no database query and loads no database driver — it answers from fixtures only',
147
147
  },
148
- {
149
- file: 'scripts/bongos/seed-bv1-privacy-invites-tasks.js',
150
- allow: [
151
- { text: "profile_state='", count: 1, why: 'a one-shot historical seeder: this sits inside a task DESCRIPTION string, quoting the predicate as prose describing the work to be done. Its subject IS this predicate.' },
152
- { text: 'NOT hide_stats', count: 1, why: 'the second half of that same quoted sentence in that same task description — prose, not a query.' },
153
- ],
154
- obligation: (code) => !/\.query\s*\(/.test(code),
155
- demands: 'issues no database query at all — it posts task rows over HTTP',
156
- },
157
- {
158
- file: 'scripts/bongos/seed-bv1-r01-delta-tasks.js',
159
- allow: [
160
- { text: "profile_state='", count: 1, why: 'the ADR 0171 R01 seeder: the predicate is quoted as prose inside the task briefs this script files, the same shape as the privacy seeder above.' },
161
- { text: 'NOT hide_stats', count: 1, why: 'the second half of that same quoted sentence in that same brief — prose describing work, never executed.' },
162
- ],
163
- obligation: (code) => !/\.query\s*\(/.test(code),
164
- demands: 'issues no database query at all — it posts task rows over HTTP',
165
- },
166
148
  ];
167
149
 
168
150
  // Every SELECT against platform_identity_accounts that is deliberately NOT a
@@ -43,7 +43,7 @@ test('cmdOn normalizes BOTH restore paths — a pre-retirement snapshot cannot r
43
43
 
44
44
  test('the CODEOWNERS file is gone; its GLOB guards remain where they enforce', () => {
45
45
  assert.ok(!fs.existsSync(path.join(ROOT, 'CODEOWNERS')), 'CODEOWNERS must not exist (ADR 0162)');
46
- const gateReview = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'gate-review.js'), 'utf8');
46
+ const gateReview = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'), 'utf8');
47
47
  assert.match(gateReview, /'CODEOWNERS'/, 'the HARD_FLOOR glob stays — reintroducing the file is itself gate-flagged');
48
48
  });
49
49
 
@@ -68,7 +68,7 @@ async function withPort(port, fn) {
68
68
  // The port the routers below are driven through (section 3); restored after
69
69
  // each gate test swaps in its own.
70
70
  const ATOMS = [
71
- 'autonomy.fence.manage', 'criterion.satisfy', 'criterion.unsatisfy', 'gate_approval.decide', 'override_request.decide',
71
+ 'autonomy.fence.manage', 'criterion.satisfy', 'criterion.unsatisfy', 'override_request.decide',
72
72
  'core.pin.move', 'builder.budget.set', 'goal.create', 'goal.reorder', 'version.focus.set', 'project.settings.manage',
73
73
  'security.report.adjudicate', 'access_request.review', 'government.manage',
74
74
  ];
@@ -211,7 +211,6 @@ app.use(attachFail);
211
211
  for (const rel of [
212
212
  'modules/autonomy/routes/autonomy.js',
213
213
  'modules/lifecycle/routes/done-when.js',
214
- 'modules/lifecycle/routes/gate-approvals.js',
215
214
  'modules/lifecycle/routes/tasks.js',
216
215
  'modules/lifecycle/routes/goals.js',
217
216
  'modules/lifecycle/routes/versions.js',
@@ -264,7 +263,6 @@ const REQUESTS = {
264
263
  'POST /autonomy/builders/:builderId/pause': ['POST', `/autonomy/builders/${BAD}/pause`, { paused: false }],
265
264
  'POST /done-when/:criterionId/satisfy': ['POST', `/done-when/${BAD}/satisfy`, {}],
266
265
  'POST /done-when/:criterionId/unsatisfy': ['POST', `/done-when/${BAD}/unsatisfy`, {}],
267
- 'POST /gate-approvals/:pr/approve': ['POST', `/gate-approvals/${BAD}/approve`, {}],
268
266
  'POST /override-requests/:id/decide': ['POST', `/override-requests/${BAD}/decide`, { decision: 'approve' }],
269
267
  'POST /provisioning/instances/:id/core-upgrade': ['POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3' }],
270
268
  'POST /provisioning/instances/:id/update-channel': ['POST', `/provisioning/instances/${BAD}/update-channel`, { channel: 'nightly' }],
@@ -26,6 +26,7 @@
26
26
  // ids far above any real row, and deletes them at the end.
27
27
 
28
28
  import pg from 'pg';
29
+ import { insertTestBuilders } from './helpers.mjs';
29
30
  import { createRequire } from 'node:module';
30
31
  const require = createRequire(import.meta.url);
31
32
 
@@ -45,38 +46,48 @@ async function t(name, fn) {
45
46
  }
46
47
  const assert = (await import('node:assert')).strict;
47
48
 
48
- // Minimal stand-ins ONLY when the real tables are absent (throwaway container);
49
- // against a migrated database these are no-ops and the real tables are used.
50
- await pool.query(`
51
- CREATE TABLE IF NOT EXISTS builders (
52
- id bigserial PRIMARY KEY, github_login text, display_name text,
53
- status text DEFAULT 'active', system_role text);
54
- CREATE TABLE IF NOT EXISTS government_builder_ranks (
55
- builder_id bigint, rank_key text);
56
- `);
49
+ // Needs a MIGRATED database (the integration lane's). The stand-in tables this once
50
+ // created when the real ones were absent drifted from the schema (builders.github_id
51
+ // became NOT NULL) and are gone (task 1004863); the builder rows go through the shared
52
+ // helper so the next required column is added in one place.
53
+
54
+ // ONE TRANSACTION, ALWAYS ROLLED BACK (task 1004863). The machine principal below needs a
55
+ // real system_role, and system_role is UNIQUE with only two legal values — both held by
56
+ // the seeded system builders on any migrated database. So the fixture releases the seeded
57
+ // 'bfg' role inside this transaction, gives it to its own throwaway machine, and the
58
+ // rollback restores everything. The library under test is pointed at the same client, so
59
+ // it reads exactly the rows the fixture wrote.
60
+ const db = await pool.connect();
61
+ await db.query('BEGIN');
62
+ await db.query("UPDATE builders SET system_role = NULL WHERE system_role = 'bfg'");
57
63
 
58
64
  // Ids far above any real row, and every login prefixed, so cleanup is exact and
59
65
  // this can never touch a real builder or a real rank assignment.
60
66
  const B = 990000000;
61
67
  const ID = { xenos: B + 1, thetes: B + 2, metic: B + 3, archon: B + 4, gone: B + 5, bot: B + 6, ops: B + 7 };
62
68
  const cleanup = async () => {
63
- await pool.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
64
- await pool.query('DELETE FROM builders WHERE id >= $1', [B]);
69
+ await db.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
70
+ await db.query('DELETE FROM builders WHERE id >= $1', [B]);
65
71
  };
66
72
  await cleanup();
67
- await pool.query(`INSERT INTO builders (id, github_login, display_name, status, system_role) VALUES
68
- ($1,'gbmdb-x','Xenos','active',NULL),($2,'gbmdb-t','Thetes','active',NULL),
69
- ($3,'gbmdb-m','Metic','active',NULL),($4,'gbmdb-a','Archon','active',NULL),
70
- ($5,'gbmdb-gone','Deactivated Archon','inactive',NULL),
71
- ($6,'gbmdb-bot','A Machine','active','bfg'),
72
- ($7,'gbmdb-ops','Custom Ops','active',NULL)`,
73
- [ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
74
- await pool.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
73
+ await insertTestBuilders(db, [
74
+ { id: ID.xenos, login: 'gbmdb-x', display_name: 'Xenos', status: 'active', system_role: null },
75
+ { id: ID.thetes, login: 'gbmdb-t', display_name: 'Thetes', status: 'active', system_role: null },
76
+ { id: ID.metic, login: 'gbmdb-m', display_name: 'Metic', status: 'active', system_role: null },
77
+ { id: ID.archon, login: 'gbmdb-a', display_name: 'Archon', status: 'active', system_role: null },
78
+ { id: ID.gone, login: 'gbmdb-gone', display_name: 'Deactivated Archon', status: 'inactive', system_role: null },
79
+ { id: ID.bot, login: 'gbmdb-bot', display_name: 'A Machine', status: 'active', system_role: 'bfg' },
80
+ { id: ID.ops, login: 'gbmdb-ops', display_name: 'Custom Ops', status: 'active', system_role: null },
81
+ ], { onConflict: '' });
82
+ // The custom rank is a real government_ranks row: government_builder_ranks.rank_key is a
83
+ // foreign key into it on a migrated database.
84
+ await db.query("INSERT INTO government_ranks (rank_key, label) VALUES ('gbmdb-ops-rank', 'Custom Ops (test)')");
85
+ await db.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
75
86
  ($1,'xenos'),($2,'thetes'),($3,'metic'),($4,'archon'),($5,'archon'),($6,'archon'),($7,'gbmdb-ops-rank')`,
76
87
  [ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
77
88
 
78
89
  const api = require(process.cwd() + '/src/module-api.js');
79
- api.pool.query = (...a) => pool.query(...a);
90
+ api.pool.query = (...a) => db.query(...a); // the library reads through the same transaction
80
91
  const { resolveBoardMembers, isBoardMember } = require(process.cwd() + '/modules/government/board-membership.js');
81
92
 
82
93
  const board = (membership) => ({ membership, pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
@@ -120,13 +131,13 @@ await t('the deactivated and the machine are STILL excluded under the widened fo
120
131
  });
121
132
 
122
133
  await t('no duplicate rows when a builder matches through two keys', async () => {
123
- await pool.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
134
+ await db.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
124
135
  try {
125
136
  const l = await logins('rank:archon,gbmdb-ops-rank');
126
137
  assert.deepEqual(l.filter((x) => x === 'a').length, 1,
127
138
  'DISTINCT holds — a double-counted member silently raises the consent bar');
128
139
  } finally {
129
- await pool.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
140
+ await db.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
130
141
  }
131
142
  });
132
143
 
@@ -145,6 +156,8 @@ await t('an unparseable predicate still seats nobody against a real db', async (
145
156
  });
146
157
 
147
158
  await cleanup();
159
+ await db.query('ROLLBACK');
160
+ db.release();
148
161
  console.log(`\n${passed} passed, ${failed} failed`);
149
162
  await pool.end();
150
163
  process.exit(failed ? 1 : 0);