@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,408 @@
|
|
|
1
|
+
// tests/government_asks.mjs — THE ASK: one enforced shape for every request an
|
|
2
|
+
// AI agent puts to a person (task 1004703, goal 1000111).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT GUARDS.
|
|
5
|
+
// 1. Every rule of the shape (modules/government/asks.js checkAsk), one case
|
|
6
|
+
// each: the question (≤ 12 words, one, ends '?', no markup), the answers (2
|
|
7
|
+
// or 3; a key, a label of ≤ 4 words and a one-line result each; keys and acts
|
|
8
|
+
// in their grammar), the pick (one of the answers, a one-line why), the seat
|
|
9
|
+
// (a routable one), the "if nobody answers" line and the riding-along detail.
|
|
10
|
+
// 2. A slop ask is refused with a plain reason: the prototype's 238-word
|
|
11
|
+
// override ask, sent as the question, names every fix in one paragraph.
|
|
12
|
+
// 3. The server and the hall hold ONE rule set: gov-living.js askFormat and
|
|
13
|
+
// checkAsk give the same verdict, rule for rule, on every shared case, and
|
|
14
|
+
// every card in the past-asks corpus (tests/fixtures/government-ask-cards.json)
|
|
15
|
+
// is a valid stored ask.
|
|
16
|
+
// 4. The table's CHECKs are the structural floor (government_028): twelve
|
|
17
|
+
// words, the question mark, two or three answers, the pick offered, the seat.
|
|
18
|
+
// 5. The docket attaches each matter's stored ask, only to matters the viewer
|
|
19
|
+
// sees; a failing read attaches none and the city still stands. findMatter
|
|
20
|
+
// reads one matter through the same per-source gate.
|
|
21
|
+
// 6. Through the real routers: POST /blockers refuses a slop ask and files
|
|
22
|
+
// nothing, stores a good one against blockers:<id>; GET /blockers carries it;
|
|
23
|
+
// POST /government/asks/check is a dry run; PUT /government/asks/:matter
|
|
24
|
+
// writes only onto a matter its writer can see.
|
|
25
|
+
//
|
|
26
|
+
// DB-free: every read and write is stubbed by property.
|
|
27
|
+
// Run: node --test --test-reporter=tap tests/government_asks.mjs
|
|
28
|
+
|
|
29
|
+
import { strict as assert } from 'node:assert';
|
|
30
|
+
import { test, after } from 'node:test';
|
|
31
|
+
import http from 'node:http';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
40
|
+
const FX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
41
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
42
|
+
// real past asks as the cards they should have been (test data; the hall never reads it)
|
|
43
|
+
const CARDS = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'government-ask-cards.json'), 'utf8')).cards;
|
|
44
|
+
|
|
45
|
+
const asks = require(path.join(GOV, 'asks.js'));
|
|
46
|
+
const asksDb = require(path.join(GOV, 'asks-db.js'));
|
|
47
|
+
const docket = require(path.join(GOV, 'docket.js'));
|
|
48
|
+
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
49
|
+
const G = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'gov-living.js'));
|
|
50
|
+
|
|
51
|
+
const GOOD = {
|
|
52
|
+
question: 'Turn unattended building back on?',
|
|
53
|
+
options: [
|
|
54
|
+
{ key: 'on', label: 'Turn it on', result: 'Agents work through the night.', act: 'go ahead' },
|
|
55
|
+
{ key: 'off', label: 'Keep it off', result: 'Nothing builds unless someone starts a session.' },
|
|
56
|
+
],
|
|
57
|
+
recommend: { key: 'off', why: 'It was paused for a check.' },
|
|
58
|
+
waits_on: 'owner',
|
|
59
|
+
if_nobody_answers: 'It stays off.',
|
|
60
|
+
};
|
|
61
|
+
const card = (over) => ({ ...JSON.parse(JSON.stringify(GOOD)), ...over });
|
|
62
|
+
const rulesOf = (r) => r.errors.map((e) => `${e.field}:${e.rule}`).sort();
|
|
63
|
+
|
|
64
|
+
// ── 1. every rule ───────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
test('a good ask passes, and comes back in its stored form', () => {
|
|
67
|
+
const r = asks.checkAsk({ ...card({ question: ' Turn unattended building back on? ' }), detail: ' The long story. ', extra: 'dropped' });
|
|
68
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
69
|
+
assert.deepEqual(r.errors, []);
|
|
70
|
+
assert.equal(r.question_words, 5);
|
|
71
|
+
assert.deepEqual(r.ask, {
|
|
72
|
+
question: 'Turn unattended building back on?',
|
|
73
|
+
options: [
|
|
74
|
+
{ key: 'on', label: 'Turn it on', result: 'Agents work through the night.', act: 'go ahead' },
|
|
75
|
+
{ key: 'off', label: 'Keep it off', result: 'Nothing builds unless someone starts a session.' },
|
|
76
|
+
],
|
|
77
|
+
recommend: { key: 'off', why: 'It was paused for a check.' },
|
|
78
|
+
waits_on: 'owner',
|
|
79
|
+
if_nobody_answers: 'It stays off.',
|
|
80
|
+
detail: 'The long story.',
|
|
81
|
+
});
|
|
82
|
+
assert.equal(asks.checkAsk(card({ question: 'Upgrade the setup service, version 1.20.82, now?' })).ok, true, 'a version number is not a sentence break');
|
|
83
|
+
assert.equal(asks.checkAsk(card({ options: [...GOOD.options, { key: 'later', label: 'Ask me later', result: 'It comes back tomorrow.' }] })).ok, true, 'three answers');
|
|
84
|
+
for (const seat of ['owner', 'lead:planning', 'board:charter', 'committee:review.testers']) assert.equal(asks.checkAsk(card({ waits_on: seat })).ok, true, seat);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// One case per rule: the ask that breaks exactly that rule, and the error it gets.
|
|
88
|
+
const RULE_CASES = {
|
|
89
|
+
'ask:missing': null,
|
|
90
|
+
'question:missing': card({ question: '' }),
|
|
91
|
+
'question:too_long': card({ question: 'Should we turn the unattended building back on now that the evaluation is done?' }),
|
|
92
|
+
'question:not_a_question': card({ question: 'Turn unattended building back on.' }),
|
|
93
|
+
'question:more_than_one': card({ question: 'It is off. Turn it on?' }),
|
|
94
|
+
'question:markup': card({ question: 'Turn **unattended** building on?' }),
|
|
95
|
+
'options:count': card({ options: [GOOD.options[1]] }),
|
|
96
|
+
'options[1]:missing': card({ options: [GOOD.options[0], 'off'], recommend: { key: 'on', why: 'x' } }),
|
|
97
|
+
'options[1]:no_key': card({ options: [GOOD.options[0], { ...GOOD.options[1], key: '' }], recommend: { key: 'on', why: 'x' } }),
|
|
98
|
+
'options[1]:duplicate': card({ options: [GOOD.options[0], { ...GOOD.options[1], key: 'on' }], recommend: { key: 'on', why: 'x' } }),
|
|
99
|
+
'options[1]:bad_key': card({ options: [GOOD.options[0], { ...GOOD.options[1], key: 'Keep It Off' }], recommend: { key: 'on', why: 'x' } }),
|
|
100
|
+
'options[1]:no_label': card({ options: [GOOD.options[0], { ...GOOD.options[1], label: ' ' }] }),
|
|
101
|
+
'options[0]:label_too_long': card({ options: [{ key: 'on', label: 'Turn it on right now please', result: 'On.' }, GOOD.options[1]] }),
|
|
102
|
+
'options[1].result:missing': card({ options: [GOOD.options[0], { key: 'off', label: 'Keep it off' }] }),
|
|
103
|
+
'options[1].result:not_one_line': card({ options: [GOOD.options[0], { key: 'off', label: 'Keep it off', result: 'Off.\nAnd quiet.' }] }),
|
|
104
|
+
'options[1].result:markup': card({ options: [GOOD.options[0], { key: 'off', label: 'Keep it off', result: 'See https://example.com for why.' }] }),
|
|
105
|
+
'options[0]:bad_act': card({ options: [{ ...GOOD.options[0], act: 'DROP TABLE;' }, GOOD.options[1]] }),
|
|
106
|
+
'recommend:missing': card({ recommend: null }),
|
|
107
|
+
'recommend:unknown_key': card({ recommend: { key: 'maybe', why: 'It was paused.' } }),
|
|
108
|
+
'recommend:no_why': card({ recommend: { key: 'off', why: '' } }),
|
|
109
|
+
'recommend:why_too_long': card({ recommend: { key: 'off', why: 'It was paused for a check that took a long time and is not finished yet so we should wait a bit longer.' } }),
|
|
110
|
+
'waits_on:missing': card({ waits_on: '' }),
|
|
111
|
+
'waits_on:unknown_seat': card({ waits_on: 'whoever is around' }),
|
|
112
|
+
'if_nobody_answers:missing': card({ if_nobody_answers: '' }),
|
|
113
|
+
'if_nobody_answers:not_one_line': card({ if_nobody_answers: 'It stays off until somebody comes back and turns it on again by hand, which could be weeks.' }),
|
|
114
|
+
'detail:not_text': { ...card({}), detail: { long: true } },
|
|
115
|
+
'detail:too_long': { ...card({}), detail: 'x'.repeat(asks.CAPS.detail + 1) },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
test('each rule of the shape refuses the ask that breaks it, and names it', () => {
|
|
119
|
+
for (const [rule, c] of Object.entries(RULE_CASES)) {
|
|
120
|
+
const r = asks.checkAsk(c);
|
|
121
|
+
assert.equal(r.ok, false, rule);
|
|
122
|
+
assert.equal(r.ask, null, `${rule}: nothing to store`);
|
|
123
|
+
assert.deepEqual(rulesOf(r), [rule], `${rule}: exactly that rule, got ${JSON.stringify(r.errors)}`);
|
|
124
|
+
for (const e of r.errors) assert.match(e.message, /^[A-Z"0-9].*\.$/, `${rule}: a plain sentence`);
|
|
125
|
+
}
|
|
126
|
+
assert.equal(asks.checkAsk(card({ options: [...GOOD.options, GOOD.options[0], GOOD.options[1]] })).errors.some((e) => e.rule === 'count'), true, 'four answers is too many');
|
|
127
|
+
assert.equal(asks.checkAsk(card({ options: [] })).errors[0].message, 'Offer 2 or 3 tap answers, not 0.');
|
|
128
|
+
// the rules are the prototype's, number for number
|
|
129
|
+
assert.deepEqual(asks.ASK_RULES, G.ASK_RULES);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── 2. the slop ask, refused in plain words ─────────────────────────────────
|
|
133
|
+
|
|
134
|
+
test('the prototype\'s 238-word override ask, sent as the question, is refused with a plain reason', () => {
|
|
135
|
+
const m = fx('government__docket').matters.find((x) => x.id === 'override_requests:1000017');
|
|
136
|
+
const text = [m.title, m.detail].filter(Boolean).join('\n\n');
|
|
137
|
+
assert.ok(asks.words(text).length >= 200, 'it is the long one');
|
|
138
|
+
const r = asks.checkAsk({ question: text, options: [], recommend: null, waits_on: '', if_nobody_answers: '' });
|
|
139
|
+
assert.equal(r.ok, false);
|
|
140
|
+
const rules = new Set(r.errors.map((e) => e.rule));
|
|
141
|
+
for (const rule of ['too_long', 'more_than_one', 'count', 'missing']) assert.ok(rules.has(rule), rule);
|
|
142
|
+
const line = asks.refusalLine(r.errors);
|
|
143
|
+
assert.match(line, /^This ask was refused: a person must be able to answer it at a glance\. /);
|
|
144
|
+
assert.match(line, /The question is \d+ words; 12 at most\./);
|
|
145
|
+
assert.match(line, /Offer 2 or 3 tap answers, not 0\./);
|
|
146
|
+
assert.match(line, /Say who this waits on\./);
|
|
147
|
+
assert.doesNotMatch(line, /undefined|\[object/);
|
|
148
|
+
assert.equal(asks.refusalLine([]), '');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── 3. one rule set: the server's and the hall's ────────────────────────────
|
|
152
|
+
|
|
153
|
+
test('the server and the hall give the same verdict on every shared rule', () => {
|
|
154
|
+
// the server-only rules (a stored ask's grammar and caps) are not the hall's
|
|
155
|
+
const SERVER_ONLY = new Set(['options[1]:bad_key', 'options[0]:bad_act', 'waits_on:unknown_seat', 'detail:not_text', 'detail:too_long', 'ask:missing']);
|
|
156
|
+
let compared = 0;
|
|
157
|
+
for (const [rule, c] of Object.entries(RULE_CASES)) {
|
|
158
|
+
if (SERVER_ONLY.has(rule)) continue;
|
|
159
|
+
const server = asks.checkAsk(c);
|
|
160
|
+
const hall = G.askFormat(c);
|
|
161
|
+
assert.equal(server.ok, hall.ok, `${rule}: same ok`);
|
|
162
|
+
assert.deepEqual(rulesOf(server), rulesOf(hall), `${rule}: same rules`);
|
|
163
|
+
assert.deepEqual(server.errors.map((e) => e.message).sort(), hall.errors.map((e) => e.message).sort(), `${rule}: same words`);
|
|
164
|
+
compared += 1;
|
|
165
|
+
}
|
|
166
|
+
assert.ok(compared >= 20, `${compared} rules compared`);
|
|
167
|
+
assert.equal(G.askFormat(null).ok, false, 'no card is refused by both');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('every card in the past-asks corpus is a valid stored ask', () => {
|
|
171
|
+
const all = CARDS;
|
|
172
|
+
assert.ok(all.length >= 20);
|
|
173
|
+
for (const a of all) {
|
|
174
|
+
const r = asks.checkAsk(a);
|
|
175
|
+
assert.equal(r.ok, true, `${a.id}: ${JSON.stringify(r.errors)}`);
|
|
176
|
+
assert.equal(G.askFormat(a).ok, true, `${a.id}: the hall agrees`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── 4. the table's floor ────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
test('government_028: the table refuses a slop row on its own', () => {
|
|
183
|
+
const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_028_asks.sql'), 'utf8');
|
|
184
|
+
assert.match(sql, /CREATE TABLE IF NOT EXISTS government_asks/);
|
|
185
|
+
assert.match(sql, /matter_id\s+text\s+PRIMARY KEY/);
|
|
186
|
+
assert.match(sql, /array_length\(regexp_split_to_array\(btrim\(question\), '\\s\+'\), 1\) <= 12/, 'twelve words');
|
|
187
|
+
assert.match(sql, /question ~ '\\\?\$'/, 'a question mark');
|
|
188
|
+
assert.match(sql, /question !~ '\[\\r\\n\]'/, 'one line');
|
|
189
|
+
assert.match(sql, /jsonb_array_length\(options\) BETWEEN 2 AND 3/, 'two or three answers');
|
|
190
|
+
assert.match(sql, /options @> jsonb_build_array\(jsonb_build_object\('key', recommend_key\)\)/, 'the pick is offered');
|
|
191
|
+
// the seat grammar is the module's, building for building
|
|
192
|
+
const seat = /waits_on ~ '([^']+)'/.exec(sql)[1];
|
|
193
|
+
for (const b of asks.BUILDINGS) assert.ok(seat.includes(b), b);
|
|
194
|
+
const re = new RegExp(seat);
|
|
195
|
+
for (const s of ['owner', 'lead:planning', 'board:charter', 'committee:review.testers']) assert.equal(re.test(s), true, s);
|
|
196
|
+
for (const s of ['the owner', 'lead:nowhere', 'you', '']) assert.equal(re.test(s), false, s);
|
|
197
|
+
for (const s of ['owner', 'lead:planning', 'committee:review.testers', 'lead:nowhere', 'you']) assert.equal(asks.SEAT.test(s), re.test(s), `${s}: the module and the table agree`);
|
|
198
|
+
assert.doesNotMatch(sql, /\bINSERT INTO\b/, 'no ask is seeded');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── 5. the docket carries asks ──────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
const VIEWER = { id: 42, rank: 'metic' };
|
|
204
|
+
const NOW = Date.parse('2026-10-09T12:00:00Z');
|
|
205
|
+
function contributions(rows) {
|
|
206
|
+
return {
|
|
207
|
+
sources: new Map([
|
|
208
|
+
['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true }, read: async () => rows.blockers || [] }],
|
|
209
|
+
['override_requests', { source: 'override_requests', building: 'release', gate: { permission: 'override_request.decide' }, read: async () => rows.overrides || [] }],
|
|
210
|
+
]),
|
|
211
|
+
readings: new Map(), floors: new Map(), histories: new Map(), desks: new Map(),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const ROWS = {
|
|
215
|
+
blockers: [{ id: 7, title: 'Pick a host', state: 'needs', rule: 'decide', opened_at: '2026-10-01T00:00:00Z' }, { id: 8, title: 'No ask here', state: 'needs', rule: 'decide' }],
|
|
216
|
+
overrides: [{ id: 5, title: 'Merge without review', state: 'needs', rule: 'decide' }],
|
|
217
|
+
};
|
|
218
|
+
const STORED = { ...asks.checkAsk(GOOD).ask, written_at: '2026-10-09T11:00:00.000Z' };
|
|
219
|
+
|
|
220
|
+
test('the docket attaches each matter\'s stored ask, only to the matters the viewer sees', async () => {
|
|
221
|
+
const asked = [];
|
|
222
|
+
const readAsks = async (ids) => { asked.push(...ids); return new Map([['blockers:7', STORED], ['override_requests:5', STORED]]); };
|
|
223
|
+
const seen = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: contributions(ROWS), now: NOW, readAsks });
|
|
224
|
+
const by = Object.fromEntries(seen.matters.map((m) => [m.id, m]));
|
|
225
|
+
assert.deepEqual(by['blockers:7'].ask, STORED, 'the matter carries its ask');
|
|
226
|
+
assert.equal('ask' in by['blockers:8'], false, 'a matter with no ask carries none');
|
|
227
|
+
assert.equal(by['override_requests:5'], undefined, 'a matter the viewer cannot see is not there');
|
|
228
|
+
assert.ok(!asked.includes('override_requests:5'), 'nor is its ask read');
|
|
229
|
+
// with the atom, it is there and carries its ask
|
|
230
|
+
const metic = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'override_request.decide'], contributions: contributions(ROWS), now: NOW, readAsks });
|
|
231
|
+
assert.deepEqual(metic.matters.find((m) => m.id === 'override_requests:5').ask, STORED);
|
|
232
|
+
// a failing read attaches none, and the docket still stands
|
|
233
|
+
const broken = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: contributions(ROWS), now: NOW, readAsks: async () => { throw new Error('db down'); } });
|
|
234
|
+
assert.equal(broken.matters.length, 2);
|
|
235
|
+
assert.ok(broken.matters.every((m) => !('ask' in m)));
|
|
236
|
+
// and with no reader (a test, or a docket built without the db) none is attached
|
|
237
|
+
const none = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: contributions(ROWS), now: NOW });
|
|
238
|
+
assert.ok(none.matters.every((m) => !('ask' in m)));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('findMatter reads one matter through the same gate the docket applies', async () => {
|
|
242
|
+
const c = contributions(ROWS);
|
|
243
|
+
const find = (matterId, permissions = ['board.vote.cast']) => docket.findMatter({ viewer: VIEWER, permissions, matterId, contributions: c, now: NOW });
|
|
244
|
+
assert.equal((await find('blockers:7')).matter.id, 'blockers:7');
|
|
245
|
+
assert.equal((await find('blockers:9')).reason, 'not_found');
|
|
246
|
+
assert.equal((await find('override_requests:5')).reason, 'not_yours');
|
|
247
|
+
assert.equal((await find('override_requests:5', ['override_request.decide'])).matter.title, 'Merge without review');
|
|
248
|
+
assert.equal((await find('nowhere:1')).reason, 'no_such_source');
|
|
249
|
+
assert.equal((await find('access_requests:1')).reason, 'no_such_source', 'a declared source no module contributes');
|
|
250
|
+
const failing = { ...c, sources: new Map([['blockers', { building: 'planning', gate: { anyBuilder: true }, read: async () => { throw new Error('x'); } }]]) };
|
|
251
|
+
assert.equal((await docket.findMatter({ viewer: VIEWER, permissions: [], matterId: 'blockers:7', contributions: failing })).reason, 'unavailable');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── 6. through the real routers ─────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
257
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
258
|
+
const authMod = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
259
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
260
|
+
const blockersDb = require(path.join(ROOT, 'modules', 'ideas', 'blockers.js'));
|
|
261
|
+
const express = require('express');
|
|
262
|
+
|
|
263
|
+
// The viewer's atoms, per request (the header stands in for the resolver).
|
|
264
|
+
let perms = ['board.vote.cast'];
|
|
265
|
+
const real = {
|
|
266
|
+
apiRequireBuilder: api.requireBuilder, authRequireBuilder: authMod.requireBuilder, requirePermission: authMod.requirePermission,
|
|
267
|
+
resolve: resolver.resolveBuilderPermissions, saveAsk: asksDb.saveAsk, asksFor: asksDb.asksFor,
|
|
268
|
+
create: blockersDb.createBlocker, list: blockersDb.listOpenBlockers, count: blockersDb.countOpenBlockers, get: blockersDb.getBlocker,
|
|
269
|
+
};
|
|
270
|
+
const fakeBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'metic', github_login: 'someone' }; next(); };
|
|
271
|
+
api.requireBuilder = fakeBuilder;
|
|
272
|
+
authMod.requireBuilder = fakeBuilder;
|
|
273
|
+
authMod.requirePermission = (...atoms) => (req, res, next) => (atoms.every((a) => perms.includes(a)) ? next() : res.fail('forbidden', 403));
|
|
274
|
+
resolver.resolveBuilderPermissions = async () => perms.slice();
|
|
275
|
+
|
|
276
|
+
// an in-memory government_asks and blockers table
|
|
277
|
+
const store = new Map();
|
|
278
|
+
const filed = [];
|
|
279
|
+
asksDb.saveAsk = async ({ matterId, ask, builderId }) => { const row = { ...ask, written_at: '2026-10-09T12:00:00.000Z' }; store.set(matterId, { row, builderId }); return row; };
|
|
280
|
+
asksDb.asksFor = async (ids) => new Map(ids.filter((id) => store.has(id)).map((id) => [id, store.get(id).row]));
|
|
281
|
+
let nextId = 100;
|
|
282
|
+
blockersDb.createBlocker = async ({ title, bodyMd }) => { const b = { id: String(nextId++), title, body_md: bodyMd, status: 'open', created_at: '2026-10-09T12:00:00.000Z', blocking_task_ids: [] }; filed.push(b); return b; };
|
|
283
|
+
blockersDb.listOpenBlockers = async () => filed.slice();
|
|
284
|
+
blockersDb.countOpenBlockers = async () => filed.length;
|
|
285
|
+
blockersDb.getBlocker = async (id) => filed.find((b) => String(b.id) === String(id)) || null;
|
|
286
|
+
|
|
287
|
+
seams._reset();
|
|
288
|
+
const app = express();
|
|
289
|
+
app.use(express.json());
|
|
290
|
+
app.use(attachFail);
|
|
291
|
+
app.use(require(path.join(GOV, 'routes', 'government.js'))());
|
|
292
|
+
app.use(require(path.join(ROOT, 'modules', 'ideas', 'routes', 'blockers.js'))());
|
|
293
|
+
const server = http.createServer(app);
|
|
294
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
295
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
296
|
+
after(() => {
|
|
297
|
+
server.close();
|
|
298
|
+
api.requireBuilder = real.apiRequireBuilder; authMod.requireBuilder = real.authRequireBuilder; authMod.requirePermission = real.requirePermission;
|
|
299
|
+
resolver.resolveBuilderPermissions = real.resolve; asksDb.saveAsk = real.saveAsk; asksDb.asksFor = real.asksFor;
|
|
300
|
+
blockersDb.createBlocker = real.create; blockersDb.listOpenBlockers = real.list; blockersDb.countOpenBlockers = real.count; blockersDb.getBlocker = real.get;
|
|
301
|
+
seams._reset();
|
|
302
|
+
});
|
|
303
|
+
async function call(method, p, body) {
|
|
304
|
+
const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
305
|
+
let json = null; try { json = await res.json(); } catch { /* none */ }
|
|
306
|
+
return { status: res.status, json };
|
|
307
|
+
}
|
|
308
|
+
const SLOP = { question: 'I have been looking at the deploy and there are several things you should know. First the pipeline is red. Can you look?', options: [], waits_on: '', if_nobody_answers: '' };
|
|
309
|
+
|
|
310
|
+
test('POST /blockers refuses a slop ask in plain words and files nothing', async () => {
|
|
311
|
+
const before = filed.length;
|
|
312
|
+
const r = await call('POST', '/blockers', { title: 'The deploy is red', body_md: 'Long story.', ask: SLOP });
|
|
313
|
+
assert.equal(r.status, 400);
|
|
314
|
+
assert.equal(r.json.error.code, 'ask_refused');
|
|
315
|
+
assert.match(r.json.error.message, /^This ask was refused: /);
|
|
316
|
+
assert.match(r.json.error.message, /The question is \d+ words; 12 at most\./);
|
|
317
|
+
assert.ok(r.json.error.details.errors.some((e) => e.field === 'options' && e.rule === 'count'));
|
|
318
|
+
assert.equal(filed.length, before, 'no blocker was filed');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('POST /blockers stores a good ask against blockers:<id>, and GET /blockers carries it', async () => {
|
|
322
|
+
const r = await call('POST', '/blockers', { title: 'The deploy is red', body_md: 'The long story rides along here.', ask: { ...GOOD, detail: 'The long story rides along here.' } });
|
|
323
|
+
assert.equal(r.status, 201, JSON.stringify(r.json));
|
|
324
|
+
const id = r.json.blocker.id;
|
|
325
|
+
assert.equal(r.json.ask.question, GOOD.question);
|
|
326
|
+
assert.deepEqual(store.get(`blockers:${id}`).row.options, asks.checkAsk(GOOD).ask.options);
|
|
327
|
+
assert.equal(store.get(`blockers:${id}`).builderId, 42, 'who wrote it');
|
|
328
|
+
const list = await call('GET', '/blockers');
|
|
329
|
+
const row = list.json.blockers.find((b) => b.id === id);
|
|
330
|
+
assert.equal(row.ask.question, GOOD.question);
|
|
331
|
+
const one = await call('GET', `/blockers/${id}`);
|
|
332
|
+
assert.equal(one.json.blocker.ask.recommend.key, 'off');
|
|
333
|
+
// a blocker filed without an ask still files, and carries none
|
|
334
|
+
const plain = await call('POST', '/blockers', { title: 'Plain blocker' });
|
|
335
|
+
assert.equal(plain.status, 201);
|
|
336
|
+
assert.equal(plain.json.ask, null);
|
|
337
|
+
assert.equal((await call('GET', '/blockers')).json.blockers.find((b) => b.id === plain.json.blocker.id).ask, null);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('POST /government/asks/check is a dry run: the verdict, nothing stored', async () => {
|
|
341
|
+
const size = store.size;
|
|
342
|
+
const ok = await call('POST', '/government/asks/check', GOOD);
|
|
343
|
+
assert.equal(ok.status, 200);
|
|
344
|
+
assert.equal(ok.json.ok, true);
|
|
345
|
+
assert.equal(ok.json.question_words, 5);
|
|
346
|
+
const wrapped = await call('POST', '/government/asks/check', { ask: GOOD });
|
|
347
|
+
assert.equal(wrapped.json.ok, true, 'wrapped as { ask } too');
|
|
348
|
+
const bad = await call('POST', '/government/asks/check', SLOP);
|
|
349
|
+
assert.equal(bad.status, 400);
|
|
350
|
+
assert.equal(bad.json.error.code, 'ask_refused');
|
|
351
|
+
assert.deepEqual(bad.json.error.details.rules, asks.ASK_RULES);
|
|
352
|
+
assert.equal(store.size, size, 'nothing stored');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('PUT /government/asks/:matter writes only onto a matter its writer can see, and refuses slop', async () => {
|
|
356
|
+
const id = filed[filed.length - 1].id;
|
|
357
|
+
perms = ['board.vote.cast'];
|
|
358
|
+
assert.equal((await call('PUT', `/government/asks/blockers:${id}`, SLOP)).json.error.code, 'ask_refused');
|
|
359
|
+
assert.equal(store.has(`blockers:${id}`), false, 'the slop ask was not stored');
|
|
360
|
+
const ok = await call('PUT', `/government/asks/blockers:${id}`, GOOD);
|
|
361
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.json));
|
|
362
|
+
assert.equal(ok.json.matter_id, `blockers:${id}`);
|
|
363
|
+
assert.equal(store.get(`blockers:${id}`).row.question, GOOD.question);
|
|
364
|
+
// replaced whole by the next card
|
|
365
|
+
await call('PUT', `/government/asks/blockers:${id}`, card({ question: 'Ship the fix tonight?' }));
|
|
366
|
+
assert.equal(store.get(`blockers:${id}`).row.question, 'Ship the fix tonight?');
|
|
367
|
+
assert.equal((await call('PUT', '/government/asks/blockers:999999', GOOD)).status, 404, 'no such matter');
|
|
368
|
+
assert.equal((await call('PUT', '/government/asks/not-a-matter', GOOD)).json.error.code, 'bad_matter_id');
|
|
369
|
+
assert.equal((await call('PUT', '/government/asks/nowhere:1', GOOD)).status, 404, 'no such source');
|
|
370
|
+
perms = [];
|
|
371
|
+
assert.equal((await call('PUT', `/government/asks/blockers:${id}`, GOOD)).status, 403, 'without the docket\'s atom');
|
|
372
|
+
perms = ['board.vote.cast'];
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ── 7. passing an ask on (task 1004705, the direction desk) ─────────────────
|
|
376
|
+
|
|
377
|
+
test('POST /government/asks/:matter/pass: only whoever it waits on hands it to another seat; the words stay', async () => {
|
|
378
|
+
const govDb = require(path.join(GOV, 'db.js'));
|
|
379
|
+
const realSeats = { leads: govDb.listLeads, archons: govDb.listActiveArchons };
|
|
380
|
+
// the viewer (42) leads Planning; the owner seat is Archon 1
|
|
381
|
+
govDb.listLeads = async () => [{ id: 1, building: 'planning', builder_id: 42, removed_at: null }];
|
|
382
|
+
govDb.listActiveArchons = async () => [{ id: 1, github_login: 'founder' }];
|
|
383
|
+
// the blocker needs a person only for one who may resolve it (blocker.review)
|
|
384
|
+
perms = ['board.vote.cast', 'blocker.review'];
|
|
385
|
+
try {
|
|
386
|
+
const b = await call('POST', '/blockers', { title: 'Which provider' });
|
|
387
|
+
const id = `blockers:${b.json.blocker.id}`;
|
|
388
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, { seat: 'owner' })).json.error.code, 'no_ask', 'nothing stored yet');
|
|
389
|
+
await call('PUT', `/government/asks/${id}`, card({ waits_on: 'owner' }));
|
|
390
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, { seat: 'lead:planning' })).json.error.code, 'not_yours', 'it waits on the owner, not on the viewer');
|
|
391
|
+
await call('PUT', `/government/asks/${id}`, card({ waits_on: 'lead:planning' }));
|
|
392
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, { seat: 'the boss' })).json.error.code, 'unknown_seat');
|
|
393
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, {})).status, 400, 'a seat is required');
|
|
394
|
+
const ok = await call('POST', `/government/asks/${id}/pass`, { seat: 'owner' });
|
|
395
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.json));
|
|
396
|
+
assert.equal(store.get(id).row.waits_on, 'owner');
|
|
397
|
+
assert.equal(store.get(id).row.question, card({}).question, 'the ask\'s words are kept');
|
|
398
|
+
assert.deepEqual([ok.json.route.seat, ok.json.route.yours], ['owner', false], 'it left the viewer\'s to do');
|
|
399
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, { seat: 'lead:planning' })).json.error.code, 'not_yours', 'and the viewer cannot take it back');
|
|
400
|
+
perms = [];
|
|
401
|
+
assert.equal((await call('POST', `/government/asks/${id}/pass`, { seat: 'owner' })).status, 403, 'without the docket\'s atom');
|
|
402
|
+
perms = ['board.vote.cast'];
|
|
403
|
+
} finally {
|
|
404
|
+
perms = ['board.vote.cast'];
|
|
405
|
+
govDb.listLeads = realSeats.leads;
|
|
406
|
+
govDb.listActiveArchons = realSeats.archons;
|
|
407
|
+
}
|
|
408
|
+
});
|