@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
package/tests/landing_page.mjs
CHANGED
|
@@ -568,15 +568,16 @@ test('the Projects band deep-links the hub\'s views; About and Docs are footer l
|
|
|
568
568
|
// the profile band: the person, My projects, Account, Sign out as the honest anchor.
|
|
569
569
|
// Account fills the slot that waited for a settings page (task 1004259) and is
|
|
570
570
|
// apex-only, because /account is served only where platform-identity runs; so is
|
|
571
|
-
// Invitations, in the same region, for /invitations (task 1004276)
|
|
571
|
+
// Invitations, in the same region, for /invitations (task 1004276), and Connections
|
|
572
|
+
// for /connections (task 1004278).
|
|
572
573
|
assert.match(body, /<div class="menuBand" id="menuProfile" data-menu="profile" hidden>/);
|
|
573
574
|
assert.match(body, /<p class="bandWho" id="bandWho"><\/p>/);
|
|
574
575
|
assert.match(body, /<a class="bandT" href="\/projects\?view=mine">My projects<\/a>/);
|
|
575
|
-
assert.match(body, /<!--apex-only:start--><li class="bandCell"><a class="bandT" href="\/invitations">Invitations<\/a><\/li><li class="bandCell"><a class="bandT" href="\/account">Account<\/a><\/li><!--apex-only:end-->/);
|
|
576
|
+
assert.match(body, /<!--apex-only:start--><li class="bandCell"><a class="bandT" href="\/invitations">Invitations<\/a><\/li><li class="bandCell"><a class="bandT" href="\/connections">Connections<\/a><\/li><li class="bandCell"><a class="bandT" href="\/account">Account<\/a><\/li><!--apex-only:end-->/);
|
|
576
577
|
assert.doesNotMatch(body, /class="bandSlot"/, 'the slot is filled, not kept beside its item');
|
|
577
578
|
assert.match(body, /<a class="bandT signout" href="\/api\/bongos\/auth\/logout">Sign out<\/a>/);
|
|
578
579
|
// a crawler reads every destination without scripting on
|
|
579
|
-
for (const copy of ['Sky', 'Table', 'Your projects', 'New project', 'About', 'Docs', 'My projects', 'Invitations', 'Account', 'Sign out']) {
|
|
580
|
+
for (const copy of ['Sky', 'Table', 'Your projects', 'New project', 'About', 'Docs', 'My projects', 'Invitations', 'Connections', 'Account', 'Sign out']) {
|
|
580
581
|
assert.ok(body.includes('>' + copy + '<'), `"${copy}" is only reachable with scripting on`);
|
|
581
582
|
}
|
|
582
583
|
// the menu container is .menuBand: .band is this page's own content sections
|
|
@@ -614,7 +615,25 @@ test('the retired gate assets (world/assets/clouds|creatures|plates) are gone
|
|
|
614
615
|
assert.doesNotMatch(HTML, /\bworld\/assets\//, 'the page links no path under the retired world/assets/ folder');
|
|
615
616
|
assert.doesNotMatch(WORLD, /\bworld\/assets\//, 'world.css references no path under the retired folder');
|
|
616
617
|
// the chrome landing's own plates (task 1003298) live at assets/world/ and are untouched by this cleanup
|
|
617
|
-
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-bongos-chrome-b.
|
|
618
|
+
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-bongos-chrome-b.webp')), 'the chrome plate at assets/world/ is untouched by this cleanup');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Dark mode's first paint fetched the black plates as JPEG at nearly twice the bytes
|
|
622
|
+
// light mode spent on the same sculpture; both halves of each pair are WebP now. The
|
|
623
|
+
// JPEG stays beside the plate as its source, and because a catalog art_url can name it.
|
|
624
|
+
test('the dark plates are WebP; the JPEG each is encoded from stays beside it (task 1003649)', () => {
|
|
625
|
+
const plates = { '--plate-set': 'r2-chrome-blobs-set', '--plate-bead': 'r2-chrome-blob-one', '--plate-bongos': 'r2-bongos-chrome-b' };
|
|
626
|
+
for (const [token, file] of Object.entries(plates)) {
|
|
627
|
+
assert.match(WORLD, new RegExp(`${token}: url\\(/assets/world/${file}\\.webp\\);`), `${token} names the WebP`);
|
|
628
|
+
const dir = path.join(LANDING, 'assets', 'world');
|
|
629
|
+
const head = fs.readFileSync(path.join(dir, `${file}.webp`)).subarray(0, 12);
|
|
630
|
+
assert.equal(head.toString('latin1', 0, 4) + head.toString('latin1', 8, 12), 'RIFFWEBP', `${file}.webp is a WebP file`);
|
|
631
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(dir, `${file}.webp.json`), 'utf8'));
|
|
632
|
+
assert.match(sidecar.encoded, /libwebp/, `${file}.webp.json records the encoding`);
|
|
633
|
+
assert.ok(sidecar.prompt, `${file}.webp.json carries the prompt`);
|
|
634
|
+
assert.ok(fs.existsSync(path.join(dir, `${file}.jpg`)) && fs.existsSync(path.join(dir, `${file}.jpg.json`)), `${file}.jpg stays beside its sidecar`);
|
|
635
|
+
}
|
|
636
|
+
assert.doesNotMatch(WORLD, /\.jpg\)/, 'world.css requests no JPEG plate');
|
|
618
637
|
});
|
|
619
638
|
|
|
620
639
|
// ── the CLI download (task 1004257) ─────────────────────────────────────────
|
|
@@ -87,7 +87,10 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// task 1004685 (the living-city prototype): the held-seat nav and the My agents tab,
|
|
91
|
+
// then the reads it shares with the governor pages (window.__hallReads); task 1004713:
|
|
92
|
+
// the seats read from /me's server-resolved positions block.
|
|
93
|
+
const SHELL_SHA = 'd50a4cc5e6c954b5';
|
|
91
94
|
|
|
92
95
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
96
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -36,6 +36,13 @@
|
|
|
36
36
|
//
|
|
37
37
|
// 5. `seen_at` AND `truncated` NEVER REACH THE SCREEN.
|
|
38
38
|
//
|
|
39
|
+
// 6. YOUR OWN TAB IS THIS PROJECT'S CUT (ADR 0337 D5.4, task 1004279): only
|
|
40
|
+
// the connections who are also active builders here, read through the
|
|
41
|
+
// route's `?here=1`, modelled below against an in-memory `builders` table.
|
|
42
|
+
// The request queues and everyone else live on the hub's /connections, and
|
|
43
|
+
// the tab links there. It never reads the queues at all, because that read
|
|
44
|
+
// stamps `seen_at` on rows it would not render.
|
|
45
|
+
//
|
|
39
46
|
// DB-free by construction — no Postgres, no network beyond loopback. The last
|
|
40
47
|
// test proves this file runs in the lane that actually executes on a PR; three
|
|
41
48
|
// tests once declared a lane they were in neither of and asserted nothing for
|
|
@@ -84,6 +91,8 @@ const PROV = 9006; // provisional → not addressable at all
|
|
|
84
91
|
const SEC = 9007; // private AND connected to ME → addressable to ME only
|
|
85
92
|
const NEW = 9008; // never asked → the Connect button's home
|
|
86
93
|
const COOL = 9009; // declined ME 10 days ago → the swallowed re-ask
|
|
94
|
+
const FAR = 9010; // connected to ME, builds on another project only → off the own tab
|
|
95
|
+
const GONE = 9011; // connected to ME, deactivated here → off the own tab
|
|
87
96
|
const OUTSIDER = 7777; // a second session, to prove "and to nobody else"
|
|
88
97
|
|
|
89
98
|
// ===========================================================================
|
|
@@ -93,6 +102,9 @@ const OUTSIDER = 7777; // a second session, to prove "and to nobody else"
|
|
|
93
102
|
let accounts = [];
|
|
94
103
|
let edges = [];
|
|
95
104
|
let nextEdgeId = 1;
|
|
105
|
+
// This instance's own builders (core's `builders` table: github_id is text
|
|
106
|
+
// there) — who "builds on this project" for the own tab's `?here=1` cut.
|
|
107
|
+
let builders = new Map();
|
|
96
108
|
|
|
97
109
|
const acct = (githubId, handle, name, over = {}) => ({
|
|
98
110
|
id: githubId, github_id: String(githubId), github_login: handle, display_name: name,
|
|
@@ -127,7 +139,11 @@ function seed() {
|
|
|
127
139
|
acct(NEW, 'newbie', 'Newbie'),
|
|
128
140
|
acct(COOL, 'cooled', 'Cooled'),
|
|
129
141
|
acct(OUTSIDER, 'outsider', 'Outsider'),
|
|
142
|
+
acct(FAR, 'faraway', 'Far Away'),
|
|
143
|
+
acct(GONE, 'gone', 'Gone'),
|
|
130
144
|
];
|
|
145
|
+
builders = new Map([ME, ADA, BO, SEC, NEW].map((g) => [String(g), 'active']));
|
|
146
|
+
builders.set(String(GONE), 'deactivated');
|
|
131
147
|
edges = [];
|
|
132
148
|
nextEdgeId = 1;
|
|
133
149
|
edge(ME, ADA, 'accepted');
|
|
@@ -280,12 +296,18 @@ const pool = {
|
|
|
280
296
|
// ---- my accepted connections -----------------------------------------
|
|
281
297
|
if (/^WITH edge AS \(/.test(sql)) {
|
|
282
298
|
const floor = accountFloor(sql, { axis: 'active' });
|
|
299
|
+
// The project cut, read OUT OF the statement: the exact clause the domain
|
|
300
|
+
// exports, or no builders term at all. Any other spelling throws, so a
|
|
301
|
+
// drifted join can never quietly read as the full list.
|
|
302
|
+
const here = sql.includes(HERE_SQL);
|
|
303
|
+
if (!here && /\bbuilders\b/.test(sql)) throw new Error(`fake pool: a builders clause this interpreter cannot read — ${sql}`);
|
|
283
304
|
const gid = params[0]; const limit = Number(params[1]); const offset = Number(params[2]);
|
|
284
305
|
const mine = edges
|
|
285
306
|
.filter((r) => r.status === 'accepted'
|
|
286
307
|
&& (String(r.requester_github_id) === String(gid) || String(r.addressee_github_id) === String(gid)))
|
|
287
308
|
.map((r) => ({ id: r.id, gid: String(r.requester_github_id) === String(gid) ? r.addressee_github_id : r.requester_github_id, resolved_at: r.resolved_at }))
|
|
288
309
|
.filter((e) => floor(findAccount(e.gid)))
|
|
310
|
+
.filter((e) => !here || builders.get(String(e.gid)) === 'active')
|
|
289
311
|
.sort((a, b) => (b.resolved_at || 0) - (a.resolved_at || 0) || b.id - a.id)
|
|
290
312
|
.slice(offset, offset + limit);
|
|
291
313
|
return { rows: mine.map((e) => ({ id: String(e.id), connected_at: iso(e.resolved_at), ...counterparty(e.gid) })), rowCount: mine.length };
|
|
@@ -407,6 +429,7 @@ require.cache[moduleApiPath] = {
|
|
|
407
429
|
},
|
|
408
430
|
};
|
|
409
431
|
const buildConnectionRoutes = require('../modules/platform-identity/routes/connections.js');
|
|
432
|
+
const { HERE_SQL } = require('../modules/platform-identity/connections.js');
|
|
410
433
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
411
434
|
|
|
412
435
|
const app = express();
|
|
@@ -462,7 +485,9 @@ function makeEl() {
|
|
|
462
485
|
// Boot the real profile-connections.js. `transport` defaults to the live
|
|
463
486
|
// loopback client; a test may pass a canned one for a shape only a hostile or
|
|
464
487
|
// future server could send.
|
|
465
|
-
|
|
488
|
+
const HUB = 'https://hub.example';
|
|
489
|
+
|
|
490
|
+
function boot({ transport, hub = HUB } = {}) {
|
|
466
491
|
const els = new Map();
|
|
467
492
|
const $ = (sel) => { if (!els.has(sel)) els.set(sel, makeEl()); return els.get(sel); };
|
|
468
493
|
$('#conn-scroll').hidden = true; // the markup ships it hidden
|
|
@@ -472,7 +497,7 @@ function boot({ transport } = {}) {
|
|
|
472
497
|
console: { error() {}, warn() {}, log() {} },
|
|
473
498
|
window: {
|
|
474
499
|
BongosClient: { createClient: () => api },
|
|
475
|
-
OTB: { $, escapeHtml, fmtDate: (d) => String(d).slice(0, 10), toast: (m) => toasts.push(m) },
|
|
500
|
+
OTB: { $, escapeHtml, fmtDate: (d) => String(d).slice(0, 10), toast: (m) => toasts.push(m), hubOrigin: () => hub },
|
|
476
501
|
OTBKit: {
|
|
477
502
|
makePager: (prefix, key) => ({ prefix, key, page: 1, perPage: 25 }),
|
|
478
503
|
makeLedger: ({ mount, rowHtml, emptyMsg }) => ({
|
|
@@ -737,7 +762,7 @@ test('this page never asks a question about anyone else’s connections', async
|
|
|
737
762
|
await page.render(profileOf('bo-was', 12), 99);
|
|
738
763
|
assert.ok(asked.length > 0);
|
|
739
764
|
for (const path of asked) {
|
|
740
|
-
assert.match(path, /^\/api\/bongos\/connections(\/(requests|degree\/[^/]+))?$/,
|
|
765
|
+
assert.match(path, /^\/api\/bongos\/connections(\/(requests|degree\/[^/]+)|\?here=1)?$/,
|
|
741
766
|
`the section may only read its own scope; ${path} names a subject the session did not`);
|
|
742
767
|
assert.doesNotMatch(path, /\/builders\//, 'a per-builder connections read would be a public list, and there is deliberately no such route');
|
|
743
768
|
}
|
|
@@ -845,9 +870,11 @@ test('the mutual count is the length of the names beside it, not a number the re
|
|
|
845
870
|
test('seen_at never reaches the screen, on either side of a request', async () => {
|
|
846
871
|
reset();
|
|
847
872
|
const stamped = edge(ADA, ME, 'pending', { created_at: Date.now() - DAY, seen_at: Date.now() - 3600000 });
|
|
873
|
+
// Delete the accepted ADA–ME edge so the pending one is the pair's only row.
|
|
874
|
+
edges = edges.filter((r) => !(r.status === 'accepted' && r.requester_github_id === ME && r.addressee_github_id === ADA));
|
|
848
875
|
const page = boot();
|
|
849
|
-
await page.render(profileOf('
|
|
850
|
-
assert.match(page.html(), /
|
|
876
|
+
await page.render(profileOf('ada', 11), 99); // ADA's profile: her request is the decision there
|
|
877
|
+
assert.match(page.html(), /They have asked to connect\./);
|
|
851
878
|
assert.doesNotMatch(page.html(), /seen/i);
|
|
852
879
|
assert.doesNotMatch(page.html(), new RegExp(new Date(stamped.seen_at).toISOString().slice(0, 10) + 'T'));
|
|
853
880
|
// and the API never even hands the requester one
|
|
@@ -857,45 +884,83 @@ test('seen_at never reaches the screen, on either side of a request', async () =
|
|
|
857
884
|
assert.ok(!('seen_at' in asRequester.data.outgoing[0]), 'an outgoing row must carry no read receipt');
|
|
858
885
|
});
|
|
859
886
|
|
|
887
|
+
test('your own tab never reads the request queues, so it never marks a request seen', async () => {
|
|
888
|
+
reset();
|
|
889
|
+
const ask = edge(NEW, ME, 'pending', { created_at: Date.now() - DAY });
|
|
890
|
+
const page = boot();
|
|
891
|
+
await page.render(profileOf('octo', 7), 7);
|
|
892
|
+
assert.equal(page.$('#conn-scroll').hidden, false);
|
|
893
|
+
assert.ok(!asked.includes('/api/bongos/connections/requests'),
|
|
894
|
+
'GET /connections/requests stamps seen_at on what it returns — "delivered" — so a read this tab does not render would mark a request seen that nobody saw');
|
|
895
|
+
assert.equal(ask.seen_at, null, 'the incoming row is still unseen after the own tab rendered');
|
|
896
|
+
});
|
|
897
|
+
|
|
860
898
|
// ===========================================================================
|
|
861
899
|
// 7. Your own profile — the list, the queues, and the actions on them
|
|
862
900
|
// ===========================================================================
|
|
863
901
|
|
|
864
|
-
test('your own
|
|
902
|
+
test('your own tab lists only the connections who build on this project, and links to the hub for the rest', async () => {
|
|
865
903
|
reset();
|
|
866
|
-
edge(
|
|
867
|
-
edge(
|
|
904
|
+
edge(ME, FAR, 'accepted'); // a connection on another project only
|
|
905
|
+
edge(GONE, ME, 'accepted'); // a connection whose seat here is deactivated
|
|
906
|
+
edge(NEW, ME, 'pending', { created_at: Date.now() - DAY, note: 'worth comparing notes' });
|
|
907
|
+
edge(ME, CY, 'pending', { created_at: Date.now() - DAY });
|
|
868
908
|
const page = boot();
|
|
869
909
|
await page.render(profileOf('octo', 7), 7);
|
|
870
910
|
assert.equal(page.$('#conn-scroll').hidden, false);
|
|
911
|
+
assert.ok(asked.includes('/api/bongos/connections?here=1'), 'the own tab reads the project cut, not the full list');
|
|
871
912
|
const html = page.html();
|
|
872
|
-
assert.match(html, /2 connections\./, 'the count is the length of the list the viewer was handed');
|
|
873
|
-
assert.equal((html.match(/conn-degree--1/g) || []).length, 2, 'every
|
|
913
|
+
assert.match(html, /2 of your connections build on this project\./, 'the count is the length of the list the viewer was handed');
|
|
914
|
+
assert.equal((html.match(/conn-degree--1/g) || []).length, 2, 'every listed connection is 1st by definition');
|
|
874
915
|
assert.equal((html.match(/>1st</g) || []).length, 2, '…and it says so — the class alone is not the label');
|
|
875
|
-
assert.match(html, /
|
|
876
|
-
assert.match(html, /worth comparing notes/);
|
|
877
|
-
assert.match(html, /1 request you have sent/);
|
|
878
|
-
assert.match(html, /data-act="withdraw"/);
|
|
879
|
-
assert.match(html, /data-act="remove"/);
|
|
916
|
+
assert.match(html, /@ada/);
|
|
880
917
|
// SEC is private, and it is in MY OWN list — own scope floors on consent only.
|
|
881
918
|
assert.match(html, /@sec/);
|
|
919
|
+
assert.doesNotMatch(html, /@faraway/, 'a connection who does not build here is the hub page’s, not this tab’s');
|
|
920
|
+
assert.doesNotMatch(html, /@gone/, 'a deactivated seat is not building here');
|
|
921
|
+
assert.match(html, /data-act="remove"/);
|
|
922
|
+
// The queues moved to the hub: none drawn, none decidable here.
|
|
923
|
+
assert.doesNotMatch(html, /asking to connect|you have sent|worth comparing notes/);
|
|
924
|
+
assert.doesNotMatch(html, /data-act="(accept|decline|withdraw)"/);
|
|
925
|
+
assert.match(html, /<a class="pbtn" id="conn-hub-link" href="https:\/\/hub\.example\/connections">Open your Connections page<\/a>/);
|
|
882
926
|
});
|
|
883
927
|
|
|
884
|
-
test('
|
|
928
|
+
test('with no hub origin the own tab draws no link out, and still lists what it has', async () => {
|
|
885
929
|
reset();
|
|
886
|
-
const
|
|
930
|
+
const page = boot({ hub: null });
|
|
931
|
+
await page.render(profileOf('octo', 7), 7);
|
|
932
|
+
assert.equal(page.$('#conn-scroll').hidden, false);
|
|
933
|
+
assert.doesNotMatch(page.html(), /conn-hub-link|Connections page/);
|
|
934
|
+
assert.match(page.html(), /2 of your connections build on this project\./);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test('no connections here yet says so in one line, and the empty ledger says how one arrives', async () => {
|
|
938
|
+
reset();
|
|
939
|
+
builders = new Map([[String(ME), 'active']]); // ADA and SEC build elsewhere
|
|
887
940
|
const page = boot();
|
|
888
941
|
await page.render(profileOf('octo', 7), 7);
|
|
942
|
+
assert.match(page.html(), /None of your connections build on this project yet\./);
|
|
943
|
+
assert.match(page.html(), /When someone you are connected with joins this project, they show up here\./);
|
|
944
|
+
assert.match(page.html(), /id="conn-hub-link"/, 'the way to the rest stays, which is when it matters most');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test('accepting and declining are decided on the asker’s profile; a decline tells them nothing', async () => {
|
|
948
|
+
reset();
|
|
949
|
+
edge(CY, ME, 'pending', { created_at: Date.now() - DAY });
|
|
950
|
+
const page = boot();
|
|
951
|
+
await page.render(profileOf('cy', 13), 99);
|
|
952
|
+
const ask = edges.find((r) => r.requester_github_id === CY && r.addressee_github_id === ME);
|
|
889
953
|
await click(page, { act: 'accept', id: String(ask.id) });
|
|
890
954
|
assert.ok(page.toasts.includes('Connected.'));
|
|
955
|
+
assert.match(page.html(), />1st</, 'the label re-reads as 1st once accepted');
|
|
891
956
|
|
|
892
957
|
reset();
|
|
893
958
|
const spurned = edge(NEW, ME, 'pending', { created_at: Date.now() - DAY });
|
|
894
959
|
const page2 = boot();
|
|
895
|
-
await page2.render(profileOf('
|
|
960
|
+
await page2.render(profileOf('newbie', 18), 99);
|
|
896
961
|
await click(page2, { act: 'decline', id: String(spurned.id) });
|
|
897
962
|
assert.ok(page2.toasts.includes('Declined.'));
|
|
898
|
-
assert.doesNotMatch(page2.html(), /
|
|
963
|
+
assert.doesNotMatch(page2.html(), /data-act="(accept|decline)"/, 'the decision leaves the page');
|
|
899
964
|
// The requester's own page says nothing about it.
|
|
900
965
|
session = { github_id: NEW, github_login: 'newbie' };
|
|
901
966
|
const theirs = boot();
|
|
@@ -910,7 +975,7 @@ test('removing a connection takes it out of the list', async () => {
|
|
|
910
975
|
const id = /data-act="remove" data-id="(\d+)"/.exec(page.html())[1];
|
|
911
976
|
await click(page, { act: 'remove', id });
|
|
912
977
|
assert.ok(page.toasts.includes('Connection removed.'));
|
|
913
|
-
assert.match(page.html(), /1
|
|
978
|
+
assert.match(page.html(), /1 of your connections builds on this project\./);
|
|
914
979
|
});
|
|
915
980
|
|
|
916
981
|
// ===========================================================================
|
|
@@ -935,8 +1000,7 @@ function suggestTransport(suggestions, { onConnect } = {}) {
|
|
|
935
1000
|
if (onConnect) onConnect(opts.body && opts.body.handle);
|
|
936
1001
|
return { status: 201, ok: true, data: { status: 'pending' } };
|
|
937
1002
|
}
|
|
938
|
-
if (method === 'GET' && path === '/api/bongos/connections') return { status: 200, ok: true, data: { connections: [], count: 0 } };
|
|
939
|
-
if (method === 'GET' && path === '/api/bongos/connections/requests') return { status: 200, ok: true, data: { incoming: [], outgoing: [] } };
|
|
1003
|
+
if (method === 'GET' && path === '/api/bongos/connections?here=1') return { status: 200, ok: true, data: { connections: [], count: 0 } };
|
|
940
1004
|
if (method === 'GET' && path === '/api/bongos/connections/suggestions') {
|
|
941
1005
|
calls.suggestions++;
|
|
942
1006
|
return { status: 200, ok: true, data: { suggestions, count: suggestions.length } };
|
|
@@ -972,8 +1036,7 @@ test('a failed suggestions read leaves the rest of the own-profile section intac
|
|
|
972
1036
|
const page = boot({
|
|
973
1037
|
transport: {
|
|
974
1038
|
request: async (_m, path) => {
|
|
975
|
-
if (path === '/api/bongos/connections') return { status: 200, ok: true, data: { connections: [], count: 0 } };
|
|
976
|
-
if (path === '/api/bongos/connections/requests') return { status: 200, ok: true, data: { incoming: [], outgoing: [] } };
|
|
1039
|
+
if (path === '/api/bongos/connections?here=1') return { status: 200, ok: true, data: { connections: [], count: 0 } };
|
|
977
1040
|
return { status: 500, ok: false, data: null }; // suggestions read fails
|
|
978
1041
|
},
|
|
979
1042
|
},
|
|
@@ -981,7 +1044,7 @@ test('a failed suggestions read leaves the rest of the own-profile section intac
|
|
|
981
1044
|
await page.render(profileOf('octo', 7), 7);
|
|
982
1045
|
assert.equal(page.$('#conn-scroll').hidden, false, 'suggestions are additive, never load-bearing for the section itself');
|
|
983
1046
|
assert.doesNotMatch(page.html(), /Builders you may know/);
|
|
984
|
-
assert.match(page.html(), /
|
|
1047
|
+
assert.match(page.html(), /None of your connections build on this project yet\./);
|
|
985
1048
|
});
|
|
986
1049
|
|
|
987
1050
|
test('clicking Connect on a suggestion sends the ask by handle and refreshes the section', async () => {
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1944,6 +1944,21 @@ test('the kit proves the picker: a state renders it on the owner\'s feed-carried
|
|
|
1944
1944
|
assert.ok(names.some((n) => n.startsWith('manage flows: its planet (task 1003347)')), 'the save probe');
|
|
1945
1945
|
assert.ok(names.some((n) => n.includes('handed back to the platform')), 'the reset probe');
|
|
1946
1946
|
assert.ok(names.some((n) => n.includes('its planet at 390')), 'the fold probe');
|
|
1947
|
+
// The reset must undo something (task 1003645): on a fresh page Demo's row carries
|
|
1948
|
+
// no pick and the head already shows the feed's, so a lone reset probe passed
|
|
1949
|
+
// whether or not the reset worked. It now continues the page a pick was saved
|
|
1950
|
+
// on, and that pick is not the template the head falls back to.
|
|
1951
|
+
const resetAt = probes.probes.findIndex((p) => p.name.includes('handed back to the platform'));
|
|
1952
|
+
const reset = probes.probes[resetAt];
|
|
1953
|
+
const picked = probes.probes[resetAt - 1];
|
|
1954
|
+
const headOf = (p) => (p.expect.attr || []).find((a) => a[0] === '#mOrb .worldOrb')?.[2];
|
|
1955
|
+
assert.equal(reset.continue, true, 'the reset runs on the page the pick was saved on');
|
|
1956
|
+
assert.ok(reset.steps.some((s) => s[0] === 'click' && s[1] === '[data-pick-reset]'), 'and presses the reset');
|
|
1957
|
+
assert.ok(picked.steps.some((s) => s[0] === 'click' && s[1] === '[data-pick-save]'), 'the probe before it saves a pick');
|
|
1958
|
+
const feed = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'featured.json'), 'utf8'));
|
|
1959
|
+
const demo = (feed.projects || feed).find((p) => p.origin === 'https://demo.cloudbongos.com');
|
|
1960
|
+
assert.equal(headOf(reset), demo.planet_template, 'the head falls back to the feed’s template');
|
|
1961
|
+
assert.ok(headOf(picked) && headOf(picked) !== headOf(reset), 'the saved pick differs from the fall-back, or the reset proves nothing');
|
|
1947
1962
|
const saveProbe = probes.probes.find((p) => p.name.startsWith('manage flows: its planet (task 1003347)'));
|
|
1948
1963
|
assert.ok(saveProbe.expect.text.some((t) => t[1] === 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.'), 'the route\'s no-restart receipt, verbatim');
|
|
1949
1964
|
const stub = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'serve.js'), 'utf8');
|
package/tests/render_check.mjs
CHANGED
|
@@ -25,8 +25,8 @@ test('a shared asset reaches every page in its dir whose HTML names it', () => {
|
|
|
25
25
|
assert.deepEqual(names(['modules/hall-ui/public/sky-data.js']), ['goal-map.states.json', 'thinking.states.json']);
|
|
26
26
|
assert.deepEqual(names(['modules/hall-ui/public/text-fit.js']), ['goals.states.json']);
|
|
27
27
|
// the account page links the hub's sheet too since task 1004259, the guild page since task 1002301,
|
|
28
|
-
// and the
|
|
29
|
-
assert.deepEqual(names(['modules/public-landing/public/assets/cosmos.css']), ['account.states.json', 'guild.states.json', 'invitations.states.json', 'projects.states.json']);
|
|
28
|
+
// the invitations page since task 1004276, and the connections page since task 1004278
|
|
29
|
+
assert.deepEqual(names(['modules/public-landing/public/assets/cosmos.css']), ['account.states.json', 'connections.states.json', 'guild.states.json', 'invitations.states.json', 'projects.states.json']);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test('the page a builder changed comes before the pages a shared file reaches, so the render cap never drops it', () => {
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
// PURE decision point — given a shipped task's security flag, it returns the
|
|
9
9
|
// list of { channel, line } pairs the deploy step posts. This test proves:
|
|
10
10
|
// 1. security_sensitive=true → ONLY the Archon-only #security-ship-feed.
|
|
11
|
-
// 2. security_sensitive=false → the builder #ship-feed
|
|
12
|
-
//
|
|
11
|
+
// 2. security_sensitive=false → the builder #ship-feed, every ship, and the
|
|
12
|
+
// owner-facing #ship-news ONLY for a feature or a bug, as the plain
|
|
13
|
+
// summary with no id (task 1002846 / idea 1000458, the owner's pick: a
|
|
14
|
+
// kind filter). Any other kind, or none, stays off #ship-news.
|
|
13
15
|
// 3. a falsy summary → [] (the long-standing no-summary, no-broadcast).
|
|
14
16
|
// 4. the security branch can NEVER return ship_news / ship_feed, for ANY
|
|
15
|
-
// taskId/summary — the load-bearing safety property (a leak here
|
|
16
|
-
// an unpatched exploit on a public channel).
|
|
17
|
+
// taskId/summary/kind — the load-bearing safety property (a leak here
|
|
18
|
+
// discloses an unpatched exploit on a public channel).
|
|
17
19
|
//
|
|
18
20
|
// Run: node tests/ship_broadcast_routing.mjs
|
|
19
21
|
|
|
@@ -63,24 +65,63 @@ test('security_sensitive=true NEVER posts to a public channel', () => {
|
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
// ---- 2. non-security → #ship-feed
|
|
68
|
+
// ---- 2. non-security → #ship-feed always; #ship-news for a feature or a bug ----
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
const ALL_KINDS = ['feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture', 'cleanup', 'decision', 'blocker-resolution', 'verify'];
|
|
71
|
+
|
|
72
|
+
test('the news kinds are exactly feature and bug, and every one is a real task kind', () => {
|
|
73
|
+
const { NEWS_KINDS } = require('../modules/discord/ship-broadcast.js');
|
|
74
|
+
const kinds = new Set(require('../modules/lifecycle/task-classifier.js').VALID_KINDS);
|
|
75
|
+
assert.deepEqual([...NEWS_KINDS].sort(), ['bug', 'feature']);
|
|
76
|
+
for (const k of NEWS_KINDS) assert.ok(kinds.has(k), `${k} is a kind a task can carry`);
|
|
77
|
+
for (const k of ALL_KINDS) assert.ok(kinds.has(k), `the test's kind list stays real: ${k}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a feature → the #ship-feed ledger line, then the plain #ship-news headline (no id)', () => {
|
|
69
81
|
const targets = ship.chooseBroadcastTargets({
|
|
70
82
|
securitySensitive: false,
|
|
71
83
|
taskId: 669,
|
|
72
84
|
summary: 'the keeper has a name now',
|
|
85
|
+
kind: 'feature',
|
|
73
86
|
});
|
|
74
87
|
assert.deepEqual(targets, [
|
|
75
88
|
{ channel: 'ship_feed', line: '#669 shipped — the keeper has a name now' },
|
|
89
|
+
{ channel: 'ship_news', line: 'the keeper has a name now' },
|
|
76
90
|
]);
|
|
77
91
|
});
|
|
78
92
|
|
|
79
|
-
test('
|
|
80
|
-
const targets = ship.chooseBroadcastTargets({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
test('a bug fix is news too', () => {
|
|
94
|
+
const targets = ship.chooseBroadcastTargets({ securitySensitive: false, taskId: 70, summary: 'the map loads again', kind: 'bug' });
|
|
95
|
+
assert.deepEqual(targets.map((t) => t.channel), ['ship_feed', 'ship_news']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('every other kind stays on #ship-feed alone', () => {
|
|
99
|
+
for (const kind of ALL_KINDS.filter((k) => k !== 'feature' && k !== 'bug')) {
|
|
100
|
+
const targets = ship.chooseBroadcastTargets({ securitySensitive: false, taskId: 9, summary: 'tidied the cache', kind });
|
|
101
|
+
assert.deepEqual(targets, [{ channel: 'ship_feed', line: '#9 shipped — tidied the cache' }], `kind=${kind}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('a missing or unknown kind posts no headline (a quiet #ship-news is the safe failure)', () => {
|
|
106
|
+
for (const kind of [undefined, null, '', 'Feature', 'BUG', 'features', 1]) {
|
|
107
|
+
const targets = ship.chooseBroadcastTargets({ securitySensitive: false, taskId: 42, summary: 'routine fix', kind });
|
|
108
|
+
assert.deepEqual(targets.map((t) => t.channel), ['ship_feed'], `kind=${String(kind)}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('undefined securitySensitive falls through to the public path (the kind rule still applies)', () => {
|
|
113
|
+
assert.deepEqual(ship.chooseBroadcastTargets({ taskId: 42, summary: 'routine fix' }).map((t) => t.channel), ['ship_feed']);
|
|
114
|
+
assert.deepEqual(ship.chooseBroadcastTargets({ taskId: 42, summary: 'new door', kind: 'feature' }).map((t) => t.channel), ['ship_feed', 'ship_news']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the #ship-news headline never opens with #<id>, so applause can never map it to a task', () => {
|
|
118
|
+
const news = ship.chooseBroadcastTargets({ securitySensitive: false, taskId: 12, summary: 'search finds builders', kind: 'feature' })
|
|
119
|
+
.find((t) => t.channel === 'ship_news');
|
|
120
|
+
assert.doesNotMatch(news.line, /^#\d/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a page approval stays a #ship-feed line, whatever its kind', () => {
|
|
124
|
+
const targets = ship.chooseBroadcastTargets({ securitySensitive: false, taskId: 31, summary: 'the landing reads better', kind: 'feature', pageTweak: true, pageArtist: 'Ana' });
|
|
84
125
|
assert.deepEqual(targets.map((t) => t.channel), ['ship_feed']);
|
|
85
126
|
});
|
|
86
127
|
|
|
@@ -95,11 +136,11 @@ test('falsy summary → [] (no broadcast), regardless of the security flag', ()
|
|
|
95
136
|
|
|
96
137
|
// ---- 4. exhaustive: the security branch is leak-proof across many inputs ----
|
|
97
138
|
|
|
98
|
-
test('exhaustive: security_sensitive=true never leaks to ship_news/ship_feed for any id/summary', () => {
|
|
139
|
+
test('exhaustive: security_sensitive=true never leaks to ship_news/ship_feed for any id/summary/kind', () => {
|
|
99
140
|
const summaries = ['x', 'a long-ish value summary with spaces', '#641 cross-ref in summary', '🛡️ unicode'];
|
|
100
141
|
for (const taskId of [1, 42, 990, 123456]) {
|
|
101
|
-
for (const summary of summaries) {
|
|
102
|
-
const targets = ship.chooseBroadcastTargets({ securitySensitive: true, taskId, summary });
|
|
142
|
+
for (const summary of summaries) for (const kind of [...ALL_KINDS, undefined]) for (const pageTweak of [false, true]) {
|
|
143
|
+
const targets = ship.chooseBroadcastTargets({ securitySensitive: true, taskId, summary, kind, pageTweak });
|
|
103
144
|
assert.equal(targets.length, 1, 'security ship posts to exactly one channel');
|
|
104
145
|
assert.equal(targets[0].channel, 'security_ship_feed');
|
|
105
146
|
for (const { channel } of targets) {
|
|
@@ -112,7 +153,7 @@ test('exhaustive: security_sensitive=true never leaks to ship_news/ship_feed for
|
|
|
112
153
|
// ---- purity: same input → deeply-equal output, no shared mutable state ----
|
|
113
154
|
|
|
114
155
|
test('pure: repeated calls return independent, deeply-equal arrays', () => {
|
|
115
|
-
const args = { securitySensitive: false, taskId: 5, summary: 's' };
|
|
156
|
+
const args = { securitySensitive: false, taskId: 5, summary: 's', kind: 'feature' };
|
|
116
157
|
const a = ship.chooseBroadcastTargets(args);
|
|
117
158
|
const b = ship.chooseBroadcastTargets(args);
|
|
118
159
|
assert.deepEqual(a, b);
|
|
@@ -121,5 +162,5 @@ test('pure: repeated calls return independent, deeply-equal arrays', () => {
|
|
|
121
162
|
assert.equal(ship.chooseBroadcastTargets(args)[0].channel, 'ship_feed');
|
|
122
163
|
});
|
|
123
164
|
|
|
124
|
-
console.log(`\nship broadcast routing (task 990) tests: ${passed} passed, ${failed} failed`);
|
|
165
|
+
console.log(`\nship broadcast routing (task 990, task 1002846) tests: ${passed} passed, ${failed} failed`);
|
|
125
166
|
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -56,10 +56,10 @@ const PUBLIC_CHANNELS = new Set(['ship_news', 'ship_feed']);
|
|
|
56
56
|
|
|
57
57
|
// ---- 1. postShipBroadcast: routing parity with chooseBroadcastTargets ----
|
|
58
58
|
|
|
59
|
-
await atest('non-security
|
|
59
|
+
await atest('non-security cleanup → posts #ship-feed only (not news, task 1002846)', async () => {
|
|
60
60
|
const { fn, calls } = fakePoster();
|
|
61
61
|
const res = await shipBroadcast.postShipBroadcast(
|
|
62
|
-
{ id: 669, value_summary: 'the keeper has a name now', security_sensitive: false },
|
|
62
|
+
{ id: 669, value_summary: 'the keeper has a name now', security_sensitive: false, kind: 'cleanup' },
|
|
63
63
|
{ postWebhook: fn, log: quietLog }
|
|
64
64
|
);
|
|
65
65
|
assert.deepEqual(calls, [
|
|
@@ -69,10 +69,29 @@ await atest('non-security task → posts #ship-feed only (#ship-news paused, tas
|
|
|
69
69
|
assert.equal(res.skipped, null);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
await atest('security
|
|
72
|
+
await atest('non-security feature → #ship-feed, then the plain #ship-news headline, read off the row’s kind', async () => {
|
|
73
73
|
const { fn, calls } = fakePoster();
|
|
74
74
|
const res = await shipBroadcast.postShipBroadcast(
|
|
75
|
-
{ id:
|
|
75
|
+
{ id: 670, value_summary: 'builders can find each other', security_sensitive: false, kind: 'feature' },
|
|
76
|
+
{ postWebhook: fn, log: quietLog }
|
|
77
|
+
);
|
|
78
|
+
assert.deepEqual(calls, [
|
|
79
|
+
{ channel: 'ship_feed', line: '#670 shipped — builders can find each other' },
|
|
80
|
+
{ channel: 'ship_news', line: 'builders can find each other' },
|
|
81
|
+
]);
|
|
82
|
+
assert.deepEqual(res.posted, ['ship_feed', 'ship_news']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await atest('a row with no kind posts no headline', async () => {
|
|
86
|
+
const { fn, calls } = fakePoster();
|
|
87
|
+
await shipBroadcast.postShipBroadcast({ id: 671, value_summary: 'x', security_sensitive: false }, { postWebhook: fn, log: quietLog });
|
|
88
|
+
assert.deepEqual(calls.map((c) => c.channel), ['ship_feed']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await atest('security task → ONLY #security-ship-feed (never a public channel), even a bug', async () => {
|
|
92
|
+
const { fn, calls } = fakePoster();
|
|
93
|
+
const res = await shipBroadcast.postShipBroadcast(
|
|
94
|
+
{ id: 990, value_summary: 'patched the credit-theft hole', security_sensitive: true, kind: 'bug' },
|
|
76
95
|
{ postWebhook: fn, log: quietLog }
|
|
77
96
|
);
|
|
78
97
|
assert.equal(calls.length, 1);
|
|
@@ -112,9 +131,9 @@ await atest('missing task / id → skipped: no_task', async () => {
|
|
|
112
131
|
});
|
|
113
132
|
|
|
114
133
|
await atest('best-effort: a throwing webhook never propagates', async () => {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
134
|
+
// A non-news ship has a single target (#ship-feed). Prove the per-channel
|
|
135
|
+
// try/catch still swallows a failing post: a throwing webhook must never
|
|
136
|
+
// propagate, and nothing is recorded posted.
|
|
118
137
|
const calls = [];
|
|
119
138
|
const flaky = async (channel) => {
|
|
120
139
|
calls.push(channel);
|
|
@@ -135,6 +154,20 @@ await atest('best-effort: a throwing webhook never propagates', async () => {
|
|
|
135
154
|
assert.deepEqual(res.posted, [], 'a failed post is not recorded as posted');
|
|
136
155
|
});
|
|
137
156
|
|
|
157
|
+
await atest('best-effort: a failing #ship-feed does not cost a feature its #ship-news headline', async () => {
|
|
158
|
+
const calls = [];
|
|
159
|
+
const flaky = async (channel) => {
|
|
160
|
+
calls.push(channel);
|
|
161
|
+
if (channel === 'ship_feed') throw new Error('webhook 500 / not configured');
|
|
162
|
+
};
|
|
163
|
+
const res = await shipBroadcast.postShipBroadcast(
|
|
164
|
+
{ id: 43, value_summary: 'a new door', security_sensitive: false, kind: 'feature' },
|
|
165
|
+
{ postWebhook: flaky, log: quietLog }
|
|
166
|
+
);
|
|
167
|
+
assert.deepEqual(calls, ['ship_feed', 'ship_news'], 'each target is attempted');
|
|
168
|
+
assert.deepEqual(res.posted, ['ship_news'], 'only the post that went through is recorded');
|
|
169
|
+
});
|
|
170
|
+
|
|
138
171
|
// ---- 2. broadcastHandledByServer gate (ship.js) ----
|
|
139
172
|
|
|
140
173
|
// Set the env BEFORE requiring ship.js is unnecessary — resolveDeployMode +
|
|
@@ -214,6 +214,25 @@ test('gateApplies: a vendored instance (no fitness.js on either side) stands the
|
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
// task 1004728: a vendored instance keeps an untracked `scripts` link into
|
|
218
|
+
// node_modules/@bongos/core (hidden by .git/info/exclude; grading needs it), so
|
|
219
|
+
// the core's fitness.js is ON DISK while the repo tracks none. The disk check read
|
|
220
|
+
// that as a gated repo and blocked every land on toast. An excluded plain file
|
|
221
|
+
// stands in for the link here: same on-disk shape, and portable to Windows.
|
|
222
|
+
test('gateApplies: an UNTRACKED fitness.js on disk (the vendored scripts link) does not gate the repo', () => {
|
|
223
|
+
const { dir, baseSha } = gitRepo(false);
|
|
224
|
+
try {
|
|
225
|
+
fs.mkdirSync(path.join(dir, 'scripts', 'gds'), { recursive: true });
|
|
226
|
+
fs.writeFileSync(path.join(dir, 'scripts', 'gds', 'fitness.js'), 'process.exit(1);\n');
|
|
227
|
+
fs.appendFileSync(path.join(dir, '.git', 'info', 'exclude'), '/scripts\n');
|
|
228
|
+
assert.equal(gate.gateApplies(dir, baseSha), false);
|
|
229
|
+
const verdict = gate.gateMergeResult({ repoRoot: dir, baseSha, log: () => {} });
|
|
230
|
+
assert.deepEqual(verdict, { block: false, reason: 'not-applicable' });
|
|
231
|
+
} finally {
|
|
232
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
217
236
|
test('gateApplies: a merge that DELETES fitness.js from a gated base is still measured (and blocked)', () => {
|
|
218
237
|
const { dir, baseSha } = gitRepo(true);
|
|
219
238
|
try {
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -43,14 +43,14 @@ test('the shipped states files validate and sit beside the page they name', () =
|
|
|
43
43
|
}
|
|
44
44
|
// the apex is dark only (task 1003336): its states files name the one mode, and
|
|
45
45
|
// render.js reads it as the default so a plain `--page /` never renders light
|
|
46
|
-
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json']) {
|
|
46
|
+
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json', 'modules/public-landing/public/connections.states.json', 'modules/public-landing/public/connections-empty.states.json', 'modules/public-landing/public/connections-inactive.states.json']) {
|
|
47
47
|
assert.deepEqual(readJson(rel).modes, ['dark'], `${rel}: the apex renders dark only`);
|
|
48
48
|
assert.equal(readJson(rel).modeQuery, false, `${rel}: no query walks a second mode`);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('the shipped probes files validate', () => {
|
|
53
|
-
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json', 'modules/public-landing/public/guild.probes.json', 'modules/public-landing/public/invitations.probes.json']) {
|
|
53
|
+
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json', 'modules/public-landing/public/guild.probes.json', 'modules/public-landing/public/invitations.probes.json', 'modules/public-landing/public/connections.probes.json']) {
|
|
54
54
|
const v = lib.validateProbesFile(readJson(rel));
|
|
55
55
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
56
56
|
}
|
|
@@ -470,6 +470,29 @@ await test('NO profile is not an error — the row renders exactly as it always
|
|
|
470
470
|
assert.match(row.textContent, /octocat/);
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
+
// The handle the vouch carried (ADR 0333 D5, task 1004673): when the hub is down the
|
|
474
|
+
// live profile is null, and the stored handle is what the queue can still call them.
|
|
475
|
+
await test('a vouched row with NO profile names the platform handle its vouch carried', async () => {
|
|
476
|
+
const h = await boot({ rows: { pending: withProfile(null, { applicant_handle: 'mona-h' }), invited: [], dismissed: [] } });
|
|
477
|
+
const facts = h.rows()[0].querySelector('.ov-row__facts');
|
|
478
|
+
assert.match(facts.textContent, /On the platform as @mona-h/);
|
|
479
|
+
assert.equal(facts.querySelectorAll('a').length, 0, 'plain text: the hub has no person page to point at');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await test('the handle stays off a row that drew its profile, and off an unvouched row', async () => {
|
|
483
|
+
const drew = await boot({ rows: { pending: withProfile(PUBLIC_PROFILE, { applicant_handle: 'mona-h' }), invited: [], dismissed: [] } });
|
|
484
|
+
assert.doesNotMatch(drew.rows()[0].textContent, /On the platform as/, 'the live record is shown instead');
|
|
485
|
+
const unvouched = await boot({ rows: { pending: withProfile(null, { vouched: false, applicant_handle: 'mona-h' }), invited: [], dismissed: [] } });
|
|
486
|
+
assert.doesNotMatch(unvouched.rows()[0].textContent, /On the platform as/, 'a name anyone could type has no vouched handle');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await test('the vouched handle is escaped', async () => {
|
|
490
|
+
const h = await boot({ rows: { pending: withProfile(null, { applicant_handle: '<b>x</b>' }), invited: [], dismissed: [] } });
|
|
491
|
+
const facts = h.rows()[0].querySelector('.ov-row__facts');
|
|
492
|
+
assert.equal(facts.querySelectorAll('b').length, 0);
|
|
493
|
+
assert.match(facts.textContent, /@<b>x<\/b>/);
|
|
494
|
+
});
|
|
495
|
+
|
|
473
496
|
await test('an unrecognised view shape draws nothing rather than guessing', async () => {
|
|
474
497
|
const h = await boot({ rows: { pending: withProfile({ view: 'everything', account: { bio: 'x' } }), invited: [], dismissed: [] } });
|
|
475
498
|
assert.equal(h.rows()[0].querySelector('.ov-row__profile'), null,
|