@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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// tests/rank_change_broadcast_port.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004962 — the rank-change Discord announcement posts IN-PROCESS through
|
|
4
|
+
// the discord module's `discord.post` port, never by spawning
|
|
5
|
+
// `node scripts/discord/post.js` (50-100 MB + ~300 ms CPU per rank change, with
|
|
6
|
+
// nothing bounding a burst). Drives the real PATCH /builders/:id/rank route with
|
|
7
|
+
// auth + db stubbed, and pins the three cases the task names:
|
|
8
|
+
//
|
|
9
|
+
// 1. posted — the port is called once with the #ship-news line, and no
|
|
10
|
+
// child process is started.
|
|
11
|
+
// 2. module off — no `discord.post` provider: the rank change still 200s and
|
|
12
|
+
// nothing is posted.
|
|
13
|
+
// 3. post throws — a provider that throws (sync) or rejects (async) never fails
|
|
14
|
+
// the rank change.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/rank_change_broadcast_port.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import http from 'node:http';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const express = require('express');
|
|
24
|
+
|
|
25
|
+
delete process.env.CHAT_DRY_RUN; // the dry-run guard would hide the port call
|
|
26
|
+
|
|
27
|
+
// Record any child process the route starts. Patched BEFORE the router loads, so
|
|
28
|
+
// a destructured `execFile`/`spawn` picks up the recorder.
|
|
29
|
+
const cp = require('node:child_process');
|
|
30
|
+
const spawned = [];
|
|
31
|
+
for (const fn of ['execFile', 'spawn', 'exec', 'fork']) {
|
|
32
|
+
cp[fn] = (...args) => { spawned.push([fn, args[0], args[1]]); throw new Error(`test: ${fn} must not be called`); };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const authPath = require.resolve('../src/bongos/auth.js');
|
|
36
|
+
require.cache[authPath] = {
|
|
37
|
+
id: authPath, filename: authPath, loaded: true,
|
|
38
|
+
exports: {
|
|
39
|
+
requireBuilder: (req, _res, next) => {
|
|
40
|
+
req.builder = { id: '1', rank: 'archon', github_login: 'archon1', display_name: 'Archon One' };
|
|
41
|
+
next();
|
|
42
|
+
},
|
|
43
|
+
requireRank: () => (_req, _res, next) => next(),
|
|
44
|
+
requirePermission: () => (_req, _res, next) => next(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const fakeDb = {
|
|
49
|
+
RANKS: ['xenos', 'thetes', 'metic', 'archon'],
|
|
50
|
+
setBuilderRank: async ({ builderId, newRank }) => ({
|
|
51
|
+
builder: { id: builderId, rank: newRank, display_name: 'Ada', github_login: 'ada' },
|
|
52
|
+
previous_rank: 'thetes',
|
|
53
|
+
direction: 'promotion',
|
|
54
|
+
credit_delta: 5,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
const dbPath = require.resolve('../src/bongos/db.js');
|
|
58
|
+
require.cache[dbPath] = { id: dbPath, filename: dbPath, loaded: true, exports: fakeDb };
|
|
59
|
+
|
|
60
|
+
const seams = require('../src/module-seams.js');
|
|
61
|
+
const buildBuildersRouter = require('../src/bongos/routes/builders.js');
|
|
62
|
+
const { fireDiscordBroadcast } = buildBuildersRouter;
|
|
63
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
64
|
+
|
|
65
|
+
const app = express();
|
|
66
|
+
app.use(express.json());
|
|
67
|
+
app.use(attachFail);
|
|
68
|
+
app.use('/api/gds', buildBuildersRouter());
|
|
69
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
|
|
70
|
+
const { port } = server.address();
|
|
71
|
+
|
|
72
|
+
function patchRank(id, rank) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const data = JSON.stringify({ rank });
|
|
75
|
+
const req = http.request({
|
|
76
|
+
host: '127.0.0.1', port, method: 'PATCH', path: `/api/gds/builders/${id}/rank`,
|
|
77
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) },
|
|
78
|
+
}, (res) => {
|
|
79
|
+
const chunks = [];
|
|
80
|
+
res.on('data', (c) => chunks.push(c));
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
let json = null;
|
|
83
|
+
try { json = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch (_) {}
|
|
84
|
+
resolve({ status: res.statusCode, json });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
req.on('error', reject);
|
|
88
|
+
req.write(data);
|
|
89
|
+
req.end();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const settle = () => new Promise((r) => setTimeout(r, 20));
|
|
94
|
+
let passed = 0;
|
|
95
|
+
let failed = 0;
|
|
96
|
+
async function t(name, fn) {
|
|
97
|
+
seams._reset();
|
|
98
|
+
spawned.length = 0;
|
|
99
|
+
try { await fn(); passed++; console.log(`ok ${name}`); } catch (e) { failed++; console.log(`FAIL ${name}\n ${e && e.stack}`); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await t('posted: the route calls discord.post once with the #ship-news line, no child process', async () => {
|
|
103
|
+
const posts = [];
|
|
104
|
+
seams.registerProvider('discord.post', async (msg) => { posts.push(msg); return { posted: true, channel: msg.channel }; });
|
|
105
|
+
const r = await patchRank(7, 'metic');
|
|
106
|
+
assert.equal(r.status, 200);
|
|
107
|
+
await settle();
|
|
108
|
+
assert.equal(posts.length, 1);
|
|
109
|
+
assert.equal(posts[0].channel, 'ship_news');
|
|
110
|
+
assert.equal(posts[0].text, r.json.broadcast);
|
|
111
|
+
assert.match(posts[0].text, /Ada has been raised to the rank of Metic/);
|
|
112
|
+
assert.deepEqual(spawned, []);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await t('module off: no discord.post provider — the rank change succeeds and nothing is posted', async () => {
|
|
116
|
+
const r = await patchRank(7, 'metic');
|
|
117
|
+
assert.equal(r.status, 200);
|
|
118
|
+
assert.equal(r.json.builder.rank, 'metic');
|
|
119
|
+
await settle();
|
|
120
|
+
assert.deepEqual(spawned, []);
|
|
121
|
+
assert.deepEqual(await fireDiscordBroadcast('hello'), { posted: false, reason: 'no_discord' });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await t('post throws synchronously: the rank change still succeeds', async () => {
|
|
125
|
+
let calls = 0;
|
|
126
|
+
seams.registerProvider('discord.post', () => { calls++; throw new Error('webhook exploded'); });
|
|
127
|
+
const r = await patchRank(7, 'metic');
|
|
128
|
+
assert.equal(r.status, 200);
|
|
129
|
+
assert.equal(r.json.builder.rank, 'metic');
|
|
130
|
+
await settle();
|
|
131
|
+
assert.equal(calls, 1);
|
|
132
|
+
assert.deepEqual(await fireDiscordBroadcast('hello'), { posted: false, reason: 'post_failed' });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await t('post rejects: the rank change still succeeds and no rejection escapes', async () => {
|
|
136
|
+
const unhandled = [];
|
|
137
|
+
const onUnhandled = (e) => unhandled.push(e);
|
|
138
|
+
process.on('unhandledRejection', onUnhandled);
|
|
139
|
+
let calls = 0;
|
|
140
|
+
seams.registerProvider('discord.post', async () => { calls++; throw new Error('discord 500'); });
|
|
141
|
+
const r = await patchRank(7, 'metic');
|
|
142
|
+
assert.equal(r.status, 200);
|
|
143
|
+
await settle();
|
|
144
|
+
process.off('unhandledRejection', onUnhandled);
|
|
145
|
+
assert.equal(calls, 1);
|
|
146
|
+
assert.deepEqual(unhandled, []);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.close();
|
|
150
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
151
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// tests/seed_tasks.mjs — scripts/bongos/seed-tasks.js, the one data-driven task seeder (task 1004979).
|
|
2
|
+
//
|
|
3
|
+
// The load-bearing case is EQUIVALENCE: the three per-version seed scripts it replaced
|
|
4
|
+
// (seed-bongos-v1-idea-routing-tasks.js, seed-bv1-profiles-tasks.js,
|
|
5
|
+
// seed-bv2-ideator-surface-tasks.js) each became a spec under tests/fixtures/seed-tasks/, and
|
|
6
|
+
// seed-tasks.js must send exactly the writes the old script sent — every POST /tasks body,
|
|
7
|
+
// dependency edge, promotion and follow-up PATCH, in order. The proof ran LIVE first: the first
|
|
8
|
+
// version of this test (task 1004979's first commit) ran each old script and seed-tasks.js
|
|
9
|
+
// against the same fake API and compared both the writes and the resulting task rows.
|
|
10
|
+
// The old scripts are gone now, so OLD_WRITE_HASHES pins what that run recorded: the sha256 of
|
|
11
|
+
// the canonical write log each old script produced. Any change to what seed-tasks.js sends for
|
|
12
|
+
// these specs moves the hash.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/seed_tasks.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { startFakeApi, fetchApi, canonical } from './fixtures/seed-tasks/fake-api.mjs';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const SEEDER = path.join(ROOT, 'scripts/bongos/seed-tasks.js');
|
|
29
|
+
const EXAMPLE = path.join(ROOT, 'scripts/bongos/seed-tasks.example.json');
|
|
30
|
+
const FIX = path.join(ROOT, 'tests/fixtures/seed-tasks');
|
|
31
|
+
const { validateSpec, seedTasks } = require(SEEDER);
|
|
32
|
+
|
|
33
|
+
// spec fixture -> the old script it replaced, and the sha256 of that script's write log.
|
|
34
|
+
const HISTORICAL = [
|
|
35
|
+
{ spec: 'bongos-v1-idea-routing.json', old: 'seed-bongos-v1-idea-routing-tasks.js', hash: 'f5bc176890077a62754812cf69666bd49ba0da441dafa84702e1883156c302f8' },
|
|
36
|
+
{ spec: 'bv1-profiles.json', old: 'seed-bv1-profiles-tasks.js', hash: 'cf81ee14131a20fe5a07c05b7c5fe3362bc940d5c5592031ad911b709db129a7' },
|
|
37
|
+
{ spec: 'bv2-ideator-surface.json', old: 'seed-bv2-ideator-surface-tasks.js', hash: '5f8ed981593dfa4738b6dd97208ef0b0aec01a4d7e3eca8abc76caa5d8901f5c' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
let passed = 0, failed = 0;
|
|
41
|
+
async function test(name, fn) {
|
|
42
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
43
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.stack || err.message}`); }
|
|
44
|
+
}
|
|
45
|
+
const readSpec = (f) => JSON.parse(fs.readFileSync(path.join(FIX, f), 'utf8'));
|
|
46
|
+
const sha = (writes) => crypto.createHash('sha256').update(canonical(writes)).digest('hex');
|
|
47
|
+
const quiet = () => {};
|
|
48
|
+
const noExit = (code) => { throw new Error(`seeder exited ${code}`); };
|
|
49
|
+
|
|
50
|
+
function run(cmd, args, env) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const p = spawn(cmd, args, { cwd: ROOT, env: { ...process.env, ...env } });
|
|
53
|
+
let out = '';
|
|
54
|
+
p.stdout.on('data', (d) => { out += d; });
|
|
55
|
+
p.stderr.on('data', (d) => { out += d; });
|
|
56
|
+
p.on('close', (code) => resolve({ code, out }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runNew(spec) {
|
|
61
|
+
const api = await startFakeApi();
|
|
62
|
+
try {
|
|
63
|
+
await seedTasks(spec, { api: fetchApi(api.base), log: quiet, exit: noExit });
|
|
64
|
+
return { writes: api.writes(), tasks: api.tasks };
|
|
65
|
+
} finally { await api.close(); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- equivalence with the three scripts it replaced -------------------------------------
|
|
69
|
+
|
|
70
|
+
for (const h of HISTORICAL) {
|
|
71
|
+
await test(`${h.spec} sends exactly the writes ${h.old} sent`, async () => {
|
|
72
|
+
const spec = readSpec(h.spec);
|
|
73
|
+
assert.deepEqual(validateSpec(spec), [], 'the historical spec is valid');
|
|
74
|
+
const next = await runNew(spec);
|
|
75
|
+
assert.equal(next.tasks.length, spec.tasks.length, 'every task was filed');
|
|
76
|
+
assert.equal(sha(next.writes), h.hash, 'same writes, same order, as the old script sent');
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- the example spec and the dry run ----------------------------------------------------
|
|
81
|
+
|
|
82
|
+
await test('the example spec is valid', async () => {
|
|
83
|
+
assert.deepEqual(validateSpec(JSON.parse(fs.readFileSync(EXAMPLE, 'utf8'))), []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('--dry-run prints the plan and files nothing', async () => {
|
|
87
|
+
const api = await startFakeApi();
|
|
88
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-dry-'));
|
|
89
|
+
try {
|
|
90
|
+
const r = await run(process.execPath, [SEEDER, EXAMPLE, '--dry-run'], { GDS_API_BASE: api.base, BONGOS_API_BASE: api.base, HOME: home, USERPROFILE: home });
|
|
91
|
+
assert.equal(r.code, 0, r.out);
|
|
92
|
+
const spec = JSON.parse(fs.readFileSync(EXAMPLE, 'utf8'));
|
|
93
|
+
for (const t of spec.tasks) assert.ok(r.out.includes(`${t.ref} `) && r.out.includes(t.title), `plan lists ${t.ref}`);
|
|
94
|
+
assert.match(r.out, /Nothing was filed\./);
|
|
95
|
+
assert.equal(api.requests.length, 0, 'not one request reached the API');
|
|
96
|
+
} finally { await api.close(); fs.rmSync(home, { recursive: true, force: true }); }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await test('an invalid spec is refused before any write', async () => {
|
|
100
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-bad-'));
|
|
101
|
+
const file = path.join(dir, 'bad.json');
|
|
102
|
+
fs.writeFileSync(file, JSON.stringify({ version_id: 'V', goal_id: 1, source_ref_prefix: 'p', tasks: [] }));
|
|
103
|
+
const r = await run(process.execPath, [SEEDER, file, '--dry-run'], {});
|
|
104
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
assert.equal(r.code, 1);
|
|
106
|
+
assert.match(r.out, /tasks must be a non-empty array/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---- validation ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
const base = () => ({
|
|
112
|
+
version_id: 'V', goal_id: 7, source_ref_prefix: 'p',
|
|
113
|
+
tasks: [
|
|
114
|
+
{ ref: 'R1', title: 't1', description: 'd', kind: 'feature', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'] },
|
|
115
|
+
{ ref: 'R2', title: 't2', description: 'd', kind: 'verify', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'], deps: ['R1'] },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('validation names each broken thing', async () => {
|
|
120
|
+
const s = base();
|
|
121
|
+
s.tasks[1].ref = 'R1';
|
|
122
|
+
assert.ok(validateSpec(s).some((e) => /duplicate ref/.test(e)));
|
|
123
|
+
const u = base(); u.tasks[1].deps = ['R9'];
|
|
124
|
+
assert.ok(validateSpec(u).some((e) => /unknown ref R9/.test(e)));
|
|
125
|
+
const self = base(); self.tasks[1].deps = ['R2'];
|
|
126
|
+
assert.ok(validateSpec(self).some((e) => /depends on itself/.test(e)));
|
|
127
|
+
const p = base(); p.promote = ['R7'];
|
|
128
|
+
assert.ok(validateSpec(p).some((e) => /promote: unknown ref R7/.test(e)));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test('a positional "Cn" criterion ref is refused unless the spec opts in (learning 1000188)', async () => {
|
|
132
|
+
const s = base(); s.tasks[0].criterion_ids = ['C1'];
|
|
133
|
+
assert.ok(validateSpec(s).some((e) => /positional criterion ref/.test(e)));
|
|
134
|
+
s.allow_positional_criteria = true;
|
|
135
|
+
assert.deepEqual(validateSpec(s), []);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---- the seeding contract: baseline, replay, verify ---------------------------------------
|
|
139
|
+
|
|
140
|
+
await test('a re-run replays every task and still verifies (only created links are tallied)', async () => {
|
|
141
|
+
const api = await startFakeApi({ goalCriteria: { 7: { 'slug-a': 4 } } });
|
|
142
|
+
try {
|
|
143
|
+
const first = await seedTasks(base(), { api: fetchApi(api.base), log: quiet, exit: noExit });
|
|
144
|
+
assert.equal(first.created, 2);
|
|
145
|
+
const again = await seedTasks(base(), { api: fetchApi(api.base), log: quiet, exit: noExit });
|
|
146
|
+
assert.equal(again.replayed, 2);
|
|
147
|
+
assert.equal(api.tasks.length, 2, 'the replay filed nothing new');
|
|
148
|
+
} finally { await api.close(); }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await test('a criterion link that did not land exits 1', async () => {
|
|
152
|
+
const api = await startFakeApi();
|
|
153
|
+
const lying = fetchApi(api.base);
|
|
154
|
+
// A server that files the task but drops its criterion link: the GET shows zero.
|
|
155
|
+
const transport = async (m, p, b) => (m === 'GET' ? { status: 200, ok: true, json: { criteria: [] } } : lying(m, p, b));
|
|
156
|
+
let code = null; const lines = [];
|
|
157
|
+
try {
|
|
158
|
+
await seedTasks(base(), { api: transport, log: (l) => lines.push(l), exit: (c) => { code = c; } });
|
|
159
|
+
} finally { await api.close(); }
|
|
160
|
+
assert.equal(code, 1);
|
|
161
|
+
assert.ok(lines.some((l) => /VERIFY FAILED/.test(l)));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await test('a follow-up link waits for the task it names', async () => {
|
|
165
|
+
const s = base();
|
|
166
|
+
s.links = [{ method: 'PATCH', path: '/inbox/55', body: { status: 'promoted', promoted_to_task_id: { $task: 'R2' } } }];
|
|
167
|
+
const api = await startFakeApi();
|
|
168
|
+
try {
|
|
169
|
+
await seedTasks(s, { api: fetchApi(api.base), log: quiet, exit: noExit });
|
|
170
|
+
const patch = api.writes().find((w) => w.method === 'PATCH');
|
|
171
|
+
assert.deepEqual(patch.body, { status: 'promoted', promoted_to_task_id: Number(api.tasks[1].id) });
|
|
172
|
+
} finally { await api.close(); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
176
|
+
if (failed) process.exitCode = 1;
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
import { strict as assert } from 'node:assert';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
18
|
|
|
20
19
|
const require = createRequire(import.meta.url);
|
|
21
20
|
const {
|
|
@@ -225,11 +224,8 @@ test('a fully re-run seed (every create a replay) plans nothing and verifies OK'
|
|
|
225
224
|
assert.deepEqual(diffCriterionLinks(tallyCreated(responses), [{ criterion_id: 'ours', linked_tasks: 2 }], { baseline: { ours: 2 } }), []);
|
|
226
225
|
});
|
|
227
226
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
assert.match(src, /baselineSeedCriteria\(/, 'it takes a baseline');
|
|
231
|
-
assert.match(src, /if \(!r\.json\.idempotent\) criterionIdListsPerTask\.push\(t\.crit\);/, 'and tallies only created tasks');
|
|
232
|
-
});
|
|
227
|
+
// That the real seeder baselines and skips replays when it tallies is proven by behaviour, not
|
|
228
|
+
// by its source text: tests/seed_tasks.mjs re-runs a spec against a fake API (task 1004979).
|
|
233
229
|
|
|
234
230
|
await testAsync('baselineSeedCriteria snapshots the goal before any write', async () => {
|
|
235
231
|
const logs = [];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// tests/site_classified_once.mjs — a request's site is worked out ONCE (task 1004963).
|
|
2
|
+
//
|
|
3
|
+
// THE COST THIS PINS. About fifteen global handlers in src/bongos/serve-internal.js
|
|
4
|
+
// each asked "is this the builders host?" / "is this a module-surface host?", and
|
|
5
|
+
// every answer re-parsed the Host header (the builders question also re-parsed the
|
|
6
|
+
// configured buildersOrigin URL). mountInternalSurfaces now classifies the request
|
|
7
|
+
// at the top of its chain into req.bongosSite and every predicate reads that field.
|
|
8
|
+
//
|
|
9
|
+
// HOW IT IS OBSERVED. A front middleware, mounted BEFORE mountInternalSurfaces,
|
|
10
|
+
// replaces the Host header value with a String object whose toLowerCase() counts
|
|
11
|
+
// its calls — every parse of the Host starts with that call. With the fix, the
|
|
12
|
+
// whole chain parses it exactly once (the classifier) whatever handler answers;
|
|
13
|
+
// without it, a hall request parses it once per host-routed handler it passes.
|
|
14
|
+
// (Counting plain property READS would be the wrong measure: siteOf re-reads the
|
|
15
|
+
// raw header on purpose, to check the cached answer is for the same Host.)
|
|
16
|
+
//
|
|
17
|
+
// WHAT ELSE IT PINS. The routing answer per host is unchanged — the hall, the
|
|
18
|
+
// canonical /builders redirects, the apex /docs, the API on both hosts — and the
|
|
19
|
+
// cache can never answer for a Host it was not computed from.
|
|
20
|
+
//
|
|
21
|
+
// node:http, not fetch: fetch silently drops a Host override (see
|
|
22
|
+
// tests/module_asset_subdomain.mjs for the trap).
|
|
23
|
+
//
|
|
24
|
+
// Run: node --test tests/site_classified_once.mjs
|
|
25
|
+
|
|
26
|
+
import { test } from 'node:test';
|
|
27
|
+
import assert from 'node:assert/strict';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import http from 'node:http';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
const APEX = 'example.com';
|
|
34
|
+
const HALL = 'builders.example.com';
|
|
35
|
+
|
|
36
|
+
// The count of Host parses for the request currently in flight. One request at a
|
|
37
|
+
// time (the helpers below await each), so a single counter is unambiguous.
|
|
38
|
+
let hostReads = 0;
|
|
39
|
+
|
|
40
|
+
// The same text as the real Host, but every toLowerCase() is counted. One instance
|
|
41
|
+
// per request, so siteOf's identity check against the cached raw value still holds.
|
|
42
|
+
class CountedHost extends String {
|
|
43
|
+
toLowerCase() { hostReads += 1; return String(this).toLowerCase(); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function withServer(fn) {
|
|
47
|
+
const express = require('express');
|
|
48
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
49
|
+
const app = express();
|
|
50
|
+
app.use((req, _res, next) => {
|
|
51
|
+
hostReads = 0;
|
|
52
|
+
req.headers.host = new CountedHost(req.headers.host);
|
|
53
|
+
next();
|
|
54
|
+
});
|
|
55
|
+
mountInternalSurfaces(app);
|
|
56
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
57
|
+
try { await fn(server.address().port); } finally { await new Promise((r) => server.close(r)); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function get(port, path, host) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const req = http.request({ host: '127.0.0.1', port, path, headers: { Host: host } }, (res) => {
|
|
63
|
+
let body = '';
|
|
64
|
+
res.setEncoding('utf8');
|
|
65
|
+
res.on('data', (c) => { body += c; });
|
|
66
|
+
res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || null, body, reads: hostReads }));
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Requests that walk deep into the host-routed chain on each kind of host. The
|
|
74
|
+
// hall index on the builders host passes EVERY host-routed handler down to the
|
|
75
|
+
// final hall static; an unknown apex path falls through the whole chain to Express'
|
|
76
|
+
// default 404. Before the fix these read the Host header a dozen-plus times.
|
|
77
|
+
const WALKS = [
|
|
78
|
+
['hall index on the builders host', '/', HALL],
|
|
79
|
+
['hall asset on the builders host', '/builders/style.css', HALL],
|
|
80
|
+
['unknown path on the builders host', '/no-such-page-xyz', HALL],
|
|
81
|
+
['unknown path on the apex', '/no-such-page-xyz', APEX],
|
|
82
|
+
['apex /docs', '/docs', APEX],
|
|
83
|
+
['apex /builders redirect', '/builders/watch', APEX],
|
|
84
|
+
['API healthz on the builders host', '/api/bongos/healthz', HALL],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
test('the Host header is parsed once per request, whichever handler answers', async () => {
|
|
88
|
+
await withServer(async (port) => {
|
|
89
|
+
for (const [label, path, host] of WALKS) {
|
|
90
|
+
const r = await get(port, path, host);
|
|
91
|
+
assert.equal(r.reads, 1, `${label}: Host read ${r.reads} times — the site must be classified once and read from req.bongosSite`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('routing per host is unchanged', async () => {
|
|
97
|
+
await withServer(async (port) => {
|
|
98
|
+
// Apex /builders/* → the builders subdomain (301, path + query carried).
|
|
99
|
+
let r = await get(port, '/builders/watch?x=1', APEX);
|
|
100
|
+
assert.equal(r.status, 301);
|
|
101
|
+
assert.equal(r.location, `https://${HALL}/watch?x=1`);
|
|
102
|
+
// On the builders host the /builders prefix is redundant → clean path.
|
|
103
|
+
r = await get(port, '/builders/watch', HALL);
|
|
104
|
+
assert.equal(r.status, 301);
|
|
105
|
+
assert.equal(r.location, '/watch');
|
|
106
|
+
// The builders host serves the hall at its root.
|
|
107
|
+
r = await get(port, '/', HALL);
|
|
108
|
+
assert.equal(r.status, 200);
|
|
109
|
+
assert.match(r.body, /<html/i);
|
|
110
|
+
// Retired directory → /roster, prefix kept on the apex and dropped on the hall.
|
|
111
|
+
r = await get(port, '/builders/people', HALL);
|
|
112
|
+
assert.equal(r.status, 301, 'the canonical redirect answers first on the hall host');
|
|
113
|
+
assert.equal(r.location, '/people');
|
|
114
|
+
// /docs: the apex serves the API reference.
|
|
115
|
+
r = await get(port, '/docs', APEX);
|
|
116
|
+
assert.equal(r.status, 200);
|
|
117
|
+
// The API is host-agnostic.
|
|
118
|
+
for (const host of [APEX, HALL]) {
|
|
119
|
+
r = await get(port, '/api/bongos/healthz', host);
|
|
120
|
+
assert.equal(r.status, 200, `API healthz on ${host}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('siteOf caches on the request and never answers for a different Host', () => {
|
|
126
|
+
const { siteOf, isBuildersHost, isStatusHost } = require('../src/bongos/serve-internal.js');
|
|
127
|
+
const req = { headers: { host: 'Builders.Example.com:8443' } };
|
|
128
|
+
const first = siteOf(req);
|
|
129
|
+
assert.equal(first.host, 'builders.example.com');
|
|
130
|
+
assert.equal(first.builders, true);
|
|
131
|
+
assert.equal(req.bongosSite, first, 'the classification lives on the request');
|
|
132
|
+
assert.equal(siteOf(req), first, 'a second read reuses it');
|
|
133
|
+
// A changed Host is re-classified, not served from the stale entry.
|
|
134
|
+
req.headers.host = 'status.example.com';
|
|
135
|
+
assert.equal(isBuildersHost(req), false);
|
|
136
|
+
assert.equal(isStatusHost(req), true);
|
|
137
|
+
assert.notEqual(req.bongosSite, first);
|
|
138
|
+
// A missing Host is the apex, as before.
|
|
139
|
+
const bare = { headers: {} };
|
|
140
|
+
assert.equal(isBuildersHost(bare), false);
|
|
141
|
+
assert.equal(isStatusHost(bare), false);
|
|
142
|
+
});
|
|
@@ -112,6 +112,29 @@ test('countTraining counts both halves of training in one round trip', async ()
|
|
|
112
112
|
assert.equal(q.calls.length, 1);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
// task 1004923: GET /me's specialities read is ONE query for all of them.
|
|
116
|
+
test('describeTrainingFor reads owner + both counts for every id in one round trip', async () => {
|
|
117
|
+
const q = fake([{ rows: [
|
|
118
|
+
{ id: '7', owner_kind: 'builder', owner_builder_id: '1', documents: '3', learnings: '2' },
|
|
119
|
+
{ id: '8', owner_kind: 'project', owner_builder_id: null, documents: '0', learnings: '5' },
|
|
120
|
+
], rowCount: 2 }]);
|
|
121
|
+
const out = await db.describeTrainingFor(['7', 8, '7'], q);
|
|
122
|
+
assert.equal(q.calls.length, 1, 'one query, however many specialities');
|
|
123
|
+
assert.match(q.calls[0].sql, /c\.id = ANY\(\$1::bigint\[\]\)/);
|
|
124
|
+
assert.match(q.calls[0].sql, /specialities_documents d WHERE d\.speciality_id = c\.id/);
|
|
125
|
+
assert.match(q.calls[0].sql, /specialities_learnings l WHERE l\.speciality_id = c\.id/);
|
|
126
|
+
assert.deepEqual(q.calls[0].params, [['7', '8']], 'ids are de-duplicated before they are sent');
|
|
127
|
+
assert.deepEqual(out.get('7'), { owner_kind: 'builder', owner_builder_id: '1', documentCount: 3, learningCount: 2 });
|
|
128
|
+
assert.deepEqual(out.get('8'), { owner_kind: 'project', owner_builder_id: null, documentCount: 0, learningCount: 5 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('describeTrainingFor with no ids asks the database nothing', async () => {
|
|
132
|
+
const q = fake();
|
|
133
|
+
const out = await db.describeTrainingFor([], q);
|
|
134
|
+
assert.equal(out.size, 0);
|
|
135
|
+
assert.equal(q.calls.length, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
115
138
|
test('taskDiscipline reads the CORE tasks table by id', async () => {
|
|
116
139
|
const q = fake([{ rows: [{ discipline: 'engineer' }], rowCount: 1 }]);
|
|
117
140
|
assert.equal(await db.taskDiscipline('1234', q), 'engineer');
|
|
@@ -78,8 +78,11 @@ await test('a malformed name in the stored set is never relayed', () => {
|
|
|
78
78
|
// ---- the whole path: row → real port → real Conductor -----------------------
|
|
79
79
|
const store = { adoptions: [] };
|
|
80
80
|
db.listAdoptions = async () => store.adoptions.map((a) => ({ ...a }));
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// The port's ONE batched read (task 1004923); the per-row pair it replaced must
|
|
82
|
+
// never be reached again, so those throw rather than answer.
|
|
83
|
+
db.describeTrainingFor = async (ids) => new Map(ids.map((id) => [String(id), { owner_kind: 'builder', owner_builder_id: 1, documentCount: 0, learningCount: 0 }]));
|
|
84
|
+
db.countTraining = async () => { throw new Error('per-row countTraining on the GET /me path'); };
|
|
85
|
+
db.getSpeciality = async () => { throw new Error('per-row getSpeciality on the GET /me path'); };
|
|
83
86
|
skills.installedSkillsCached = () => new Map([['recall', {}], ['status', {}]]);
|
|
84
87
|
require('../modules/specialities/routes/specialities.js')();
|
|
85
88
|
const port = api.resolveOptional('specialities');
|
|
@@ -147,4 +150,47 @@ await test('no permission or materialisation surface reads enabled_skills', () =
|
|
|
147
150
|
}
|
|
148
151
|
});
|
|
149
152
|
|
|
153
|
+
// ---- task 1004923: the GET /me read is batched, not one pair per adoption ----
|
|
154
|
+
await test('three active adoptions cost ONE training read, and each keeps its own owner and counts', async () => {
|
|
155
|
+
const saved = db.describeTrainingFor;
|
|
156
|
+
const calls = [];
|
|
157
|
+
db.describeTrainingFor = async (ids) => {
|
|
158
|
+
calls.push(ids.map(String));
|
|
159
|
+
return new Map([
|
|
160
|
+
['9', { owner_kind: 'builder', owner_builder_id: 1, documentCount: 4, learningCount: 0 }],
|
|
161
|
+
['10', { owner_kind: 'builder', owner_builder_id: 77, documentCount: 0, learningCount: 2 }],
|
|
162
|
+
// 11 has no catalogue row: counted as nothing, and not treated as self-authored.
|
|
163
|
+
]);
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
store.adoptions = [
|
|
167
|
+
row({ id: 9, name: 'Economics', contract_md: 'Mine.' }),
|
|
168
|
+
row({ id: 10, name: 'Law', discipline: 'engineer', contract_md: 'Theirs.' }),
|
|
169
|
+
row({ id: 11, name: 'Music', discipline: 'artist', contract_md: 'Gone.' }),
|
|
170
|
+
row({ id: 12, name: 'Old', discipline: 'ui', active: false }),
|
|
171
|
+
];
|
|
172
|
+
const out = await port.describeActiveFor(1);
|
|
173
|
+
assert.equal(calls.length, 1, 'one batched read, not one per adoption');
|
|
174
|
+
assert.deepEqual(calls[0], ['9', '10', '11'], 'only ACTIVE adoptions are asked about');
|
|
175
|
+
const by = Object.fromEntries(out.map((d) => [d.name, d]));
|
|
176
|
+
assert.deepEqual(Object.keys(by).sort(), ['Economics', 'Law', 'Music']);
|
|
177
|
+
assert.match(by.Economics.contract, /4 document\(s\)/);
|
|
178
|
+
assert.doesNotMatch(by.Economics.contract, /stated working preferences/, 'your own speciality stays unfenced');
|
|
179
|
+
assert.match(by.Law.contract, /2 learning\(s\)/);
|
|
180
|
+
assert.match(by.Law.contract, /stated working preferences/, 'another builder\'s speciality carries its provenance');
|
|
181
|
+
assert.match(by.Music.contract, /stated working preferences/, 'a row the read did not return is never self-authored');
|
|
182
|
+
} finally { db.describeTrainingFor = saved; }
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await test('no active adoption asks the database nothing more', async () => {
|
|
186
|
+
const saved = db.describeTrainingFor;
|
|
187
|
+
let called = 0;
|
|
188
|
+
db.describeTrainingFor = async () => { called++; return new Map(); };
|
|
189
|
+
try {
|
|
190
|
+
store.adoptions = [row({ active: false })];
|
|
191
|
+
assert.deepEqual(await port.describeActiveFor(1), []);
|
|
192
|
+
assert.equal(called, 0);
|
|
193
|
+
} finally { db.describeTrainingFor = saved; }
|
|
194
|
+
});
|
|
195
|
+
|
|
150
196
|
summary();
|