@bongos/core 1.21.176 → 1.21.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +186 -496
- package/.claude/skills/planning-session/SKILL.md +2 -2
- 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/config/scheduled-routines.json +3 -8
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
- package/docs/api/openapi.json +1 -87
- package/docs/api-reference.md +1 -8
- package/docs/canonical-permissions.md +4 -4
- package/docs/copy-inventory.md +25 -39
- package/docs/copy-registry.json +73 -200
- package/docs/file-map.md +6 -11
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -2
- package/docs/onboarding/diagrams/README.md +3 -7
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/page-inventory.json +4 -7
- package/docs/page-readings.json +1520 -1569
- package/docs/recipes/ops-gotchas.md +3 -1
- package/modules/autonomy/db.js +9 -0
- package/modules/autonomy/module.json +2 -2
- package/modules/autonomy/pollers/daily-checks.js +262 -0
- package/modules/autonomy/pollers/routine-timers.js +7 -5
- package/modules/autonomy/rank-roles.js +64 -0
- package/modules/autonomy/server-schedule.js +37 -0
- 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/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 +36 -0
- package/scripts/bongos/adopt-preflight.js +3 -3
- package/scripts/bongos/api-alias-caller-scan.js +2 -2
- package/scripts/bongos/audit-authorship.sh +1 -8
- package/scripts/bongos/audit-rank-roles.js +15 -56
- package/scripts/bongos/bump-version.js +46 -9
- package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
- package/scripts/bongos/diagram-facts.js +1 -1
- package/scripts/bongos/do-api.js +1 -1
- package/scripts/bongos/doc-cli-guard.js +2 -0
- package/scripts/bongos/doc-control-claims.js +2 -2
- package/scripts/bongos/fitness-checks-identity.js +4 -15
- package/scripts/bongos/gen-diagrams.js +3 -3
- 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/pulse.js +5 -3
- package/scripts/bongos/routine-schedule.js +38 -3
- package/scripts/bongos/run-unit-tests.js +17 -5
- package/scripts/bongos/ship-regen.js +2 -3
- package/scripts/gds/{goal-names.js → hard-floor.js} +3 -4
- package/scripts/security/dep-audit.js +50 -124
- 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/autonomy_daily_checks.mjs +201 -0
- package/tests/bump_version.mjs +52 -18
- package/tests/claude_materialize.mjs +12 -12
- package/tests/copy_desk_flags_db.mjs +22 -14
- package/tests/effective_visibility_predicate.mjs +0 -18
- package/tests/gating_review_retired.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/routine_capture_kind.mjs +18 -7
- package/tests/routine_schedule.mjs +42 -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/workflow_exec_paths.mjs +9 -11
- package/modules/lifecycle/routes/gate-approvals.js +0 -299
- package/scripts/bongos/artifact-staleness.js +0 -267
- package/scripts/bongos/ci-grade.js +0 -82
- package/scripts/bongos/classify-discipline.js +0 -153
- package/scripts/bongos/codemod-rename-src-gds.js +0 -145
- package/scripts/bongos/coreB-carve-proof.sh +0 -725
- package/scripts/bongos/gate-review.js +0 -330
- package/scripts/bongos/goal-names.js +0 -102
- package/scripts/bongos/rename-goals.js +0 -109
- package/scripts/bongos/seed-adopt-tasks.js +0 -75
- package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
- package/scripts/bongos/seed-board-room-tasks.js +0 -196
- package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
- package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
- package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
- package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
- package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
- package/scripts/bongos/seed-bongos-tasks.js +0 -214
- package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
- package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
- package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
- package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
- package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
- package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
- package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
- package/scripts/bongos/seed-governance-tasks.js +0 -340
- package/scripts/bongos/seed-medusa-tasks.js +0 -440
- package/scripts/bongos/seed-module-upstreaming.js +0 -219
- package/scripts/bongos/seed-productization-tasks.js +0 -235
- package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
- package/scripts/bongos/seed-provisioning-tasks.js +0 -83
- package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
- package/scripts/bongos/seed-v2-world-tasks.js +0 -283
- package/scripts/gds/artifact-staleness.js +0 -7
- package/scripts/gds/ci-grade.js +0 -7
- package/scripts/gds/classify-discipline.js +0 -7
- package/scripts/gds/codemod-rename-src-gds.js +0 -7
- package/scripts/gds/coreB-carve-proof.sh +0 -8
- package/scripts/gds/gate-review.js +0 -7
- package/scripts/gds/rename-goals.js +0 -7
- package/scripts/gds/seed-adopt-tasks.js +0 -7
- package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
- package/scripts/gds/seed-board-room-tasks.js +0 -6
- package/scripts/gds/seed-bongos-app-tasks.js +0 -7
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
- package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
- package/scripts/gds/seed-bongos-tasks.js +0 -7
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
- package/scripts/gds/seed-bv1-community-tasks.js +0 -6
- package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
- package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
- package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
- package/scripts/gds/seed-gds-v4-tasks.js +0 -7
- package/scripts/gds/seed-governance-tasks.js +0 -7
- package/scripts/gds/seed-medusa-tasks.js +0 -7
- package/scripts/gds/seed-module-upstreaming.js +0 -7
- package/scripts/gds/seed-productization-tasks.js +0 -7
- package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
- package/scripts/gds/seed-provisioning-tasks.js +0 -7
- package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
- package/scripts/gds/seed-v2-world-tasks.js +0 -7
- package/scripts/migrate_cost_ledger.sh +0 -178
- package/tests/artifact_staleness.mjs +0 -207
- package/tests/gate_approvals.mjs +0 -611
- package/tests/gate_review.mjs +0 -152
- package/tests/goal_names.mjs +0 -120
- package/tests/newcomer_generator.mjs +0 -48
|
@@ -126,6 +126,16 @@ function carve(parent, names, why) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const ADDED_SINCE_R101 = [
|
|
129
|
+
// task 1004859: the hard-floor list moved out of gate-review.js (retired below) into
|
|
130
|
+
// its own file, which keeps the self-protection gate-review.js had. Anchored on the
|
|
131
|
+
// retired entry so it lands where the list used to live.
|
|
132
|
+
{
|
|
133
|
+
after: 'scripts/bongos/gate-review.js',
|
|
134
|
+
glob: 'scripts/bongos/hard-floor.js',
|
|
135
|
+
why: 'the trust-boundary hard-floor list, moved out of the removed gate-review.js; '
|
|
136
|
+
+ 'publish-manifest.js and adopt-preflight.js read it, so an edit here changes what '
|
|
137
|
+
+ 'the public mirror withholds and what an adopted repo is warned about.',
|
|
138
|
+
},
|
|
129
139
|
// task 1003366, audit ref MIT1 (2026-08-29 security audit, goal 1000084). The EIGHT
|
|
130
140
|
// execution roots the merge resolver runs out of a cloned branch. The audit named four
|
|
131
141
|
// of them ("the three gen-* scripts and .gitattributes"); modules/lifecycle/executed-
|
|
@@ -330,6 +340,12 @@ const RENAMED_SINCE_R101 = [
|
|
|
330
340
|
// protection from code that still runs. Applied LAST, after renames and additions,
|
|
331
341
|
// so an addition anchored on a retired entry still resolves.
|
|
332
342
|
const RETIRED_SINCE_R101 = [
|
|
343
|
+
...['gate-review.js', 'ci-grade.js'].map((f) => ({
|
|
344
|
+
glob: `scripts/bongos/${f}`,
|
|
345
|
+
why: 'task 1004859 deleted the two drivers of the planned gate-review and grade-gate '
|
|
346
|
+
+ 'CI checks (ADR 0058). No workflow ever ran them and branch protection requires '
|
|
347
|
+
+ 'only the unit check; the hard-floor list gate-review.js held moved to hard-floor.js.',
|
|
348
|
+
})),
|
|
333
349
|
...['boxes.js', 'box-access.js', 'box-credential.js'].map((f) => ({
|
|
334
350
|
glob: `modules/dev-box/${f}`,
|
|
335
351
|
why: 'task 1003892 (goal 1000120) deleted the dev-box module outright — the rank floor '
|
|
@@ -446,7 +462,7 @@ test('every glob added since R101 carries a documented reason', () => {
|
|
|
446
462
|
// floor-gating ordinary source at Metic — src/bongos/logger.js, src/branding.js and
|
|
447
463
|
// src/build-info.js are all in the closure — and locking newcomers out of routine work.
|
|
448
464
|
// The entrypoints are different in kind: they exist ONLY to be executed by the pipeline,
|
|
449
|
-
// which is exactly what group 'pipeline' already means for ship.js and
|
|
465
|
+
// which is exactly what group 'pipeline' already means for ship.js and hard-floor.js.
|
|
450
466
|
test('every execution root in the closure is a protected surface (MIT1 drift tripwire)', () => {
|
|
451
467
|
const ec = require(path.join(ROOT, 'modules', 'lifecycle', 'executed-closure.js'));
|
|
452
468
|
// ENTRYPOINTS lists both script-tree spellings across the C-series rename window; only
|
|
@@ -657,10 +673,10 @@ test('every credential reader in the tree is a protected surface (1002891 tripwi
|
|
|
657
673
|
}
|
|
658
674
|
});
|
|
659
675
|
|
|
660
|
-
// ---------- the hard-floor subset:
|
|
676
|
+
// ---------- the hard-floor subset: the second list may narrow, never diverge ----------
|
|
661
677
|
//
|
|
662
678
|
// task 1002875, from idea 1000620 (raised as a major by the BV1.R101 ship grader).
|
|
663
|
-
// scripts/bongos/gate-review.js
|
|
679
|
+
// scripts/bongos/hard-floor.js (gate-review.js until task 1004859) keeps HARD_FLOOR_GLOBS — the rank-INDEPENDENT
|
|
664
680
|
// escalate-to-owner set — and the comment above it declares the array to be "A subset of
|
|
665
681
|
// PROTECTED_GLOBS". That was a checkable invariant stated in prose and enforced by nothing.
|
|
666
682
|
//
|
|
@@ -673,13 +689,13 @@ test('every credential reader in the tree is a protected surface (1002891 tripwi
|
|
|
673
689
|
//
|
|
674
690
|
// Checked at BOTH altitudes on purpose. The source literal is what a reviewer reads and
|
|
675
691
|
// what the decision asked be parsed; the exported array is what compileGlobMatcher actually
|
|
676
|
-
// compiles
|
|
692
|
+
// compiles in hard-floor.js. Asserting they agree means a future refactor that
|
|
677
693
|
// computes or filters the list cannot leave the visible source saying one thing while the
|
|
678
694
|
// gate enforces another.
|
|
679
695
|
function hardFloorGlobsFromSource() {
|
|
680
|
-
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', '
|
|
696
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'), 'utf8');
|
|
681
697
|
const block = src.match(/const HARD_FLOOR_GLOBS = \[([\s\S]*?)\n\];/);
|
|
682
|
-
assert.ok(block, 'could not find the HARD_FLOOR_GLOBS array literal in
|
|
698
|
+
assert.ok(block, 'could not find the HARD_FLOOR_GLOBS array literal in hard-floor.js — '
|
|
683
699
|
+ 'if it was renamed or restructured, this test must be taught the new shape rather '
|
|
684
700
|
+ 'than deleted: it is the only thing holding the declared subset honest.');
|
|
685
701
|
return [...block[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
|
|
@@ -688,21 +704,21 @@ function hardFloorGlobsFromSource() {
|
|
|
688
704
|
test('every hard-floor glob is a protected surface (1002875 subset tripwire)', () => {
|
|
689
705
|
const fromSource = hardFloorGlobsFromSource();
|
|
690
706
|
assert.ok(fromSource.length >= 20,
|
|
691
|
-
`fixture sanity: parsed ${fromSource.length} hard-floor globs out of
|
|
707
|
+
`fixture sanity: parsed ${fromSource.length} hard-floor globs out of hard-floor.js, `
|
|
692
708
|
+ 'and the array has held about two dozen since it was written. A parse that returns '
|
|
693
709
|
+ 'few or none passes the subset check vacuously.');
|
|
694
710
|
|
|
695
|
-
const gr = require(path.join(ROOT, 'scripts', 'bongos', '
|
|
711
|
+
const gr = require(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'));
|
|
696
712
|
assert.deepEqual(fromSource, gr.HARD_FLOOR_GLOBS,
|
|
697
|
-
'the HARD_FLOOR_GLOBS literal in
|
|
713
|
+
'the HARD_FLOOR_GLOBS literal in hard-floor.js no longer matches the array the module '
|
|
698
714
|
+ 'exports and compiles into its matcher. The source and the enforced list must be the '
|
|
699
715
|
+ 'same thing, or a reviewer reading the file is reading a list the gate does not use.');
|
|
700
716
|
|
|
701
717
|
const registry = new Set(ppc.PROTECTED_GLOBS);
|
|
702
718
|
for (const glob of gr.HARD_FLOOR_GLOBS) {
|
|
703
719
|
assert.ok(registry.has(glob),
|
|
704
|
-
`'${glob}' is a HARD FLOOR surface in scripts/bongos/
|
|
705
|
-
+ '
|
|
720
|
+
`'${glob}' is a HARD FLOOR surface in scripts/bongos/hard-floor.js — a trust-boundary path `
|
|
721
|
+
+ 'whatever the author\'s rank — but it is not in '
|
|
706
722
|
+ 'modules/government/protected-surfaces.json, so no rank floor covers it at all. '
|
|
707
723
|
+ 'The hard floor is declared to be a SUBSET of the protected set: it may be '
|
|
708
724
|
+ 'narrower (most protected paths are ordinary Metic work and should not ping the '
|
|
@@ -1290,8 +1306,8 @@ test('compileGlobMatcher folds BOTH sides', () => {
|
|
|
1290
1306
|
assert.ok(!m(null), 'nor is a missing path');
|
|
1291
1307
|
});
|
|
1292
1308
|
|
|
1293
|
-
test('
|
|
1294
|
-
const gr = require(path.join(ROOT, 'scripts', 'bongos', '
|
|
1309
|
+
test('the hard-floor matcher folds a CASE-VARIANT gate surface', () => {
|
|
1310
|
+
const gr = require(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'));
|
|
1295
1311
|
const literal = gr.HARD_FLOOR_GLOBS.find((g) => !g.includes('*') && !g.endsWith('/'));
|
|
1296
1312
|
assert.ok(literal, 'no literal hard-floor glob — this test is reading the wrong shape');
|
|
1297
1313
|
assert.ok(gr.matchesHardFloor(literal), 'the exact spelling must escalate (sanity)');
|
|
@@ -510,7 +510,6 @@ test('each contributed source gates on EXACTLY the atom its own list route requi
|
|
|
510
510
|
const factories = [
|
|
511
511
|
'modules/ideas/routes/blockers.js',
|
|
512
512
|
'modules/lifecycle/routes/lifecycle.js',
|
|
513
|
-
'modules/lifecycle/routes/gate-approvals.js',
|
|
514
513
|
'modules/onboarding/routes/access-requests.js',
|
|
515
514
|
'modules/security/routes/reports.js',
|
|
516
515
|
'modules/autonomy/routes/autonomy.js',
|
|
@@ -528,7 +527,6 @@ test('each contributed source gates on EXACTLY the atom its own list route requi
|
|
|
528
527
|
override_requests: ['modules/lifecycle/routes/tasks.js', 'GET /override-requests'],
|
|
529
528
|
removal_proposals: ['modules/lifecycle/routes/removal-proposals.js', 'GET /removal-proposals'],
|
|
530
529
|
criteria_uat: ['modules/lifecycle/routes/done-when.js', 'GET /done-when/pending-review'],
|
|
531
|
-
gate_approvals: ['modules/lifecycle/routes/gate-approvals.js', 'GET /gate-approvals'],
|
|
532
530
|
access_requests: ['modules/onboarding/routes/access-requests.js', 'GET /access-requests'],
|
|
533
531
|
redteam_reports: ['modules/security/routes/reports.js', 'GET /security/reports'],
|
|
534
532
|
core_update: ['src/bongos/routes/core-update.js', 'GET /core-update'],
|
|
@@ -24,7 +24,6 @@ const lifeActs = require(path.join(ROOT, 'modules', 'lifecycle', 'matter-acts.js
|
|
|
24
24
|
const lifeDb = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
|
|
25
25
|
const doneWhen = require(path.join(ROOT, 'modules', 'lifecycle', 'done-when.js'));
|
|
26
26
|
const doneWhenRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'done-when.js'));
|
|
27
|
-
const gateRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'gate-approvals.js'));
|
|
28
27
|
const lifeDocket = require(path.join(ROOT, 'modules', 'lifecycle', 'governor-docket.js'));
|
|
29
28
|
const buildersRoute = require(path.join(ROOT, 'src', 'bongos', 'routes', 'builders.js'));
|
|
30
29
|
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
@@ -47,7 +46,6 @@ test('each building\'s act is checked strictly when the matter is opened: shape,
|
|
|
47
46
|
const v = decisionBoard.validateMatterAction;
|
|
48
47
|
assert.deepEqual(v('automation', { type: 'switch_on' }), { ok: true, action: { type: 'switch_on' } });
|
|
49
48
|
assert.deepEqual(v('automation', { type: 'allow_goal', goal_id: '12' }).action, { type: 'allow_goal', goal_id: 12 });
|
|
50
|
-
assert.deepEqual(v('release', { type: 'approve_gate', pr: 1500 }).action, { type: 'approve_gate', pr: 1500 });
|
|
51
49
|
assert.deepEqual(v('release', { type: 'approve_override', request_id: 4 }).action, { type: 'approve_override', request_id: 4 });
|
|
52
50
|
assert.deepEqual(v('review', { type: 'satisfy_criterion', criterion_id: 9 }).action, { type: 'satisfy_criterion', criterion_id: 9 });
|
|
53
51
|
assert.deepEqual(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: 50 }).action, { type: 'set_budget', builder_id: 7, monthly_usd: 50 });
|
|
@@ -58,6 +56,8 @@ test('each building\'s act is checked strictly when the matter is opened: shape,
|
|
|
58
56
|
assert.equal(v('automation', { type: 'satisfy_criterion', criterion_id: 9 }).reason, 'bad_action', 'an act from another building');
|
|
59
57
|
assert.equal(v('automation', { type: 'switch_on', goal_id: 3 }).reason, 'unknown_field');
|
|
60
58
|
assert.equal(v('automation', { type: 'allow_goal' }).field, 'action.goal_id');
|
|
59
|
+
// task 1004859 removed the gate-approval route, so Release no longer carries an act for it
|
|
60
|
+
assert.equal(v('release', { type: 'approve_gate', pr: 1500 }).reason, 'bad_action', 'the retired gate act is refused');
|
|
61
61
|
assert.equal(v('automation', { type: 'allow_goal', goal_id: 0 }).reason, 'bad_action');
|
|
62
62
|
assert.equal(v('automation', { type: 'allow_goal', goal_id: true }).reason, 'bad_action');
|
|
63
63
|
assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: -1 }).reason, 'bad_action');
|
|
@@ -161,7 +161,7 @@ test('a passed Review matter signs the criterion off by override, once', async (
|
|
|
161
161
|
assert.match(signed[0].reason, /passed Review matter/);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
test('a passed Release matter approves an override
|
|
164
|
+
test('a passed Release matter approves an override; a request already decided is said, not thrown', async () => {
|
|
165
165
|
const calls = [];
|
|
166
166
|
await stub(lifeDb, {
|
|
167
167
|
decideOverrideRequest: async (a) => {
|
|
@@ -169,14 +169,12 @@ test('a passed Release matter approves an override or clears a gate; a request a
|
|
|
169
169
|
if (a.requestId === 5) throw Object.assign(new Error('closed'), { code: 'REQUEST_NOT_OPEN' });
|
|
170
170
|
return {};
|
|
171
171
|
},
|
|
172
|
-
},
|
|
172
|
+
}, async () => {
|
|
173
173
|
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 4 } })).out.acted.done, true);
|
|
174
174
|
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 5 } })).out.acted.reason, 'request_not_open');
|
|
175
|
-
|
|
176
|
-
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_gate', pr: 2 } })).out.acted.reason, 'pr_not_open');
|
|
177
|
-
}));
|
|
175
|
+
});
|
|
178
176
|
assert.deepEqual(calls[0], ['override', { requestId: 4, archonId: '42', decision: 'approve', note: 'A passed Release matter (matter 8) decided it.' }]);
|
|
179
|
-
assert.equal(calls
|
|
177
|
+
assert.equal(calls.length, 2);
|
|
180
178
|
});
|
|
181
179
|
|
|
182
180
|
test('a passed Treasury matter sets the limit through the same write as PATCH /builders/:id/budget', async () => {
|
package/tests/hall_city.mjs
CHANGED
|
@@ -384,7 +384,7 @@ const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1
|
|
|
384
384
|
|
|
385
385
|
test('the page reads the three feeds and draws the docket\'s pins and line', async () => {
|
|
386
386
|
const soon = new Date(Date.now() + 2 * 3600000).toISOString();
|
|
387
|
-
const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: '
|
|
387
|
+
const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: 'override_requests:3', building: 'release', state: 'moving' }), matter({ id: 'removal_proposals:5', building: 'disputes', state: 'settled' })];
|
|
388
388
|
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
389
389
|
assert.deepEqual([...p.calls].sort(), ['/api/bongos/goals', '/api/bongos/goals/rollups', '/api/bongos/government/docket']);
|
|
390
390
|
const pins = p.byId.get('cityPins').children;
|
|
@@ -182,8 +182,9 @@ test('every act maps to the route that does it, with the body that route accepts
|
|
|
182
182
|
const ov = card([{ id: 'override_requests:9', source: 'override_requests', building: 'release', state: 'needs', title: 'Release', ref: { type: 'override_request', id: '9' } }], 'override_requests:9');
|
|
183
183
|
assert.deepEqual(C.request(ov, 'release'), { method: 'POST', url: '/api/bongos/override-requests/9/decide', body: { decision: 'approve' } });
|
|
184
184
|
assert.equal(C.request(ov, 'hold'), null, 'holding records nothing');
|
|
185
|
+
// task 1004859 removed POST /gate-approvals/:pr/approve: a stale gate matter must send nothing
|
|
185
186
|
const gate = card([{ id: 'gate_approvals:812', source: 'gate_approvals', building: 'release', state: 'needs', title: 'A PR', ref: { type: 'pull_request', id: 812 } }], 'gate_approvals:812');
|
|
186
|
-
assert.
|
|
187
|
+
assert.equal(C.request(gate, 'release'), null, 'the retired gate route is never called');
|
|
187
188
|
});
|
|
188
189
|
|
|
189
190
|
// Task 1004559 moved the core update onto the Release desk, check first: the ONLY
|
|
@@ -406,7 +407,6 @@ const SOURCE_FIXTURES = () => {
|
|
|
406
407
|
{ id: 'redteam_reports:4', source: 'redteam_reports', building: 'disputes', rule: 'decide', state: 'needs', title: 'Security report: sign-in limit skipped', href: '/watch', ref: { type: 'vulnerability_report', id: '4' }, acts: [{ key: 'confirm' }, { key: 'dispute' }] },
|
|
407
408
|
{ id: 'criteria_uat:3', source: 'criteria_uat', building: 'review', rule: 'signoff', state: 'needs', title: 'The export runs nightly', href: '/goals#/goal/7', ref: { type: 'criterion', id: '3' }, acts: [{ key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] }, { key: 'force', options: [{ key: 'tested_by_hand', label: 'tested by hand' }] }] },
|
|
408
409
|
{ id: 'core_update:1.21.20', source: 'core_update', building: 'release', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } },
|
|
409
|
-
{ id: 'gate_approvals:88', source: 'gate_approvals', building: 'release', rule: 'decide', state: 'needs', title: 'PR 88 waits at the gate', href: '/gate', ref: { type: 'pull_request', id: '88' } },
|
|
410
410
|
{ id: 'autobongos_fence:project', source: 'autobongos_fence', building: 'automation', rule: 'decide', state: 'needs', title: 'Unattended building is off', href: '/autobongos', ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_on' }] },
|
|
411
411
|
{ id: 'module_rechecks:7', source: 'module_rechecks', building: 'release', rule: 'rule', state: 'needs', title: 'image-tools 1.2.0 failed its security check', href: '/modules', ref: { type: 'module_version', id: 'image-tools@1.2.0' }, acts: [{ key: 'recheck', options: [{ key: 'fixed', label: 'fixed since' }] }] },
|
|
412
412
|
// task 1004712: a dry pot's borrow from the reserve
|
|
@@ -91,9 +91,9 @@ test('the shared confirm modal sits on --z-modal, closed by one hairline and no
|
|
|
91
91
|
const modal = css.slice(css.indexOf('.ov-modal {'), css.indexOf('.ov-modal .scroll__h'));
|
|
92
92
|
assert.ok(!/box-shadow/.test(modal), 'no shadow under the dialog');
|
|
93
93
|
assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
|
|
94
|
-
// task 1004829
|
|
95
|
-
//
|
|
96
|
-
assert.ok(!/ov-modal-bg/.test(read('gate.js')), 'gate.js opens no modal
|
|
94
|
+
// task 1004829 made the Gate's approve an ask card with no modal; task 1004859
|
|
95
|
+
// removed the approve altogether. The page still opens no modal — and styles none.
|
|
96
|
+
assert.ok(!/ov-modal-bg/.test(read('gate.js')), 'gate.js opens no modal');
|
|
97
97
|
assert.ok(!/-modal-bg/.test(read('gate.css')), 'gate.css styles no modal of its own');
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -303,15 +303,19 @@ test('a failed load on Watch and Sessions is said in the page body', () => {
|
|
|
303
303
|
assert.match(read('sessions.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
-
test('the
|
|
306
|
+
test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is told the controls are the Archon\'s', () => {
|
|
307
|
+
// task 1004859 (owner, 2026-10-25: "Keep page, drop PR list"). The held-PR list
|
|
308
|
+
// waited on a gate-review CI check that never ran; its route is gone, so the page
|
|
309
|
+
// must not call it, and the URL stays /gate so bookmarks hold.
|
|
307
310
|
const js = read('gate.js');
|
|
308
|
-
|
|
309
|
-
assert.
|
|
310
|
-
assert.match(js, /if \(
|
|
311
|
-
assert.match(js, /
|
|
312
|
-
assert.match(js, /'data-act': 'approve'/);
|
|
313
|
-
assert.match(js, /Waiting for approval/, 'everyone else reads the facts as a plain row');
|
|
311
|
+
assert.ok(!/['\`]\/gate-approvals/.test(js), 'gate.js no longer calls the removed /gate-approvals routes');
|
|
312
|
+
assert.ok(!/renderRow\(pr|OTBAsk/.test(js), 'no held-PR row or ask card is drawn');
|
|
313
|
+
assert.match(js, /if \(rank !== 'archon'\) \{ showArchonOnly\(\); return; \}/, 'a Metic gets the note, not an empty page');
|
|
314
|
+
assert.match(js, /The Autobongos controls are for the \$\{rankLabel\('archon'\)\}/, 'the rank is named from the pack');
|
|
314
315
|
const html = read('gate.html');
|
|
315
|
-
assert.
|
|
316
|
-
assert.match(html, /
|
|
316
|
+
assert.match(html, /<title>Autobongos — /, 'the page says what it is');
|
|
317
|
+
assert.match(html, /data-page-title="Autobongos"/);
|
|
318
|
+
assert.match(html, /id="gate-archon-only"/);
|
|
319
|
+
assert.ok(!/gate-list|hall-ask\.js/.test(html), 'the list mount and the ask-card script are gone');
|
|
320
|
+
assert.match(read('shell.js'), /\{ id: 'gate', label: 'Autobongos', icon: 'gate', href: '\/gate'/, 'the nav names it, at the same URL');
|
|
317
321
|
});
|
|
@@ -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);
|