@bongos/core 1.23.22 → 1.24.2
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 +933 -413
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +57 -7
- package/docs/page-readings.json +1210 -1027
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +158 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +38 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- 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 +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -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 +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -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 +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +230 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/government-walk.js +3 -0
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/access_holders.mjs +214 -0
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -206,3 +206,147 @@ test('a backlog of rounds the lane must skip costs at most ten reads a pass', as
|
|
|
206
206
|
await lane.pickRound({ api, state: {}, matchProtected, readInventory: () => INV, readRound: async () => { reads++; return round(1); } });
|
|
207
207
|
assert.equal(reads, 10);
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
// ---- land only, going live waits for a person (task 1004974) -----------------
|
|
211
|
+
//
|
|
212
|
+
// The unattended path may reach its finish line (WAITING FOR THE ARTIST; the
|
|
213
|
+
// artist's approval then lands it like any confirmed task) and must never be the
|
|
214
|
+
// one that asks for a live pin move. Proven here: the hand-off fence refuses a
|
|
215
|
+
// prompt or script list naming a pin door, and a scan of the text the worker
|
|
216
|
+
// actually follows finds none. The SERVER side (whether the pin doors refuse the
|
|
217
|
+
// worker's own session) is tests/autobongos_tweak_pin_identity.mjs.
|
|
218
|
+
|
|
219
|
+
const fence = require('../modules/autonomy/fence.js');
|
|
220
|
+
const fs = require('node:fs');
|
|
221
|
+
const path = require('node:path');
|
|
222
|
+
const ROOT = path.resolve(path.dirname(require('node:url').fileURLToPath(import.meta.url)), '..');
|
|
223
|
+
|
|
224
|
+
// One planted line per door: the fence must refuse each, and name it.
|
|
225
|
+
const PLANTED = [
|
|
226
|
+
['core-upgrade', 'node scripts/bongos/api.js POST /api/bongos/provisioning/instances/41/core-upgrade --body \'{"to":"1.19.1"}\''],
|
|
227
|
+
['core-upgrade', 'node scripts/bongos/provision-core-upgrade.js --instance 41'],
|
|
228
|
+
['update-channel', 'node scripts/bongos/api.js POST /api/bongos/provisioning/instances/41/update-channel'],
|
|
229
|
+
['update-channel', 'set the update channel to minor'],
|
|
230
|
+
['npm-release', 'node scripts/bongos/api.js POST /api/bongos/npm-release/release'],
|
|
231
|
+
['core.pin.move', 'you hold core.pin.move, so use it'],
|
|
232
|
+
['core-update', 'node scripts/bongos/api.js POST /api/bongos/software-update/request'],
|
|
233
|
+
['render-deploy', 'node scripts/bongos/api.js POST /api/bongos/render-deploy/act'],
|
|
234
|
+
['bongos upgrade', 'ssh host bongos upgrade --to 1.19.1'],
|
|
235
|
+
['pin move', 'then move the live pin so the artist sees it'],
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
test('the hand-off fence passes the real prompt, the real lane scripts and the real skill', () => {
|
|
239
|
+
const prompt = lane.buildTweakPrompt({ task: { id: 901 }, pageId: 'landing:index' });
|
|
240
|
+
const skillText = lane.readSkill();
|
|
241
|
+
const cmds = lane.skillCommands(skillText);
|
|
242
|
+
assert.ok(cmds.some((c) => /ship\.js/.test(c)) && cmds.some((c) => /tweak-halt\.js/.test(c)), 'the skill\'s command lines were found, so the scan is not vacuous');
|
|
243
|
+
assert.equal(lane.guardHandOff({ prompt, skillText }).go, true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('the hand-off fence refuses a prompt or a script list that names a pin door', () => {
|
|
247
|
+
const prompt = lane.buildTweakPrompt({ task: { id: 901 }, pageId: 'landing:index' });
|
|
248
|
+
for (const [door, line] of PLANTED) {
|
|
249
|
+
const inScripts = fence.refusePinMove({ prompt, scripts: [...lane.LANE_SCRIPTS, line] });
|
|
250
|
+
assert.equal(inScripts.code, 'pin_move', `script list: ${line}`);
|
|
251
|
+
assert.ok(inScripts.doors.includes(door), `names ${door}: ${inScripts.doors}`);
|
|
252
|
+
assert.equal(fence.refusePinMove({ prompt: `${prompt}\n${line}`, scripts: [...lane.LANE_SCRIPTS] }).code, 'pin_move', `prompt: ${line}`);
|
|
253
|
+
const skill = `${lane.readSkill()}\n\`\`\`bash\n${line}\n\`\`\`\n`;
|
|
254
|
+
assert.equal(lane.guardHandOff({ prompt, skillText: skill }).code, 'pin_move', `skill command: ${line}`);
|
|
255
|
+
}
|
|
256
|
+
assert.equal(fence.refusePinMove({ prompt, scripts: null }).go, false, 'an unreadable list refuses');
|
|
257
|
+
assert.equal(fence.refusePinMove({ prompt: undefined, scripts: [] }).go, false, 'an unreadable prompt refuses');
|
|
258
|
+
assert.equal(lane.guardHandOff({ prompt, skillText: null }).go, false, 'an unreadable skill refuses');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('iteration: a planted pin move in the skill stops the round before the claim (no tree, no worker)', async () => {
|
|
262
|
+
const api = fakeApi({ pages: [submitted('landing:index', 901)], rounds: { 901: round(901) } });
|
|
263
|
+
for (const readSkill of [
|
|
264
|
+
() => `${lane.readSkill()}\n\`\`\`bash\n${PLANTED[0][1]}\n\`\`\`\n`,
|
|
265
|
+
() => { throw new Error('ENOENT'); },
|
|
266
|
+
]) {
|
|
267
|
+
const h = harness({ api });
|
|
268
|
+
h.deps.laneDeps.readSkill = readSkill;
|
|
269
|
+
const row = await runner.iteration(opts, h.deps);
|
|
270
|
+
assert.equal(row.event, 'fenced');
|
|
271
|
+
assert.equal(row.lane, 'tweak');
|
|
272
|
+
assert.equal(row.code, 'pin_move');
|
|
273
|
+
assert.deepEqual(h.calls, [], 'nothing ran: no worktree, no claim, no worker');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('iteration: the unattended path reaches its finish line and nothing it ran names a pin door', async () => {
|
|
278
|
+
const h = harness({ api: fakeApi({ pages: [submitted('landing:index', 901)], rounds: { 901: round(901) } }) });
|
|
279
|
+
const row = await runner.iteration(opts, h.deps);
|
|
280
|
+
assert.equal(row.outcome, 'waiting_for_artist', 'held at a passed grade for the artist, whose approval lands it');
|
|
281
|
+
assert.ok(h.calls.length >= 3, 'the tree, the claim and the worker all ran');
|
|
282
|
+
for (const c of h.calls) {
|
|
283
|
+
const said = [c.script, ...(c.args || []), c.prompt || ''].join(' ');
|
|
284
|
+
assert.deepEqual(fence.pinMoveDoors(said), [], `${c.script} named a pin door`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// The scan of what the worker follows. Two matchers, so a bug in the fence's own
|
|
289
|
+
// patterns cannot make this scan agree with it: the fence's, and a literal one.
|
|
290
|
+
const LITERAL_DOORS = /core[-_]upgrade|update[-_]channel|core\.pin\.move|software-update|render-deploy\/act|bongos\s+upgrade|move\s+the\s+(?:live\s+)?pin/i;
|
|
291
|
+
const scanned = () => [
|
|
292
|
+
['the tweak skill (its unattended path follows every step)', fs.readFileSync(path.join(ROOT, lane.SKILL_PATH), 'utf8')],
|
|
293
|
+
['the tweak lane', fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'autobongos-tweak-lane.js'), 'utf8')],
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
test('the unattended skill and the lane name no pin door', () => {
|
|
297
|
+
for (const [what, text] of scanned()) {
|
|
298
|
+
assert.ok(text.length > 1000, `${what} was read`);
|
|
299
|
+
assert.deepEqual(fence.pinMoveDoors(text), [], `${what} names a pin door`);
|
|
300
|
+
assert.equal(LITERAL_DOORS.exec(text), null, `${what} names a pin door`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// The fence's list is named doors, so it is kept honest from the code: every
|
|
305
|
+
// MUTATING route gated on core.pin.move (directly, or through a variable holding
|
|
306
|
+
// that gate) or on the owner-or-pin gate must be matched by a pattern. A new pin
|
|
307
|
+
// door added anywhere reds this until fence.js names it.
|
|
308
|
+
function pinGatedMutatingRoutes() {
|
|
309
|
+
const files = [];
|
|
310
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
311
|
+
for (const m of fs.readdirSync(modulesDir)) {
|
|
312
|
+
const dir = path.join(modulesDir, m, 'routes');
|
|
313
|
+
if (fs.existsSync(dir)) for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) files.push(path.join(dir, f));
|
|
314
|
+
}
|
|
315
|
+
const coreRoutes = path.join(ROOT, 'src', 'bongos', 'routes');
|
|
316
|
+
for (const f of fs.readdirSync(coreRoutes)) if (f.endsWith('.js')) files.push(path.join(coreRoutes, f));
|
|
317
|
+
const out = [];
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
320
|
+
// Variables that hold the atom gate (npm-release's `mayPreview`, say).
|
|
321
|
+
const gateVars = [...src.matchAll(/(?:const|let)\s+(\w+)\s*=\s*[\w.]*requirePermission\(\s*'core\.pin\.move'\s*\)/g)].map((m) => m[1]);
|
|
322
|
+
const calls = [...src.matchAll(/router\.(post|put|patch|delete)\(/g)];
|
|
323
|
+
calls.forEach((m, i) => {
|
|
324
|
+
const body = src.slice(m.index, i + 1 < calls.length ? calls[i + 1].index : src.length).split(/\n\s*router\.(?:get|use)\(/)[0];
|
|
325
|
+
const gated = /requirePermission\(\s*'core\.pin\.move'\s*\)|requireOwnProjectOrPin/.test(body)
|
|
326
|
+
|| gateVars.some((v) => new RegExp(`\\b${v}\\b`).test(body));
|
|
327
|
+
const route = /\(\s*['`]([^'`]+)['`]/.exec(body);
|
|
328
|
+
if (gated && route) out.push({ file: path.relative(ROOT, file), method: m[1], route: route[1] });
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
test('every mutating route behind the pin gates is a door the fence names', () => {
|
|
335
|
+
const routes = pinGatedMutatingRoutes();
|
|
336
|
+
const seen = routes.map((r) => r.route);
|
|
337
|
+
for (const known of ['/provisioning/instances/:id/core-upgrade', '/provisioning/instances/:id/update-channel', '/render-deploy/act', '/npm-release/release']) {
|
|
338
|
+
assert.ok(seen.includes(known), `the enumeration found ${known} (so it is not vacuous): ${seen.join(', ')}`);
|
|
339
|
+
}
|
|
340
|
+
for (const r of routes) {
|
|
341
|
+
assert.notDeepEqual(fence.pinMoveDoors(`POST /api/bongos${r.route}`), [], `${r.method.toUpperCase()} ${r.route} (${r.file}) is a pin door the fence does not name`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('the scan fails on a planted pin move in either file', () => {
|
|
346
|
+
for (const [what, text] of scanned()) {
|
|
347
|
+
for (const [door, line] of PLANTED) {
|
|
348
|
+
assert.ok(fence.pinMoveDoors(`${text}\n${line}\n`).includes(door), `${what}: ${door} not caught`);
|
|
349
|
+
}
|
|
350
|
+
assert.ok(LITERAL_DOORS.test(`${text}\n${PLANTED[0][1]}\n`), `${what}: the literal scan misses core-upgrade`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/autobongos_tweak_pin_identity.mjs — does a live pin door refuse the
|
|
2
|
+
// unattended worker's own session? (task 1004974)
|
|
3
|
+
//
|
|
4
|
+
// "Land only. Ask before going live." The lane never ASKS for a pin move
|
|
5
|
+
// (tests/autobongos_tweak_lane.mjs proves the hand-off fence and the scan). This
|
|
6
|
+
// file asks the other half: if a worker wandered past its prompt, would the
|
|
7
|
+
// server stop it?
|
|
8
|
+
//
|
|
9
|
+
// THE WORKER'S IDENTITY. autobongos-run.js hands a worker HOME and USERPROFILE
|
|
10
|
+
// (WORKER_ENV_ALLOW), so the worker reads the runner builder's own CLI session
|
|
11
|
+
// file and calls the instance as that builder, with a `cli` session — the same
|
|
12
|
+
// token the builder uses at a terminal. Nothing on the request says "unattended".
|
|
13
|
+
//
|
|
14
|
+
// What runs here is REAL: auth.requireBuilder resolving a session token, the
|
|
15
|
+
// provisioning router, its owner-or-pin gate, the act-alone gate and the handler.
|
|
16
|
+
// Only the session lookup, the permission resolver and the instance rows are
|
|
17
|
+
// doubles.
|
|
18
|
+
//
|
|
19
|
+
// THE KNOWN GAP. The refusals the owner's rule asks for are `todo` tests: today
|
|
20
|
+
// the owner's runner session is ADMITTED by the owner door, and an Archon
|
|
21
|
+
// runner's by the core.pin.move atom, so a worker running as either could queue
|
|
22
|
+
// a live move. Filed as private security report 1000030 (task 1004974); the
|
|
23
|
+
// todo tests turn into ordinary tests when the fix lands. The control test below
|
|
24
|
+
// proves this harness CAN see a refusal, so a todo failing here is the gap and
|
|
25
|
+
// not a broken harness.
|
|
26
|
+
//
|
|
27
|
+
// Run: node --test tests/autobongos_tweak_pin_identity.mjs
|
|
28
|
+
|
|
29
|
+
import assert from 'node:assert/strict';
|
|
30
|
+
import { test, after } from 'node:test';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
|
|
35
|
+
// The session double must be installed BEFORE auth.js loads (it destructures
|
|
36
|
+
// lookupSession at require time — the tests/core_upgrade_owner_door.mjs precedent).
|
|
37
|
+
const dbKernel = require('../src/bongos/db-kernel.js');
|
|
38
|
+
// The runner builder is Metic+ (the lane's floor). 7 owns the hosted project 41;
|
|
39
|
+
// 1 is the hub's Archon, who owns nothing hosted here; 8 is a stranger.
|
|
40
|
+
const SESSIONS = {
|
|
41
|
+
ownerrunner: { builder_id: 7, github_id: '9001', rank: 'metic', source: 'cli', status: 'active' },
|
|
42
|
+
archonrunner: { builder_id: 1, github_id: '9000', rank: 'archon', source: 'cli', status: 'active' },
|
|
43
|
+
strangerrunner: { builder_id: 8, github_id: '9002', rank: 'metic', source: 'cli', status: 'active' },
|
|
44
|
+
};
|
|
45
|
+
dbKernel.lookupSession = async (token) => SESSIONS[token] || null;
|
|
46
|
+
|
|
47
|
+
const seams = require('../src/module-seams.js');
|
|
48
|
+
const catalog = require('../modules/government/catalog.js');
|
|
49
|
+
const api = require('../src/module-api.js');
|
|
50
|
+
const prov = require('../modules/provisioning/provisioning.js');
|
|
51
|
+
const runner = require('../scripts/bongos/autobongos-run.js');
|
|
52
|
+
|
|
53
|
+
const TENANT = Object.freeze({ id: 41, slug: 'hosted-shop', status: 'active', hosting_shape: 'cloud-host', owner_builder_id: 7, update_channel: 'pinned' });
|
|
54
|
+
const ROWS = new Map([[41, TENANT]]);
|
|
55
|
+
|
|
56
|
+
seams._reset();
|
|
57
|
+
seams.registerProvider('government', {
|
|
58
|
+
resolveBuilderPermissions: async (id) => catalog.seedForRank(Object.values(SESSIONS).find((s) => s.builder_id === id).rank),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const queued = [];
|
|
62
|
+
const saved = { get: prov.getInstanceById, enq: prov.enqueueIntent, rec: prov.recordEvent, pool: Object.getOwnPropertyDescriptor(api, 'pool') };
|
|
63
|
+
prov.getInstanceById = async (_db, n) => ROWS.get(Number(n)) || null;
|
|
64
|
+
prov.enqueueIntent = async (_db, id, action, requestedBy, targetVersion) => {
|
|
65
|
+
queued.push({ id, action, requestedBy, targetVersion });
|
|
66
|
+
return { intent: { id: queued.length, action, state: 'pending', target_version: targetVersion }, created: true };
|
|
67
|
+
};
|
|
68
|
+
prov.recordEvent = async () => {};
|
|
69
|
+
const channelWrites = [];
|
|
70
|
+
Object.defineProperty(api, 'pool', { value: { query: async (sql, params) => { channelWrites.push(params); return { rows: [] }; } }, configurable: true, writable: true });
|
|
71
|
+
|
|
72
|
+
const routePath = require.resolve('../modules/provisioning/routes/core-upgrade.js');
|
|
73
|
+
delete require.cache[routePath];
|
|
74
|
+
const express = require('express');
|
|
75
|
+
const app = express();
|
|
76
|
+
app.use(express.json());
|
|
77
|
+
app.use((req, res, next) => { require('../src/bongos/middleware/error-envelope.js').attachFail(req, res, next); });
|
|
78
|
+
app.use(require(routePath)());
|
|
79
|
+
const server = app.listen(0);
|
|
80
|
+
await new Promise((r) => server.once('listening', r));
|
|
81
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
82
|
+
|
|
83
|
+
after(() => {
|
|
84
|
+
server.close();
|
|
85
|
+
prov.getInstanceById = saved.get; prov.enqueueIntent = saved.enq; prov.recordEvent = saved.rec;
|
|
86
|
+
Object.defineProperty(api, 'pool', saved.pool);
|
|
87
|
+
delete require.cache[routePath];
|
|
88
|
+
seams._reset();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// A real live move: no dry_run, a named target.
|
|
92
|
+
const move = (token, id = 41) => fetch(`${base}/provisioning/instances/${id}/core-upgrade`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
95
|
+
body: JSON.stringify({ to: '1.19.1033' }),
|
|
96
|
+
});
|
|
97
|
+
const channel = (token, id = 41) => fetch(`${base}/provisioning/instances/${id}/update-channel`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
100
|
+
body: JSON.stringify({ channel: 'minor' }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the worker carries its builder\'s own CLI session: the home directories pass through', () => {
|
|
104
|
+
const env = runner.workerEnv({ HOME: '/home/runner', USERPROFILE: 'C:\\Users\\runner', DATABASE_URL: 'postgres://x' });
|
|
105
|
+
assert.equal(env.HOME, '/home/runner');
|
|
106
|
+
assert.equal(env.USERPROFILE, 'C:\\Users\\runner');
|
|
107
|
+
assert.equal(env.DATABASE_URL, undefined, 'the allowlist is live, so the home dirs are deliberate');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('control: a stranger\'s runner session is refused a live move, so this harness can see a refusal', async () => {
|
|
111
|
+
const r = await move('strangerrunner');
|
|
112
|
+
assert.equal(r.status, 403);
|
|
113
|
+
assert.equal(queued.length, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const GAP = 'known gap, task 1004974: the server cannot tell an unattended worker from its builder at a terminal';
|
|
117
|
+
|
|
118
|
+
test('the OWNER\'s runner session is refused a live move on the owner\'s own project', { todo: GAP }, async () => {
|
|
119
|
+
const before = queued.length;
|
|
120
|
+
const r = await move('ownerrunner');
|
|
121
|
+
assert.equal(r.status, 403, `a worker running as the owner queued a live move (status ${r.status})`);
|
|
122
|
+
assert.equal(queued.length, before);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('the OWNER\'s runner session is refused a change to the project\'s update rule', { todo: GAP }, async () => {
|
|
126
|
+
const before = channelWrites.length;
|
|
127
|
+
const r = await channel('ownerrunner');
|
|
128
|
+
assert.equal(r.status, 403, `a worker running as the owner changed which moves the runner makes (status ${r.status})`);
|
|
129
|
+
assert.equal(channelWrites.length, before);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('an ARCHON runner session is refused a live move on a hosted project', { todo: GAP }, async () => {
|
|
133
|
+
const before = queued.length;
|
|
134
|
+
const r = await move('archonrunner');
|
|
135
|
+
assert.equal(r.status, 403, `a worker running as the Archon queued a live move (status ${r.status})`);
|
|
136
|
+
assert.equal(queued.length, before);
|
|
137
|
+
});
|
|
@@ -16,7 +16,7 @@ import { makeRunner } from './helpers.mjs';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const {
|
|
19
|
-
CLASSES, DEDUP_FIX_AT, classifySession, buildBackfillPlan, renderReport,
|
|
19
|
+
CLASSES, CLASS_NOTE, DEDUP_FIX_AT, classifySession, buildBackfillPlan, renderReport,
|
|
20
20
|
} = require('../scripts/bongos/backfill-subagent-rewards.js');
|
|
21
21
|
const {
|
|
22
22
|
computeSessionTokenReward, sessionTrueCostUsd, awardSessionTokenReward,
|
|
@@ -105,6 +105,22 @@ await test('no_subagent_record: no cost row means unrecoverable — never guesse
|
|
|
105
105
|
assert.equal(r.subagentUsd, undefined, 'no invented figure');
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// Task 1003640: most sessions in this bucket simply never ran a subagent, so the
|
|
109
|
+
// reason must lead with that ordinary case, and name the failed-hook case only as
|
|
110
|
+
// the rarer possibility the data cannot tell apart, never as the cause.
|
|
111
|
+
await test('no_subagent_record: the reason does not blame a failed cost hook for a session that used no subagent', () => {
|
|
112
|
+
const note = CLASS_NOTE[CLASSES.NO_SUBAGENT_RECORD];
|
|
113
|
+
assert.match(note, /^[^.]*no subagent/i, 'the ordinary case (no subagent used) is named first');
|
|
114
|
+
assert.match(note, /nothing (is )?owed/i, 'says the ordinary case owes nothing');
|
|
115
|
+
assert.doesNotMatch(note, /: the cost hook never ran/, 'does not assert the hook failure as the cause');
|
|
116
|
+
assert.match(note, /cannot (be )?tell|cannot tell|indistinguishable|same trace/i, 'says the two cases look the same in the data');
|
|
117
|
+
const plan = buildBackfillPlan([row({ subagent_usd: null, subagent_recorded_at: null })],
|
|
118
|
+
{ since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
119
|
+
const { text } = renderReport(plan);
|
|
120
|
+
assert.doesNotMatch(text, /Sessions whose cost hook never ran left no trace at all/,
|
|
121
|
+
'the closing summary no longer reads every no-record session as a lost hook');
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
await test('pre_split: a legacy single `session` row lumps both legs and cannot be separated', () => {
|
|
109
125
|
const r = classifySession(
|
|
110
126
|
row({ subagent_usd: null, subagent_recorded_at: null, has_pre_split_cost_row: true }), OPTS
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// tests/backup_trigger_lock.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004924 — POST /backup/trigger runs a ~2 minute bash dump. Two at once on
|
|
4
|
+
// one disk is the thing to prevent, so the route holds a single-flight lock: a
|
|
5
|
+
// second trigger while one runs is 409 backup_already_running, and the lock frees
|
|
6
|
+
// when the dump finishes. The lock existed but nothing pinned it; and a spawn
|
|
7
|
+
// that threw synchronously never reached the callback, so the lock stuck and
|
|
8
|
+
// every later trigger 409'd until a restart.
|
|
9
|
+
//
|
|
10
|
+
// Hermetic: the auth gates are stubbed (the rank gate itself is tested in its own
|
|
11
|
+
// suites), and execFile is the router's injected seam — no real dump runs.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/backup_trigger_lock.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import { makeRunner } from './helpers.mjs';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const express = require('express');
|
|
22
|
+
const auth = require('../src/bongos/auth.js');
|
|
23
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
24
|
+
|
|
25
|
+
// Stub the gates BEFORE building: the router reads them at build time.
|
|
26
|
+
const pass = (req, _res, next) => { req.builder = { id: 'b-test' }; next(); };
|
|
27
|
+
auth.requireBuilder = pass;
|
|
28
|
+
auth.requirePermission = () => (_req, _res, next) => next();
|
|
29
|
+
|
|
30
|
+
const buildBackupRouter = require('../src/bongos/routes/backup.js');
|
|
31
|
+
const { test, summary } = makeRunner();
|
|
32
|
+
|
|
33
|
+
async function withServer(execFile, fn) {
|
|
34
|
+
const app = express();
|
|
35
|
+
app.use(attachFail);
|
|
36
|
+
app.use(buildBackupRouter({ execFile }));
|
|
37
|
+
const server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)); });
|
|
38
|
+
const port = server.address().port;
|
|
39
|
+
// node:http with no keep-alive agent, not fetch: fetch's pooled sockets are
|
|
40
|
+
// still closing when summary() calls process.exit, which aborts on Windows
|
|
41
|
+
// (libuv UV_HANDLE_CLOSING assertion) after every case has passed.
|
|
42
|
+
const trigger = () => new Promise((resolve, reject) => {
|
|
43
|
+
const req = http.request({ host: '127.0.0.1', port, path: '/backup/trigger', method: 'POST', agent: false }, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', (c) => { data += c; });
|
|
46
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data) }));
|
|
47
|
+
});
|
|
48
|
+
req.on('error', reject);
|
|
49
|
+
req.end();
|
|
50
|
+
});
|
|
51
|
+
try { await fn(trigger); } finally { await new Promise((r) => server.close(r)); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await test('a second trigger while a dump runs is 409; once it finishes the next one starts', async () => {
|
|
55
|
+
const spawned = [];
|
|
56
|
+
const execFile = (cmd, args, opts, cb) => { spawned.push(cb); };
|
|
57
|
+
await withServer(execFile, async (trigger) => {
|
|
58
|
+
assert.equal((await trigger()).status, 202);
|
|
59
|
+
const second = await trigger();
|
|
60
|
+
assert.equal(second.status, 409);
|
|
61
|
+
assert.equal(second.body.error.code, 'backup_already_running');
|
|
62
|
+
assert.equal(spawned.length, 1, 'never two dumps at once');
|
|
63
|
+
|
|
64
|
+
spawned[0](null, '', ''); // the dump finishes
|
|
65
|
+
assert.equal((await trigger()).status, 202);
|
|
66
|
+
assert.equal(spawned.length, 2);
|
|
67
|
+
spawned[1](new Error('pg_dump failed'), '', 'boom'); // a FAILED dump frees the lock too
|
|
68
|
+
assert.equal((await trigger()).status, 202);
|
|
69
|
+
spawned[2](null, '', '');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await test('a spawn that throws synchronously frees the lock instead of wedging it', async () => {
|
|
74
|
+
let calls = 0;
|
|
75
|
+
let throwIt = true;
|
|
76
|
+
const execFile = (cmd, args, opts, cb) => {
|
|
77
|
+
calls++;
|
|
78
|
+
if (throwIt) throw new Error('spawn EAGAIN');
|
|
79
|
+
setImmediate(() => cb(null, '', ''));
|
|
80
|
+
};
|
|
81
|
+
await withServer(execFile, async (trigger) => {
|
|
82
|
+
const first = await trigger();
|
|
83
|
+
assert.equal(first.status, 500);
|
|
84
|
+
assert.equal(first.body.error.code, 'backup_start_failed');
|
|
85
|
+
throwIt = false;
|
|
86
|
+
const second = await trigger();
|
|
87
|
+
assert.equal(second.status, 202, 'the lock was released, so a retry starts a dump');
|
|
88
|
+
assert.equal(calls, 2);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// task 1004966 — the in-memory flag above cannot see the nightly timer, which runs
|
|
93
|
+
// the script outside this process. The cross-process guard is the flock INSIDE
|
|
94
|
+
// db-backup-nightly.sh, so it only covers the manual trigger if the trigger runs
|
|
95
|
+
// that very script — the one the systemd unit's ExecStart names.
|
|
96
|
+
await test('the trigger runs the same script as the nightly unit, so both take its flock', async () => {
|
|
97
|
+
const fs = require('node:fs');
|
|
98
|
+
const path = require('node:path');
|
|
99
|
+
const { BACKUP_SCRIPT, SKIPPED_MARK } = buildBackupRouter;
|
|
100
|
+
const root = path.resolve(path.dirname(require('node:url').fileURLToPath(import.meta.url)), '..');
|
|
101
|
+
const script = path.join(root, 'scripts', 'bongos', 'db-backup-nightly.sh');
|
|
102
|
+
assert.equal(path.resolve(BACKUP_SCRIPT), path.resolve(script));
|
|
103
|
+
const unit = fs.readFileSync(path.join(root, 'scripts', 'bongos', 'gds-db-backup.service'), 'utf8');
|
|
104
|
+
assert.match(unit, /^ExecStart=.*\/db-backup-nightly\.sh\s*$/m, 'the timer runs the same script');
|
|
105
|
+
const src = fs.readFileSync(script, 'utf8');
|
|
106
|
+
assert.match(src, /flock -n 9/, 'the script takes the lock');
|
|
107
|
+
assert.ok(src.includes(SKIPPED_MARK), 'the script prints the skip line the trigger listens for');
|
|
108
|
+
|
|
109
|
+
const runs = [];
|
|
110
|
+
const execFile = (cmd, args, opts, cb) => { runs.push([cmd, args]); setImmediate(() => cb(null, `${SKIPPED_MARK} another backup is running`, '')); };
|
|
111
|
+
await withServer(execFile, async (trigger) => {
|
|
112
|
+
assert.equal((await trigger()).status, 202);
|
|
113
|
+
await new Promise((r) => setImmediate(r));
|
|
114
|
+
assert.deepEqual(runs[0], ['bash', [BACKUP_SCRIPT]]);
|
|
115
|
+
assert.equal((await trigger()).status, 202, 'a skipped run frees the in-memory flag too');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
summary();
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// tests/blockers_people.mjs — open blockers UNDER THE PEOPLE they wait on (task 1004999).
|
|
2
|
+
//
|
|
3
|
+
// WHAT IT GUARDS.
|
|
4
|
+
// * Every route the City carries now names the PEOPLE in its seat, not only the seat.
|
|
5
|
+
// * groupByPerson lists each open blocker under every person its seat resolves to,
|
|
6
|
+
// with the City's own rule (seatOf → resolveSeat): owner, a seated Lead, an
|
|
7
|
+
// unseated building falling to the owner, a board's members, and nobody.
|
|
8
|
+
// * The grouping and the City agree on every blocker — they are one rule.
|
|
9
|
+
// * Seating a different Lead moves the blocker with nothing on the blocker edited.
|
|
10
|
+
// * blockersByPerson reads the real contributed source, its asks and the seats.
|
|
11
|
+
// * The hall draws it: the viewer first as "You", the seat in words, "Nobody yet",
|
|
12
|
+
// escaping, and the City card's "waits on Lars, the owner".
|
|
13
|
+
//
|
|
14
|
+
// DB-free: the government db reads, the resolver and the asks read are stubbed.
|
|
15
|
+
// Run: node --test tests/blockers_people.mjs
|
|
16
|
+
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const gov = (f) => path.join(ROOT, 'modules', 'government', f);
|
|
27
|
+
const routing = require(gov('routing.js'));
|
|
28
|
+
const waiting = require(gov('waiting.js'));
|
|
29
|
+
const leads = require(gov('leads.js'));
|
|
30
|
+
const people = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'blockers-by-person.js'));
|
|
31
|
+
|
|
32
|
+
const NOW = Date.parse('2026-10-29T12:00:00Z');
|
|
33
|
+
const ARCHONS = [{ id: 1, github_login: 'lars', display_name: 'Lars' }];
|
|
34
|
+
const owner = leads.ownerOf(ARCHONS);
|
|
35
|
+
const planningLead = { id: 9, building: 'planning', builder_id: 42, github_login: 'ana', display_name: 'Ana', removed_at: null };
|
|
36
|
+
const releaseLead = { id: 10, building: 'release', builder_id: 43, github_login: 'ben', display_name: 'Ben', removed_at: null };
|
|
37
|
+
const ask = (waitsOn) => ({ question: 'Allow it?', waits_on: waitsOn, if_nobody_answers: 'It stays stuck.' });
|
|
38
|
+
const blocker = (id, over = {}) => ({
|
|
39
|
+
id: `blockers:${id}`, source: 'blockers', building: 'planning', state: 'needs', title: `Blocker ${id}`,
|
|
40
|
+
opened_at: `2026-10-2${id % 10}T00:00:00Z`, deadline: null, href: `/blockers#blocker-${id}`, ref: { type: 'blocker', id: String(id) }, ...over,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a route names the people in its seat, not only the seat', () => {
|
|
44
|
+
const seats = { leads: [planningLead], owner, holders: { charter: [{ builder_id: 7, github_login: 'cy', display_name: 'Cy' }, { builder_id: 8, github_login: 'di' }] } };
|
|
45
|
+
const byOwner = routing.routeMatter(blocker(1, { ask: ask('owner') }), '1', seats, NOW);
|
|
46
|
+
assert.deepEqual(byOwner.people, [{ id: '1', login: 'lars', name: 'Lars' }]);
|
|
47
|
+
const byLead = routing.routeMatter(blocker(2), '1', seats, NOW);
|
|
48
|
+
assert.equal(byLead.seat, 'lead:planning');
|
|
49
|
+
assert.deepEqual(byLead.people, [{ id: '42', login: 'ana', name: 'Ana' }]);
|
|
50
|
+
const unseated = routing.routeMatter(blocker(3, { building: 'treasury' }), '1', seats, NOW);
|
|
51
|
+
assert.deepEqual([unseated.seat, unseated.fell_to_owner, unseated.people.map((p) => p.name)], ['owner', true, ['Lars']]);
|
|
52
|
+
const board = routing.routeMatter(blocker(4, { ask: ask('board:charter') }), '1', seats, NOW);
|
|
53
|
+
assert.deepEqual(board.people, [{ id: '7', login: 'cy', name: 'Cy' }, { id: '8', login: 'di', name: 'di' }]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('the name map is built once per request: routeMatter takes it, and routeAll gives the same routes', () => {
|
|
57
|
+
const seats = { leads: [planningLead], owner, holders: {} };
|
|
58
|
+
// a passed map is the one used — so routeAll can hand every matter the one it built
|
|
59
|
+
const dir = new Map([['42', { id: '42', login: 'x', name: 'From the map' }]]);
|
|
60
|
+
assert.deepEqual(routing.routeMatter(blocker(1), '1', seats, NOW, dir).people, [{ id: '42', login: 'x', name: 'From the map' }]);
|
|
61
|
+
const ms = [blocker(1), blocker(2, { ask: ask('owner') }), blocker(3, { building: 'treasury' })];
|
|
62
|
+
const one = ms.map((m) => routing.routeMatter(m, '42', seats, NOW));
|
|
63
|
+
const all = ms.map((m) => ({ ...m }));
|
|
64
|
+
routing.routeAll(all, '42', seats, NOW);
|
|
65
|
+
assert.deepEqual(all.map((m) => m.route), one);
|
|
66
|
+
const src = fs.readFileSync(gov('routing.js'), 'utf8');
|
|
67
|
+
const body = src.slice(src.indexOf('function routeAll'), src.indexOf('\n}', src.indexOf('function routeAll')));
|
|
68
|
+
assert.match(body, /const dir = directoryOf\(ctx\);[\s\S]*routeMatter\(m, viewerId, ctx, now, dir\)/, 'built once, outside the loop');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('groupByPerson: each blocker under every person its seat resolves to; nobody is never dropped', () => {
|
|
72
|
+
const seats = { leads: [planningLead], owner, holders: { charter: [{ builder_id: 7, display_name: 'Cy' }, { builder_id: 42, display_name: 'Ana' }], rules: [] } };
|
|
73
|
+
const matters = [
|
|
74
|
+
blocker(1, { ask: ask('owner') }),
|
|
75
|
+
blocker(2), // ask-less → lead:planning → Ana
|
|
76
|
+
blocker(3, { building: 'treasury' }), // unseated → the owner
|
|
77
|
+
blocker(4, { ask: ask('board:charter') }), // two members
|
|
78
|
+
blocker(5, { ask: ask('board:rules') }), // nobody seated → falls to the owner
|
|
79
|
+
blocker(6, { state: 'settled', ask: ask('owner') }), // settled is not waiting
|
|
80
|
+
];
|
|
81
|
+
const g = waiting.groupByPerson(matters, seats);
|
|
82
|
+
const ids = (name) => g.people.find((x) => x.person.name === name).items.map((i) => i.id);
|
|
83
|
+
assert.deepEqual(ids('Lars'), ['1', '3', '5']);
|
|
84
|
+
assert.deepEqual(ids('Ana'), ['2', '4']);
|
|
85
|
+
assert.deepEqual(ids('Cy'), ['4']);
|
|
86
|
+
assert.deepEqual(g.people.map((p) => [p.person.name, p.count]), [['Lars', 3], ['Ana', 2], ['Cy', 1]], 'most waiting first');
|
|
87
|
+
const two = g.people.find((x) => x.person.name === 'Ana').items.find((i) => i.id === '2');
|
|
88
|
+
assert.deepEqual([two.seat, two.as, two.asked, two.fell_to_owner], ['lead:planning', 'the Planning Lead', 'lead:planning', false]);
|
|
89
|
+
const three = g.people.find((x) => x.person.name === 'Lars').items.find((i) => i.id === '3');
|
|
90
|
+
assert.deepEqual([three.seat, three.asked, three.fell_to_owner], ['owner', 'lead:treasury', true]);
|
|
91
|
+
// no owner at all: an owner-seat blocker waits on nobody, and is listed there
|
|
92
|
+
const empty = waiting.groupByPerson([blocker(1, { ask: ask('owner') })], { leads: [], owner: leads.ownerOf([]), holders: {} });
|
|
93
|
+
assert.deepEqual([empty.people.length, empty.nobody.map((i) => i.id)], [0, ['1']]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the grouping and the City agree on every blocker', () => {
|
|
97
|
+
const seats = { leads: [planningLead, releaseLead], owner, holders: { charter: [{ builder_id: 7, display_name: 'Cy' }] } };
|
|
98
|
+
const matters = [blocker(1), blocker(2, { ask: ask('owner') }), blocker(3, { ask: ask('lead:release') }), blocker(4, { ask: ask('board:charter') }), blocker(5, { building: 'people' }), blocker(6, { ask: ask('committee:release.ops') })];
|
|
99
|
+
const g = waiting.groupByPerson(matters, seats);
|
|
100
|
+
for (const m of matters) {
|
|
101
|
+
const city = routing.routeMatter(m, null, seats, NOW).people.map((p) => p.id).sort();
|
|
102
|
+
const here = g.people.filter((x) => x.items.some((i) => i.matter_id === m.id)).map((x) => x.person.id).sort();
|
|
103
|
+
assert.deepEqual(here, city, `${m.id}: the same people on both`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('seating a different Lead moves the blocker; nothing on the blocker changes', () => {
|
|
108
|
+
const m = [blocker(1)];
|
|
109
|
+
const before = waiting.groupByPerson(m, { leads: [planningLead], owner, holders: {} });
|
|
110
|
+
const after = waiting.groupByPerson(m, { leads: [{ ...planningLead, builder_id: 77, github_login: 'eve', display_name: 'Eve' }], owner, holders: {} });
|
|
111
|
+
assert.deepEqual(before.people.map((p) => p.person.name), ['Ana']);
|
|
112
|
+
assert.deepEqual(after.people.map((p) => p.person.name), ['Eve']);
|
|
113
|
+
const removed = waiting.groupByPerson(m, { leads: [{ ...planningLead, removed_at: '2026-10-28T00:00:00Z' }], owner, holders: {} });
|
|
114
|
+
assert.deepEqual(removed.people.map((p) => p.person.name), ['Lars'], 'a removed Lead hands it back to the owner');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('forViewer: the viewer\'s own group, or an empty one', () => {
|
|
118
|
+
const g = waiting.groupByPerson([blocker(1), blocker(2, { ask: ask('owner') })], { leads: [planningLead], owner, holders: {} });
|
|
119
|
+
assert.deepEqual(waiting.forViewer(g, 42).items.map((i) => i.id), ['1']);
|
|
120
|
+
assert.deepEqual(waiting.forViewer(g, '1').items.map((i) => i.id), ['2']);
|
|
121
|
+
assert.deepEqual([waiting.forViewer(g, 5).count, waiting.forViewer(g, 5).items], [0, []]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('blockersByPerson reads the contributed blockers, their asks and the seats', async () => {
|
|
125
|
+
const docket = require(gov('docket.js'));
|
|
126
|
+
const resolver = require(gov('resolver.js'));
|
|
127
|
+
const asksDb = require(gov('asks-db.js'));
|
|
128
|
+
const db = require(gov('db.js'));
|
|
129
|
+
const real = { perms: resolver.resolveBuilderPermissions, asks: asksDb.asksFor, leads: db.listLeads, archons: db.listActiveArchons };
|
|
130
|
+
resolver.resolveBuilderPermissions = async () => ['board.vote.cast'];
|
|
131
|
+
asksDb.asksFor = async (ids) => new Map(ids.filter((id) => id === 'blockers:7').map((id) => [id, ask('owner')]));
|
|
132
|
+
db.listLeads = async () => [planningLead];
|
|
133
|
+
db.listActiveArchons = async () => ARCHONS;
|
|
134
|
+
const rows = [{ id: 7, title: 'Token needs the workflow scope', created_at: '2026-10-29T10:00:00Z' }, { id: 8, title: 'Pick a host', created_at: '2026-10-28T10:00:00Z' }];
|
|
135
|
+
const contributions = { sources: new Map([['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true }, read: async () => rows.map((b) => ({ id: b.id, title: b.title, state: 'needs', opened_at: b.created_at, href: `/blockers#blocker-${b.id}`, ref: { type: 'blocker', id: b.id } })) }]]) };
|
|
136
|
+
try {
|
|
137
|
+
const r = await docket.blockersByPerson({ id: 42, rank: 'metic' }, { contributions, now: NOW });
|
|
138
|
+
assert.equal(r.available, true);
|
|
139
|
+
assert.deepEqual(r.people.map((p) => [p.person.name, p.items.map((i) => i.id)]), [['Ana', ['8']], ['Lars', ['7']]]);
|
|
140
|
+
assert.deepEqual(r.yours.items.map((i) => i.id), ['8'], 'Ana sees her own');
|
|
141
|
+
const off = await docket.blockersByPerson({ id: 42 }, { contributions: { sources: new Map() }, now: NOW });
|
|
142
|
+
assert.deepEqual([off.available, off.reason, off.people], [false, 'off', []], 'no source is said, not shown as empty');
|
|
143
|
+
db.listLeads = async () => { throw new Error('db down'); };
|
|
144
|
+
const down = await docket.blockersByPerson({ id: 42 }, { contributions, now: NOW });
|
|
145
|
+
assert.deepEqual([down.available, down.reason], [false, 'unavailable'], 'a failed seats read groups nothing rather than guessing');
|
|
146
|
+
} finally {
|
|
147
|
+
resolver.resolveBuilderPermissions = real.perms; asksDb.asksFor = real.asks; db.listLeads = real.leads; db.listActiveArchons = real.archons;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
152
|
+
const fmtDate = (iso) => iso.slice(0, 10);
|
|
153
|
+
|
|
154
|
+
test('the Blockers page: the viewer first as "You", the seat in words, "Nobody yet", escaped', () => {
|
|
155
|
+
const g = waiting.groupByPerson([
|
|
156
|
+
blocker(1, { ask: ask('owner'), title: '<b>Token</b>' }),
|
|
157
|
+
blocker(2), blocker(3, { building: 'treasury' }),
|
|
158
|
+
blocker(4, { ask: ask('board:rules') }),
|
|
159
|
+
], { leads: [planningLead], owner: leads.ownerOf([]), holders: { rules: [] } });
|
|
160
|
+
const html = people.peopleHtml({ available: true, ...g }, { esc, fmtDate, viewerId: 42, blockerHref: (id) => `#/blocker/${id}` });
|
|
161
|
+
assert.ok(html.indexOf('>You<') < html.indexOf('>Nobody yet<'), 'the viewer first');
|
|
162
|
+
assert.match(html, /as the Planning Lead · filed 2026-10-22/);
|
|
163
|
+
assert.match(html, /href="#\/blocker\/2"/);
|
|
164
|
+
assert.match(html, /<b>Token<\/b>/, 'a title is escaped');
|
|
165
|
+
assert.ok(!html.includes('<b>Token'), 'never raw');
|
|
166
|
+
assert.equal(people.peopleHtml({ available: false, people: [], nobody: [] }, { esc, fmtDate }), '', 'unavailable draws nothing');
|
|
167
|
+
assert.equal(people.peopleHtml({ available: true, people: [], nobody: [] }, { esc, fmtDate }), '', 'nothing open draws nothing');
|
|
168
|
+
assert.equal(people.asWords({ as: 'the owner', asked: 'lead:treasury', fell_to_owner: true }), 'as the owner, since Treasury has no Lead');
|
|
169
|
+
assert.equal(people.asWords({ as: 'the owner', asked: 'owner', fell_to_owner: false }), 'as the owner');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('Collab\'s block draws only the viewer\'s own, and nothing when nothing waits', () => {
|
|
173
|
+
const mine = { available: true, count: 1, items: [{ id: '9', title: 'Pick a host', as: 'the Planning Lead', asked: 'lead:planning', opened_at: '2026-10-28T00:00:00Z' }] };
|
|
174
|
+
const html = people.mineHtml(mine, { esc, fmtDate, blockerHref: (id) => `#/blocker/${id}` });
|
|
175
|
+
assert.match(html, /Pick a host/);
|
|
176
|
+
assert.match(html, /as the Planning Lead/);
|
|
177
|
+
assert.equal(people.mineHtml({ available: true, count: 0, items: [] }, { esc, fmtDate }), '');
|
|
178
|
+
assert.equal(people.mineHtml(null, { esc, fmtDate }), '');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('the pages carry the sections and load the script', () => {
|
|
182
|
+
const pub = (f) => fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', f), 'utf8');
|
|
183
|
+
assert.match(pub('blockers.html'), /id="blockers-people-scroll"[^>]*hidden/);
|
|
184
|
+
assert.match(pub('blockers.html'), /<script src="\/builders\/blockers-by-person.js"><\/script>/);
|
|
185
|
+
assert.match(pub('collab.html'), /id="cb-blockers-scroll"[^>]*hidden/);
|
|
186
|
+
assert.match(pub('collab.html'), /<script src="\/builders\/blockers-by-person.js"><\/script>/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('the City card says who: "waits on Lars, the owner"', () => {
|
|
190
|
+
const deck = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'street-deck.js'));
|
|
191
|
+
const who = deck.whoWords;
|
|
192
|
+
assert.equal(who({ label: 'the owner', people: [{ name: 'Lars' }] }), 'Lars, the owner');
|
|
193
|
+
assert.equal(who({ label: 'the board', people: [{ name: 'Cy' }, { login: 'di' }] }), 'Cy and di, the board');
|
|
194
|
+
assert.equal(who({ label: 'the board', people: [{ name: 'A' }, { name: 'B' }, { name: 'C' }] }), 'A and 2 others, the board');
|
|
195
|
+
assert.equal(who({ label: 'the owner', people: [] }), 'the owner', 'nobody named: the seat alone');
|
|
196
|
+
const foot = deck.footOf({ waits_on: { real: true, label: 'the Planning Lead', people: [{ name: 'Ana' }] } }, '');
|
|
197
|
+
assert.equal(foot.rest, 'It waits on Ana, the Planning Lead.');
|
|
198
|
+
});
|