@bongos/core 1.21.112 → 1.21.114
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 +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// tests/government_laws.mjs — STANDING ORDERS: the project's laws for its AI
|
|
2
|
+
// agents (task 1004706, goal 1000111 "government is how a project steers its AI").
|
|
3
|
+
//
|
|
4
|
+
// What this pins (the task's three done-when lines, and the walls behind them):
|
|
5
|
+
// 1. LAWS CAN BE PROPOSED, VOTED AND LISTED. A proposal (enact, repeal, or
|
|
6
|
+
// both) is validated strictly and filed as an amendment of kind 'laws',
|
|
7
|
+
// sat under the constitution IN FORCE; the board's ordinary vote decides
|
|
8
|
+
// it; GET /government/laws lists the book.
|
|
9
|
+
// 2. EACH LAW RECORDS WHO PASSED IT AND WHEN. A row names the sitting that
|
|
10
|
+
// passed it, its proposer and passed_at (the database clock, set by the
|
|
11
|
+
// trigger); the book reads the government it was decided under and who
|
|
12
|
+
// said yes. A repeal records its sitting and when.
|
|
13
|
+
// 3. NO ROUTE WRITES A LAW EXCEPT A PASSED AMENDMENT. One SQL writer (db.js),
|
|
14
|
+
// called from one place (law-board.js), only inside the transaction that
|
|
15
|
+
// closes a passed laws amendment, under a guard; the SQL carries the same
|
|
16
|
+
// condition; government_027's trigger refuses every other INSERT/UPDATE and
|
|
17
|
+
// every DELETE; no route names the write; the amendment routes cannot file
|
|
18
|
+
// a laws amendment by another door; and every government route that is not
|
|
19
|
+
// the propose route, driven for real, writes nothing.
|
|
20
|
+
//
|
|
21
|
+
// No law is seeded: the first laws are the owner's to ratify (task 1004708).
|
|
22
|
+
// DB-free: db reads/writes are monkeypatched by property.
|
|
23
|
+
//
|
|
24
|
+
// Run: node --test tests/government_laws.mjs
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import { test, after } from 'node:test';
|
|
28
|
+
import http from 'node:http';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
37
|
+
const laws = require(path.join(GOV, 'laws.js'));
|
|
38
|
+
const lawBoard = require(path.join(GOV, 'law-board.js'));
|
|
39
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
40
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
41
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
42
|
+
|
|
43
|
+
const MIGRATION = fs.readFileSync(path.join(GOV, 'migrations', 'government_027_laws.sql'), 'utf8');
|
|
44
|
+
const MIGRATION_CODE = MIGRATION.replace(/--[^\n]*/g, '');
|
|
45
|
+
|
|
46
|
+
async function withDb(stubs, fn) {
|
|
47
|
+
const originals = {};
|
|
48
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
49
|
+
try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
|
|
53
|
+
const ASK = { text: 'Ask like a human', effect_on_ai: 'Every question to a person is one short card with tap answers.', locks: [{ setting: 'ask_format', value: 'card' }] };
|
|
54
|
+
|
|
55
|
+
// ── 1. what a law is: validated strictly ────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
test('a law is one short line, an optional effect line, and at most eight locks — validated strictly, nothing corrected', () => {
|
|
58
|
+
const ok = laws.validateLawsProposal({ enact: [ASK, { text: ' Land only. Ask before going live ', locks: [{ setting: 'wandering', max: 'focused' }, { setting: 'parallel_agents', max: 3 }] }] });
|
|
59
|
+
assert.equal(ok.ok, true, JSON.stringify(ok));
|
|
60
|
+
assert.deepEqual(ok.proposal.enact[0], { text: 'Ask like a human', effect_on_ai: ASK.effect_on_ai, locks: [{ setting: 'ask_format', value: 'card' }] });
|
|
61
|
+
assert.equal(ok.proposal.enact[1].text, 'Land only. Ask before going live', 'whitespace trimmed, nothing else');
|
|
62
|
+
assert.equal(ok.proposal.enact[1].effect_on_ai, null);
|
|
63
|
+
assert.deepEqual(ok.proposal.repeal, []);
|
|
64
|
+
|
|
65
|
+
const refused = [
|
|
66
|
+
[{}, 'nothing_to_change'],
|
|
67
|
+
[{ enact: [], repeal: [] }, 'nothing_to_change'],
|
|
68
|
+
[null, 'bad_proposal'],
|
|
69
|
+
[{ enact: [ASK], extra: 1 }, 'unknown_field'],
|
|
70
|
+
[{ enact: [{ text: 'no' }] }, 'bad_text'],
|
|
71
|
+
[{ enact: [{ text: 'x'.repeat(laws.LAW_TEXT_MAX + 1) }] }, 'bad_text'],
|
|
72
|
+
[{ enact: [{ text: 'Two\nlines' }] }, 'bad_text'],
|
|
73
|
+
[{ enact: [{ text: 42 }] }, 'bad_text'],
|
|
74
|
+
[{ enact: [{ text: 'Fine words', effect_on_ai: 'two\nlines' }] }, 'bad_effect'],
|
|
75
|
+
[{ enact: [{ text: 'Fine words', author: 'me' }] }, 'unknown_field'],
|
|
76
|
+
[{ enact: [{ text: 'Fine words', locks: 'ask_format' }] }, 'bad_locks'],
|
|
77
|
+
[{ enact: [{ text: 'Fine words', locks: Array.from({ length: laws.MAX_LOCKS + 1 }, (_, i) => ({ setting: `s${i}`, value: 1 })) }] }, 'bad_locks'],
|
|
78
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'Bad Key', value: 1 }] }] }, 'bad_lock_setting'],
|
|
79
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format' }] }] }, 'lock_needs_value_or_max'],
|
|
80
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format', value: 'card', max: 'card' }] }] }, 'lock_needs_value_or_max'],
|
|
81
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format', value: {} }] }] }, 'bad_lock_value'],
|
|
82
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format', max: true }] }] }, 'bad_lock_max'],
|
|
83
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format', value: 'card', why: 'x' }] }] }, 'unknown_field'],
|
|
84
|
+
[{ enact: [{ text: 'Fine words', locks: [{ setting: 'ask_format', value: 'card' }, { setting: 'ask_format', value: 'free_text' }] }] }, 'setting_locked_twice'],
|
|
85
|
+
[{ enact: [ASK, { text: 'ask like a HUMAN' }] }, 'duplicate_law'],
|
|
86
|
+
[{ enact: Array.from({ length: 11 }, (_, i) => ({ text: `Law number ${i}` })) }, 'bad_enact'],
|
|
87
|
+
[{ repeal: [0] }, 'bad_law_id'],
|
|
88
|
+
[{ repeal: ['x'] }, 'bad_law_id'],
|
|
89
|
+
[{ repeal: [3, '3'] }, 'duplicate_law_id'],
|
|
90
|
+
];
|
|
91
|
+
for (const [input, reason] of refused) {
|
|
92
|
+
const v = laws.validateLawsProposal(input);
|
|
93
|
+
assert.equal(v.ok, false, `${JSON.stringify(input)} must be refused`);
|
|
94
|
+
assert.equal(v.reason, reason, `${JSON.stringify(input)}: ${JSON.stringify(v)}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('against the laws in force: a repeal names one in force, no law twice, no setting locked by two laws', () => {
|
|
99
|
+
const inForce = [{ id: '3', text: 'Ask like a human', locks: [{ setting: 'ask_format', value: 'card' }] }, { id: '4', text: 'No spend limit', locks: [] }];
|
|
100
|
+
const p = (x) => laws.validateLawsProposal(x).proposal;
|
|
101
|
+
assert.deepEqual(laws.checkAgainstInForce(p({ repeal: ['9'] }), inForce), { ok: false, reason: 'not_in_force', law_id: '9' });
|
|
102
|
+
assert.equal(laws.checkAgainstInForce(p({ enact: [{ text: 'ask like a human' }] }), inForce).reason, 'already_in_force');
|
|
103
|
+
const locked = laws.checkAgainstInForce(p({ enact: [{ text: 'Free text is fine', locks: [{ setting: 'ask_format', value: 'free_text' }] }] }), inForce);
|
|
104
|
+
assert.equal(locked.reason, 'setting_already_locked');
|
|
105
|
+
assert.equal(locked.law_id, '3');
|
|
106
|
+
assert.equal(laws.checkAgainstInForce(p({ enact: [{ text: 'A', locks: [] }, { text: 'Cards only', locks: [{ setting: 'render', value: 'minimal' }] }, { text: 'Rich cards', locks: [{ setting: 'render', value: 'rich' }] }].slice(1) }), inForce).reason, 'setting_already_locked', 'two new laws cannot lock one setting');
|
|
107
|
+
// repeal-and-replace in one amendment is the way to change a law
|
|
108
|
+
assert.deepEqual(laws.checkAgainstInForce(p({ repeal: ['3'], enact: [{ text: 'Ask like a human', locks: [{ setting: 'ask_format', value: 'free_text' }] }] }), inForce), { ok: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── 1. proposed: an amendment of kind 'laws', under the board in force ──────
|
|
112
|
+
|
|
113
|
+
test('proposing files a laws amendment, sat under the constitution IN FORCE, in the record\'s own words', async () => {
|
|
114
|
+
let created = null; let opened = null;
|
|
115
|
+
await withDb({
|
|
116
|
+
listLaws: async () => [],
|
|
117
|
+
createAmendment: async (args) => { created = args; return { id: '7', ...args }; },
|
|
118
|
+
openBoardItem: async (args) => { opened = args; return { id: '31', subject_type: args.subjectType, subject_id: args.subjectId, constitution: args.constitution }; },
|
|
119
|
+
}, async () => {
|
|
120
|
+
const out = await board.proposeLaws({ enact: [ASK], proposedBy: '42' });
|
|
121
|
+
assert.equal(out.ok, true, JSON.stringify(out));
|
|
122
|
+
});
|
|
123
|
+
assert.equal(created.kind, 'laws');
|
|
124
|
+
assert.deepEqual(created.proposed, { laws: { enact: [{ text: ASK.text, effect_on_ai: ASK.effect_on_ai, locks: ASK.locks }], repeal: [] } });
|
|
125
|
+
assert.equal(created.rationaleMd, 'Law: Ask like a human.');
|
|
126
|
+
assert.equal(String(created.proposedBy), '42');
|
|
127
|
+
assert.equal(opened.subjectType, 'constitutional_amendment', 'one amendment subject, one pass path');
|
|
128
|
+
assert.equal(opened.constitution.pass_rule, 'first_ratifier', 'decided under the board in force (this checkout: monarchy)');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('proposing refuses a malformed law, a repeat, an absent law and a missing proposer — before any write', async () => {
|
|
132
|
+
const never = async () => { throw new Error('must not be reached'); };
|
|
133
|
+
await withDb({
|
|
134
|
+
listLaws: async () => [{ id: '3', text: 'Ask like a human', locks: [] }],
|
|
135
|
+
createAmendment: never,
|
|
136
|
+
openBoardItem: never,
|
|
137
|
+
}, async () => {
|
|
138
|
+
assert.equal((await board.proposeLaws({ enact: [{ text: 'Ask like a human' }], proposedBy: '42' })).reason, 'already_in_force');
|
|
139
|
+
assert.equal((await board.proposeLaws({ repeal: ['8'], proposedBy: '42' })).reason, 'not_in_force');
|
|
140
|
+
assert.equal((await board.proposeLaws({ enact: [{ text: 'Two\nlines' }], proposedBy: '42' })).reason, 'bad_text');
|
|
141
|
+
assert.equal((await board.proposeLaws({ enact: [{ text: 'Fine words' }], proposedBy: null })).reason, 'missing_proposer');
|
|
142
|
+
assert.equal((await board.proposeLaws({ proposedBy: '42' })).reason, 'nothing_to_change');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── 1+2. voted: a passed sitting writes the laws, inside its close ───────────
|
|
147
|
+
|
|
148
|
+
const lawsAmendment = { id: '7', kind: 'laws', proposed_by: '42', proposed: { laws: { enact: [{ text: ASK.text, effect_on_ai: ASK.effect_on_ai, locks: ASK.locks }], repeal: ['3'] } } };
|
|
149
|
+
const amendmentItem = (over = {}) => ({ id: '31', subject_type: 'constitutional_amendment', subject_id: '7', opened_by: '42', closed_at: null, expired: false, constitution: MONARCHY, ...over });
|
|
150
|
+
|
|
151
|
+
// A closeBoardItem stub that behaves like the real one: afterClose runs INSIDE
|
|
152
|
+
// the "transaction"; a throw from it rolls the close back (nothing is closed).
|
|
153
|
+
function txClose(log) {
|
|
154
|
+
return async ({ itemId, outcome, afterClose }) => {
|
|
155
|
+
const closedRow = { ...amendmentItem({ id: itemId }), closed_at: 'T', outcome };
|
|
156
|
+
const client = { tx: true };
|
|
157
|
+
if (typeof afterClose === 'function') {
|
|
158
|
+
try { await afterClose(client, closedRow); } catch (err) { log.push('rolled_back'); throw err; }
|
|
159
|
+
}
|
|
160
|
+
log.push('committed');
|
|
161
|
+
return closedRow;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test('a PASSED laws amendment writes its laws inside the closing transaction, naming the sitting and the proposer — and never touches the board block', async () => {
|
|
166
|
+
const log = [];
|
|
167
|
+
let written = null;
|
|
168
|
+
const origApply = config.applyBoardAmendment;
|
|
169
|
+
// closeItem catches an apply failure and only logs it, so a throwing stub would
|
|
170
|
+
// pass silently: count the calls instead.
|
|
171
|
+
let boardWrites = 0;
|
|
172
|
+
config.applyBoardAmendment = () => { boardWrites += 1; return {}; };
|
|
173
|
+
try {
|
|
174
|
+
await withDb({
|
|
175
|
+
getVotesForItem: async () => [{ voter_id: '42', direction: 'yes' }],
|
|
176
|
+
getAmendment: async (id) => { assert.equal(String(id), '7'); return lawsAmendment; },
|
|
177
|
+
applyLaws: async (client, args) => {
|
|
178
|
+
assert.equal(client.tx, true, 'the closing transaction\'s client');
|
|
179
|
+
written = args;
|
|
180
|
+
return { enacted: [{ id: 11, text: ASK.text }], repealed: [{ id: 3 }] };
|
|
181
|
+
},
|
|
182
|
+
closeBoardItem: txClose(log),
|
|
183
|
+
getBoardItemAuthor: async () => '42',
|
|
184
|
+
insertAuthorRatifyKarma: async () => true,
|
|
185
|
+
}, async () => {
|
|
186
|
+
const out = await board.closeItem(amendmentItem());
|
|
187
|
+
assert.equal(out.outcome, 'passed');
|
|
188
|
+
assert.deepEqual(out.applied_laws, { enacted: [{ id: '11', text: ASK.text }], repealed: ['3'] });
|
|
189
|
+
assert.equal(out.applied_constitution, undefined);
|
|
190
|
+
assert.equal(out.applied_decision_rules, undefined);
|
|
191
|
+
});
|
|
192
|
+
} finally { config.applyBoardAmendment = origApply; }
|
|
193
|
+
assert.equal(boardWrites, 0, 'the board block was not rewritten');
|
|
194
|
+
assert.deepEqual(log, ['committed']);
|
|
195
|
+
assert.equal(String(written.itemId), '31', 'who passed it: the sitting');
|
|
196
|
+
assert.equal(String(written.proposedBy), '42', 'who proposed it');
|
|
197
|
+
assert.deepEqual(written.enact.map((l) => l.text), [ASK.text]);
|
|
198
|
+
assert.deepEqual(written.repeal, ['3']);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('a RETURNED laws amendment writes nothing', async () => {
|
|
202
|
+
await withDb({
|
|
203
|
+
getVotesForItem: async () => [{ voter_id: '9', direction: 'no', reason_md: 'r', section_key: 'fit' }],
|
|
204
|
+
getAmendment: async () => { throw new Error('must not be reached — a return applies nothing'); },
|
|
205
|
+
applyLaws: async () => { throw new Error('must not be reached'); },
|
|
206
|
+
closeBoardItem: async ({ outcome, afterClose }) => { assert.equal(afterClose, null); return { ...amendmentItem(), closed_at: 'T', outcome }; },
|
|
207
|
+
}, async () => {
|
|
208
|
+
const out = await board.closeItem(amendmentItem());
|
|
209
|
+
assert.equal(out.outcome, 'returned');
|
|
210
|
+
assert.equal(out.applied_laws, undefined);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('a failed write rolls the close back — the sitting is retried, never passed-but-unapplied', async () => {
|
|
215
|
+
const log = [];
|
|
216
|
+
await withDb({
|
|
217
|
+
getVotesForItem: async () => [{ voter_id: '42', direction: 'yes' }],
|
|
218
|
+
getAmendment: async () => lawsAmendment,
|
|
219
|
+
applyLaws: async () => { throw new Error('trigger refused'); },
|
|
220
|
+
closeBoardItem: txClose(log),
|
|
221
|
+
}, async () => {
|
|
222
|
+
await assert.rejects(() => board.closeItem(amendmentItem()), /trigger refused/);
|
|
223
|
+
});
|
|
224
|
+
assert.deepEqual(log, ['rolled_back']);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('THE GUARD: only a passed laws amendment, naming its own amendment, may write a law', async () => {
|
|
228
|
+
const passed = { id: '31', subject_type: 'constitutional_amendment', subject_id: '7', outcome: 'passed' };
|
|
229
|
+
assert.equal(laws.lawsWriteAllowed(passed, lawsAmendment), true);
|
|
230
|
+
const refused = [
|
|
231
|
+
[{ ...passed, outcome: 'returned' }, lawsAmendment, 'a returned sitting'],
|
|
232
|
+
[{ ...passed, outcome: 'withdrawn' }, lawsAmendment, 'a withdrawn sitting'],
|
|
233
|
+
[{ ...passed, outcome: null }, lawsAmendment, 'an open sitting'],
|
|
234
|
+
[{ ...passed, subject_type: 'rules_matter' }, lawsAmendment, 'a matter, not an amendment'],
|
|
235
|
+
[{ ...passed, subject_type: 'full_idea' }, lawsAmendment, 'an idea'],
|
|
236
|
+
[passed, { ...lawsAmendment, kind: 'constitution' }, 'a board-block amendment'],
|
|
237
|
+
[passed, { ...lawsAmendment, kind: 'decision_rules' }, 'a who-decides amendment'],
|
|
238
|
+
[passed, { ...lawsAmendment, kind: undefined }, 'an amendment of no kind'],
|
|
239
|
+
[{ ...passed, subject_id: '8' }, lawsAmendment, 'another amendment\'s item'],
|
|
240
|
+
[null, lawsAmendment, 'no item'],
|
|
241
|
+
[passed, null, 'no amendment'],
|
|
242
|
+
];
|
|
243
|
+
await withDb({ applyLaws: async () => { throw new Error('the write must never be reached'); } }, async () => {
|
|
244
|
+
for (const [row, am, why] of refused) {
|
|
245
|
+
assert.equal(laws.lawsWriteAllowed(row, am), false, why);
|
|
246
|
+
await assert.rejects(() => lawBoard.applyPassedLaws({ tx: true }, row, am), /only be written by a passed laws amendment/, why);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// and a passed amendment whose stored row was altered is not applied
|
|
250
|
+
await withDb({ applyLaws: async () => { throw new Error('the write must never be reached'); } }, async () => {
|
|
251
|
+
await assert.rejects(() => lawBoard.applyPassedLaws({ tx: true }, passed, { ...lawsAmendment, proposed: { laws: { enact: [{ text: 'x' }] } } }), /no longer validates/);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── 3. no route writes a law except a passed amendment ──────────────────────
|
|
256
|
+
|
|
257
|
+
function sourceFiles() {
|
|
258
|
+
const files = [];
|
|
259
|
+
const walk = (dir) => {
|
|
260
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
261
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
262
|
+
const p = path.join(dir, e.name);
|
|
263
|
+
if (e.isDirectory()) walk(p);
|
|
264
|
+
else if (/\.(js|mjs|cjs)$/.test(e.name)) files.push(p);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
for (const d of ['modules', 'src', 'scripts']) walk(path.join(ROOT, d));
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
const rel = (f) => path.relative(ROOT, f).replace(/\\/g, '/');
|
|
271
|
+
|
|
272
|
+
test('nothing else writes a law: one SQL writer, one caller, no route, and the database refuses the rest', () => {
|
|
273
|
+
const files = sourceFiles();
|
|
274
|
+
const writers = files.filter((f) => /(INSERT INTO|UPDATE|DELETE FROM)\s+government_laws\b/.test(fs.readFileSync(f, 'utf8'))).map(rel);
|
|
275
|
+
assert.deepEqual(writers, ['modules/government/db.js'], 'exactly one SQL writer');
|
|
276
|
+
const callers = files.filter((f) => /\.applyLaws\(/.test(fs.readFileSync(f, 'utf8'))).map(rel);
|
|
277
|
+
assert.deepEqual(callers, ['modules/government/law-board.js']);
|
|
278
|
+
const lawBoardSrc = fs.readFileSync(path.join(GOV, 'law-board.js'), 'utf8');
|
|
279
|
+
assert.equal((lawBoardSrc.match(/db\.applyLaws\(/g) || []).length, 1, 'called once, inside applyPassedLaws');
|
|
280
|
+
// applyPassedLaws is reached only from the close's in-transaction hook
|
|
281
|
+
const boardSrc = fs.readFileSync(path.join(GOV, 'board.js'), 'utf8');
|
|
282
|
+
assert.equal((boardSrc.match(/lawBoard\.applyPassedLaws\(client, closedRow, amendment\)/g) || []).length, 1, 'one call, inside afterClose');
|
|
283
|
+
const applyCallers = files.filter((f) => /applyPassedLaws\(/.test(fs.readFileSync(f, 'utf8'))).map(rel).sort();
|
|
284
|
+
assert.deepEqual(applyCallers, ['modules/government/board.js', 'modules/government/law-board.js']);
|
|
285
|
+
// a laws amendment is filed in exactly one place
|
|
286
|
+
const kindLaws = files.filter((f) => /kind:\s*'laws'/.test(fs.readFileSync(f, 'utf8'))).map(rel);
|
|
287
|
+
assert.deepEqual(kindLaws, ['modules/government/law-board.js'], 'only proposeLaws files a laws amendment');
|
|
288
|
+
// no route file names the write
|
|
289
|
+
const routeFiles = files.filter((f) => /[\\/]routes[\\/]/.test(f));
|
|
290
|
+
for (const f of routeFiles) {
|
|
291
|
+
assert.ok(!/applyLaws|applyPassedLaws|government_laws/.test(fs.readFileSync(f, 'utf8')), `${rel(f)} must not write a law`);
|
|
292
|
+
}
|
|
293
|
+
// the SQL carries the same condition, for both the enact and the repeal
|
|
294
|
+
const dbSrc = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
295
|
+
assert.equal((dbSrc.match(/i\.outcome = 'passed' AND a\.kind = 'laws'/g) || []).length, 2, 'enact and repeal both carry the condition');
|
|
296
|
+
// the trigger refuses every other write, and a delete outright
|
|
297
|
+
assert.match(MIGRATION_CODE, /CREATE OR REPLACE TRIGGER government_laws_amendment_only\s+BEFORE INSERT OR UPDATE OR DELETE ON government_laws/);
|
|
298
|
+
assert.match(MIGRATION_CODE, /i\.subject_type = 'constitutional_amendment'\s+AND i\.outcome = 'passed'\s+AND a\.kind = 'laws'/);
|
|
299
|
+
assert.match(MIGRATION_CODE, /IF TG_OP = 'DELETE' THEN\s+RAISE EXCEPTION/);
|
|
300
|
+
assert.match(MIGRATION_CODE, /IF NOT government_laws_passed_laws_item\(NEW\.passed_by_item_id\) THEN\s+RAISE EXCEPTION/, 'an insert names a passed laws sitting');
|
|
301
|
+
assert.match(MIGRATION_CODE, /IF NOT government_laws_passed_laws_item\(NEW\.repealed_by_item_id\) THEN\s+RAISE EXCEPTION/, 'a repeal names a passed laws sitting');
|
|
302
|
+
assert.match(MIGRATION_CODE, /IF OLD\.repealed_at IS NOT NULL THEN\s+RAISE EXCEPTION/, 'a repealed law never changes again');
|
|
303
|
+
assert.match(MIGRATION_CODE, /NEW\.text IS DISTINCT FROM OLD\.text/, 'a law in force cannot be edited');
|
|
304
|
+
assert.match(MIGRATION_CODE, /NEW\.passed_at := now\(\)/, 'when it passed is the database clock, not the writer\'s word');
|
|
305
|
+
assert.match(MIGRATION_CODE, /CHECK \(kind IN \('constitution', 'decision_rules', 'laws'\)\)/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('the migration seeds no law — the first laws are the owner\'s to ratify', () => {
|
|
309
|
+
assert.doesNotMatch(MIGRATION_CODE, /INSERT INTO government_laws/);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── the routes, through the real router ─────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
315
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
316
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
317
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
318
|
+
const express = require('express');
|
|
319
|
+
|
|
320
|
+
test('the two laws routes are pinned: proposing on the amendment atom, reading where the room reads', () => {
|
|
321
|
+
const pins = rrc.MODULE_EXPECTED_RANKS.government;
|
|
322
|
+
assert.equal(pins['POST /government/board/laws'], 'perm:board.item.open');
|
|
323
|
+
assert.equal(pins['GET /government/laws'], 'perm:board.vote.cast');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const realRequireBuilder = api.requireBuilder;
|
|
327
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon' }; next(); };
|
|
328
|
+
seams._reset();
|
|
329
|
+
let held = ['government.manage', 'board.item.open', 'board.vote.cast'];
|
|
330
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
|
|
331
|
+
const buildGovernmentRouter = require(path.join(GOV, 'routes', 'government.js'));
|
|
332
|
+
const app = express();
|
|
333
|
+
app.use(express.json());
|
|
334
|
+
app.use(attachFail);
|
|
335
|
+
app.use(buildGovernmentRouter());
|
|
336
|
+
const server = http.createServer(app);
|
|
337
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
338
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
339
|
+
after(() => { server.close(); api.requireBuilder = realRequireBuilder; seams._reset(); });
|
|
340
|
+
|
|
341
|
+
async function call(method, p, body) {
|
|
342
|
+
const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
343
|
+
let json = null; try { json = await res.json(); } catch { /* none */ }
|
|
344
|
+
return { status: res.status, json };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
test('POST /government/board/laws files the amendment; a bad law is a named 400; a repeat is 409', async () => {
|
|
348
|
+
await withDb({
|
|
349
|
+
listLaws: async () => [{ id: '3', text: 'No spend limit', locks: [] }],
|
|
350
|
+
createAmendment: async (a) => ({ id: '7', ...a }),
|
|
351
|
+
openBoardItem: async (a) => ({ id: '31', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution }),
|
|
352
|
+
}, async () => {
|
|
353
|
+
const ok = await call('POST', '/government/board/laws', { enact: [ASK] });
|
|
354
|
+
assert.equal(ok.status, 201, JSON.stringify(ok.json));
|
|
355
|
+
assert.equal(ok.json.item.subject_type, 'constitutional_amendment');
|
|
356
|
+
assert.equal(ok.json.amendment.kind, 'laws');
|
|
357
|
+
assert.equal(ok.json.proposal.enact[0].text, ASK.text);
|
|
358
|
+
const bad = await call('POST', '/government/board/laws', { enact: [{ text: 'Two\nlines' }] });
|
|
359
|
+
assert.equal(bad.status, 400);
|
|
360
|
+
assert.equal(bad.json.error.code, 'invalid_laws');
|
|
361
|
+
const again = await call('POST', '/government/board/laws', { enact: [{ text: 'No spend limit' }] });
|
|
362
|
+
assert.equal(again.status, 409);
|
|
363
|
+
assert.equal(again.json.error.code, 'already_in_force');
|
|
364
|
+
const gone = await call('POST', '/government/board/laws', { repeal: [99] });
|
|
365
|
+
assert.equal(gone.status, 409);
|
|
366
|
+
assert.equal(gone.json.error.code, 'not_in_force');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('a laws amendment cannot come in by another door: the constitution and who-decides routes refuse its shape', async () => {
|
|
371
|
+
const never = async () => { throw new Error('must not be reached'); };
|
|
372
|
+
await withDb({ createAmendment: never, openBoardItem: never, applyLaws: never, listDecisionRules: async () => [], getRankByKey: async () => null }, async () => {
|
|
373
|
+
const asConstitution = await call('POST', '/government/board/items', { subject_type: 'constitutional_amendment', proposal: { laws: { enact: [ASK] } } });
|
|
374
|
+
assert.ok(asConstitution.status >= 400 && asConstitution.status < 500, `constitution door: ${asConstitution.status}`);
|
|
375
|
+
const asRules = await call('POST', '/government/board/decision-rules', { rules: [{ laws: { enact: [ASK] } }] });
|
|
376
|
+
assert.ok(asRules.status >= 400 && asRules.status < 500, `who-decides door: ${asRules.status}`);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('GET /government/laws lists the book: each law with who passed it (the government, proposer, who said yes) and when', async () => {
|
|
381
|
+
let asked = null;
|
|
382
|
+
await withDb({
|
|
383
|
+
listLaws: async (opts) => {
|
|
384
|
+
asked = opts;
|
|
385
|
+
return [{
|
|
386
|
+
id: '11', text: ASK.text, effect_on_ai: ASK.effect_on_ai, locks: ASK.locks, proposed_by: '42', passed_by_item_id: '31',
|
|
387
|
+
passed_at: '2026-10-09T12:00:00.000Z', repealed_by_item_id: null, repealed_at: null,
|
|
388
|
+
decided_under: { membership: 'rank:metic+', pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true },
|
|
389
|
+
proposer_login: 'ada', proposer_name: 'Ada',
|
|
390
|
+
}];
|
|
391
|
+
},
|
|
392
|
+
listYesVoters: async (ids) => { assert.deepEqual(ids, ['31']); return [{ item_id: '31', voter_id: '43', github_login: 'ben', display_name: 'Ben' }, { item_id: '31', voter_id: '44', github_login: 'cy', display_name: null }]; },
|
|
393
|
+
}, async () => {
|
|
394
|
+
const r = await call('GET', '/government/laws');
|
|
395
|
+
assert.equal(r.status, 200, JSON.stringify(r.json));
|
|
396
|
+
assert.deepEqual(asked, { includeRepealed: false });
|
|
397
|
+
assert.equal(r.json.in_force, 1);
|
|
398
|
+
const law = r.json.laws[0];
|
|
399
|
+
assert.equal(law.text, ASK.text);
|
|
400
|
+
assert.deepEqual(law.locks, ASK.locks);
|
|
401
|
+
assert.equal(law.in_force, true);
|
|
402
|
+
assert.equal(law.passed_at, '2026-10-09T12:00:00.000Z', 'when');
|
|
403
|
+
assert.equal(law.passed_by.item_id, '31', 'which sitting');
|
|
404
|
+
assert.equal(law.passed_by.government, 'Timocratic Republic', 'the government it was decided under, by name');
|
|
405
|
+
assert.deepEqual(law.passed_by.proposer, { id: '42', login: 'ada', name: 'Ada' });
|
|
406
|
+
assert.deepEqual(law.passed_by.yes.map((p) => p.name), ['Ben', 'cy'], 'who said yes');
|
|
407
|
+
assert.match(r.json.changed_by, /passed amendment only/);
|
|
408
|
+
const all = await call('GET', '/government/laws?include=repealed');
|
|
409
|
+
assert.equal(all.status, 200);
|
|
410
|
+
assert.deepEqual(asked, { includeRepealed: true });
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('a repealed law stays on the record with when and by which sitting', async () => {
|
|
415
|
+
await withDb({
|
|
416
|
+
listLaws: async () => [{ id: '3', text: 'No spend limit', effect_on_ai: null, locks: [], proposed_by: null, passed_by_item_id: '20', passed_at: 'T1', repealed_by_item_id: '33', repealed_at: 'T2', decided_under: MONARCHY }],
|
|
417
|
+
listYesVoters: async () => [],
|
|
418
|
+
}, async () => {
|
|
419
|
+
const v = await board.lawsView({ includeRepealed: true });
|
|
420
|
+
assert.equal(v.in_force, 0);
|
|
421
|
+
assert.equal(v.laws[0].in_force, false);
|
|
422
|
+
assert.equal(v.laws[0].repealed_at, 'T2');
|
|
423
|
+
assert.equal(v.laws[0].repealed_by_item_id, '33');
|
|
424
|
+
assert.equal(v.laws[0].passed_by.government, 'Monarchy / BDFL');
|
|
425
|
+
assert.equal(v.laws[0].passed_by.proposer, null, 'a departed proposer reads as nobody, never a guess');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('a laws amendment is never read as a board-block divergence', async () => {
|
|
430
|
+
await withDb({
|
|
431
|
+
listAmendmentHistory: async () => [{ item_id: '31', amendment_id: '7', kind: 'laws', outcome: 'passed', closed_at: 'T', proposed: lawsAmendment.proposed }],
|
|
432
|
+
countActiveBuilders: async () => 1,
|
|
433
|
+
listDecisionRules: async () => [],
|
|
434
|
+
}, async () => {
|
|
435
|
+
const v = await board.constitutionView({});
|
|
436
|
+
assert.equal(v.divergence, null, 'a passed laws amendment says nothing about the board block');
|
|
437
|
+
assert.equal(v.history[0].kind, 'laws');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('without the atoms the laws routes 403', async () => {
|
|
442
|
+
held = ['board.vote.cast'];
|
|
443
|
+
try {
|
|
444
|
+
assert.equal((await call('POST', '/government/board/laws', { enact: [ASK] })).status, 403, 'proposing needs board.item.open');
|
|
445
|
+
} finally { held = ['government.manage', 'board.item.open', 'board.vote.cast']; }
|
|
446
|
+
held = [];
|
|
447
|
+
try {
|
|
448
|
+
assert.equal((await call('GET', '/government/laws')).status, 403, 'reading needs board.vote.cast');
|
|
449
|
+
} finally { held = ['government.manage', 'board.item.open', 'board.vote.cast']; }
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('every government route, driven for real, writes no law — only POST /government/board/laws files one, and it is a proposal', async () => {
|
|
453
|
+
const router = buildGovernmentRouter();
|
|
454
|
+
const routes = [];
|
|
455
|
+
for (const layer of router.stack) {
|
|
456
|
+
if (!layer.route) continue;
|
|
457
|
+
for (const m of Object.keys(layer.route.methods)) routes.push([m.toUpperCase(), layer.route.path]);
|
|
458
|
+
}
|
|
459
|
+
assert.ok(routes.length >= 20, `${routes.length} routes found`);
|
|
460
|
+
const writes = [];
|
|
461
|
+
const filed = [];
|
|
462
|
+
const stubs = {};
|
|
463
|
+
for (const k of Object.keys(db)) {
|
|
464
|
+
if (typeof db[k] !== 'function') continue;
|
|
465
|
+
stubs[k] = async () => [];
|
|
466
|
+
}
|
|
467
|
+
stubs.applyLaws = async () => { writes.push('applyLaws'); return { enacted: [], repealed: [] }; };
|
|
468
|
+
stubs.createAmendment = async (a) => { filed.push(a.kind || 'constitution'); return { id: '7', ...a }; };
|
|
469
|
+
stubs.openBoardItem = async (a) => ({ id: '31', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution });
|
|
470
|
+
const origApply = config.applyBoardAmendment;
|
|
471
|
+
config.applyBoardAmendment = () => ({}); // never the real file
|
|
472
|
+
const hits = {};
|
|
473
|
+
try {
|
|
474
|
+
await withDb(stubs, async () => {
|
|
475
|
+
for (const [method, p] of routes) {
|
|
476
|
+
const url = p.replace(/:[A-Za-z]+/g, '1');
|
|
477
|
+
const before = filed.length;
|
|
478
|
+
// every body a laws amendment could hide in: the laws shape itself, and each other door's shape carrying it
|
|
479
|
+
const bodies = method === 'GET' ? [undefined] : [{ enact: [ASK] }, { subject_type: 'constitutional_amendment', proposal: { laws: { enact: [ASK] } } }, { rules: [{ laws: { enact: [ASK] } }] }, { area: 'rules', title: 'Ask like a human', action: { laws: { enact: [ASK] } } }, { direction: 'yes' }];
|
|
480
|
+
for (const body of bodies) await call(method, url, body);
|
|
481
|
+
if (filed.length > before) hits[`${method} ${p}`] = filed.slice(before);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
} finally { config.applyBoardAmendment = origApply; }
|
|
485
|
+
assert.deepEqual(writes, [], 'no route reached the write');
|
|
486
|
+
const lawsFilers = Object.entries(hits).filter(([, kinds]) => kinds.includes('laws')).map(([k]) => k);
|
|
487
|
+
assert.deepEqual(lawsFilers, ['POST /government/board/laws'], 'only the propose route files a laws amendment');
|
|
488
|
+
});
|