@bongos/core 1.21.177 → 1.21.179

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 (124) hide show
  1. package/.bongos-core.json +147 -152
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +0 -6
  4. package/clients/bongos-client/index.cjs +0 -6
  5. package/clients/bongos-client/index.d.ts +0 -6
  6. package/clients/bongos-client/index.mjs +0 -6
  7. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
  8. package/docs/api/openapi.json +1 -87
  9. package/docs/api-reference.md +1 -8
  10. package/docs/canonical-permissions.md +3 -3
  11. package/docs/copy-inventory.md +50 -64
  12. package/docs/copy-registry.json +109 -236
  13. package/docs/file-map.md +3 -3
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/modules-contract.md +2 -2
  16. package/docs/page-inventory.json +4 -7
  17. package/docs/page-readings.json +1520 -1569
  18. package/modules/government/catalog.js +10 -7
  19. package/modules/government/decision-board.js +0 -1
  20. package/modules/government/decision-rules.js +0 -1
  21. package/modules/government/docket.js +0 -1
  22. package/modules/government/protected-surfaces.js +1 -1
  23. package/modules/government/protected-surfaces.json +2 -3
  24. package/modules/hall-ui/public/atlas.html +1 -1
  25. package/modules/hall-ui/public/blockers.html +1 -1
  26. package/modules/hall-ui/public/board-room.html +1 -1
  27. package/modules/hall-ui/public/city.html +1 -1
  28. package/modules/hall-ui/public/collab.html +1 -1
  29. package/modules/hall-ui/public/commands.html +1 -1
  30. package/modules/hall-ui/public/copy-desk.html +1 -1
  31. package/modules/hall-ui/public/deploy.html +1 -1
  32. package/modules/hall-ui/public/diagrams.html +1 -1
  33. package/modules/hall-ui/public/drachmae.html +1 -1
  34. package/modules/hall-ui/public/fleet.html +1 -1
  35. package/modules/hall-ui/public/gate.css +4 -26
  36. package/modules/hall-ui/public/gate.html +12 -27
  37. package/modules/hall-ui/public/gate.js +26 -176
  38. package/modules/hall-ui/public/gate.states.json +4 -22
  39. package/modules/hall-ui/public/goal-map.html +1 -1
  40. package/modules/hall-ui/public/goals.html +1 -1
  41. package/modules/hall-ui/public/government.html +1 -1
  42. package/modules/hall-ui/public/idea.html +1 -1
  43. package/modules/hall-ui/public/ideas.html +1 -1
  44. package/modules/hall-ui/public/index.html +1 -1
  45. package/modules/hall-ui/public/modules.html +1 -1
  46. package/modules/hall-ui/public/my-agents.html +1 -1
  47. package/modules/hall-ui/public/primer.html +1 -1
  48. package/modules/hall-ui/public/profile.html +1 -1
  49. package/modules/hall-ui/public/project-settings.html +1 -1
  50. package/modules/hall-ui/public/ranks.html +1 -1
  51. package/modules/hall-ui/public/roadmap.html +1 -1
  52. package/modules/hall-ui/public/roster.html +1 -1
  53. package/modules/hall-ui/public/sessions.html +1 -1
  54. package/modules/hall-ui/public/settings.html +1 -1
  55. package/modules/hall-ui/public/shell.js +1 -1
  56. package/modules/hall-ui/public/street-card.js +5 -9
  57. package/modules/hall-ui/public/street-desk.js +2 -2
  58. package/modules/hall-ui/public/street-inside.js +1 -1
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/task.html +1 -1
  62. package/modules/hall-ui/public/thinking.html +1 -1
  63. package/modules/hall-ui/public/tweak-editor.html +1 -1
  64. package/modules/hall-ui/public/watch.html +1 -1
  65. package/modules/hall-ui/public/work.html +1 -1
  66. package/modules/hall-ui/public/work.js +67 -14
  67. package/modules/lifecycle/matter-acts.js +0 -6
  68. package/modules/lifecycle/module.json +0 -1
  69. package/package-lock.json +2 -2
  70. package/package.json +1 -1
  71. package/release-notes.json +28 -0
  72. package/scripts/bongos/adopt-preflight.js +3 -3
  73. package/scripts/bongos/audit-authorship.sh +1 -3
  74. package/scripts/bongos/bump-version.js +46 -9
  75. package/scripts/bongos/doc-cli-guard.js +2 -0
  76. package/scripts/bongos/doc-control-claims.js +2 -2
  77. package/scripts/bongos/government-walk.js +3 -9
  78. package/scripts/bongos/hard-floor.js +52 -0
  79. package/scripts/bongos/publish-manifest.js +3 -3
  80. package/scripts/bongos/run-unit-tests.js +17 -5
  81. package/scripts/gds/hard-floor.js +6 -0
  82. package/src/bongos/module-scope-map.js +2 -2
  83. package/src/bongos/permission-path-check.js +1 -1
  84. package/src/bongos/route-rank-check.js +1 -8
  85. package/src/bongos/routes.js +1 -2
  86. package/src/module-api.js +1 -1
  87. package/src/modules.js +1 -1
  88. package/tests/acclaim_feed_evidence_db.mjs +42 -23
  89. package/tests/adr_0089_scope.mjs +7 -5
  90. package/tests/bump_version.mjs +52 -18
  91. package/tests/copy_desk_flags_db.mjs +22 -14
  92. package/tests/gating_review_retired.mjs +1 -1
  93. package/tests/goal_sort_staleness.mjs +1 -1
  94. package/tests/government_act_alone_buildings.mjs +1 -3
  95. package/tests/government_board_membership_db.mjs +35 -22
  96. package/tests/government_protected_surfaces.mjs +29 -13
  97. package/tests/governor_docket.mjs +0 -2
  98. package/tests/governor_matter_acts.mjs +6 -8
  99. package/tests/hall_city.mjs +1 -1
  100. package/tests/hall_matter_card.mjs +2 -2
  101. package/tests/hall_oversight_world.mjs +16 -12
  102. package/tests/hall_street_desk.mjs +4 -3
  103. package/tests/hall_street_inside.mjs +1 -1
  104. package/tests/helpers.mjs +34 -0
  105. package/tests/howto_state.mjs +33 -56
  106. package/tests/howto_state_db.mjs +68 -0
  107. package/tests/idea_credit_streams_db.mjs +11 -3
  108. package/tests/integration_parked_ratchet.mjs +7 -1
  109. package/tests/nav_permission_atoms.mjs +1 -1
  110. package/tests/page_notes_db.mjs +12 -8
  111. package/tests/page_tweak_no_rearm.mjs +50 -0
  112. package/tests/publish_manifest.mjs +64 -0
  113. package/tests/search_isolation.mjs +5 -103
  114. package/tests/search_isolation_db.mjs +140 -0
  115. package/tests/session_earnings_mirror_db.mjs +2 -0
  116. package/tests/work_board_live_refresh.mjs +236 -0
  117. package/tests/workflow_exec_paths.mjs +9 -11
  118. package/modules/lifecycle/routes/gate-approvals.js +0 -299
  119. package/scripts/bongos/ci-grade.js +0 -82
  120. package/scripts/bongos/gate-review.js +0 -330
  121. package/scripts/gds/ci-grade.js +0 -7
  122. package/scripts/gds/gate-review.js +0 -7
  123. package/tests/gate_approvals.mjs +0 -611
  124. package/tests/gate_review.mjs +0 -152
@@ -51,7 +51,7 @@ function board() {
51
51
 
52
52
  // ── a long-winded docket: every source, long words everywhere ─────────────────
53
53
  const LONG = 'The extraordinarily long-winded proposal to reorganise every single floor of the hall before winter';
54
- const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['gate_approvals', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
54
+ const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
55
55
  function matter(over = {}) {
56
56
  return { id: 'blockers:1', source: 'blockers', building: 'planning', rule: 'majority', state: 'needs', title: 'A matter', short: 'a matter', where: 'Planning', opened_at: '2026-09-29T10:00:00.000Z', deadline: null, proposer: null, tally: null, href: null, ref: null, ...over };
57
57
  }
@@ -78,6 +78,9 @@ function longMatters() {
78
78
  }
79
79
  }
80
80
  out.push(matter({ id: 'board:900', source: 'board', building: 'planning', short: 'idea to ratify', title: LONG, state: 'needs' }));
81
+ // A second override waiting at Release: task 1004859 removed the gate-approval source that
82
+ // used to fill a Release floor, and the stress docket must still reach its 30 rows.
83
+ out.push(matter({ id: 'override_requests:901', source: 'override_requests', building: 'release', title: `${LONG} 901`, short: 'a matter with a very long short description', state: 'needs', ref: { id: 901 } }));
81
84
  return out;
82
85
  }
83
86
  const ROOM = { open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '7' }, votes: [] }, { id: '900', subject_type: 'full_idea', subject: { title: LONG }, votes: [] }], closed: [] };
@@ -176,7 +179,6 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
176
179
  matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } }),
177
180
  matter({ id: 'goal_join_requests:12', source: 'goal_join_requests', building: 'people', state: 'needs', proposer: { name: 'Kai Mo' }, href: '/builders/goal/42', ref: { id: 12 } }),
178
181
  matter({ id: 'override_requests:13', source: 'override_requests', building: 'release', state: 'needs', ref: { id: 13 } }),
179
- matter({ id: 'gate_approvals:14', source: 'gate_approvals', building: 'release', state: 'needs', ref: { id: 14 } }),
180
182
  matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', ref: { type: 'board_item', id: '1' } }),
181
183
  ];
182
184
  const dk = docket(ms);
@@ -189,7 +191,6 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
189
191
  const rel = D.rowsFor('release', 0, dk, { now: NOW });
190
192
  assert.deepEqual(rel[0].actions.map((a) => a.label), ['release', 'hold']);
191
193
  assert.equal(C.request(rel[0].card, 'release').url, '/api/bongos/override-requests/13/decide');
192
- assert.equal(C.request(D.rowsFor('release', 5, dk, { now: NOW })[0].card, 'release').url, '/api/bongos/gate-approvals/14/approve');
193
194
  // a vote: yes and no, and the no's reason is a chip, never free text
194
195
  const vote = row('charter', 0);
195
196
  assert.deepEqual(vote.actions.map((a) => [a.key, a.label]), [['for', 'yes'], ['object', 'no']]);
@@ -54,7 +54,7 @@ function proto(mode, st = {}) {
54
54
  function matter(over = {}) {
55
55
  return { id: 'blockers:1', source: 'blockers', building: 'planning', rule: 'majority', state: 'needs', title: 'A matter', short: 'a matter', where: 'Planning', opened_at: '2026-09-29T10:00:00.000Z', deadline: null, proposer: null, tally: null, href: null, ref: null, ...over };
56
56
  }
57
- const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['gate_approvals', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
57
+ const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
58
58
  const READINGS = [['flow', 'review'], ['promises', 'planning'], ['money', 'treasury'], ['people', 'people'], ['fairness', 'disputes'], ['rules', 'rules']];
59
59
  function docket(matters = [], over = {}) {
60
60
  const counts = { needs: 0, moving: 0, settled: 0 };
package/tests/helpers.mjs CHANGED
@@ -1136,3 +1136,37 @@ export function fakeHttpsGet(routes) {
1136
1136
  };
1137
1137
  return { get, asked };
1138
1138
  }
1139
+
1140
+ // insertTestBuilders — insert throwaway `builders` rows that satisfy the MIGRATED schema
1141
+ // (task 1004863). builders.github_id became NOT NULL + UNIQUE, and the database tests
1142
+ // that each hand-wrote `INSERT INTO builders (id, github_login, …)` broke on it at once,
1143
+ // unnoticed, because the lanes that ran them were weekly or absent. One spelling here
1144
+ // fixes the drift, not just the column: a future NOT NULL column is added in one place.
1145
+ //
1146
+ // `db` is anything with `query(text, params)` (a pg Pool, or a client inside a
1147
+ // transaction). Each row is `{ id, login, ...columns }`: `github_id` defaults to
1148
+ // `test-gh-<id>` (unique per id, never a real GitHub id) and `display_name` to the login;
1149
+ // any other builders column may be named (rank, status, system_role, total_credits …).
1150
+ // A column one row names and another does not is written as DEFAULT for the other, so
1151
+ // the table's own defaults still apply. Column names come from the caller's literal
1152
+ // fixtures, never from input. `onConflict` is appended verbatim (default
1153
+ // `ON CONFLICT (id) DO NOTHING`; pass '' for none). Returns the inserted rows.
1154
+ export async function insertTestBuilders(db, rows, { onConflict = 'ON CONFLICT (id) DO NOTHING' } = {}) {
1155
+ if (!rows.length) return [];
1156
+ const shaped = rows.map(({ id, login, ...rest }) => {
1157
+ if (id == null || !login) throw new Error('insertTestBuilders: every row needs an id and a login');
1158
+ return { id, github_id: `test-gh-${id}`, github_login: login, display_name: login, ...rest };
1159
+ });
1160
+ const cols = [...new Set(shaped.flatMap((r) => Object.keys(r)))];
1161
+ const params = [];
1162
+ const tuples = shaped.map((r) => `(${cols.map((c) => {
1163
+ if (!(c in r)) return 'DEFAULT';
1164
+ params.push(r[c]);
1165
+ return `$${params.length}`;
1166
+ }).join(', ')})`);
1167
+ const { rows: out } = await db.query(
1168
+ `INSERT INTO builders (${cols.map((c) => `"${c}"`).join(', ')}) VALUES ${tuples.join(', ')} ${onConflict} RETURNING *`,
1169
+ params,
1170
+ );
1171
+ return out;
1172
+ }
@@ -1,11 +1,16 @@
1
1
  // tests/howto_state.mjs — per-person read/unread state for the how-to steps (task 1004634)
2
2
  //
3
- // Two halves. The DB-free half drives howto-state.js against an in-memory pool that
4
- // honours the three statements it issues, so the contract runs everywhere: unread by
5
- // default, mark read, skip all, a read upgrades a skip (never the reverse), the first
6
- // read time is kept, reset some or all, and an id the guide does not name is refused.
7
- // The DB half applies the real migration and runs the same calls against Postgres; it
8
- // skips when no Postgres is reachable (DATABASE_URL), like copy_desk_flags_db.mjs.
3
+ // The DB-free half. It drives howto-state.js against an in-memory pool that honours the
4
+ // three statements it issues, so the contract runs everywhere: unread by default, mark
5
+ // read, skip all, a read upgrades a skip (never the reverse), the first read time is
6
+ // kept, reset some or all, and an id the guide does not name is refused.
7
+ //
8
+ // The SAME contract runs against Postgres in tests/howto_state_db.mjs, which imports
9
+ // `contract` from here (task 1004863). That half used to live in this file, where it ran
10
+ // in the DB-free unit lane against whatever database DATABASE_URL reached — creating a
11
+ // stand-in `builders` table there if one was missing — and skipped in CI, so no lane
12
+ // ever ran it. Split out, it is in the integration lane and uses the migrated schema.
13
+ // The tests below run only when this file is the entry point, so importing it runs none.
9
14
  //
10
15
  // Run: node tests/howto_state.mjs
11
16
 
@@ -60,7 +65,8 @@ function fakePool() {
60
65
  };
61
66
  }
62
67
 
63
- async function contract(label, deps, A, B) {
68
+ // `test` is the caller's runner, so each file counts and reports its own cases.
69
+ export async function contract(test, label, deps, A, B) {
64
70
  const first = guide.STEPS[0].id;
65
71
  const second = guide.STEPS[1].id;
66
72
 
@@ -126,53 +132,24 @@ async function contract(label, deps, A, B) {
126
132
  });
127
133
  }
128
134
 
129
- await test('shapeState ignores a stored mark for a step the guide no longer has', async () => {
130
- const s = hs.shapeState([{ step_id: 'retired_step', how: 'read', read_at: new Date() }]);
131
- assert.equal(s.read, 0);
132
- assert.equal(s.total, guide.STEPS.length);
133
- });
134
-
135
- await test('the module declares its migration and the routes are mounted', async () => {
136
- const mod = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/onboarding/module.json'), 'utf8'));
137
- assert.equal(mod.contributes.migrations, true);
138
- assert.ok(fs.existsSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql')));
139
- const src = fs.readFileSync(path.join(ROOT, 'modules/onboarding/routes/onboarding.js'), 'utf8');
140
- for (const r of ["router.get('/me/howto'", "router.post('/me/howto/read'", "router.post('/me/howto/reset'"]) assert.ok(src.includes(r), r);
141
- });
142
-
143
- await contract('in-memory', { pool: fakePool() }, 1, 2);
144
-
145
- // ── the real database ────────────────────────────────────────────────────────
146
- const { Pool } = require('pg');
147
- const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
148
- let dbUp = true;
149
- try { await pool.query('SELECT 1'); } catch (e) {
150
- dbUp = false;
151
- console.log(` (db half SKIPPED — no reachable Postgres: ${e.code || e.message}. Set DATABASE_URL to run.)`);
152
- }
153
- if (dbUp) {
154
- await pool.query(`
155
- CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
156
- CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text);
157
- `);
158
- await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8'));
159
- await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8')); // idempotent
160
- const A = 910000001;
161
- const B = 910000002;
162
- await pool.query('INSERT INTO builders (id, github_login) VALUES ($1, $2), ($3, $4) ON CONFLICT (id) DO NOTHING', [A, 'howto-a', B, 'howto-b']);
163
- await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]);
164
- try {
165
- await contract('postgres', { pool }, A, B);
166
- await test('postgres: the CHECK refuses a malformed step id and a bad how', async () => {
167
- await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id) VALUES ($1, 'Bad Id')", [A]));
168
- await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id, how) VALUES ($1, 'welcome', 'seen')", [A]));
169
- });
170
- } finally {
171
- await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]).catch(() => {});
172
- await pool.query('DELETE FROM builders WHERE id IN ($1, $2)', [A, B]).catch(() => {});
173
- }
174
- }
175
- await pool.end().catch(() => {});
135
+ const isMain = Boolean(process.argv[1]) && fs.realpathSync(url.fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
136
+ if (isMain) {
137
+ await test('shapeState ignores a stored mark for a step the guide no longer has', async () => {
138
+ const s = hs.shapeState([{ step_id: 'retired_step', how: 'read', read_at: new Date() }]);
139
+ assert.equal(s.read, 0);
140
+ assert.equal(s.total, guide.STEPS.length);
141
+ });
176
142
 
177
- console.log(`\n${passed} passed, ${failed} failed`);
178
- if (failed) process.exit(1);
143
+ await test('the module declares its migration and the routes are mounted', async () => {
144
+ const mod = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/onboarding/module.json'), 'utf8'));
145
+ assert.equal(mod.contributes.migrations, true);
146
+ assert.ok(fs.existsSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql')));
147
+ const src = fs.readFileSync(path.join(ROOT, 'modules/onboarding/routes/onboarding.js'), 'utf8');
148
+ for (const r of ["router.get('/me/howto'", "router.post('/me/howto/read'", "router.post('/me/howto/reset'"]) assert.ok(src.includes(r), r);
149
+ });
150
+
151
+ await contract(test, 'in-memory', { pool: fakePool() }, 1, 2);
152
+
153
+ console.log(`\n${passed} passed, ${failed} failed`);
154
+ if (failed) process.exit(1);
155
+ }
@@ -0,0 +1,68 @@
1
+ // tests/howto_state_db.mjs — the how-to read/unread contract against a REAL Postgres
2
+ // (task 1004634; split out of tests/howto_state.mjs by task 1004863).
3
+ //
4
+ // tests/howto_state.mjs runs the contract against an in-memory pool in the DB-free unit
5
+ // lane. This runs the SAME contract (imported, not copied) against the real migration
6
+ // and the real CHECK constraints, in the integration lane (run-unit-tests.js INTEGRATION).
7
+ // It used to be the second half of that file, where it ran in the unit lane against
8
+ // whatever DATABASE_URL reached, created a stand-in `builders` table there if one was
9
+ // missing, and skipped in CI — so no lane ever executed it, and it broke unnoticed when
10
+ // builders.github_id became NOT NULL.
11
+ //
12
+ // Needs a MIGRATED database (scripts/migrate.sh). SKIPS (exit 0) with no reachable
13
+ // Postgres, like every *_db test.
14
+ //
15
+ // Run: DATABASE_URL=postgres://... node tests/howto_state_db.mjs
16
+
17
+ import { strict as assert } from 'node:assert';
18
+ import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import url from 'node:url';
22
+ import { contract } from './howto_state.mjs';
23
+ import { insertTestBuilders } from './helpers.mjs';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
27
+ const { Pool } = require('pg');
28
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
29
+
30
+ try {
31
+ await pool.query('SELECT 1');
32
+ } catch (e) {
33
+ console.log(`howto_state_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
34
+ await pool.end().catch(() => {});
35
+ process.exit(0);
36
+ }
37
+
38
+ let passed = 0;
39
+ let failed = 0;
40
+ async function test(name, fn) {
41
+ try { await fn(); passed++; console.log(` ok ${name}`); }
42
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
43
+ }
44
+
45
+ const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8');
46
+ await test('postgres: the migration re-applies cleanly over the migrated schema (idempotent)', async () => {
47
+ await pool.query(MIGRATION);
48
+ await pool.query(MIGRATION);
49
+ });
50
+
51
+ const A = 910000101;
52
+ const B = 910000102;
53
+ await insertTestBuilders(pool, [{ id: A, login: 'howto-a' }, { id: B, login: 'howto-b' }]);
54
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]);
55
+ try {
56
+ await contract(test, 'postgres', { pool }, A, B);
57
+ await test('postgres: the CHECK refuses a malformed step id and a bad how', async () => {
58
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id) VALUES ($1, 'Bad Id')", [A]), (e) => e.code === '23514');
59
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id, how) VALUES ($1, 'welcome', 'seen')", [A]), (e) => e.code === '23514');
60
+ });
61
+ } finally {
62
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]).catch(() => {});
63
+ await pool.query('DELETE FROM builders WHERE id IN ($1, $2)', [A, B]).catch(() => {});
64
+ await pool.end().catch(() => {});
65
+ }
66
+
67
+ console.log(`\nhowto_state_db: ${passed} passed, ${failed} failed`);
68
+ if (failed) process.exit(1);
@@ -133,13 +133,21 @@ async function fixture(client, { completeness = 70, developed = false, taskCount
133
133
  idea_id: idea.id,
134
134
  grade: 'full',
135
135
  completeness_score: completeness,
136
- ...(developer ? { developed_by: developer.id } : {}),
136
+ // developed_by and developed_at travel as a pair (ideas_grades_developed_pair_chk).
137
+ ...(developer ? { developed_by: developer.id, developed_at: new Date() } : {}),
138
+ });
139
+ // status is named, not placeholder-filled: versions_status_check admits only the
140
+ // lifecycle values, and 'building' is the one with no uniqueness rule of its own
141
+ // (versions_one_planning_idx allows exactly one 'planning' row, which the seeds own).
142
+ const version = await insertRow(client, 'versions', {
143
+ id: `IC6-${Date.now()}-${Math.random()}`.slice(0, 40), status: 'building',
137
144
  });
138
- const version = await insertRow(client, 'versions', { id: `IC6-${Date.now()}-${Math.random()}`.slice(0, 40) });
139
145
  const tasks = [];
140
146
  for (let i = 0; i < taskCount; i++) {
141
147
  const task = await insertRow(client, 'tasks', {
142
- version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped',
148
+ // discipline is named for the same reason as the version status above:
149
+ // tasks_discipline_check admits only the live crafts.
150
+ version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: 'engineer',
143
151
  });
144
152
  await insertRow(client, 'ideas_task_lineage', {
145
153
  task_id: task.id, idea_id: idea.id, origin: 'promote',
@@ -51,7 +51,13 @@ const { INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING } = require('../scripts/b
51
51
  // LOWERED 11 → 6 by task 1004861: claim_gate_open_blocker, claim_gate_rebase,
52
52
  // clawback_confirmed_backlog, needs_rebase and reward_mode_cost_plus_only stopped adding a
53
53
  // second 'planning' version (and had three stale fixtures under it repaired).
54
- const MAX_PARKED = 6;
54
+ // LOWERED 6 → 1 by task 1004863: acclaim_feed_evidence_db, copy_desk_flags_db,
55
+ // government_board_membership_db, idea_credit_streams_db and session_earnings_mirror_db had
56
+ // stale fixtures (a missing builders.github_id, retired status/discipline values, a sliced
57
+ // copy of a route's SQL); repaired, they gate again.
58
+ // LOWERED 1 → 0: status-probe, the last entry, was deleted with its test by task 1004858.
59
+ // Nothing is parked; a new entry now needs this raised, deliberately.
60
+ const MAX_PARKED = 0;
55
61
 
56
62
  // The one test that proves the PUBLISHED package works end to end: packaged, installed into
57
63
  // a consumer, installed again from its own shipped lockfile, migrated onto EMPTY databases,
@@ -93,7 +93,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
93
93
  // says a held core version was tried and rolled back; task 1004731: the one-time
94
94
  // "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852).
96
- const SHELL_SHA = 'bf052d6fabeb1953';
96
+ const SHELL_SHA = '670b2f71085c13b6';
97
97
 
98
98
  test('shell.js has not changed without its cache-bust being bumped', () => {
99
99
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -10,6 +10,7 @@ import { strict as assert } from 'node:assert';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import url from 'node:url';
13
+ import { insertTestBuilders } from './helpers.mjs';
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
  const { Pool } = require('pg');
@@ -42,15 +43,15 @@ async function refuses(sql, params, constraint) {
42
43
  throw new Error(`expected ${constraint || 'a refusal'}, but the write landed`);
43
44
  }
44
45
 
45
- await pool.query(`
46
- CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
47
- CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text, display_name text);
48
- CREATE TABLE IF NOT EXISTS idea_inbox (id bigserial PRIMARY KEY, title text, status text);
49
- `);
46
+ // Needs a MIGRATED database (the integration lane's). The stand-in builders /
47
+ // idea_inbox tables this once created drifted from the schema (builders.github_id
48
+ // became NOT NULL) and are gone (task 1004863).
50
49
  const A = 910000001;
51
50
  const B = 910000002;
52
- await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES ($1,$2,$3),($4,$5,$6) ON CONFLICT (id) DO NOTHING',
53
- [A, 'pntest-a', 'Page Notes Test A', B, 'pntest-b', '']);
51
+ await insertTestBuilders(pool, [
52
+ { id: A, login: 'pntest-a', display_name: 'Page Notes Test A' },
53
+ { id: B, login: 'pntest-b', display_name: '' },
54
+ ]);
54
55
 
55
56
  const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/page-notes/migrations/page_notes_001_notes.sql'), 'utf8');
56
57
  await pool.query(MIGRATION);
@@ -137,7 +138,6 @@ await test('deleting a doc takes its images with it', async () => {
137
138
  });
138
139
 
139
140
  await clean();
140
- await pool.query('DELETE FROM builders WHERE id = ANY($1::bigint[])', [[A, B]]).catch(() => {});
141
141
  await test('THE SITE LIST (task 1004499): one surface, submitted only, newest first, read through its own index', async () => {
142
142
  await pool.query(MIGRATION_2);
143
143
  const mk = async (author, page, at) => {
@@ -162,6 +162,10 @@ await test('THE SITE LIST (task 1004499): one surface, submitted only, newest fi
162
162
  await clean();
163
163
  });
164
164
 
165
+ // The builders go LAST (task 1004863): the site-list case above writes notes authored
166
+ // by A and B, and page_notes_docs.author_id is a foreign key into builders.
167
+ await clean();
168
+ await pool.query('DELETE FROM builders WHERE id = ANY($1::bigint[])', [[A, B]]).catch(() => {});
165
169
  await pool.end();
166
170
  console.log(`page_notes_db: ${passed} passed, ${failed} failed`);
167
171
  process.exit(failed ? 1 : 0);
@@ -0,0 +1,50 @@
1
+ // tests/page_tweak_no_rearm.mjs — a page tweak held for its artist is never
2
+ // re-armed for auto-merge from outside github-push (task 1004859).
3
+ //
4
+ // WHY. ADR 0341 D7 holds a page tweak until its artist signs it off. The one
5
+ // caller that could re-arm auto-merge on an open PR was the hall's gate-approval
6
+ // route (POST /gate-approvals/:pr/approve → recheckPullRequest), and
7
+ // tests/gate_approvals.mjs "layer 3" pinned that it passed { arm: false } for a
8
+ // held tweak. Task 1004859 deleted that route with the dead held-PR list. The
9
+ // guarantee now rests on there being NO other caller, so this pins that: a
10
+ // new caller of recheckPullRequest must come with its own held-tweak check
11
+ // and its own test, and this file is where it gets noticed.
12
+ //
13
+ // DB-free, no network. Run: node --test tests/page_tweak_no_rearm.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { execFileSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const OWNER = 'modules/lifecycle/github-push.js';
24
+
25
+ // Code only: a comment naming the function is not a call.
26
+ const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
27
+
28
+ function callers(files, read) {
29
+ return files.filter((f) => f !== OWNER && /recheckPullRequest\s*\(/.test(stripComments(read(f))));
30
+ }
31
+
32
+ test('nothing outside github-push calls recheckPullRequest (the only re-arm path was the removed gate route)', () => {
33
+ const files = execFileSync('git', ['ls-files', 'src', 'modules', 'scripts'], { cwd: ROOT, encoding: 'utf8' })
34
+ .split('\n').filter((f) => /\.(c|m)?js$/.test(f) && !/\/tests?\//.test(f));
35
+ assert.ok(files.length > 500, `only ${files.length} files scanned — the sweep is not reading the tree`);
36
+ const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
37
+ assert.deepEqual(callers(files, read), [],
38
+ 'a new caller re-arms auto-merge on an open PR: it must check heldForArtistFailClosed for the PR\'s task '
39
+ + 'and pass { arm: false } when held (ADR 0341 D7), with a test, then be listed here');
40
+ assert.ok(!fs.existsSync(path.join(ROOT, 'modules/lifecycle/routes/gate-approvals.js')), 'the gate route stays removed');
41
+ });
42
+
43
+ test('the sweep would catch a caller (counterfactual), and ignores one in a comment', () => {
44
+ const fake = {
45
+ 'modules/x/a.js': 'await githubPush.recheckPullRequest({ number: 1 });',
46
+ 'modules/x/b.js': '// recheckPullRequest(...) is described here only',
47
+ [OWNER]: 'async function recheckPullRequest() {}',
48
+ };
49
+ assert.deepEqual(callers(Object.keys(fake), (f) => fake[f]), ['modules/x/a.js']);
50
+ });
@@ -22,6 +22,7 @@ import { createRequire } from 'node:module';
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import url from 'node:url';
25
+ import { execFileSync } from 'node:child_process';
25
26
 
26
27
  const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
27
28
  const require = createRequire(import.meta.url);
@@ -324,5 +325,68 @@ t('no publishable test statically imports an excluded module (R89 boundary)', ()
324
325
  assert.equal(offenders.length, 0, '\n ' + offenders.join('\n '));
325
326
  });
326
327
 
328
+ // ── hall-preview fixtures stay out of the core; the harness stays in (task 1004888) ──
329
+ // scripts/hall-preview/fixtures/ is ~2.4 MB of captured hall data from THIS instance
330
+ // (owner login, world name, prior instance name). INSTANCE_EXCLUDE keeps it out of the
331
+ // published core (task 1002219, audit H5); this pins that, so an edit to the exclude
332
+ // list cannot silently start shipping the fixtures to every hosted project again.
333
+ // The selection is built the way package-core builds it: enumerate the git tree
334
+ // (`git ls-tree -r` on a ref) and keep what isPublishable() admits.
335
+ const HALL_PREVIEW = 'scripts/hall-preview/';
336
+ const HALL_FIXTURES = `${HALL_PREVIEW}fixtures/`;
337
+ // The harness files the preview cannot boot without: the server and its two local
338
+ // requires (./live-identity, ./pref-writes). A named floor, so the check below can
339
+ // never pass by finding no harness at all.
340
+ const HALL_HARNESS_FLOOR = ['server.js', 'live-identity.js', 'pref-writes.js'].map((f) => HALL_PREVIEW + f);
341
+
342
+ function treeFiles() {
343
+ // package-core's ref path: every blob on HEAD. A core checkout with no git history
344
+ // (a consumer's vendored core) falls back to walking the disk, as --from-worktree does.
345
+ try {
346
+ const out = execFileSync('git', ['ls-tree', '-r', '--name-only', 'HEAD'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 });
347
+ return out.split('\n').filter(Boolean);
348
+ } catch (_) {
349
+ const files = [];
350
+ const walk = (rel) => {
351
+ for (const ent of fs.readdirSync(path.join(ROOT, rel), { withFileTypes: true })) {
352
+ const r = rel ? `${rel}/${ent.name}` : ent.name;
353
+ if (ent.isDirectory()) walk(r); else files.push(r);
354
+ }
355
+ };
356
+ if (fs.existsSync(path.join(ROOT, HALL_PREVIEW))) walk(HALL_PREVIEW.slice(0, -1));
357
+ return files;
358
+ }
359
+ }
360
+
361
+ t('hall-preview fixtures are excluded from the core (path rule, task 1004888)', () => {
362
+ // Holds even where the fixtures are not on disk (a consumer core never has them).
363
+ for (const p of [`${HALL_FIXTURES}me.json`, `${HALL_FIXTURES}nested/deep.json`, `${HALL_FIXTURES}tasks.json`]) {
364
+ assert.equal(isPublishable(p), false, `${p} would ship in the published core — keep '${HALL_FIXTURES}' in INSTANCE_EXCLUDE`);
365
+ }
366
+ for (const p of HALL_HARNESS_FLOOR) {
367
+ assert.equal(isPublishable(p), true, `${p} is the preview harness and must still publish`);
368
+ }
369
+ });
370
+
371
+ t('the package-core selection carries no hall-preview fixture, but keeps the harness (task 1004888)', () => {
372
+ const hall = treeFiles().filter((f) => f.startsWith(HALL_PREVIEW));
373
+ const fixtures = hall.filter((f) => f.startsWith(HALL_FIXTURES));
374
+ const harness = hall.filter((f) => !f.startsWith(HALL_FIXTURES));
375
+ const { publish } = partitionPaths(hall);
376
+ const leaked = publish.filter((f) => f.startsWith(HALL_FIXTURES));
377
+ assert.equal(leaked.length, 0, `${leaked.length} hall-preview fixture(s) would ship in the core, e.g. ${leaked.slice(0, 3).join(', ')} — keep '${HALL_FIXTURES}' in INSTANCE_EXCLUDE`);
378
+ // Every harness file on the tree publishes, and the named floor is among them
379
+ // whenever the harness exists here at all (the source repo always has it).
380
+ const dropped = harness.filter((f) => !publish.includes(f));
381
+ assert.equal(dropped.length, 0, `hall-preview harness file(s) dropped from the core: ${dropped.join(', ')}`);
382
+ if (harness.length) {
383
+ for (const p of HALL_HARNESS_FLOOR) assert.ok(publish.includes(p), `${p} missing from the core selection`);
384
+ }
385
+ // Non-vacuous in the source repo: the fixtures are there to be excluded.
386
+ if (fs.existsSync(path.join(ROOT, HALL_FIXTURES))) {
387
+ assert.ok(fixtures.length > 0, `found ${HALL_FIXTURES} on disk but no fixture in the enumerated tree — the enumeration is broken, so this check proves nothing`);
388
+ }
389
+ });
390
+
327
391
  console.log(`\n${passed} passed, ${failed} failed`);
328
392
  process.exit(failed ? 1 : 0);
@@ -14,15 +14,13 @@
14
14
  // to searchChunks and IGNORES any attempt to widen scope via the body
15
15
  // (builder_id / rank / min_rank / owner_builder_id). This is the
16
16
  // "can't be tricked" proof.
17
- // C. DB-backed (auto-SKIPS without a reachable Postgres): seeds rank-gated +
18
- // owner-scoped chunks and runs the REAL searchChunks in a ROLLED-BACK
19
- // transaction for the done-when cases (xenos-vs-protected, cross-builder
20
- // memory, owner-sees-own). The same cases were also proven against the prod
21
- // DB in a rolled-back transaction at ship time (the authoritative evidence).
17
+ // C. DB-backed: lives in tests/search_isolation_db.mjs (task 1004863). It was
18
+ // the third layer of this file, whose comment said it ran in the integration
19
+ // gate — but this file was never in that set, so in CI Layer C always skipped
20
+ // and no lane executed it. Split out, it is in the INTEGRATION set, and A + B
21
+ // stay here in the DB-free unit lane.
22
22
  //
23
23
  // Run: node tests/search_isolation.mjs
24
- // (Layer C runs only where a Postgres is reachable — the integration gate / any
25
- // machine with one; it skips cleanly otherwise so the DB-free unit gate stays green.)
26
24
 
27
25
  import { strict as assert } from 'node:assert';
28
26
  import { createRequire } from 'node:module';
@@ -136,102 +134,6 @@ try {
136
134
  search.searchChunks = orig.searchChunks;
137
135
  }
138
136
 
139
- // ---------------------------------------------------------------------------
140
- // Layer C — DB-backed real-query scoping (auto-skips without Postgres)
141
- // ---------------------------------------------------------------------------
142
- console.log('\nLayer C — real searchChunks against a live DB (skips if unreachable):');
143
-
144
- let pool = null;
145
- try {
146
- // task 1003207: this said `../src/db`, which has not existed since the kernel
147
- // carve — so the require ALWAYS threw, pool stayed null, and Layer C skipped
148
- // even against a live Postgres. It was a dead layer reporting as a skip.
149
- ({ pool } = require('../src/bongos/pool'));
150
- } catch { pool = null; }
151
-
152
- async function dbReachable() {
153
- if (!pool) return null;
154
- try {
155
- const c = await pool.connect();
156
- try { await c.query('SELECT 1'); return c; } catch { c.release(); return null; }
157
- } catch { return null; }
158
- }
159
-
160
- const client = await dbReachable();
161
- if (!client) {
162
- console.log(' SKIP no reachable Postgres — Layer C runs in the integration gate / wherever a Postgres is reachable.');
163
- console.log(' (The same scoping cases were proven on the prod DB in a rolled-back transaction at ship time.)');
164
- // END THE POOL ON THE SKIP PATH TOO (task 1003848). Only the else-branch below
165
- // ended it, so when Postgres was unreachable the Pool survived with libuv handles
166
- // still closing from its failed connect — and the process.exit() at the bottom of
167
- // this file then tore down the loop underneath them. On Windows that surfaces as
168
- // `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src\win\async.c`, exit
169
- // 0xC0000409: a process-teardown crash AFTER '5 passed, 0 failed', which reads as
170
- // a failing suite in the unit gate while every assertion actually passed.
171
- if (pool) await pool.end().catch(() => {});
172
- } else {
173
- try {
174
- await client.query('BEGIN');
175
- await client.query(`SET LOCAL pg_trgm.word_similarity_threshold = ${search.TRGM_THRESHOLD}`);
176
- // builder 1 exists (example-owner); seed a protected (metic) chunk, a public one,
177
- // and a chunk owned by builder 1 — all matching q='enforcement'.
178
- await client.query(`INSERT INTO search_chunks (source_kind, source_ref, chunk_index, heading_path, title, body, min_rank, owner_builder_id, content_hash) VALUES
179
- ('doc','__iso__/protected.md',0,'p','PROT','trust boundary permission core enforcement','metic',NULL,'i1'),
180
- ('doc','__iso__/public.md',0,'pub','PUB','trust boundary public enforcement overview','xenos',NULL,'i2'),
181
- ('db','__iso__/memory:1',0,'m','MEM1','builder one private enforcement memo','xenos',1,'i3')`);
182
-
183
- const titles = (rows) => rows.map((r) => r.title).sort().join(',');
184
-
185
- await t('xenos caller sees the public chunk but NOT the metic-protected one (xenos-vs-protected)', async () => {
186
- const rows = await search.searchChunks({ q: 'enforcement', builderId: 2, builderRank: 'xenos', limit: 50 }, { client });
187
- const ts = titles(rows);
188
- assert.ok(ts.includes('PUB'), `expected PUB, got [${ts}]`);
189
- assert.ok(!ts.includes('PROT'), `xenos must NOT see the metic chunk, got [${ts}]`);
190
- });
191
-
192
- await t('metic caller sees the protected chunk too', async () => {
193
- const rows = await search.searchChunks({ q: 'enforcement', builderId: 2, builderRank: 'metic', limit: 50 }, { client });
194
- const ts = titles(rows);
195
- assert.ok(ts.includes('PROT') && ts.includes('PUB'), `metic should see PROT+PUB, got [${ts}]`);
196
- });
197
-
198
- await t('builder 2 does NOT see builder 1\'s owned chunk (cross-builder memory)', async () => {
199
- const rows = await search.searchChunks({ q: 'enforcement', builderId: 2, builderRank: 'archon', limit: 50 }, { client });
200
- assert.ok(!rows.some((r) => r.title === 'MEM1'), 'an Archon STILL must not see another builder\'s private chunk');
201
- });
202
-
203
- await t('builder 1 DOES see their own owned chunk (owner-sees-own)', async () => {
204
- const rows = await search.searchChunks({ q: 'enforcement', builderId: 1, builderRank: 'xenos', limit: 50 }, { client });
205
- assert.ok(rows.some((r) => r.title === 'MEM1'), 'a builder must see their own private chunk');
206
- });
207
-
208
- // The public port (task 1002534, ADR 0367): the Discord #support answerer
209
- // has no session, so its scope is fixed — shared xenos docs on the
210
- // allowlist, nothing else, whatever matches the query.
211
- await client.query(`INSERT INTO search_chunks (source_kind, source_ref, chunk_index, heading_path, title, body, min_rank, owner_builder_id, content_hash) VALUES
212
- ('doc','__pub__/allowed/guide.md',0,'g','PUBOK','support enforcement guide','xenos',NULL,'p1'),
213
- ('doc','__pub__/allowed/locked.md',0,'l','PUBMETIC','support enforcement locked','metic',NULL,'p2'),
214
- ('doc','__pub__/allowed/mine.md',0,'m','PUBOWNED','support enforcement owned','xenos',1,'p3'),
215
- ('db','__pub__/allowed/task:1',0,'d','PUBDB','support enforcement task prose','xenos',NULL,'p4'),
216
- ('doc','__pub__/elsewhere.md',0,'e','PUBOFFLIST','support enforcement off the list','xenos',NULL,'p5')`);
217
-
218
- await t('searchPublicDocs returns ONLY a shared xenos doc on the allowlist (no metic, owned, db, or off-list row)', async () => {
219
- const rows = await search.searchPublicDocs({ q: 'support enforcement', refs: ['__pub__/allowed/'], limit: 10 }, { client });
220
- const ts = titles(rows);
221
- assert.equal(ts, 'PUBOK', `expected only PUBOK, got [${ts}]`);
222
- assert.ok(rows[0].body.includes('support enforcement guide'), 'the body comes back with the hit');
223
- });
224
-
225
- await t('searchPublicDocs with no allowlist returns nothing at all', async () => {
226
- assert.deepEqual(await search.searchPublicDocs({ q: 'support enforcement', refs: [] }, { client }), []);
227
- });
228
- } finally {
229
- await client.query('ROLLBACK').catch(() => {});
230
- client.release();
231
- await pool.end().catch(() => {});
232
- }
233
- }
234
-
235
137
  console.log(`\n${passed} passed, ${failed} failed`);
236
138
  // EXIT CODE, NOT process.exit() (task 1003848). A hard exit tears the event loop down
237
139
  // underneath any handle that is still closing — and pg leaves one behind after a