@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,655 @@
|
|
|
1
|
+
// tests/hall_gov_real_site.mjs — ON A REAL SITE THE PROTOTYPE IS NOT THERE
|
|
2
|
+
// (task 1004701: land the living-city prototype as the hall's visual base).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT GUARDS. The living city, the street, the decision deck, the inside of
|
|
5
|
+
// every building and My agents were built over real reads plus ONE labelled
|
|
6
|
+
// prototype overlay (GET /proto/governor-overlay), served by the hall-preview
|
|
7
|
+
// harness only. Task 1004717 deleted it: no page asks for it any more. This file
|
|
8
|
+
// boots every page with every real fixture, a stale server still answering the
|
|
9
|
+
// overlay route, and requires that:
|
|
10
|
+
// * no page asks for the overlay, and nothing on any page carries `data-proto`;
|
|
11
|
+
// * no text the overlay used to invent (a Lead's charter, a law, a prototype
|
|
12
|
+
// setting, a human rewrite of an ask) appears anywhere in the page's text,
|
|
13
|
+
// attributes or markup;
|
|
14
|
+
// * no prototype key, office storey, law book of invented laws or pot is drawn;
|
|
15
|
+
// * the real reads still draw (the docket's asks, the readings, the real rows);
|
|
16
|
+
// * a law the project really passed (GET /government/laws, task 1004706) stands
|
|
17
|
+
// as real, while the overlay's invented laws still do not;
|
|
18
|
+
// * who leads each building (GET /government/leads, task 1004709) stands as
|
|
19
|
+
// real: a Lead the government appointed on their building, and the owner on
|
|
20
|
+
// every unseated one, with nothing marked prototype;
|
|
21
|
+
// * the shell's seat-gated nav stays today's nav (unknown is never "none").
|
|
22
|
+
// And, as the control, that the same scan DOES find those words when a real read
|
|
23
|
+
// carries them, so a scan that quietly finds nothing cannot pass.
|
|
24
|
+
//
|
|
25
|
+
// Run: node --test --test-reporter=tap tests/hall_gov_real_site.mjs
|
|
26
|
+
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import assert from 'node:assert/strict';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import vm from 'node:vm';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
38
|
+
const FX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
39
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
40
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
41
|
+
const GovPositions = require(path.join(PUB, 'gov-positions.js'));
|
|
42
|
+
const BUILDINGS = ['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury'];
|
|
43
|
+
const OVERLAY_URL = '/api/bongos/proto/governor-overlay';
|
|
44
|
+
|
|
45
|
+
// ── the deleted overlay's own words ─────────────────────────────────────────
|
|
46
|
+
// Phrases the prototype overlay used to invent (its laws, a Lead's charter, its
|
|
47
|
+
// prototype settings, a rewrite of an ask), that no real fixture and no page
|
|
48
|
+
// source holds: if one of these is on a page, something invented it.
|
|
49
|
+
const PAGE_SOURCES = ['city.html', 'street.html', 'my-agents.html', 'city.js', 'city-draw.js', 'street.js', 'street-draw.js', 'street-depth.js',
|
|
50
|
+
'street-deck.js', 'street-office.js', 'street-inside.js', 'street-card.js', 'street-desk.js', 'street-amend.js', 'my-agents.js', 'gov-living.js', 'gov-feeds.js', 'gov-positions.js']
|
|
51
|
+
.map(read).join('\n');
|
|
52
|
+
const REAL_TEXT = fs.readdirSync(FX).filter((f) => f.endsWith('.json')).map((f) => fs.readFileSync(path.join(FX, f), 'utf8')).join('\n');
|
|
53
|
+
const OVERLAY_PHRASES = ['Ask everything up front', 'Build the approved design exactly', 'Planning floors: goals, priorities, the order of work.',
|
|
54
|
+
'When an agent is stuck', 'Go live without asking', 'Judge a high-severity security report?'];
|
|
55
|
+
// a stale server that still answers the old route, with those words in it
|
|
56
|
+
const OLD_OVERLAY = {
|
|
57
|
+
_proto: 'Prototype data: concepts not yet in the database',
|
|
58
|
+
owner: { id: '3' },
|
|
59
|
+
laws: [{ id: 'skip', text: OVERLAY_PHRASES[0] }, { id: 'land', text: OVERLAY_PHRASES[1] }],
|
|
60
|
+
leads: [{ building: 'planning', lead: '1000025', charter: { scope: OVERLAY_PHRASES[2] }, deputies: [], committees: [] }],
|
|
61
|
+
personal_settings: [{ key: 'on_stuck', label: OVERLAY_PHRASES[3], values: ['ask'], value: 'ask' }, { key: 'auto_go_live', label: OVERLAY_PHRASES[4], values: ['no'], value: 'no' }],
|
|
62
|
+
asks: [{ id: 'redteam_reports:1000009', question: OVERLAY_PHRASES[5], options: [{ key: 'a', label: 'A', result: 'a' }, { key: 'b', label: 'B', result: 'b' }], recommend: { key: 'a', why: 'x' }, waits_on: 'owner', if_nobody_answers: 'x' }],
|
|
63
|
+
};
|
|
64
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
65
|
+
|
|
66
|
+
// ── one fake DOM for every page ─────────────────────────────────────────────
|
|
67
|
+
// Every element of the page's HTML that has an id stands as a node, with its
|
|
68
|
+
// attributes and its static markup; scripts build the rest with createElement or
|
|
69
|
+
// innerHTML. Enough of querySelector/closest/matches for the pages' own selectors.
|
|
70
|
+
function fakeDom(htmlFile) {
|
|
71
|
+
const byId = new Map();
|
|
72
|
+
const roots = [];
|
|
73
|
+
const parseSel = (sel) => sel.split(',').map((s) => s.trim()).filter(Boolean);
|
|
74
|
+
const matchOne = (el, w) => {
|
|
75
|
+
let m;
|
|
76
|
+
if ((m = /^\[([\w-]+)="([^"]*)"\]$/.exec(w))) return el.attrs[m[1]] === m[2];
|
|
77
|
+
if ((m = /^\[([\w-]+)\]$/.exec(w))) return m[1] in el.attrs;
|
|
78
|
+
if ((m = /^\.([\w-]+)$/.exec(w))) return String(el.className).split(' ').includes(m[1]);
|
|
79
|
+
if ((m = /^#([\w-]+)$/.exec(w))) return el.attrs.id === m[1];
|
|
80
|
+
if (/^[a-z]+$/.test(w)) return el.tagName === w;
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
const mk = (tag) => {
|
|
84
|
+
const el = {
|
|
85
|
+
tagName: tag, attrs: {}, kids: [], className: '', listeners: {}, _text: '', _html: '', parentNode: null,
|
|
86
|
+
// the property and the attribute are one, as in a browser (a page may use either)
|
|
87
|
+
get hidden() { return 'hidden' in this.attrs; },
|
|
88
|
+
set hidden(v) { if (v) this.attrs.hidden = ''; else delete this.attrs.hidden; },
|
|
89
|
+
style: { setProperty(k, v) { this[k] = v; }, removeProperty(k) { delete this[k]; } },
|
|
90
|
+
clientWidth: 1440, clientHeight: 708, offsetLeft: 0, offsetTop: 0, scrollLeft: 0, scrollTop: 0,
|
|
91
|
+
get children() { const a = this.kids.slice(); a[Symbol.isConcatSpreadable] = false; return a; },
|
|
92
|
+
get firstChild() { return this.kids[0] || null; },
|
|
93
|
+
setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'id') byId.set(String(v), this); },
|
|
94
|
+
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
|
|
95
|
+
hasAttribute(k) { return k in this.attrs; },
|
|
96
|
+
removeAttribute(k) { delete this.attrs[k]; },
|
|
97
|
+
appendChild(c) { this.kids.push(c); c.parentNode = this; return c; },
|
|
98
|
+
insertBefore(c, ref) { const i = this.kids.indexOf(ref); if (i < 0) this.kids.push(c); else this.kids.splice(i, 0, c); c.parentNode = this; return c; },
|
|
99
|
+
removeChild(c) { this.kids = this.kids.filter((x) => x !== c); return c; },
|
|
100
|
+
remove() { if (this.parentNode) this.parentNode.removeChild(this); },
|
|
101
|
+
addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); },
|
|
102
|
+
removeEventListener() {},
|
|
103
|
+
fire(t, e) { (this.listeners[t] || []).forEach((fn) => fn({ type: t, target: this, preventDefault() {}, stopPropagation() {}, ...e })); },
|
|
104
|
+
focus() {}, blur() {},
|
|
105
|
+
matches(sel) { return parseSel(sel).some((w) => matchOne(this, w)); },
|
|
106
|
+
closest(sel) { let n = this; while (n) { if (n.matches && n.matches(sel)) return n; n = n.parentNode; } return null; },
|
|
107
|
+
querySelectorAll(sel) { const out = []; const walk = (n) => n.kids.forEach((k) => { if (k.matches(sel)) out.push(k); walk(k); }); walk(this); return out; },
|
|
108
|
+
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; },
|
|
109
|
+
getBoundingClientRect() { return { left: 0, top: 0, right: 1440, bottom: 708, width: 1440, height: 708, x: 0, y: 0 }; },
|
|
110
|
+
set textContent(v) { this._text = String(v); this._html = ''; this.kids = []; },
|
|
111
|
+
get textContent() { return this._text + this.kids.map((k) => k.textContent || '').join(''); },
|
|
112
|
+
set innerHTML(v) { this._html = String(v); this._text = ''; this.kids = []; },
|
|
113
|
+
get innerHTML() { return this._html; },
|
|
114
|
+
};
|
|
115
|
+
el.classList = {
|
|
116
|
+
add: (...c) => { const s = new Set(String(el.className).split(' ').filter(Boolean)); c.forEach((x) => s.add(x)); el.className = [...s].join(' '); },
|
|
117
|
+
remove: (...c) => { el.className = String(el.className).split(' ').filter((x) => x && !c.includes(x)).join(' '); },
|
|
118
|
+
toggle: (c, on) => { const has = String(el.className).split(' ').includes(c); const want = on === undefined ? !has : !!on; if (want) el.classList.add(c); else el.classList.remove(c); return want; },
|
|
119
|
+
contains: (c) => String(el.className).split(' ').includes(c),
|
|
120
|
+
};
|
|
121
|
+
return el;
|
|
122
|
+
};
|
|
123
|
+
const html = read(htmlFile).replace(/<!--[\s\S]*?-->/g, '');
|
|
124
|
+
const VOID = /^(meta|link|img|input|br|hr|source|use|path|circle|rect|line|polygon|stop)$/;
|
|
125
|
+
// [start, end) of an element's inner markup, its own tag balanced
|
|
126
|
+
const innerOf = (tag, from) => {
|
|
127
|
+
const re = new RegExp(`<(/?)${tag}\\b[^>]*?(/?)>`, 'g');
|
|
128
|
+
re.lastIndex = from;
|
|
129
|
+
let depth = 1, m;
|
|
130
|
+
while ((m = re.exec(html))) {
|
|
131
|
+
if (m[2]) continue;
|
|
132
|
+
depth += m[1] ? -1 : 1;
|
|
133
|
+
if (!depth) return [from, m.index];
|
|
134
|
+
}
|
|
135
|
+
return [from, from];
|
|
136
|
+
};
|
|
137
|
+
const spans = [];
|
|
138
|
+
for (const m of html.matchAll(/<([a-z][a-z0-9]*)\b([^>]*)>/g)) {
|
|
139
|
+
const idm = /\bid="([A-Za-z][\w-]*)"/.exec(m[2]);
|
|
140
|
+
if (!idm) continue;
|
|
141
|
+
const el = mk(m[1]);
|
|
142
|
+
for (const a of m[2].matchAll(/\s([\w-]+)(?:="([^"]*)")?/g)) {
|
|
143
|
+
if (a[1] === 'class') el.className = a[2] || '';
|
|
144
|
+
else el.setAttribute(a[1], a[2] === undefined ? '' : a[2]);
|
|
145
|
+
}
|
|
146
|
+
const inner = VOID.test(m[1]) || /\/$/.test(m[2]) ? [0, 0] : innerOf(m[1], m.index + m[0].length);
|
|
147
|
+
spans.push({ el, outer: [m.index, inner[1]], inner });
|
|
148
|
+
roots.push(el);
|
|
149
|
+
}
|
|
150
|
+
// each element's own static markup: its inner markup less every id'd element inside it
|
|
151
|
+
// (those stand as nodes of their own, and are judged by their own `hidden`)
|
|
152
|
+
for (const s of spans) {
|
|
153
|
+
let text = '';
|
|
154
|
+
let at = s.inner[0];
|
|
155
|
+
spans.filter((o) => o !== s && o.outer[0] >= s.inner[0] && o.outer[0] < s.inner[1])
|
|
156
|
+
.sort((a, b) => a.outer[0] - b.outer[0])
|
|
157
|
+
.forEach((o) => { if (o.outer[0] >= at) { text += html.slice(at, o.outer[0]); at = Math.max(at, o.outer[1]); } });
|
|
158
|
+
text += html.slice(at, s.inner[1]);
|
|
159
|
+
s.el._html = text;
|
|
160
|
+
// the innermost id'd element holding it: a hidden one hides it too
|
|
161
|
+
const holders = spans.filter((o) => o !== s && s.outer[0] >= o.inner[0] && s.outer[0] < o.inner[1]);
|
|
162
|
+
s.el.staticParent = holders.sort((a, b) => b.inner[0] - a.inner[0])[0]?.el || null;
|
|
163
|
+
}
|
|
164
|
+
const document = {
|
|
165
|
+
documentElement: mk('html'),
|
|
166
|
+
getElementById: (id) => byId.get(id) || null,
|
|
167
|
+
querySelector: (sel) => { for (const r of roots) { if (r.matches(sel)) return r; const f = r.querySelector(sel); if (f) return f; } return null; },
|
|
168
|
+
querySelectorAll: (sel) => roots.flatMap((r) => (r.matches(sel) ? [r] : []).concat(r.querySelectorAll(sel))),
|
|
169
|
+
createElement: (t) => mk(t), createElementNS: (_n, t) => mk(t),
|
|
170
|
+
createTextNode: (t) => { const n = mk('#text'); n._text = String(t); return n; },
|
|
171
|
+
addEventListener() {},
|
|
172
|
+
};
|
|
173
|
+
return { document, byId, roots };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Everything a page shows, as one string (text, markup and attribute values of
|
|
177
|
+
// every element not hidden), and every shown element carrying data-proto.
|
|
178
|
+
function dump(roots) {
|
|
179
|
+
const parts = [];
|
|
180
|
+
const proto = [];
|
|
181
|
+
const shut = (el) => el.hidden;
|
|
182
|
+
const walk = (el) => {
|
|
183
|
+
if (shut(el)) return;
|
|
184
|
+
for (let p = el.staticParent; p; p = p.staticParent) if (shut(p)) return;
|
|
185
|
+
parts.push(el._text, el._html, ...Object.values(el.attrs));
|
|
186
|
+
if ('data-proto' in el.attrs) proto.push((el.attrs.id || el.tagName) + '.' + el.className);
|
|
187
|
+
if (/\sdata-proto(?=[\s>=])/.test(el._html)) proto.push((el.attrs.id || el.tagName) + ' (markup)');
|
|
188
|
+
el.kids.forEach(walk);
|
|
189
|
+
};
|
|
190
|
+
roots.forEach(walk);
|
|
191
|
+
return { text: parts.join('\n'), proto, shown: parts.length };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── the feeds: every real fixture, and the overlay as production answers it ──
|
|
195
|
+
const FEED = {
|
|
196
|
+
'/api/bongos/government/docket': 'government__docket', '/api/bongos/government/counts': 'government__counts', '/api/bongos/goals': 'goals', '/api/bongos/goals/rollups': 'goals__rollups',
|
|
197
|
+
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster', '/api/bongos/blockers': 'blockers',
|
|
198
|
+
'/api/bongos/public/cost-summary': 'public__cost-summary', '/api/bongos/tasks/claimable': 'tasks__claimable',
|
|
199
|
+
'/api/bongos/tasks/claimable?limit=1': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
200
|
+
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
201
|
+
'/api/bongos/government/constitution': 'government__constitution',
|
|
202
|
+
// the hub's law book, as the route answers it (no law passed yet)
|
|
203
|
+
'/api/bongos/government/laws': 'government__laws',
|
|
204
|
+
// the strictness presets as the route answers the hub (no law, so no preset in force; task 1004715)
|
|
205
|
+
'/api/bongos/government/presets': 'government__presets',
|
|
206
|
+
};
|
|
207
|
+
// a law a project really passed, as GET /government/laws answers it (not one of the
|
|
208
|
+
// overlay's: the words are this test's own, so the overlay scan cannot excuse them)
|
|
209
|
+
const A_REAL_LAW = 'Every screen change carries two shots';
|
|
210
|
+
const REAL_BOOK = {
|
|
211
|
+
laws: [{
|
|
212
|
+
id: '11', text: A_REAL_LAW, effect_on_ai: 'Agents attach a before and an after picture to screen work.', locks: [],
|
|
213
|
+
in_force: true, passed_at: '2026-10-09T12:00:00.000Z', repealed_at: null, repealed_by_item_id: null,
|
|
214
|
+
passed_by: { item_id: '31', href: '/board-room?item=31', government: 'Timocratic Republic', pass_rule: 'unanimous', proposer: { id: '42', login: 'ada', name: 'Ada' }, yes: [] },
|
|
215
|
+
}],
|
|
216
|
+
in_force: 1,
|
|
217
|
+
};
|
|
218
|
+
let lawBook = null; // a test may serve REAL_BOOK in place of the hub's empty book
|
|
219
|
+
// GET /me's laws block as the server answers it on a project with a law in force
|
|
220
|
+
// (task 1004707): what every session reads, and what the lock did to this viewer
|
|
221
|
+
let meLaws = null;
|
|
222
|
+
const ME_LAWS = {
|
|
223
|
+
inject: true,
|
|
224
|
+
contract: 'Project laws — passed by a vote of this project\'s government.\n1. ' + A_REAL_LAW,
|
|
225
|
+
laws: [{ id: '11', no: 1, text: A_REAL_LAW, effect_on_ai: 'Agents attach a before and an after picture to screen work.', locks: [{ setting: 'verbosity', value: 'detailed' }] }],
|
|
226
|
+
locks: [{ setting: 'verbosity', law_id: '11', law_no: 1, law_text: A_REAL_LAW, value: 'detailed', route: 'interaction', enforced: true, from: 'concise', to: 'detailed' }],
|
|
227
|
+
};
|
|
228
|
+
// A stored ask (task 1004703), as GET /government/docket carries it on a matter
|
|
229
|
+
// whose agent wrote its card in the ask shape. Its words are this test's own, so
|
|
230
|
+
// the overlay scan cannot excuse them.
|
|
231
|
+
const A_REAL_ASK = {
|
|
232
|
+
question: 'Pick the host for the new site?',
|
|
233
|
+
options: [
|
|
234
|
+
{ key: 'cheap', label: 'The cheap host', result: 'Ten dollars a month, slower builds.', act: 'go ahead' },
|
|
235
|
+
{ key: 'fast', label: 'The fast host', result: 'Forty dollars a month, quick builds.' },
|
|
236
|
+
],
|
|
237
|
+
recommend: { key: 'cheap', why: 'The site is small for now.' },
|
|
238
|
+
waits_on: 'owner',
|
|
239
|
+
if_nobody_answers: 'The site waits, unbuilt.',
|
|
240
|
+
detail: null,
|
|
241
|
+
written_at: '2026-10-09T12:00:00.000Z',
|
|
242
|
+
};
|
|
243
|
+
let docketAsks = false; // a test may serve the docket with every open matter carrying A_REAL_ASK
|
|
244
|
+
// Who leads each building, as GET /government/leads answers (task 1004709): Planning
|
|
245
|
+
// led by a builder the government appointed, every other building the owner's. The
|
|
246
|
+
// Lead's name and charter are this test's own words, so the overlay scan cannot excuse them.
|
|
247
|
+
const A_REAL_LEAD = 'Quill Appointee';
|
|
248
|
+
const LEADS_READ = {
|
|
249
|
+
buildings: BUILDINGS.map((b) => ({
|
|
250
|
+
building: b, name: b,
|
|
251
|
+
lead: b === 'planning' ? { id: '1', building: b, builder: { id: '777', login: 'quill', name: A_REAL_LEAD }, charter: 'Orders the work by what ships next.', since: '2026-10-09T12:00:00.000Z', appointed_by: { item_id: '40', href: '/board-room?item=40' } } : null,
|
|
252
|
+
answers: b === 'planning' ? { seat: 'lead:planning', builder_ids: ['777'], fell_to_owner: false } : { seat: 'owner', builder_ids: ['3'], fell_to_owner: true },
|
|
253
|
+
})),
|
|
254
|
+
owner: { seat: 'owner', rank: 'archon', builders: [{ id: '3', login: 'builder-2013', name: 'Builder 2013' }] },
|
|
255
|
+
seated: 1,
|
|
256
|
+
};
|
|
257
|
+
let leadsBook = null; // a test may serve LEADS_READ; otherwise the read answers 404 (an older server)
|
|
258
|
+
// The treasury (task 1004712), as GET /government/treasury answers it: one goal's pot,
|
|
259
|
+
// dry, with its borrow open on the docket (the fixture's treasury_spending:1).
|
|
260
|
+
const TREASURY_READ = (() => {
|
|
261
|
+
const t = fx('government__treasury');
|
|
262
|
+
t.pots = [t.pots.find((p) => p.dry)];
|
|
263
|
+
return t;
|
|
264
|
+
})();
|
|
265
|
+
let treasuryBook = null; // a test may serve TREASURY_READ; otherwise the read answers 404 (an older server)
|
|
266
|
+
function makeFetch(withOverlay, calls) {
|
|
267
|
+
return (url, init) => {
|
|
268
|
+
calls.push(url);
|
|
269
|
+
if (init && init.method && init.method !== 'GET') return Promise.resolve({ ok: false, status: 501, json: async () => ({}) });
|
|
270
|
+
if (url === '/api/bongos/government/docket' && docketAsks) {
|
|
271
|
+
const d = fx('government__docket');
|
|
272
|
+
d.matters.forEach((m) => { if (m.state === 'needs') m.ask = JSON.parse(JSON.stringify(A_REAL_ASK)); });
|
|
273
|
+
return Promise.resolve({ ok: true, status: 200, json: async () => d });
|
|
274
|
+
}
|
|
275
|
+
if (url === '/api/bongos/government/laws' && lawBook) return Promise.resolve({ ok: true, status: 200, json: async () => JSON.parse(JSON.stringify(lawBook)) });
|
|
276
|
+
if (url === '/api/bongos/government/leads' && leadsBook) return Promise.resolve({ ok: true, status: 200, json: async () => JSON.parse(JSON.stringify(leadsBook)) });
|
|
277
|
+
if (url === '/api/bongos/government/treasury' && treasuryBook) return Promise.resolve({ ok: true, status: 200, json: async () => JSON.parse(JSON.stringify(treasuryBook)) });
|
|
278
|
+
if (url === '/api/bongos/me' && meLaws) {
|
|
279
|
+
const me = fx('me');
|
|
280
|
+
me.laws = JSON.parse(JSON.stringify(meLaws));
|
|
281
|
+
me.interaction.effective = { ...me.interaction.effective, verbosity: 'detailed' };
|
|
282
|
+
return Promise.resolve({ ok: true, status: 200, json: async () => me });
|
|
283
|
+
}
|
|
284
|
+
if (url === OVERLAY_URL && withOverlay) return Promise.resolve({ ok: true, status: 200, json: async () => JSON.parse(JSON.stringify(OLD_OVERLAY)) });
|
|
285
|
+
const name = FEED[url];
|
|
286
|
+
if (!name) return Promise.resolve({ ok: false, status: 404, json: async () => ({ error: { code: 'not_found' } }) });
|
|
287
|
+
return Promise.resolve({ ok: true, status: 200, json: async () => fx(name) });
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function sandbox(window, document, fetch) {
|
|
291
|
+
class MutationObserver { observe() {} disconnect() {} }
|
|
292
|
+
const ctx = vm.createContext({
|
|
293
|
+
window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent,
|
|
294
|
+
isNaN, isFinite, Map, Set, setTimeout, clearTimeout, MutationObserver, Symbol, Error, RegExp, Boolean,
|
|
295
|
+
});
|
|
296
|
+
ctx.globalThis = ctx;
|
|
297
|
+
return ctx;
|
|
298
|
+
}
|
|
299
|
+
function baseWindow(hash = '') {
|
|
300
|
+
const store = new Map();
|
|
301
|
+
const ls = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) };
|
|
302
|
+
return {
|
|
303
|
+
innerWidth: 1440, innerHeight: 900, location: { hash, href: '', search: '', pathname: '/' },
|
|
304
|
+
localStorage: ls, sessionStorage: ls,
|
|
305
|
+
matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {}, removeEventListener() {},
|
|
306
|
+
history: { pushState() {}, replaceState() {} },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const settle = async (n = 8) => { for (let i = 0; i < n; i++) await new Promise((r) => setImmediate(r)); };
|
|
310
|
+
|
|
311
|
+
async function bootCity(withOverlay) {
|
|
312
|
+
const { document, byId, roots } = fakeDom('city.html');
|
|
313
|
+
const calls = [];
|
|
314
|
+
const window = baseWindow();
|
|
315
|
+
window.fetch = makeFetch(withOverlay, calls);
|
|
316
|
+
const ctx = sandbox(window, document, window.fetch);
|
|
317
|
+
// city.html loads the deck's card and sender for the card's first ask (task 1004702)
|
|
318
|
+
for (const f of ['gov-living.js', 'city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street-deck.js', 'city.js']) vm.runInContext(read(f), ctx, { filename: f });
|
|
319
|
+
await window.GovCityPage.ready;
|
|
320
|
+
await settle();
|
|
321
|
+
return { byId, roots, calls, window };
|
|
322
|
+
}
|
|
323
|
+
async function bootStreet(hash, withOverlay) {
|
|
324
|
+
const { document, byId, roots } = fakeDom('street.html');
|
|
325
|
+
const calls = [];
|
|
326
|
+
const window = baseWindow(hash);
|
|
327
|
+
window.fetch = makeFetch(withOverlay, calls);
|
|
328
|
+
const ctx = sandbox(window, document, window.fetch);
|
|
329
|
+
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',
|
|
330
|
+
'street-desk.js', 'street-amend.js', 'street-deck.js', 'street-office.js', 'street.js']) vm.runInContext(read(f), ctx, { filename: f });
|
|
331
|
+
await window.GovStreetPage.ready;
|
|
332
|
+
await settle();
|
|
333
|
+
return { byId, roots, calls, window };
|
|
334
|
+
}
|
|
335
|
+
async function bootMyAgents(withOverlay) {
|
|
336
|
+
const { document, byId, roots } = fakeDom('my-agents.html');
|
|
337
|
+
const calls = [];
|
|
338
|
+
const window = baseWindow();
|
|
339
|
+
const fetch = makeFetch(withOverlay, calls);
|
|
340
|
+
window.fetch = fetch;
|
|
341
|
+
// the page's client: the same reads, through the same 404
|
|
342
|
+
window.BongosClient = {
|
|
343
|
+
createClient: () => ({
|
|
344
|
+
request: (method, p) => fetch(p, { method }).then(async (r) => ({ ok: r.ok, status: r.status, data: r.ok ? await r.json() : null })),
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
const ctx = sandbox(window, document, fetch);
|
|
348
|
+
for (const f of ['gov-living.js', 'gov-positions.js', 'my-agents.js']) vm.runInContext(read(f), ctx, { filename: f });
|
|
349
|
+
await settle(12);
|
|
350
|
+
return { byId, roots, calls, window };
|
|
351
|
+
}
|
|
352
|
+
function noPrototype(page, where, { seatsRead = false } = {}) {
|
|
353
|
+
const { text, proto, shown } = dump(page.roots);
|
|
354
|
+
assert.ok(shown >= 3, `${where}: the scan saw the page (${shown} elements)`);
|
|
355
|
+
assert.deepEqual(proto, [], `${where}: nothing shown carries data-proto`);
|
|
356
|
+
assert.ok(!page.calls.includes(OVERLAY_URL), `${where}: no overlay is asked for`);
|
|
357
|
+
const leaked = OVERLAY_PHRASES.filter((s) => text.includes(s) || text.includes(esc(s)));
|
|
358
|
+
assert.deepEqual(leaked, [], `${where}: no word the overlay used to invent is on the page`);
|
|
359
|
+
// who leads or holds a building is known only from GET /government/leads (task
|
|
360
|
+
// 1004709); without that read the page names no Lead and no owner's seat
|
|
361
|
+
if (!seatsRead) assert.doesNotMatch(text, /\bLeads?\b|\bled by\b|held by the owner|the owner holds/, `${where}: no Lead, no owner's seat`);
|
|
362
|
+
return text;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── the control: the scan finds those words when a real read carries them ───
|
|
366
|
+
test('control: the phrases are no real fixture\'s and no page\'s; a stale overlay route changes nothing; a real law carrying one shows', async () => {
|
|
367
|
+
for (const s of OVERLAY_PHRASES) assert.ok(!REAL_TEXT.includes(s) && !PAGE_SOURCES.includes(s), s);
|
|
368
|
+
// a stale server still answers the old route: no page asks for it, nothing of it shows
|
|
369
|
+
const pages = [['city', await bootCity(true)], ['street', await bootStreet('', true)], ['deck', await bootStreet('#deck', true)],
|
|
370
|
+
['inside planning', await bootStreet('#building=planning', true)], ['my agents', await bootMyAgents(true)]];
|
|
371
|
+
for (const [where, page] of pages) noPrototype(page, where + ' (a stale overlay route)');
|
|
372
|
+
for (const f of ['city.js', 'gov-feeds.js', 'my-agents.js', 'street.js', 'street-depth.js']) assert.doesNotMatch(read(f), /governor-overlay/, f + ' names no overlay route');
|
|
373
|
+
// no prototype tag is left in the code of any living-city page, the shell or their sheets
|
|
374
|
+
// (comments may still tell the history; code, markup and storage keys may not)
|
|
375
|
+
const code = (src) => src.replace(/<!--[\s\S]*?-->/g, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1');
|
|
376
|
+
for (const f of ['city.html', 'street.html', 'my-agents.html', 'city.js', 'city-draw.js', 'street.js', 'street-draw.js', 'street-depth.js', 'street-deck.js',
|
|
377
|
+
'street-office.js', 'street-inside.js', 'my-agents.js', 'gov-living.js', 'gov-feeds.js', 'gov-positions.js', 'shell.js', 'city.css', 'street.css', 'street-deck.css', 'my-agents.css']) {
|
|
378
|
+
assert.doesNotMatch(code(read(f)), /data-proto|proto-tag|proto-key|proto_note|overlay_note|protoAsk|['"]proto[-:'"]|\.proto\b|\bproto:/, f + ': no prototype tag in its code');
|
|
379
|
+
}
|
|
380
|
+
// the scan is not vacuous: a real law in those words is on the street
|
|
381
|
+
lawBook = { laws: [{ ...REAL_BOOK.laws[0], text: OVERLAY_PHRASES[0] }], in_force: 1 };
|
|
382
|
+
try {
|
|
383
|
+
const p = await bootStreet('', false);
|
|
384
|
+
const { text } = dump(p.roots);
|
|
385
|
+
assert.ok(text.includes(OVERLAY_PHRASES[0]) || text.includes(esc(OVERLAY_PHRASES[0])), 'the scan sees the words when a page shows them');
|
|
386
|
+
} finally { lawBook = null; }
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ── a real site ─────────────────────────────────────────────────────────────
|
|
390
|
+
test('the city on a real site: the real city, no Lead, law, pot, build-out, rewrite or prototype key', async () => {
|
|
391
|
+
const p = await bootCity(false);
|
|
392
|
+
noPrototype(p, 'city');
|
|
393
|
+
assert.doesNotMatch(read('city.html'), /cityKey|data-proto/, 'no prototype key');
|
|
394
|
+
assert.equal(p.byId.get('cityHits').children.length, 9, 'the nine buildings still answer');
|
|
395
|
+
assert.ok(p.byId.get('cityHits').children.every((b) => !/led by/.test(b.attrs['aria-label'])), 'nobody "leads" a building');
|
|
396
|
+
assert.match(p.byId.get('cityLine').textContent, /\d+ agents? working/, 'the real reads still count');
|
|
397
|
+
// a building's card names nobody as holding it, and carries no rewritten ask:
|
|
398
|
+
// its first ask (task 1004702) is the docket's own, in its own words
|
|
399
|
+
for (const b of BUILDINGS) {
|
|
400
|
+
p.byId.get('cityHits').children.find((h) => h.attrs['data-building'] === b).fire('click');
|
|
401
|
+
const card = p.byId.get('cityCard');
|
|
402
|
+
assert.equal(card.attrs['data-building'], b);
|
|
403
|
+
assert.doesNotMatch(card.textContent, /\bLead\b|owner/, `${b}: no Lead, no owner's seat`);
|
|
404
|
+
const asks = card.querySelectorAll('.city-ask');
|
|
405
|
+
assert.ok(asks.length <= 1, `${b}: at most its first ask`);
|
|
406
|
+
// answers are drawn only for a matter whose agent stored its ask (the harness's treasury borrow)
|
|
407
|
+
const stored = fx('government__docket').matters.filter((m) => m.ask).map((m) => m.ask.question);
|
|
408
|
+
if (asks.length && asks[0].querySelectorAll('.ask__opt').length) assert.ok(stored.includes(asks[0].querySelectorAll('.ask__q')[0].textContent), b + ': answers only on a stored ask');
|
|
409
|
+
noPrototype(p, 'city card ' + b);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('the street on a real site: the real street, its readings and asks, no prototype layer', async () => {
|
|
414
|
+
const p = await bootStreet('', false);
|
|
415
|
+
noPrototype(p, 'street');
|
|
416
|
+
assert.doesNotMatch(read('street.html'), /streetDepthKey|data-proto/, 'no prototype key');
|
|
417
|
+
assert.ok(p.byId.get('streetSigns').children.some((a) => !a.hidden), 'the docket\'s readings stand');
|
|
418
|
+
assert.match(p.byId.get('hudNeeds').textContent, /^\d+$/, 'the header counts the real asks');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('the decision deck on a real site: the docket\'s own asks in their own words, no rewrite, no invented ask', async () => {
|
|
422
|
+
const p = await bootStreet('#deck', false);
|
|
423
|
+
noPrototype(p, 'deck');
|
|
424
|
+
const deck = p.byId.get('streetDeck');
|
|
425
|
+
assert.equal(deck.hidden, false, 'the deck opens');
|
|
426
|
+
const docketTitles = fx('government__docket').matters.map((m) => m.title).concat(fx('blockers').blockers.map((b) => b.title));
|
|
427
|
+
assert.ok(docketTitles.some((t) => deck.textContent.includes(t)), 'a real ask, as its source words it');
|
|
428
|
+
// the format's refusal is the prototype's: a real ask goes to its floor's desk, where its real acts are
|
|
429
|
+
assert.doesNotMatch(deck.textContent, /sent back|until its agent writes it|prototype data|pass it on/, 'no refusal, no key, no seats to pass to');
|
|
430
|
+
const walk = (el, out = []) => { el.kids.forEach((k) => { out.push(k); walk(k, out); }); return out; };
|
|
431
|
+
const desk = walk(deck).find((k) => k.className === 'ask__desk');
|
|
432
|
+
assert.ok(desk, 'the card sends you to its desk');
|
|
433
|
+
assert.match(desk.attrs.href, /^\/builders\/street#building=[a-z]+$/);
|
|
434
|
+
assert.match(walk(deck).find((k) => /\bask__waits\b/.test(k.className)).textContent, /^waits on (you|others)$/, 'it waits on you, or on others unnamed');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('a matter whose agent stored its ask: the deck draws the card, as real, on a real site', async () => {
|
|
438
|
+
docketAsks = true;
|
|
439
|
+
try {
|
|
440
|
+
// at Planning, where the blockers stand: a stored answer's act ('go ahead') is the blocker's own decide chip
|
|
441
|
+
const p = await bootStreet('#deck=planning', false);
|
|
442
|
+
noPrototype(p, 'deck with stored asks');
|
|
443
|
+
const deck = p.byId.get('streetDeck');
|
|
444
|
+
const walk = (el, out = []) => { el.kids.forEach((k) => { out.push(k); walk(k, out); }); return out; };
|
|
445
|
+
const all = walk(deck);
|
|
446
|
+
const card = all.find((k) => k.tagName === 'article' && /\bask\b/.test(k.className));
|
|
447
|
+
assert.ok(card, 'a card is drawn');
|
|
448
|
+
const q = all.find((k) => /\bask__q\b/.test(k.className));
|
|
449
|
+
assert.equal(q.textContent, A_REAL_ASK.question, 'its question is the stored one');
|
|
450
|
+
assert.equal('data-proto' in q.attrs, false, 'and its words are not marked prototype');
|
|
451
|
+
const opts = all.filter((k) => /\bask__opt\b/.test(k.className) && k.tagName === 'button');
|
|
452
|
+
assert.deepEqual(opts.map((o) => o.attrs['data-key']), ['cheap', 'fast'], 'its two tap answers');
|
|
453
|
+
assert.equal('aria-disabled' in opts[0].attrs, false, 'the pick takes the real decide act of the blocker, so it is live');
|
|
454
|
+
assert.ok(deck.textContent.includes('Ten dollars a month, slower builds.'), 'each with its one-line result');
|
|
455
|
+
assert.ok(deck.textContent.includes("I'd pick the cheap host. The site is small for now."), 'the pick and why, in the asker\'s voice');
|
|
456
|
+
assert.ok(deck.textContent.includes('Left alone, the site waits, unbuilt'), 'what happens if nobody answers');
|
|
457
|
+
assert.ok(all.some((k) => /\bask__pick\b/.test(k.className) && k.textContent === 'my pick'), 'the pick is marked on its answer');
|
|
458
|
+
assert.ok(!all.some((k) => /\bproto-tag\b/.test(k.className)), 'no real-or-prototype tag: every card is real');
|
|
459
|
+
// a stored ask is answered on the card itself (task 1004702); only a matter without one sends you to its desk
|
|
460
|
+
assert.doesNotMatch(deck.textContent, /sent back|until its agent writes it|prototype data|pass it on/);
|
|
461
|
+
// the city's card answers its first ask in place (task 1004702), the same stored card
|
|
462
|
+
const city = await bootCity(false);
|
|
463
|
+
noPrototype(city, 'city with stored asks');
|
|
464
|
+
for (const b of BUILDINGS) {
|
|
465
|
+
city.byId.get('cityHits').children.find((h) => h.attrs['data-building'] === b).fire('click');
|
|
466
|
+
const asks = city.byId.get('cityCard').querySelectorAll('.city-ask');
|
|
467
|
+
assert.ok(asks.length <= 1, b + ': at most its first ask');
|
|
468
|
+
if (b === 'planning') assert.ok(asks.length === 1 && asks[0].querySelectorAll('.ask__opt').length === 2, 'Planning answers its stored ask in place');
|
|
469
|
+
noPrototype(city, 'city card with stored asks ' + b);
|
|
470
|
+
}
|
|
471
|
+
} finally { docketAsks = false; }
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('inside every building on a real site: no Lead, no office storey, no build-out, no invented law, no pot', async () => {
|
|
475
|
+
for (const b of BUILDINGS) {
|
|
476
|
+
const p = await bootStreet('#building=' + b, false);
|
|
477
|
+
const text = noPrototype(p, 'inside ' + b);
|
|
478
|
+
// task 1004715: the Charter's book is the real presets read, so it stands on a real site
|
|
479
|
+
if (b === 'charter') assert.match(text, /how strict the agents run[\s\S]*no preset yet[\s\S]*Open workshop[\s\S]*Factory/, 'the Charter shows the real presets');
|
|
480
|
+
assert.ok(!p.byId.get('insideFloors').kids.some((k) => k.getAttribute('data-floor') === 'office'), `${b}: no office storey`);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('My agents on a real site: the real rows only; no laws, prototype settings or key; Make it a law and the preset strip are real', async () => {
|
|
485
|
+
const p = await bootMyAgents(false);
|
|
486
|
+
noPrototype(p, 'my agents');
|
|
487
|
+
// task 1004715: the strictness strip is GET /government/presets, so it stands on a real site
|
|
488
|
+
const strict = p.byId.get('ma-strict');
|
|
489
|
+
assert.equal('hidden' in strict.attrs, false, 'the strictness strip stands');
|
|
490
|
+
assert.match(strict.innerHTML, /No preset yet/, 'the hub has no law, so no preset is in force');
|
|
491
|
+
assert.match(strict.innerHTML, /data-preset="factory"[\s\S]*4 of \d+ lock/, 'Factory says what it would lock of your rows');
|
|
492
|
+
assert.doesNotMatch(strict.innerHTML, /data-proto/, 'and nothing on it is prototype');
|
|
493
|
+
assert.ok(p.calls.includes('/api/bongos/government/presets'), 'read from the route');
|
|
494
|
+
assert.equal(p.byId.get('ma-key'), undefined, 'no prototype key');
|
|
495
|
+
const rows = p.byId.get('ma-rows').innerHTML;
|
|
496
|
+
assert.match(rows, /data-row="technical_level"/, 'the real rows from GET /me stand');
|
|
497
|
+
for (const k of ['on_stuck', 'ask_timing', 'ask_format', 'parallel_agents', 'auto_go_live']) assert.doesNotMatch(rows, new RegExp(`data-row="${k}"`), `${k} is a prototype setting`);
|
|
498
|
+
const desk = rows.split('data-group="habits"')[0];
|
|
499
|
+
assert.doesNotMatch(desk, /law/i, 'no row is locked or capped by a law: none is in force');
|
|
500
|
+
// task 1004714: Make it a law files a real laws vote, so it stands on a real site too
|
|
501
|
+
assert.match(rows, /data-group="habits"[\s\S]*data-propose="machinery"/, 'Make it a law is offered to a builder who may open a board item');
|
|
502
|
+
assert.doesNotMatch(p.byId.get('ma-reads-body').innerHTML, /law/i, 'what the agent reads is only what it really reads');
|
|
503
|
+
assert.doesNotMatch(p.byId.get('ma-sub').textContent || p.byId.get('ma-sub')._html, /law/i, 'the subtitle names no laws');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('My agents on a real site with a law in force: the law is what the agent reads first, its lock holds the row, nothing proto', async () => {
|
|
507
|
+
meLaws = ME_LAWS;
|
|
508
|
+
try {
|
|
509
|
+
const p = await bootMyAgents(false);
|
|
510
|
+
const text = noPrototype(p, 'my agents with a real law');
|
|
511
|
+
const reads = p.byId.get('ma-reads-body').innerHTML;
|
|
512
|
+
assert.ok(reads.includes(esc(A_REAL_LAW)), 'the law is in what the agent reads');
|
|
513
|
+
assert.ok(reads.indexOf(esc(A_REAL_LAW)) < reads.indexOf('Yours'), 'first, before the builder\'s own');
|
|
514
|
+
const rows = p.byId.get('ma-rows').innerHTML;
|
|
515
|
+
assert.match(rows, /data-row="verbosity"[^>]*>[\s\S]*?class="ma-lock">/, 'the verbosity row is locked, and the lock is not marked prototype');
|
|
516
|
+
assert.ok(rows.includes(esc(A_REAL_LAW) + ' · Law 1'), 'the lock names the law and its number');
|
|
517
|
+
assert.ok(text.includes('The project\'s laws come first'), 'the subtitle now says so');
|
|
518
|
+
} finally { meLaws = null; }
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('the seat-gated nav on a real site reads /me\'s positions: real seats, no overlay; no block is unknown, never "none"', () => {
|
|
522
|
+
const overlay = OLD_OVERLAY;
|
|
523
|
+
const constitution = fx('government__constitution');
|
|
524
|
+
// no positions block (module off, read failed): unknown, whatever the overlay or constitution say
|
|
525
|
+
for (const viewer of [{ builder: fx('me').builder }, { builder: { id: '424242', rank: 'thetes' } }]) {
|
|
526
|
+
for (const o of [overlay, null]) {
|
|
527
|
+
const p = GovPositions.positionsFor({ me: viewer, overlay: o, constitution });
|
|
528
|
+
assert.equal(p.known, false, `${viewer.builder.rank}, overlay ${o ? 'read' : 'unread'}: unknown, so nothing hides`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// the server's block: known, every seat real, and the overlay changes nothing
|
|
532
|
+
const me = fx('me');
|
|
533
|
+
const p = GovPositions.positionsFor({ me, overlay: null, constitution });
|
|
534
|
+
const q = GovPositions.positionsFor({ me, overlay, constitution });
|
|
535
|
+
assert.equal(p.known, true);
|
|
536
|
+
assert.deepEqual(JSON.parse(JSON.stringify(p)), JSON.parse(JSON.stringify(q)), 'the overlay decides no seat');
|
|
537
|
+
assert.ok(p.positions.every((s) => s.source === 'real'));
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('a Lead the government appointed stands on their building on a real site; every unseated building shows the owner; nothing proto', async () => {
|
|
541
|
+
leadsBook = LEADS_READ;
|
|
542
|
+
try {
|
|
543
|
+
const city = await bootCity(false);
|
|
544
|
+
assert.ok(city.calls.includes('/api/bongos/government/leads'), 'the city reads who leads each building');
|
|
545
|
+
const text = noPrototype(city, 'city with real Leads', { seatsRead: true });
|
|
546
|
+
assert.ok(text.includes(A_REAL_LEAD), 'the Lead\'s name stands on their building');
|
|
547
|
+
const plates = city.byId.get('citySky').querySelectorAll('.city-plate');
|
|
548
|
+
const planningPlate = plates.find((p) => p.attrs['data-building'] === 'planning');
|
|
549
|
+
assert.ok(planningPlate && planningPlate.textContent === A_REAL_LEAD, 'Planning\'s plate names its Lead');
|
|
550
|
+
assert.ok(!('data-proto' in planningPlate.attrs), 'and the plate is real');
|
|
551
|
+
const ownerPlates = plates.filter((p) => /city-plate--owner/.test(p.className));
|
|
552
|
+
assert.equal(ownerPlates.length, 8, 'the eight unseated buildings each carry the owner\'s plate');
|
|
553
|
+
assert.ok(ownerPlates.every((p) => p.textContent === 'owner' && !('data-proto' in p.attrs)));
|
|
554
|
+
const hit = (b) => city.byId.get('cityHits').children.find((h) => h.attrs['data-building'] === b);
|
|
555
|
+
assert.match(hit('planning').attrs['aria-label'], new RegExp('led by ' + A_REAL_LEAD));
|
|
556
|
+
assert.match(hit('release').attrs['aria-label'], /held by the owner/);
|
|
557
|
+
hit('planning').fire('click');
|
|
558
|
+
assert.match(city.byId.get('cityCard').textContent, new RegExp('Led by ' + A_REAL_LEAD + '\\.'));
|
|
559
|
+
hit('treasury').fire('click');
|
|
560
|
+
assert.match(city.byId.get('cityCard').textContent, /hold(s)? it until a Lead is seated\./);
|
|
561
|
+
noPrototype(city, 'city card with real Leads', { seatsRead: true });
|
|
562
|
+
const street = await bootStreet('', false);
|
|
563
|
+
assert.ok(street.calls.includes('/api/bongos/government/leads'), 'the street reads it too');
|
|
564
|
+
noPrototype(street, 'street with real Leads', { seatsRead: true });
|
|
565
|
+
const doors = street.byId.get('streetDoors').children;
|
|
566
|
+
assert.match(doors.find((d) => d.getAttribute('data-door') === 'planning').attrs['aria-label'], new RegExp('led by ' + A_REAL_LEAD));
|
|
567
|
+
assert.match(doors.find((d) => d.getAttribute('data-door') === 'review').attrs['aria-label'], /held by the owner/);
|
|
568
|
+
} finally { leadsBook = null; }
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('a law the project really passed stands on a real site: the street names it, the city counts it, nothing proto', async () => {
|
|
572
|
+
lawBook = REAL_BOOK;
|
|
573
|
+
try {
|
|
574
|
+
const city = await bootCity(false);
|
|
575
|
+
assert.ok(city.calls.includes('/api/bongos/government/laws'), 'the city reads the law book');
|
|
576
|
+
noPrototype(city, 'city with a real law');
|
|
577
|
+
const plaque = city.byId.get('cityPlaque');
|
|
578
|
+
const before = fx('government__docket').readings.some((r) => r && r.key === 'rules' && r.status === 'live') ? 1 : 0;
|
|
579
|
+
const tile = plaque.kids.find((k) => k.attrs['data-key'] === 'laws');
|
|
580
|
+
assert.ok(tile, 'the plaque has a laws tile');
|
|
581
|
+
assert.equal(tile.kids[0].textContent, String(before + 1), "it counts the real law beside the docket's rule");
|
|
582
|
+
assert.ok(!('data-proto' in tile.attrs), 'and the count is real');
|
|
583
|
+
const street = await bootStreet('', false);
|
|
584
|
+
assert.ok(street.calls.includes('/api/bongos/government/laws'), 'the street reads the law book');
|
|
585
|
+
const text = noPrototype(street, 'street with a real law');
|
|
586
|
+
assert.ok(text.includes(A_REAL_LAW), 'the Rules reading names the law the project passed');
|
|
587
|
+
} finally { lawBook = null; }
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// A Lead's build-outs (task 1004711), as GET /government/leads carries them: a floor
|
|
591
|
+
// going up and a committee room built at Planning, and Release held by the owner seat
|
|
592
|
+
// the viewer sits in. The names are this test's own words, so the overlay scan cannot
|
|
593
|
+
// excuse them.
|
|
594
|
+
const A_REAL_FLOOR = 'Quill Ledger Floor';
|
|
595
|
+
const A_REAL_ROOM = 'Quill Triage Room';
|
|
596
|
+
const LEADS_BUILT = (() => {
|
|
597
|
+
const r = JSON.parse(JSON.stringify(LEADS_READ));
|
|
598
|
+
r.buildings.forEach((b) => { b.structure = { standing: [], proposed: [] }; });
|
|
599
|
+
const pl = r.buildings.find((b) => b.building === 'planning');
|
|
600
|
+
pl.structure.standing = [
|
|
601
|
+
{ id: '1', building: 'planning', capsule: 'floor', name: A_REAL_FLOOR, line: 'Where the next quarter is planned.', state: 'building', progress: 0.4, started_at: '2026-10-09T20:00:00.000Z', builder: null, allowed: { path: 'at_once', ground: 'form', why: 'The founder decides alone.' } },
|
|
602
|
+
{ id: '2', building: 'planning', capsule: 'committee', name: A_REAL_ROOM, line: 'Sorts what comes in.', state: 'built', progress: 1, started_at: '2026-10-01T00:00:00.000Z', builder: null, allowed: { path: 'vote', item_id: '44', href: '/board-room?item=44' } },
|
|
603
|
+
];
|
|
604
|
+
const rel = r.buildings.find((b) => b.building === 'release');
|
|
605
|
+
rel.answers = { seat: 'owner', builder_ids: [String(fx('government__docket').viewer.id)], fell_to_owner: true };
|
|
606
|
+
rel.build_rights = { form: 'monarchy', form_name: 'Monarchy / BDFL', holder: 'owner', capsules: Object.fromEntries(['committee', 'floor', 'annex', 'district'].map((c) => [c, { without_vote: c !== 'district', why: 'The founder decides alone.', ground: c === 'district' ? 'city' : 'form' }])) };
|
|
607
|
+
return r;
|
|
608
|
+
})();
|
|
609
|
+
|
|
610
|
+
test('a Lead\'s build-outs stand on a real site: the office storey, its floor going up and its room are real; nothing proto', async () => {
|
|
611
|
+
leadsBook = LEADS_BUILT;
|
|
612
|
+
try {
|
|
613
|
+
const p = await bootStreet('#building=planning', false);
|
|
614
|
+
const office = p.byId.get('insideFloors').kids.find((k) => k.getAttribute('data-floor') === 'office');
|
|
615
|
+
assert.ok(office, 'the office storey stands on a real site once the Leads are read');
|
|
616
|
+
const site = p.byId.get('insideFloors').kids.find((k) => /Open the build/.test(k.getAttribute('aria-label') || ''));
|
|
617
|
+
assert.ok(site && site.getAttribute('aria-label').includes(A_REAL_FLOOR), 'the floor going up is a site of its own');
|
|
618
|
+
let text = noPrototype(p, 'inside planning with real build-outs', { seatsRead: true });
|
|
619
|
+
assert.ok(text.includes(A_REAL_FLOOR), 'the floor going up is drawn');
|
|
620
|
+
office.fire('click');
|
|
621
|
+
await settle();
|
|
622
|
+
text = noPrototype(p, 'the real Lead\'s office', { seatsRead: true });
|
|
623
|
+
assert.ok(text.includes(A_REAL_ROOM), 'its committee room is a row of the office');
|
|
624
|
+
assert.ok(text.includes('40% built'), 'the floor going up says how far, from its real start');
|
|
625
|
+
assert.ok(!p.roots.some((r) => r.querySelectorAll('[data-capsule]').length), 'the viewer does not hold Planning: no build-out chips');
|
|
626
|
+
// Release is held by the owner seat the viewer sits in: the chips stand, in the server's words
|
|
627
|
+
const r = await bootStreet('#building=release', false);
|
|
628
|
+
r.byId.get('insideFloors').kids.find((k) => k.getAttribute('data-floor') === 'office').fire('click');
|
|
629
|
+
await settle();
|
|
630
|
+
noPrototype(r, 'the owner\'s office at Release', { seatsRead: true });
|
|
631
|
+
const chips = r.roots.flatMap((x) => x.querySelectorAll('[data-capsule]'));
|
|
632
|
+
assert.deepEqual(chips.map((c) => c.getAttribute('data-capsule')), ['committee', 'floor', 'annex', 'district']);
|
|
633
|
+
assert.deepEqual(chips.map((c) => c.kids[c.kids.length - 1].textContent), ['builds now', 'builds now', 'builds now', 'a vote']);
|
|
634
|
+
// a tap is the real route
|
|
635
|
+
chips[1].fire('click');
|
|
636
|
+
await settle();
|
|
637
|
+
assert.ok(r.calls.includes('/api/bongos/government/buildouts'), 'the tap posts to the server');
|
|
638
|
+
} finally { leadsBook = null; }
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('the treasury stands on a real site: real pots, the reserve, the cap off, the dry pot\'s borrow in place; nothing proto (task 1004712)', async () => {
|
|
642
|
+
leadsBook = LEADS_READ;
|
|
643
|
+
treasuryBook = TREASURY_READ;
|
|
644
|
+
try {
|
|
645
|
+
const p = await bootStreet('#building=treasury', false);
|
|
646
|
+
assert.ok(p.calls.includes('/api/bongos/government/treasury'), 'the street reads the treasury');
|
|
647
|
+
const text = noPrototype(p, 'inside the treasury with a real treasury', { seatsRead: true });
|
|
648
|
+
assert.ok(text.includes('of $750 · dry'), 'the real dry pot is drawn: its real spend against its budget');
|
|
649
|
+
assert.ok(text.includes('no spend limit'), 'the cap is off: no spend limit');
|
|
650
|
+
assert.ok(text.includes('in reserve'), 'the reserve is a real tile');
|
|
651
|
+
assert.ok(text.includes('Lend'), 'the dry pot\'s borrow is answered in place, in its stored words');
|
|
652
|
+
const city = await bootCity(false);
|
|
653
|
+
noPrototype(city, 'the city with a real treasury', { seatsRead: true });
|
|
654
|
+
} finally { leadsBook = null; treasuryBook = null; }
|
|
655
|
+
});
|