@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.
- package/.bongos-core.json +147 -152
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +0 -6
- package/clients/bongos-client/index.cjs +0 -6
- package/clients/bongos-client/index.d.ts +0 -6
- package/clients/bongos-client/index.mjs +0 -6
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
- package/docs/api/openapi.json +1 -87
- package/docs/api-reference.md +1 -8
- package/docs/canonical-permissions.md +3 -3
- package/docs/copy-inventory.md +50 -64
- package/docs/copy-registry.json +109 -236
- package/docs/file-map.md +3 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -2
- package/docs/page-inventory.json +4 -7
- package/docs/page-readings.json +1520 -1569
- package/modules/government/catalog.js +10 -7
- package/modules/government/decision-board.js +0 -1
- package/modules/government/decision-rules.js +0 -1
- package/modules/government/docket.js +0 -1
- package/modules/government/protected-surfaces.js +1 -1
- package/modules/government/protected-surfaces.json +2 -3
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.css +4 -26
- package/modules/hall-ui/public/gate.html +12 -27
- package/modules/hall-ui/public/gate.js +26 -176
- package/modules/hall-ui/public/gate.states.json +4 -22
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/hall-ui/public/street-card.js +5 -9
- package/modules/hall-ui/public/street-desk.js +2 -2
- package/modules/hall-ui/public/street-inside.js +1 -1
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/public/work.js +67 -14
- package/modules/lifecycle/matter-acts.js +0 -6
- package/modules/lifecycle/module.json +0 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/bongos/adopt-preflight.js +3 -3
- package/scripts/bongos/audit-authorship.sh +1 -3
- package/scripts/bongos/bump-version.js +46 -9
- package/scripts/bongos/doc-cli-guard.js +2 -0
- package/scripts/bongos/doc-control-claims.js +2 -2
- package/scripts/bongos/government-walk.js +3 -9
- package/scripts/bongos/hard-floor.js +52 -0
- package/scripts/bongos/publish-manifest.js +3 -3
- package/scripts/bongos/run-unit-tests.js +17 -5
- package/scripts/gds/hard-floor.js +6 -0
- package/src/bongos/module-scope-map.js +2 -2
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +1 -8
- package/src/bongos/routes.js +1 -2
- package/src/module-api.js +1 -1
- package/src/modules.js +1 -1
- package/tests/acclaim_feed_evidence_db.mjs +42 -23
- package/tests/adr_0089_scope.mjs +7 -5
- package/tests/bump_version.mjs +52 -18
- package/tests/copy_desk_flags_db.mjs +22 -14
- package/tests/gating_review_retired.mjs +1 -1
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/government_act_alone_buildings.mjs +1 -3
- package/tests/government_board_membership_db.mjs +35 -22
- package/tests/government_protected_surfaces.mjs +29 -13
- package/tests/governor_docket.mjs +0 -2
- package/tests/governor_matter_acts.mjs +6 -8
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_matter_card.mjs +2 -2
- package/tests/hall_oversight_world.mjs +16 -12
- package/tests/hall_street_desk.mjs +4 -3
- package/tests/hall_street_inside.mjs +1 -1
- package/tests/helpers.mjs +34 -0
- package/tests/howto_state.mjs +33 -56
- package/tests/howto_state_db.mjs +68 -0
- package/tests/idea_credit_streams_db.mjs +11 -3
- package/tests/integration_parked_ratchet.mjs +7 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/page_notes_db.mjs +12 -8
- package/tests/page_tweak_no_rearm.mjs +50 -0
- package/tests/publish_manifest.mjs +64 -0
- package/tests/search_isolation.mjs +5 -103
- package/tests/search_isolation_db.mjs +140 -0
- package/tests/session_earnings_mirror_db.mjs +2 -0
- package/tests/work_board_live_refresh.mjs +236 -0
- package/tests/workflow_exec_paths.mjs +9 -11
- package/modules/lifecycle/routes/gate-approvals.js +0 -299
- package/scripts/bongos/ci-grade.js +0 -82
- package/scripts/bongos/gate-review.js +0 -330
- package/scripts/gds/ci-grade.js +0 -7
- package/scripts/gds/gate-review.js +0 -7
- package/tests/gate_approvals.mjs +0 -611
- 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'], ['
|
|
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'], ['
|
|
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
|
+
}
|
package/tests/howto_state.mjs
CHANGED
|
@@ -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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = '
|
|
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);
|
package/tests/page_notes_db.mjs
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
53
|
-
|
|
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
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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
|