@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,609 @@
|
|
|
1
|
+
// tests/hall_street_deck.mjs — THE DECISION DECK and THE GOVERNMENT INSIDE EACH
|
|
2
|
+
// BUILDING (task 1004685, the living-city prototype: street-deck.js,
|
|
3
|
+
// street-office.js, the opt-in office storey in street-inside.js).
|
|
4
|
+
//
|
|
5
|
+
// WHAT IT GUARDS.
|
|
6
|
+
// * Every card the deck or an interior draws keeps the ask format: a matter's
|
|
7
|
+
// stored ask (task 1004703) and the batch card. A matter with no stored ask
|
|
8
|
+
// shows in its own words, beside its floor's desk (task 1004717 deleted the
|
|
9
|
+
// prototype overlay's rewrites and the offices' sample asks). The stored asks
|
|
10
|
+
// here are the past-asks corpus (tests/fixtures/government-ask-cards.json),
|
|
11
|
+
// attached to their matters as the server attaches them.
|
|
12
|
+
// * A real ask sends what its desk row sends: an answer's act maps to the row's
|
|
13
|
+
// own act, and the request is GovMatterCard.request() of the row's own card —
|
|
14
|
+
// the same route, never a new one. Answers that record nothing send nothing.
|
|
15
|
+
// * The queue is needs-you first, then oldest; answered and passed-on asks
|
|
16
|
+
// leave; skipped ones go to the back; a building's deck is its own asks.
|
|
17
|
+
// * Batching offers three like asks at a time, never for a recorded sign-off.
|
|
18
|
+
// * The Lead's office: seated or not as GET /government/leads says, what it may build
|
|
19
|
+
// without a vote (the server's build_rights, read, never a rule of the hall's
|
|
20
|
+
// own; task 1004710), the works standing at each building, and the
|
|
21
|
+
// drawing's office storey (opt-in: the prototype-equal drawing is pinned in
|
|
22
|
+
// tests/hall_street_inside.mjs and is untouched here).
|
|
23
|
+
// * The law book is the laws in force beside the interaction contract; the
|
|
24
|
+
// treasury's tiles, pots and cap are GET /government/treasury's.
|
|
25
|
+
// * The page, in a vm: keys pick, the card leaves and the next rises, a pip is
|
|
26
|
+
// counted, the toast speaks; a refusal keeps the card; S skips; D passes it
|
|
27
|
+
// on through the server; a build-out tap is POST /government/buildouts.
|
|
28
|
+
// * The stylesheet: tokens only, motion dies under reduced motion.
|
|
29
|
+
//
|
|
30
|
+
// Run: node --test --test-reporter=tap tests/hall_street_deck.mjs
|
|
31
|
+
|
|
32
|
+
import { test } from 'node:test';
|
|
33
|
+
import assert from 'node:assert/strict';
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import vm from 'node:vm';
|
|
37
|
+
import { createRequire } from 'node:module';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
|
+
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
42
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
43
|
+
const require = createRequire(import.meta.url);
|
|
44
|
+
require(path.join(PUB, 'city-draw.js'));
|
|
45
|
+
const S = require(path.join(PUB, 'street-draw.js'));
|
|
46
|
+
const C = require(path.join(PUB, 'street-card.js'));
|
|
47
|
+
const I = require(path.join(PUB, 'street-inside.js'));
|
|
48
|
+
require(path.join(PUB, 'street-depth.js'));
|
|
49
|
+
const D = require(path.join(PUB, 'street-desk.js'));
|
|
50
|
+
const L = require(path.join(PUB, 'gov-living.js'));
|
|
51
|
+
const K = require(path.join(PUB, 'street-deck.js'));
|
|
52
|
+
const O = require(path.join(PUB, 'street-office.js'));
|
|
53
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
54
|
+
const fx = (n) => JSON.parse(fs.readFileSync(path.join(FX, n + '.json'), 'utf8'));
|
|
55
|
+
const NOW = Date.parse('2026-10-09T15:00:00.000Z');
|
|
56
|
+
const j = (v) => JSON.parse(JSON.stringify(v));
|
|
57
|
+
// real past asks as stored asks: the server attaches a matter's stored ask as `ask`
|
|
58
|
+
const CARDS = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'government-ask-cards.json'), 'utf8')).cards;
|
|
59
|
+
function withAsks(d) {
|
|
60
|
+
d.matters.forEach((m) => {
|
|
61
|
+
const c = CARDS.find((x) => x.id === m.id);
|
|
62
|
+
if (c) m.ask = { question: c.question, options: c.options, recommend: c.recommend, waits_on: c.waits_on, if_nobody_answers: c.if_nobody_answers };
|
|
63
|
+
});
|
|
64
|
+
return d;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function feeds() {
|
|
68
|
+
return { docket: withAsks(fx('government__docket')), goals: fx('goals'), rollups: fx('goals__rollups'), blockers: fx('blockers'), cost: fx('public__cost-summary'), roster: fx('builders__roster'), me: fx('me'), treasury: fx('government__treasury'), leads: fx('government__leads') };
|
|
69
|
+
}
|
|
70
|
+
function living(over = {}) { const f = feeds(); return L.livingModel({ ...f, now: NOW, ...over }); }
|
|
71
|
+
function session() { return { done: {}, delegated: {}, skipped: [], extra: [], batch: null, noBatch: {} }; }
|
|
72
|
+
function ctx(f = feeds()) { return { docket: f.docket, goals: f.goals.goals.filter((g) => g.status === 'open'), now: NOW, viewerId: String(f.docket.viewer.id) }; }
|
|
73
|
+
const deck = (lv, s, scope, f = feeds()) => K.deckModel({ living: lv, ...ctx(f), scope, session: s });
|
|
74
|
+
|
|
75
|
+
// ── the ask format, everywhere ───────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
test('every card the deck draws keeps the ask format, the batch card included', () => {
|
|
78
|
+
const lv = living();
|
|
79
|
+
const dm = deck(lv, session());
|
|
80
|
+
assert.ok(dm.remaining >= 30, 'the real docket fills the deck');
|
|
81
|
+
for (const id of dm.queue) {
|
|
82
|
+
const a = lv.city.asks.find((x) => x.id === id);
|
|
83
|
+
assert.ok(a.shown && a.ask_real, id + ' has its stored card');
|
|
84
|
+
assert.equal(L.askFormat(a.shown).ok, true, id);
|
|
85
|
+
}
|
|
86
|
+
const from = { id: 'criteria_uat:1000184', source: 'criteria_uat', building: 'review', key: 'pass', act: null, label: 'Not me' };
|
|
87
|
+
const likes = K.likesOf(K.queueOf(lv, session(), null), from, ctx());
|
|
88
|
+
const b = K.batchAsk(from, likes.slice(0, 3), likes.length - 3);
|
|
89
|
+
assert.deepEqual(L.askFormat(b.shown), { ok: true, errors: [], question_words: 5 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a matter with no stored ask shows in its own words, beside its floor\'s desk; nothing is invented for it', () => {
|
|
93
|
+
const f = feeds();
|
|
94
|
+
f.docket = fx('government__docket');
|
|
95
|
+
const lv = L.livingModel({ ...f, now: NOW });
|
|
96
|
+
const a = lv.city.asks.find((x) => x.id === 'override_requests:1000017');
|
|
97
|
+
assert.equal(a.human, null, 'no stored ask: no card');
|
|
98
|
+
assert.equal(a.ask_real, false);
|
|
99
|
+
const cv = K.cardView(a, ctx(f));
|
|
100
|
+
assert.ok(cv.question && a.raw.title.startsWith(cv.question.replace(/…$/, '')), 'the matter\'s own words');
|
|
101
|
+
assert.deepEqual(cv.options, [], 'no tap answers are made up');
|
|
102
|
+
assert.equal(cv.desk, '/builders/street#building=' + a.building, 'its acts are on its floor\'s desk');
|
|
103
|
+
assert.doesNotMatch(cv.kicker, /sent back/, 'a matter is never "sent back" for having no card');
|
|
104
|
+
// the offices make no asks of their own any more
|
|
105
|
+
for (const k of ['proposalAsk', 'appointAsk', 'lawAsk', 'strictAsk', 'LAW_CHIPS']) assert.equal(k in O, false, k);
|
|
106
|
+
assert.equal('protoAsk' in K, false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── real asks send what their desk row sends ─────────────────────────────────
|
|
110
|
+
|
|
111
|
+
test('an answer\'s act is the matter card\'s act', () => {
|
|
112
|
+
assert.deepEqual(K.actFor('go ahead'), { key: 'decide', input: { option: 'go ahead', label: 'go ahead' } });
|
|
113
|
+
assert.deepEqual(K.actFor('force:tested_by_hand'), { key: 'force', input: { option: 'tested_by_hand' } });
|
|
114
|
+
assert.deepEqual(K.actFor('approve'), { key: 'release', input: {} });
|
|
115
|
+
assert.deepEqual(K.actFor('reject'), { key: 'hold', input: {} });
|
|
116
|
+
assert.deepEqual(K.actFor('confirm'), { key: 'confirm', input: {} });
|
|
117
|
+
assert.equal(K.actFor(null), null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a real answer is the desk row\'s own request, to the route the desk uses', () => {
|
|
121
|
+
const lv = living();
|
|
122
|
+
const f = feeds();
|
|
123
|
+
const c = ctx(f);
|
|
124
|
+
const seen = {};
|
|
125
|
+
for (const a of lv.city.asks.filter((x) => x.state === 'needs')) {
|
|
126
|
+
const cv = K.cardView(a, c);
|
|
127
|
+
const row = D.cardFor(a.id, f.docket, { now: NOW, goals: c.goals, viewerId: c.viewerId }).row;
|
|
128
|
+
for (const o of cv.options) {
|
|
129
|
+
assert.notEqual(o.plan.mode, 'off', `${a.id} ${o.label}: ${o.why}`);
|
|
130
|
+
if (o.plan.mode !== 'send') continue;
|
|
131
|
+
assert.ok(row.actions.some((x) => x.key === o.plan.key), `${a.id} ${o.plan.key} is one of the row's acts`);
|
|
132
|
+
const r = C.request(o.plan.card, o.plan.key, o.plan.input);
|
|
133
|
+
assert.ok(r && r.url.startsWith('/api/bongos/'), a.id);
|
|
134
|
+
assert.deepEqual(r, C.request(row.card, o.plan.key, o.plan.input), 'the same request the desk row sends');
|
|
135
|
+
seen[a.source + ':' + o.plan.key] = r.method + ' ' + r.url.replace(/\d+/g, 'N');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
assert.deepEqual(seen, {
|
|
139
|
+
'blockers:decide': 'POST /api/bongos/blockers/N/resolve',
|
|
140
|
+
'criteria_uat:force': 'POST /api/bongos/done-when/N/satisfy',
|
|
141
|
+
'redteam_reports:confirm': 'POST /api/bongos/security/reports/N/confirm',
|
|
142
|
+
'redteam_reports:dispute': 'POST /api/bongos/security/reports/N/dispute',
|
|
143
|
+
'override_requests:release': 'POST /api/bongos/override-requests/N/decide',
|
|
144
|
+
'autobongos_fence:switch_on': 'POST /api/bongos/autonomy/fence',
|
|
145
|
+
'treasury_spending:answer': 'POST /api/bongos/government/treasury/borrows/N/answer',
|
|
146
|
+
});
|
|
147
|
+
// a blocker's option is its own chip, so its words are the resolution note
|
|
148
|
+
const b = K.cardView(lv.city.asks.find((x) => x.id === 'blockers:1000136'), c);
|
|
149
|
+
assert.deepEqual(C.request(b.options[1].plan.card, 'decide', b.options[1].plan.input).body, { resolution_note: 'Decided on the floor desk: go ahead.' });
|
|
150
|
+
// a recorded sign-off opens its line; "not me" and "keep it off" record nothing
|
|
151
|
+
const cr = K.cardView(lv.city.asks.find((x) => x.id === 'criteria_uat:1000184'), c);
|
|
152
|
+
assert.deepEqual(cr.options.map((o) => o.plan.mode), ['record', 'send', 'note']);
|
|
153
|
+
assert.equal(cr.options[0].plan.id, '1000184');
|
|
154
|
+
const fence = K.cardView(lv.city.asks.find((x) => x.id === 'autobongos_fence:fence'), c);
|
|
155
|
+
assert.deepEqual(fence.options.map((o) => o.plan.mode), ['send', 'note']);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('a viewer the server hands no acts gets answers marked not from here, never a send', () => {
|
|
159
|
+
const f = feeds();
|
|
160
|
+
f.docket.matters.forEach((m) => { m.acts = []; m.state = m.state === 'needs' ? 'moving' : m.state; });
|
|
161
|
+
const lv = L.livingModel({ ...f, now: NOW });
|
|
162
|
+
const a = lv.city.asks.find((x) => x.id === 'redteam_reports:1000009');
|
|
163
|
+
const cv = K.cardView({ ...a, state: 'needs' }, ctx(f));
|
|
164
|
+
assert.ok(cv.options.every((o) => o.plan.mode === 'off' && o.why), JSON.stringify(cv.options.map((o) => o.plan)));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── the queue ────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
test('the deck is every ask that needs a person, needs-you first then oldest; answered asks leave, skipped go last', () => {
|
|
170
|
+
const lv = living();
|
|
171
|
+
const s = session();
|
|
172
|
+
const all = lv.city.asks.filter((a) => a.state === 'needs').map((a) => a.id);
|
|
173
|
+
const dm = deck(lv, s);
|
|
174
|
+
assert.deepEqual(dm.queue, all, 'the model\'s own order');
|
|
175
|
+
assert.equal(dm.current.id, all[0]);
|
|
176
|
+
assert.equal(dm.you, lv.city.asks.filter((a) => a.state === 'needs' && a.needs_you).length);
|
|
177
|
+
assert.equal(dm.scopes.reduce((t, x) => t + x.n, 0), all.length, 'the buildings add up to the deck');
|
|
178
|
+
assert.equal(dm.behind, 2, 'two cards stand behind the top one');
|
|
179
|
+
s.done[all[0]] = true;
|
|
180
|
+
s.skipped.push(all[1]);
|
|
181
|
+
s.delegated[all[2]] = 'owner';
|
|
182
|
+
const d2 = deck(lv, s);
|
|
183
|
+
assert.equal(d2.remaining, all.length - 2);
|
|
184
|
+
assert.equal(d2.current.id, all[3]);
|
|
185
|
+
assert.equal(d2.queue[d2.queue.length - 1], all[1], 'a skipped ask comes back last');
|
|
186
|
+
const rv = deck(lv, session(), 'review');
|
|
187
|
+
assert.ok(rv.queue.length === 21 && rv.queue.every((id) => /^criteria_uat:/.test(id)), 'a building\'s deck is its own asks');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('batching: three like asks at a time, the rest counted; never for a recorded sign-off; "one at a time" stops it', () => {
|
|
191
|
+
const lv = living();
|
|
192
|
+
const s = session();
|
|
193
|
+
s.done['criteria_uat:1000184'] = true;
|
|
194
|
+
s.batch = { id: 'criteria_uat:1000184', source: 'criteria_uat', building: 'review', key: 'pass', act: null, label: 'Not me' };
|
|
195
|
+
const dm = deck(lv, s);
|
|
196
|
+
assert.equal(dm.current.batch, true);
|
|
197
|
+
assert.equal(dm.current.likes.length, 3);
|
|
198
|
+
assert.equal(dm.current.more, 17);
|
|
199
|
+
assert.equal(dm.current.kicker, '3 more like this');
|
|
200
|
+
assert.deepEqual(dm.current.options.map((o) => o.label), ['Not me, all 3', 'One at a time']);
|
|
201
|
+
// the words (task 1004702): the asker's voice, and the last line as a sentence
|
|
202
|
+
assert.equal(dm.current.said, "They ask the same thing of the same seat. I'd answer them together.");
|
|
203
|
+
assert.match(dm.current.foot.rest, /^Left alone, they wait in your deck\. 17 more like these after\.$/);
|
|
204
|
+
assert.equal(dm.hand, 3, 'the dock says three are in hand');
|
|
205
|
+
s.batch = { ...s.batch, key: 'record', act: 'record', label: 'Record a check' };
|
|
206
|
+
assert.notEqual(deck(lv, s).current.batch, true, 'a recording is one film per promise: no batch');
|
|
207
|
+
s.batch = { ...s.batch, key: 'pass', act: null };
|
|
208
|
+
s.noBatch['criteria_uat:review'] = true;
|
|
209
|
+
assert.notEqual(deck(lv, s).current.batch, true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('passing it on: only through the server, only a stored ask that waits on you, only to the owner and the Leads in office', () => {
|
|
213
|
+
// a docket the server did not route: nothing can be passed on from here
|
|
214
|
+
const un = living({ docket: unrouted(withAsks(fx('government__docket'))) });
|
|
215
|
+
for (const a of un.city.asks.filter((x) => x.state === 'needs')) assert.deepEqual(K.seatsFor(un, a, '1000025'), [], a.id);
|
|
216
|
+
assert.equal(deck(un, session()).pass, false, 'and the deck offers no D key');
|
|
217
|
+
// routed (task 1004705), with Planning's Lead in office
|
|
218
|
+
const leads = fx('government__leads');
|
|
219
|
+
leads.buildings.find((b) => b.building === 'planning').lead = { id: '1', building: 'planning', builder: { id: '1000027', login: 'builder-2016', name: 'Builder 2016' }, charter: 'Goals.', since: '2026-10-01T00:00:00Z', appointed_by: { item_id: '5' } };
|
|
220
|
+
const routed = living({ leads });
|
|
221
|
+
assert.equal(routed.routed, true);
|
|
222
|
+
assert.equal(deck(routed, session()).pass, true);
|
|
223
|
+
for (const a of routed.city.asks.filter((x) => x.state === 'needs' && x.needs_you)) {
|
|
224
|
+
const seats = K.seatsFor(routed, a, '1000025');
|
|
225
|
+
assert.ok(seats.length >= 1 && seats.length <= 5, a.id);
|
|
226
|
+
assert.ok(seats.every((s) => s.seat !== a.waits_on.seat && s.label.length <= 16 && /^(owner|lead:\w+)$/.test(s.seat)), a.id);
|
|
227
|
+
}
|
|
228
|
+
const f = feeds();
|
|
229
|
+
f.docket = fx('government__docket');
|
|
230
|
+
const bare = L.livingModel({ ...f, leads, now: NOW });
|
|
231
|
+
for (const a of bare.city.asks.filter((x) => x.state === 'needs' && !x.ask_real)) assert.deepEqual(K.seatsFor(bare, a, '1000025'), [], a.id + ': no stored ask, nothing the server could pass on');
|
|
232
|
+
// a stored ask can be passed on (POST /government/asks/:matter/pass): the dry pot's borrow (task 1004712)
|
|
233
|
+
const borrow = routed.city.asks.find((x) => x.id === 'treasury_spending:1');
|
|
234
|
+
assert.ok(borrow.ask_real && K.seatsFor(routed, borrow, '1000025').length >= 1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── the Lead's office ────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
test('the office: who leads it, what the form of government lets it build, and what stands at each building', () => {
|
|
240
|
+
const lv = living();
|
|
241
|
+
// the hub has seated no Lead: every office stands, held by the owner
|
|
242
|
+
assert.deepEqual(L.BUILDING_KEYS.map((k) => O.officeOf(lv, k).seated), L.BUILDING_KEYS.map(() => false));
|
|
243
|
+
const pl = O.officeOf(lv, 'planning');
|
|
244
|
+
assert.equal(pl.line, 'no Lead · Builder 2013 holds it');
|
|
245
|
+
assert.ok(pl.holders.includes('1000025'), 'the owner seat answers for it, the viewer among them');
|
|
246
|
+
// the hub's constitution matches no named form: everything goes to a vote
|
|
247
|
+
assert.equal(O.buildPlan(lv, 'planning', 'floor').authorised, false);
|
|
248
|
+
assert.equal(O.buildsAlone(pl).fact, 'no · it goes to a vote');
|
|
249
|
+
assert.deepEqual(O.worksOf(lv, 'release'), [], 'no build-out stands or waits there');
|
|
250
|
+
assert.equal(O.officeOf(living({ leads: null }), 'planning'), null, 'the Leads not read: no office is invented');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('who may build without a vote is the SERVER\'s answer (GET /government/leads build_rights): the hall holds no rule of its own', () => {
|
|
254
|
+
const os = O.blank();
|
|
255
|
+
const leadsFx = fx('government__leads');
|
|
256
|
+
const DEE = { id: '1000025', login: 'builder-2012', name: 'Builder 2012' };
|
|
257
|
+
const answer = (withoutVote, why, by) => ({ without_vote: withoutVote, why, ground: by });
|
|
258
|
+
const readWith = (rights) => {
|
|
259
|
+
const r = j(leadsFx);
|
|
260
|
+
const pl = r.buildings.find((b) => b.building === 'planning');
|
|
261
|
+
pl.lead = { id: '1', building: 'planning', builder: DEE, charter: 'Goals.', since: '2026-10-09T12:00:00.000Z', appointed_by: { item_id: '40' } };
|
|
262
|
+
pl.answers = { seat: 'lead:planning', builder_ids: [DEE.id], fell_to_owner: false };
|
|
263
|
+
pl.build_rights = rights;
|
|
264
|
+
return r;
|
|
265
|
+
};
|
|
266
|
+
// a monarchy: the server says at once, and says why
|
|
267
|
+
const yes = answer(true, 'The founder decides alone, so whoever holds this building builds it out at once.', 'form');
|
|
268
|
+
const monarchy = { form: 'monarchy', form_name: 'Monarchy / BDFL', holder: 'lead:planning', capsules: { committee: yes, floor: yes, annex: yes, district: answer(false, 'A district is a new part of the city, so the board votes on it.', 'city') } };
|
|
269
|
+
const lvM = living({ leads: readWith(monarchy) });
|
|
270
|
+
assert.equal(lvM.buildings.find((b) => b.key === 'planning')._sources.build_rights, 'real');
|
|
271
|
+
assert.deepEqual(j(O.buildPlan(lvM, 'planning', 'annex', os)), { authorised: true, why: yes.why, ground: 'form' });
|
|
272
|
+
assert.equal(O.buildPlan(lvM, 'planning', 'district', os).authorised, false);
|
|
273
|
+
assert.equal(O.buildsAlone(O.officeOf(lvM, 'planning', os)).fact, 'yes, under Monarchy / BDFL');
|
|
274
|
+
// a direct democracy: a vote, in the server's words
|
|
275
|
+
const no = answer(false, 'Every builder decides changes together, so it goes to a vote.', 'form');
|
|
276
|
+
const dd = { form: 'direct-democracy', form_name: 'Direct Democracy', holder: 'lead:planning', capsules: { committee: no, floor: no, annex: no, district: no } };
|
|
277
|
+
const lvD = living({ leads: readWith(dd) });
|
|
278
|
+
assert.deepEqual(j(O.buildPlan(lvD, 'planning', 'committee', os)), { authorised: false, why: no.why, ground: 'form' });
|
|
279
|
+
assert.match(O.officeOf(lvD, 'planning').line, /^Builder 2012 · Lead since 9 Oct$/);
|
|
280
|
+
// a building's own rule, said by the server
|
|
281
|
+
const sat = answer(true, 'Planning is decided by the council rank, and its holder is one of them, so they build it out at once.', 'building');
|
|
282
|
+
const lvB = living({ leads: readWith({ form: 'direct-democracy', form_name: 'Direct Democracy', holder: 'lead:planning', capsules: { committee: sat, floor: sat, annex: sat, district: no } }) });
|
|
283
|
+
assert.equal(O.buildsAlone(O.officeOf(lvB, 'planning', os)).fact, 'yes, by how this building decides');
|
|
284
|
+
// an older server with no build_rights: nothing is built at once
|
|
285
|
+
const lvN = living({ leads: readWith(null) });
|
|
286
|
+
assert.deepEqual(j(O.buildPlan(lvN, 'planning', 'floor', os)), { authorised: false, why: 'Who may build here is not known, so the board votes on it.' });
|
|
287
|
+
// the capsules are the server's own four (modules/government/build-rights.js)
|
|
288
|
+
const BR = require(path.join(ROOT, 'modules/government/build-rights.js'));
|
|
289
|
+
assert.deepEqual(O.CAPSULES.map((c) => c.key), [...BR.CAPSULES]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('the drawing: the office is one storey on top, the lift runs to it, a live site takes the next empty floor', () => {
|
|
293
|
+
const f = feeds();
|
|
294
|
+
const base = I.buildingModel({ key: 'release', docket: f.docket, goals: [], mode: 'dark', now: NOW });
|
|
295
|
+
const office = { seated: true, line: 'Builder 2016 · Lead since 5 Sep', ini: '', rooms: [{ name: 'Testers', proposed: false }] };
|
|
296
|
+
const works = [{ kind: 'floor', state: 'site', label: 'Staging floor', line: '55% built', progress: 0.55 }];
|
|
297
|
+
const bv = I.buildingModel({ key: 'release', docket: f.docket, goals: [], mode: 'dark', now: NOW, office, works, floor: 'office' });
|
|
298
|
+
assert.equal(bv.cap, base.cap + 1, 'one storey more: the office');
|
|
299
|
+
assert.equal(bv.capFloors, base.cap, 'the office is not one of the floors in use');
|
|
300
|
+
assert.equal(bv.fill, base.fill);
|
|
301
|
+
assert.equal(bv.officeSel, true);
|
|
302
|
+
assert.equal(bv.lift.ty, -(bv.officeIdx * 52));
|
|
303
|
+
assert.ok(bv.floors.some((x) => x.index === 'office' && x.selected && !('proto' in x)));
|
|
304
|
+
assert.ok(!bv.floors.some((x) => x.index !== 'office' && x.selected), 'no floor is chosen while the office is');
|
|
305
|
+
assert.ok(bv.texts.some((t) => t.v === 'Lead\'s office'));
|
|
306
|
+
assert.ok(bv.texts.some((t) => t.v === '55%'), 'the site says how far it is');
|
|
307
|
+
assert.deepEqual(bv.sites.map((s) => [s.label, s.y]), [['Staging floor', 676 - (base.occ + 1) * 52]]);
|
|
308
|
+
const annex = I.buildingModel({ key: 'treasury', docket: f.docket, goals: [], mode: 'light', now: NOW, office, works: [{ kind: 'annex', state: 'proposed', label: 'Investments arm' }] });
|
|
309
|
+
assert.ok(annex.shapes.some((p) => p.dash === '5 4'), 'a proposed annex is a dashed ghost');
|
|
310
|
+
assert.equal(annex.hasAnnex, false);
|
|
311
|
+
const built = I.buildingModel({ key: 'planning', docket: f.docket, goals: [], mode: 'dark', now: NOW, office, extraFloors: [{ name: 'A new floor', line: 'built by a build-out' }], floor: 6 });
|
|
312
|
+
assert.equal(built.occ, base.occ === undefined ? built.occ : I.PLAN.planning.floors.length + 1);
|
|
313
|
+
assert.equal(built.builtSel, true, 'a floor a build-out put up is chosen like any floor');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('the picture the board votes on: the city\'s own building with the change drawn dashed, for every building and change', () => {
|
|
317
|
+
for (const key of L.BUILDING_KEYS) {
|
|
318
|
+
for (const c of O.CAPSULES) {
|
|
319
|
+
for (const mode of ['dark', 'light']) {
|
|
320
|
+
const p = O.isoPicture(key, c.key, mode);
|
|
321
|
+
assert.ok(p.paths.length > 4, key);
|
|
322
|
+
assert.ok(p.paths.some((x) => x.dash === '4 3'), `${key} ${c.key}: the change is dashed`);
|
|
323
|
+
assert.ok(p.viewBox.split(' ').map(Number).every(Number.isFinite));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── the law book, the treasury, strictness ───────────────────────────────────
|
|
330
|
+
|
|
331
|
+
test('the law book: the laws in force and the docket\'s standing rule, each with what the AI does; the contract every session reads', () => {
|
|
332
|
+
const law = { id: 7, no: 1, text: 'Screenshots on UI work', effect_on_ai: 'Every screen change carries a before and after shot.', locks: [], passed_at: '2026-10-08T10:00:00Z', passed_by: { government: 'the board' }, in_force: true };
|
|
333
|
+
const lv = living({ laws: { laws: [law] } });
|
|
334
|
+
const book = O.lawBook(lv);
|
|
335
|
+
assert.equal(book.rows.length, lv.city.laws.length);
|
|
336
|
+
assert.ok(book.rows.some((r) => r.text === 'Screenshots on UI work' && r.by === 'the board' && r.when === '8 Oct'), 'the law in force, who passed it, when');
|
|
337
|
+
assert.ok(book.rows.every((r) => r.text && r.ai && r.by && r.when));
|
|
338
|
+
assert.equal('chips' in book, false, 'no law is proposed from a made-up list');
|
|
339
|
+
const me = fx('me');
|
|
340
|
+
const inj = O.injection(lv, me);
|
|
341
|
+
assert.equal(inj.real, true);
|
|
342
|
+
assert.equal([inj.head].concat(inj.profile).join('\n'), me.interaction.contract.split('\n').map((x) => x.replace(/^-\s*/, '').trim()).join('\n'), 'the contract, word for word');
|
|
343
|
+
assert.ok(inj.laws.some((l) => l.text === 'Screenshots on UI work'), 'every agent reads the law');
|
|
344
|
+
assert.equal(O.lawBook(living({ laws: { laws: [] } })).rows.every((r) => r.id !== 'law:7'), true, 'none in force: none in the book');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('the treasury: the real spend as tiles, the pots per goal with the dry one first, no cap by default', () => {
|
|
348
|
+
const lv = living();
|
|
349
|
+
const t = O.treasury(lv);
|
|
350
|
+
assert.deepEqual(t.tiles.map((x) => x.v), [O.usd(lv.city.spend.month_usd), O.usd(lv.city.spend.last_month_usd), O.usd(lv.city.spend.reserve_usd)]);
|
|
351
|
+
assert.ok(t.tiles.every((x) => x.v.length <= 8 && x.l.split(' ').length <= 2), 'a tile is one number and two words');
|
|
352
|
+
assert.equal(t.pots[0].dry, true);
|
|
353
|
+
// the pots, the reserve and the cap are GET /government/treasury's (task 1004712)
|
|
354
|
+
assert.equal(t.dryAsk, 'treasury_spending:1', 'the dry pot\'s real borrow, on the docket');
|
|
355
|
+
assert.equal(t.cap.on, false);
|
|
356
|
+
assert.match(t.cap.line, /no spend limit/);
|
|
357
|
+
// a loan the treasury really made refills the pot (a visit's own answer does not)
|
|
358
|
+
const tr = fx('government__treasury');
|
|
359
|
+
tr.pots.find((p) => p.dry).lent_usd = 2350;
|
|
360
|
+
assert.equal(O.treasury(living({ treasury: tr })).pots.find((p) => p.goal === t.pots[0].goal).dry, false, 'a loan refills the pot');
|
|
361
|
+
assert.equal(O.treasury(living({ treasury: null })), null, 'the treasury not read: no book');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('the model halves are pure: no document, no fetch, no clock above the page half', () => {
|
|
365
|
+
for (const f of ['street-deck.js', 'street-office.js']) {
|
|
366
|
+
const src = read(f).split('THE PAGE HALF')[0].replace(/\/\/.*$/gm, '');
|
|
367
|
+
assert.doesNotMatch(src, /\bdocument\b|\bfetch\(|Date\.now|localStorage/, f);
|
|
368
|
+
}
|
|
369
|
+
const a = j(deck(living(), session()));
|
|
370
|
+
const b = j(deck(living(), session()));
|
|
371
|
+
assert.deepEqual(a, b, 'the same input gives the same deck');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ── the page ─────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
function fakeDom() {
|
|
377
|
+
const byId = new Map();
|
|
378
|
+
const focused = { el: null };
|
|
379
|
+
const mk = (tag) => ({
|
|
380
|
+
tagName: tag, attrs: {}, kids: [], className: '', listeners: {}, _text: '', hidden: false,
|
|
381
|
+
style: { setProperty(k, v) { this[k] = v; } },
|
|
382
|
+
clientWidth: 1440, clientHeight: 708, offsetLeft: 0, scrollLeft: 0, parentNode: null,
|
|
383
|
+
get children() { const a = this.kids.slice(); a[Symbol.isConcatSpreadable] = false; return a; },
|
|
384
|
+
get firstChild() { return this.kids[0] || null; },
|
|
385
|
+
setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'id') byId.set(String(v), this); },
|
|
386
|
+
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
|
|
387
|
+
removeAttribute(k) { delete this.attrs[k]; },
|
|
388
|
+
appendChild(c) { this.kids.push(c); c.parentNode = this; return c; },
|
|
389
|
+
removeChild(c) { this.kids = this.kids.filter((x) => x !== c); return c; },
|
|
390
|
+
addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); },
|
|
391
|
+
fire(t) { (this.listeners[t] || []).forEach((fn) => fn({ type: t, preventDefault() {} })); },
|
|
392
|
+
focus() { focused.el = this; },
|
|
393
|
+
set textContent(v) { this._text = String(v); this.kids = []; },
|
|
394
|
+
get textContent() { return this._text + this.kids.map((k) => k.textContent || '').join(''); },
|
|
395
|
+
get innerHTML() { throw new Error('no innerHTML'); },
|
|
396
|
+
set innerHTML(v) { throw new Error('no innerHTML'); },
|
|
397
|
+
});
|
|
398
|
+
const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
399
|
+
for (const id of [...html.matchAll(/id="([A-Za-z]+)"/g)].map((m) => m[1])) {
|
|
400
|
+
const el = mk('x');
|
|
401
|
+
el.setAttribute('id', id);
|
|
402
|
+
const tag = new RegExp(`<[^>]*\\bid="${id}"[^>]*>`).exec(html);
|
|
403
|
+
if (tag && /\shidden(?=[\s>])/.test(tag[0])) el.hidden = true;
|
|
404
|
+
}
|
|
405
|
+
return { document: { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_n, t) => mk(t), createTextNode: (t) => { const n = mk('#text'); n._text = String(t); return n; } }, byId, focused };
|
|
406
|
+
}
|
|
407
|
+
const FEED = {
|
|
408
|
+
'/api/bongos/government/docket': 'government__docket', '/api/bongos/goals': 'goals', '/api/bongos/goals/rollups': 'goals__rollups',
|
|
409
|
+
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster',
|
|
410
|
+
'/api/bongos/blockers': 'blockers', '/api/bongos/public/cost-summary': 'public__cost-summary',
|
|
411
|
+
// the living model's reads, as gov-feeds.js names them (one read each per page)
|
|
412
|
+
'/api/bongos/tasks/claimable?limit=1': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
413
|
+
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
414
|
+
'/api/bongos/government/constitution': 'government__constitution', '/api/bongos/government/leads': 'government__leads',
|
|
415
|
+
};
|
|
416
|
+
// The docket as a server that did not route it serves it.
|
|
417
|
+
function unrouted(d) {
|
|
418
|
+
delete d.routed;
|
|
419
|
+
d.matters.forEach((m) => { delete m.route; });
|
|
420
|
+
return d;
|
|
421
|
+
}
|
|
422
|
+
// feeds: { url: body | null } served instead of the fixture (null answers 404)
|
|
423
|
+
async function bootPage(hash, { answer = null, feeds = {} } = {}) {
|
|
424
|
+
const { document, byId, focused } = fakeDom();
|
|
425
|
+
const sent = [];
|
|
426
|
+
const got = [];
|
|
427
|
+
const store = new Map();
|
|
428
|
+
const winL = {};
|
|
429
|
+
const location = { hash, href: '' };
|
|
430
|
+
const window = {
|
|
431
|
+
innerWidth: 1440, innerHeight: 900, location,
|
|
432
|
+
localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)) },
|
|
433
|
+
matchMedia: () => ({ matches: true, addEventListener() {} }),
|
|
434
|
+
addEventListener(t, fn) { (winL[t] = winL[t] || []).push(fn); }, history: { pushState() {} },
|
|
435
|
+
};
|
|
436
|
+
const fetch = (url, init) => {
|
|
437
|
+
if (init && init.method && init.method !== 'GET') {
|
|
438
|
+
const body = JSON.parse(init.body || '{}');
|
|
439
|
+
sent.push({ method: init.method, url, body });
|
|
440
|
+
const own = answer ? answer(url, body) : null;
|
|
441
|
+
const out = own || { status: 501, body: { error: { code: 'preview_readonly' } } };
|
|
442
|
+
return Promise.resolve({ ok: out.status < 300, status: out.status, json: async () => out.body || {} });
|
|
443
|
+
}
|
|
444
|
+
got.push(url);
|
|
445
|
+
if (Object.prototype.hasOwnProperty.call(feeds, url)) {
|
|
446
|
+
const body = feeds[url];
|
|
447
|
+
return Promise.resolve(body ? { ok: true, json: async () => JSON.parse(JSON.stringify(body)) } : { ok: false, status: 404, json: async () => ({}) });
|
|
448
|
+
}
|
|
449
|
+
const name = FEED[url];
|
|
450
|
+
const body = name === 'government__docket' ? withAsks(fx(name)) : name ? fx(name) : null;
|
|
451
|
+
return Promise.resolve(name ? { ok: true, json: async () => body } : { ok: false, json: async () => ({}) });
|
|
452
|
+
};
|
|
453
|
+
window.fetch = fetch;
|
|
454
|
+
const ctxv = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map, Set, setTimeout, clearTimeout });
|
|
455
|
+
ctxv.globalThis = ctxv;
|
|
456
|
+
for (const f of ['gov-living.js', 'gov-feeds.js', 'city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street-amend.js', 'street-deck.js', 'street-office.js', 'street.js']) vm.runInContext(read(f), ctxv);
|
|
457
|
+
await window.GovStreetPage.ready;
|
|
458
|
+
for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0));
|
|
459
|
+
const key = (k) => (winL.keydown || []).forEach((fn) => fn({ key: k, target: null, preventDefault() {} }));
|
|
460
|
+
const walk = (el, pred, out = []) => { (el.kids || []).forEach((k) => { if (pred(k)) out.push(k); walk(k, pred, out); }); return out; };
|
|
461
|
+
const cls = (el, c) => walk(el, (k) => String(k.className).split(' ').includes(c));
|
|
462
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
463
|
+
return { window, byId, focused, sent, got, store, key, walk, cls, wait, deck: () => byId.get('streetDeck') };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
test('the page: #deck draws the top card over the street; a key answers it through the row\'s route, the next rises, a pip is counted', async () => {
|
|
467
|
+
const p = await bootPage('#deck=planning');
|
|
468
|
+
const box = p.deck();
|
|
469
|
+
assert.equal(box.hidden, false);
|
|
470
|
+
assert.equal(p.byId.get('streetWindow').getAttribute('data-view'), 'deck', 'the deck is open over the street');
|
|
471
|
+
assert.doesNotMatch(read('street-deck.css'), /\[data-view="deck"\] \.street-sizer/, 'the street stays in view (task 1004702)');
|
|
472
|
+
const card = p.cls(box, 'ask')[0];
|
|
473
|
+
assert.equal(card.getAttribute('data-ask'), 'blockers:1000136');
|
|
474
|
+
// the headline is what needs you (gov-living.js countsOf: the city's door says the same)
|
|
475
|
+
assert.equal(p.cls(box, 'deck-dock__n')[0].textContent, '1', 'one card in hand');
|
|
476
|
+
assert.equal(p.cls(box, 'deck-dock__of')[0].textContent, ' of 9 to do', 'Planning\'s own: its nine blockers');
|
|
477
|
+
assert.ok(/\bdeck-panel\b/.test(card.className) && /\bgov-glass\b/.test(card.className), 'the card is the glass panel');
|
|
478
|
+
assert.equal(p.cls(card, 'ask__bld')[0].textContent, 'Planning', 'the building that asks, named on the card');
|
|
479
|
+
assert.ok(p.cls(box, 'deck-dock').length === 1 && p.cls(box, 'deck-scene').length === 1, 'the dock at the foot, the scene layer under the panel');
|
|
480
|
+
assert.ok(p.walk(card, (k) => k.tagName === 'kbd').length >= 3, 'each answer wears its key');
|
|
481
|
+
assert.equal(p.cls(card, 'ask__opt').length, 3);
|
|
482
|
+
assert.equal(p.walk(box, (k) => k.getAttribute && k.getAttribute('data-proto') != null).length, 0, 'nothing on the deck is marked prototype');
|
|
483
|
+
// the deck reads nothing of its own: the street's living model, one read of each feed
|
|
484
|
+
assert.deepEqual(p.got.filter((u, i) => p.got.indexOf(u) !== i), [], 'no feed is read twice on the page');
|
|
485
|
+
assert.ok(!p.got.includes('/api/bongos/blockers'), 'the deck\'s old extra read is gone');
|
|
486
|
+
p.key('2');
|
|
487
|
+
await p.wait(450);
|
|
488
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/blockers/1000136/resolve', body: { resolution_note: 'Decided on the floor desk: go ahead.' } }]);
|
|
489
|
+
assert.match(p.byId.get('streetToast').textContent, /reached|Sent to POST \/blockers\/1000136\/resolve/);
|
|
490
|
+
assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'one pip, kept for the day');
|
|
491
|
+
const next = p.cls(p.deck(), 'ask')[0];
|
|
492
|
+
assert.equal(next.getAttribute('data-ask'), 'blockers:1000143');
|
|
493
|
+
assert.match(next.className, /\bask--rise\b/, 'the next one rises');
|
|
494
|
+
assert.equal(p.cls(p.deck(), 'deck-dock__of')[0].textContent, ' of 8 to do');
|
|
495
|
+
assert.equal(p.byId.get('hudNeeds').textContent, '37', 'the street\'s header moves with the deck');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('the page: a refusal keeps the card and counts nothing; S skips to the back', async () => {
|
|
499
|
+
const p = await bootPage('#deck=planning', { answer: () => ({ status: 409, body: { error: { code: 'not_pending' } } }) });
|
|
500
|
+
p.key('1');
|
|
501
|
+
await p.wait(60);
|
|
502
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Someone already decided this.');
|
|
503
|
+
assert.equal(p.cls(p.deck(), 'ask')[0].getAttribute('data-ask'), 'blockers:1000136', 'the card stays');
|
|
504
|
+
assert.equal(p.store.get('gov-desk-today'), undefined, 'nothing counted');
|
|
505
|
+
p.key('s');
|
|
506
|
+
await p.wait(400);
|
|
507
|
+
assert.equal(p.cls(p.deck(), 'ask')[0].getAttribute('data-ask'), 'blockers:1000143');
|
|
508
|
+
assert.equal(p.cls(p.deck(), 'deck-dock__of')[0].textContent, ' of 9 to do', 'a skip answers nothing');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('the page, routed (task 1004705): the deck holds what the server routed to you; a real ask is passed on through the server', async () => {
|
|
512
|
+
const docket = fx('government__docket');
|
|
513
|
+
const id = docket.matters.find((m) => m.state === 'needs' && m.route.yours).id;
|
|
514
|
+
docket.matters.find((m) => m.id === id).ask = {
|
|
515
|
+
question: 'Close this report as fixed?',
|
|
516
|
+
options: [{ key: 'close', label: 'Close it', result: 'The report is marked fixed.' }, { key: 'keep', label: 'Keep it open', result: 'It stays on the Disputes desk.' }],
|
|
517
|
+
recommend: { key: 'keep', why: 'Nobody has checked the fix yet.' }, waits_on: 'owner', if_nobody_answers: 'It stays on the Disputes desk.',
|
|
518
|
+
};
|
|
519
|
+
const leads = fx('government__leads');
|
|
520
|
+
const planning = leads.buildings.find((b) => b.building === 'planning');
|
|
521
|
+
planning.lead = { id: '1', building: 'planning', builder: { id: '1000027', login: 'builder-27', name: 'Builder 27' }, charter: 'Goals, priorities and the order of work.', since: '2026-10-01T00:00:00Z', appointed_by: { item_id: '5', href: '/board-room?item=5' } };
|
|
522
|
+
const yours = docket.matters.filter((m) => m.state === 'needs' && m.route.yours).length;
|
|
523
|
+
const p = await bootPage('#deck', {
|
|
524
|
+
feeds: { '/api/bongos/government/docket': docket, '/api/bongos/government/leads': leads },
|
|
525
|
+
answer: (url) => (/\/pass$/.test(url) ? { status: 200, body: { ok: true } } : null),
|
|
526
|
+
});
|
|
527
|
+
assert.equal(p.cls(p.deck(), 'deck-dock__of')[0].textContent, ' of ' + yours + ' to do', 'to do is the routes that are yours');
|
|
528
|
+
const card = p.cls(p.deck(), 'ask')[0];
|
|
529
|
+
assert.equal(card.getAttribute('data-ask'), id);
|
|
530
|
+
assert.equal(p.cls(card, 'ask__waits')[0].textContent, 'waits on you', 'the route says it is yours');
|
|
531
|
+
p.key('d');
|
|
532
|
+
const chips = p.cls(p.deck(), 'ask__seats')[0];
|
|
533
|
+
assert.ok(chips, 'the seats open');
|
|
534
|
+
const seats = p.cls(p.deck(), 'ask__seat');
|
|
535
|
+
assert.deepEqual(seats.map((s) => s.getAttribute('title')), ['Planning Lead'], 'only a real Lead in office (the owner is where it waits already)');
|
|
536
|
+
seats[0].fire('click');
|
|
537
|
+
await p.wait(450);
|
|
538
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/government/asks/' + encodeURIComponent(id) + '/pass', body: { seat: 'lead:planning' } }]);
|
|
539
|
+
assert.match(p.byId.get('streetToast').textContent, /^Passed to .+\. It leaves your deck\.$/);
|
|
540
|
+
assert.equal(p.cls(p.deck(), 'deck-dock__of')[0].textContent, ' of ' + (yours - 1) + ' to do');
|
|
541
|
+
// the next card has no stored ask: nothing to pass on
|
|
542
|
+
const next = p.cls(p.deck(), 'ask')[0];
|
|
543
|
+
assert.ok(!p.walk(next, (k) => k.getAttribute && k.getAttribute('data-act') === 'pass').length, 'no stored ask, no pass it on');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('the page: inside Planning the office opens from its top storey; a build-out tap is POST /government/buildouts', async () => {
|
|
547
|
+
const p = await bootPage('#building=planning', { answer: (url) => (/\/government\/buildouts$/.test(url) ? { status: 201, body: { ok: true, path: 'at_once', rights: { why: 'The founder decides alone.' } } } : null) });
|
|
548
|
+
const govt = p.byId.get('insideGovt');
|
|
549
|
+
assert.equal(govt.hidden, false, 'the government layer stands in the panel');
|
|
550
|
+
assert.ok(p.cls(govt, 'govx-lead').length === 1 && p.cls(govt, 'govx-stack').length === 1);
|
|
551
|
+
const office = p.byId.get('insideFloors').kids.find((b) => b.getAttribute('data-floor') === 'office');
|
|
552
|
+
assert.equal(office.getAttribute('data-proto'), null, 'nothing is marked prototype');
|
|
553
|
+
office.fire('click');
|
|
554
|
+
const desk = () => (p.byId.get('insideOpenA').hidden ? p.byId.get('insideOpenB') : p.byId.get('insideOpenA'));
|
|
555
|
+
assert.match(desk().className, /\boffice\b/);
|
|
556
|
+
const cap = p.walk(desk(), (k) => k.getAttribute && k.getAttribute('data-capsule') === 'floor')[0];
|
|
557
|
+
assert.ok(cap, 'the owner seat holds Planning, the viewer among it: the capsules are offered');
|
|
558
|
+
cap.fire('click');
|
|
559
|
+
await p.wait(60);
|
|
560
|
+
assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/government/buildouts', body: { building: 'planning', capsule: 'floor' } }]);
|
|
561
|
+
assert.equal(p.byId.get('streetToast').textContent, 'Building a new floor now. The founder decides alone.');
|
|
562
|
+
assert.equal(p.cls(p.byId.get('insideFloors'), 'office-site').length, 0, 'nothing is drawn for this visit only: the server\'s read draws it');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('the page: a build-out the server files as a vote says so; the preview stores nothing and says that too', async () => {
|
|
566
|
+
const p = await bootPage('#building=treasury', { answer: (url, body) => (body.capsule === 'committee' ? { status: 201, body: { ok: true, path: 'vote' } } : null) });
|
|
567
|
+
p.byId.get('insideFloors').kids.find((b) => b.getAttribute('data-floor') === 'office').fire('click');
|
|
568
|
+
const desk = () => (p.byId.get('insideOpenA').hidden ? p.byId.get('insideOpenB') : p.byId.get('insideOpenA'));
|
|
569
|
+
p.walk(desk(), (k) => k.getAttribute && k.getAttribute('data-capsule') === 'committee')[0].fire('click');
|
|
570
|
+
await p.wait(60);
|
|
571
|
+
assert.equal(p.sent.length, 1);
|
|
572
|
+
assert.match(p.byId.get('streetToast').textContent, /^Filed for a vote\. The card has its picture\.$/);
|
|
573
|
+
p.byId.get('insideFloors').kids.find((b) => b.getAttribute('data-floor') === 'office').fire('click');
|
|
574
|
+
p.walk(desk(), (k) => k.getAttribute && k.getAttribute('data-capsule') === 'annex')[0].fire('click');
|
|
575
|
+
await p.wait(60);
|
|
576
|
+
assert.equal(p.sent.length, 2);
|
|
577
|
+
assert.equal(p.byId.get('streetToast').textContent, 'This preview stores nothing; nothing was built.');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('the stylesheet: tokens only, the accent only on what needs you, motion dies under reduced motion, 24px targets', () => {
|
|
581
|
+
const css = read('street-deck.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
582
|
+
assert.doesNotMatch(css, /#[0-9a-f]{3,8}\b|rgba?\(/i, 'no literal colours');
|
|
583
|
+
assert.doesNotMatch(css, /:root/);
|
|
584
|
+
assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{[^}]*animation: none !important; transition: none !important;/);
|
|
585
|
+
for (const m of css.matchAll(/animation: ([a-z-]+) (\d+)(m?s)/g)) {
|
|
586
|
+
const ms = m[3] === 's' ? Number(m[2]) * 1000 : Number(m[2]);
|
|
587
|
+
if (/infinite/.test(css.slice(m.index, m.index + 120))) assert.ok(ms >= 20000, m[1] + ': an ambient loop is 20 s or more');
|
|
588
|
+
}
|
|
589
|
+
assert.doesNotMatch(css.split('@keyframes').slice(1).join(''), /opacity/, 'motion is transform only');
|
|
590
|
+
assert.match(css, /\.ask__opt \{[^}]*min-height: 60px/);
|
|
591
|
+
// task 1004702: the panel rises once and the halo breathes on a 24 s loop; both stop
|
|
592
|
+
assert.match(css, /\.ask--rise \{ animation: deck-rise \d+ms[^}]*both; \}/, 'the rise is a one-shot');
|
|
593
|
+
assert.match(css, /\.gov-halo \{[^}]*animation: gov-halo 24s ease-in-out infinite/);
|
|
594
|
+
const still = /@media \(prefers-reduced-motion: reduce\) \{\s*([^{]*)\{ animation: none !important/.exec(css);
|
|
595
|
+
assert.ok(still && /\.ask--rise\b/.test(still[1]) && /\.gov-halo\b/.test(still[1]), 'the rise and the halo both stop under reduced motion');
|
|
596
|
+
// dark is written twice; light is paper: an ink shadow and no glow
|
|
597
|
+
assert.match(css, /html\[data-theme="dark"\] body\[data-page="street"\][^{]*\{[^}]*--gov-glass:/);
|
|
598
|
+
assert.match(css, /html:not\(\[data-theme\]\) body\[data-page="street"\][^{]*\{[^}]*--gov-glass:/);
|
|
599
|
+
const light = /body\[data-page="street"\], body\[data-page="city"\] \{([^}]*)\}/.exec(css)[1];
|
|
600
|
+
assert.match(light, /--gov-glass-drop: 0 28px 70px color-mix\(in srgb, var\(--ink\)/, 'light mode: an ink shadow');
|
|
601
|
+
assert.doesNotMatch(light, /--gov-glass-drop:[^;]*var\(--accent/, 'no glow');
|
|
602
|
+
assert.match(css, /\.ask__text \{[^}]*min-height: 24px; min-width: 24px/);
|
|
603
|
+
const accent = css.split('\n').filter((l) => /var\(--accent\)|var\(--street-accent-text\)/.test(l)).map((l) => l.split('{')[0].trim());
|
|
604
|
+
assert.ok(accent.length >= 6);
|
|
605
|
+
assert.deepEqual(accent.filter((s) => !/needs|--you|--dry|focus-visible|deck-clear__tick/.test(s)), [], 'the accent lights only what needs you (and focus, and the cleared deck\'s lit tick)');
|
|
606
|
+
const html = read('street.html');
|
|
607
|
+
assert.ok(html.indexOf('/builders/street-deck.js?') > html.indexOf('/builders/gov-living.js?') && html.indexOf('/builders/street-office.js?') < html.indexOf('/builders/street.js?'), 'the deck and the offices load after the model and before the page');
|
|
608
|
+
assert.match(html, /<section class="deck" id="streetDeck"[^>]*hidden><\/section>/);
|
|
609
|
+
});
|