@bongos/core 1.23.21 → 1.24.1
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 +983 -428
- 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 +6 -3
- 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 +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -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 +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- 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 +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +4 -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 +55 -6
- package/docs/page-readings.json +1185 -1011
- 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/CLAUDE.md +15 -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/module.json +26 -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 +252 -0
- 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/catalog.js +6 -0
- package/modules/government/db.js +108 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- 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 +156 -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/city.html +15 -15
- 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/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 +24 -0
- package/modules/lifecycle/db-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- 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/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- 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/onboarding/howto-moments.js +8 -1
- 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 +8 -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 +286 -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/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 +4 -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 +129 -12
- 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 +16 -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 +3 -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/src/platform-server.js +27 -1
- 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/batched_writes_parity.mjs +340 -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/conductor_main_e2e.mjs +4 -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/per-row-loops-legacy.cjs +245 -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_archive_disposition.mjs +5 -5
- 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_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -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/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- 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/module_skills_hook.mjs +69 -0
- 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/public_landing_projects.mjs +37 -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/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- 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 +58 -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_close_route.mjs +1 -1
- 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
|
@@ -1,72 +1,295 @@
|
|
|
1
1
|
// tests/unit_runner_serial.mjs
|
|
2
2
|
//
|
|
3
|
-
// The `unit` gate runs
|
|
4
|
-
//
|
|
3
|
+
// The `unit` gate runs several test files at once, and keeps the files that share state
|
|
4
|
+
// outside their own process in a SERIAL lane that runs alone (task 1004926). This file used
|
|
5
|
+
// to pin the opposite — that the runner was strictly one-at-a-time (task 1001435) — by
|
|
6
|
+
// reading the runner's source. It now drives the runner's own pieces instead:
|
|
5
7
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// All of that reasoning is only true while execution stays serial. Make the runner
|
|
15
|
-
// concurrent and every word of it silently becomes wrong, with no test to say so —
|
|
16
|
-
// so this suite fails if that happens, and whoever does it has to revisit the note
|
|
17
|
-
// rather than leave a stale explanation behind.
|
|
18
|
-
//
|
|
19
|
-
// Static source assertions on purpose: actually observing concurrency would mean
|
|
20
|
-
// running the whole gate from inside the gate.
|
|
8
|
+
// - the pool size: UNIT_TESTS_SERIAL=1 forces one at a time, UNIT_TEST_JOBS=N sets it,
|
|
9
|
+
// the default is the CPU count;
|
|
10
|
+
// - which files go to the serial lane: a browser launch, a fixed listen port, a fixed
|
|
11
|
+
// temp path, or a name in SERIAL;
|
|
12
|
+
// - the scheduler: a serial-lane file never overlaps anything, the pool never exceeds
|
|
13
|
+
// its size, and output prints in entry order whatever order the files finish in;
|
|
14
|
+
// - the real child-process path: pass, fail, a stdin read, and a timeout, through the
|
|
15
|
+
// same runFile the gate uses.
|
|
21
16
|
//
|
|
22
17
|
// Run: node tests/unit_runner_serial.mjs
|
|
23
18
|
|
|
24
19
|
import { strict as assert } from 'node:assert';
|
|
25
20
|
import fs from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
26
22
|
import path from 'node:path';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
27
24
|
import { fileURLToPath } from 'node:url';
|
|
28
25
|
|
|
29
26
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
-
const
|
|
31
|
-
const
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const runner = require(path.join(ROOT, 'scripts', 'bongos', 'run-unit-tests.js'));
|
|
29
|
+
const { resolveJobs, serialReason, runFile, runEntries, renderResult, SERIAL, UNIT_TESTS_SERIAL_ENV, UNIT_TEST_JOBS_ENV } = runner;
|
|
32
30
|
|
|
33
31
|
let passed = 0;
|
|
34
32
|
let failed = 0;
|
|
35
|
-
function t(name, fn) {
|
|
36
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
33
|
+
async function t(name, fn) {
|
|
34
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
37
35
|
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// ── pool size ────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
await t('default pool size is the CPU count', () => {
|
|
41
|
+
assert.equal(resolveJobs({}, 8), 8);
|
|
42
|
+
assert.equal(resolveJobs({}, 0), 1, 'never below one');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await t('UNIT_TESTS_SERIAL=1 forces one at a time, whatever the machine', () => {
|
|
46
|
+
assert.equal(UNIT_TESTS_SERIAL_ENV, 'UNIT_TESTS_SERIAL');
|
|
47
|
+
for (const v of ['1', 'true', 'yes', 'on', 'TRUE']) assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: v }, 16), 1, v);
|
|
48
|
+
assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: '1', UNIT_TEST_JOBS: '8' }, 16), 1, 'serial wins over a jobs count');
|
|
49
|
+
assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: '0' }, 16), 16, '0 does not force serial');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await t('UNIT_TEST_JOBS=N sets the pool size; junk is ignored', () => {
|
|
53
|
+
assert.equal(UNIT_TEST_JOBS_ENV, 'UNIT_TEST_JOBS');
|
|
54
|
+
assert.equal(resolveJobs({ UNIT_TEST_JOBS: '3' }, 16), 3);
|
|
55
|
+
assert.equal(resolveJobs({ UNIT_TEST_JOBS: '1' }, 16), 1);
|
|
56
|
+
for (const junk of ['0', '-2', 'many', '2.5']) assert.equal(resolveJobs({ UNIT_TEST_JOBS: junk }, 6), 6, junk);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── serial-lane classification ──────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
await t('a browser launch goes to the serial lane', () => {
|
|
62
|
+
assert.equal(serialReason('x', "import { chromium } from 'playwright';"), 'launches a browser');
|
|
63
|
+
assert.equal(serialReason('x', "const b = await puppeteer.launch({ headless: 'new' });"), 'launches a browser');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await t('a fixed listen port goes to the serial lane; listen(0) does not', () => {
|
|
67
|
+
assert.equal(serialReason('x', 'server.listen(3100, () => {});'), 'listens on a fixed port');
|
|
68
|
+
assert.equal(serialReason('x', 'server.listen(8080)'), 'listens on a fixed port');
|
|
69
|
+
assert.equal(serialReason('x', 'server.listen(0, () => {});'), null);
|
|
70
|
+
assert.equal(serialReason('x', "assert.match(out, /PORT=3003/);"), null, 'a port in an assertion is not a listen');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await t('a fixed temp path goes to the serial lane; a mkdtemp prefix does not', () => {
|
|
74
|
+
assert.equal(serialReason('x', "const p = path.join(os.tmpdir(), 'probe-home');"), 'uses a fixed temp path');
|
|
75
|
+
assert.equal(serialReason('x', "const d = fs.mkdtempSync(path.join(os.tmpdir(), 'probe-'));"), null);
|
|
76
|
+
assert.equal(serialReason('x', "const d = mkdtempSync(join(os.tmpdir(), 'probe-'));"), null);
|
|
77
|
+
assert.equal(serialReason('x', 'const p = path.join(os.tmpdir(), `probe-${process.pid}`);'), null, 'a per-process name is not shared');
|
|
78
|
+
// A file with one mkdtemp and one fixed path is still serial — the fixed one decides.
|
|
79
|
+
assert.equal(serialReason('x', "fs.mkdtempSync(path.join(os.tmpdir(), 'a-'));\nconst p = path.join(os.tmpdir(), 'fixed');"), 'uses a fixed temp path');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await t('a name in SERIAL is serial whatever its source says', () => {
|
|
83
|
+
SERIAL.add('__probe_listed__');
|
|
84
|
+
try {
|
|
85
|
+
assert.equal(serialReason('__probe_listed__', ''), 'listed in SERIAL');
|
|
86
|
+
} finally { SERIAL.delete('__probe_listed__'); }
|
|
87
|
+
assert.equal(serialReason('plain', 'assert.equal(1, 1);'), null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await t('the real suite: every browser-launching test file lands in the serial lane', () => {
|
|
91
|
+
// The scan has to keep seeing the files it exists for. A pattern that silently stopped
|
|
92
|
+
// matching would put Chrome back in the pool, and green would look the same.
|
|
93
|
+
for (const name of ['page_reader', 'ui_design_skills']) {
|
|
94
|
+
const src = fs.readFileSync(path.join(ROOT, 'tests', `${name}.mjs`), 'utf8');
|
|
95
|
+
assert.equal(serialReason(name, src), 'launches a browser', name);
|
|
96
|
+
}
|
|
97
|
+
// and not everything: the pool has to have something in it
|
|
98
|
+
const sample = fs.readdirSync(path.join(ROOT, 'tests')).filter((f) => f.endsWith('.mjs')).slice(0, 200);
|
|
99
|
+
const serial = sample.filter((f) => serialReason(f, fs.readFileSync(path.join(ROOT, 'tests', f), 'utf8')));
|
|
100
|
+
assert.ok(serial.length < sample.length / 2, `most files run in the pool (${serial.length}/${sample.length} serial)`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── scheduler ────────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// A fake `run` that finishes each file after a set delay and records what overlapped.
|
|
106
|
+
function fakeRun(delays) {
|
|
107
|
+
const log = { inFlight: new Set(), maxInFlight: 0, overlapsWithSerial: [], started: [] };
|
|
108
|
+
const run = async (file) => {
|
|
109
|
+
log.inFlight.add(file);
|
|
110
|
+
log.started.push(file);
|
|
111
|
+
log.maxInFlight = Math.max(log.maxInFlight, log.inFlight.size);
|
|
112
|
+
if (file.startsWith('S') && log.inFlight.size > 1) log.overlapsWithSerial.push([...log.inFlight]);
|
|
113
|
+
for (const other of log.inFlight) if (other !== file && other.startsWith('S')) log.overlapsWithSerial.push([...log.inFlight]);
|
|
114
|
+
await new Promise((r) => setTimeout(r, delays[file] ?? 5));
|
|
115
|
+
log.inFlight.delete(file);
|
|
116
|
+
return { ok: !file.endsWith('!'), status: file.endsWith('!') ? 1 : 0, signal: null, killed: false, stdout: file.endsWith('!') ? ' FAIL case\n0 passed, 1 failed' : '1 passed, 0 failed', stderr: '' };
|
|
117
|
+
};
|
|
118
|
+
return { run, log };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const entry = (file) => ({ displayName: file, file, serial: file.startsWith('S') ? 'listed in SERIAL' : null });
|
|
122
|
+
|
|
123
|
+
await t('output prints in entry order even when later files finish first', async () => {
|
|
124
|
+
const files = ['a', 'b', 'c', 'd', 'e', 'f'];
|
|
125
|
+
// a is slowest, so every other file finishes before it
|
|
126
|
+
const { run } = fakeRun({ a: 60, b: 5, c: 10, d: 1, e: 20, f: 2 });
|
|
127
|
+
const printed = [];
|
|
128
|
+
const results = await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
|
|
129
|
+
assert.deepEqual(printed, files.map((f) => ` ok ${f}`));
|
|
130
|
+
assert.deepEqual(results.map((r) => r.outcome), files.map(() => 'ok'));
|
|
43
131
|
});
|
|
44
132
|
|
|
45
|
-
t('the
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert.
|
|
50
|
-
'a Promise.all over the test list would run suites concurrently');
|
|
51
|
-
assert.doesNotMatch(src, /require\(['"]node:worker_threads['"]\)|from ['"]node:worker_threads['"]/,
|
|
52
|
-
'a worker pool would run suites concurrently');
|
|
53
|
-
assert.doesNotMatch(src, /\bexecFile\s*\(/,
|
|
54
|
-
'the ASYNC execFile would overlap suites; the runner uses execFileSync');
|
|
133
|
+
await t('the pool runs files concurrently but never more than its size', async () => {
|
|
134
|
+
const files = Array.from({ length: 12 }, (_, i) => `p${String(i).padStart(2, '0')}`);
|
|
135
|
+
const { run, log } = fakeRun(Object.fromEntries(files.map((f) => [f, 15])));
|
|
136
|
+
await runEntries(files.map(entry), { jobs: 3, perTestTimeout: 1000, run, print: () => {} });
|
|
137
|
+
assert.equal(log.maxInFlight, 3);
|
|
55
138
|
});
|
|
56
139
|
|
|
57
|
-
t('
|
|
58
|
-
|
|
59
|
-
|
|
140
|
+
await t('a serial-lane file never runs beside anything else', async () => {
|
|
141
|
+
const files = ['S1', 'a', 'b', 'S2', 'c', 'd', 'e', 'S3'];
|
|
142
|
+
const { run, log } = fakeRun(Object.fromEntries(files.map((f) => [f, 10])));
|
|
143
|
+
const printed = [];
|
|
144
|
+
await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
|
|
145
|
+
assert.deepEqual(log.overlapsWithSerial, [], 'no serial file overlapped another run');
|
|
146
|
+
assert.ok(log.maxInFlight > 1, 'the pool part still ran in parallel');
|
|
147
|
+
assert.deepEqual(printed, files.map((f) => ` ok ${f}`), 'still printed in entry order');
|
|
60
148
|
});
|
|
61
149
|
|
|
62
|
-
t('
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
assert.
|
|
67
|
-
|
|
150
|
+
await t('jobs=1 runs everything one at a time, in entry order', async () => {
|
|
151
|
+
const files = ['S1', 'a', 'b', 'c'];
|
|
152
|
+
const { run, log } = fakeRun({ a: 15, b: 1, c: 5 });
|
|
153
|
+
await runEntries(files.map(entry), { jobs: 1, perTestTimeout: 1000, run, print: () => {} });
|
|
154
|
+
assert.equal(log.maxInFlight, 1);
|
|
155
|
+
assert.deepEqual(log.started, files);
|
|
68
156
|
});
|
|
69
157
|
|
|
158
|
+
await t('a failure keeps its verdict and detail lines, in its place', async () => {
|
|
159
|
+
const files = ['a', 'b!', 'c'];
|
|
160
|
+
const { run } = fakeRun({ a: 20, 'b!': 1, c: 1 });
|
|
161
|
+
const printed = [];
|
|
162
|
+
const results = await runEntries(files.map(entry), { jobs: 3, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
|
|
163
|
+
assert.deepEqual(results.map((r) => r.outcome), ['ok', 'fail', 'ok']);
|
|
164
|
+
assert.equal(printed[0], ' ok a');
|
|
165
|
+
assert.equal(printed[1], ' FAIL b! (exit 1)');
|
|
166
|
+
assert.ok(printed.includes(' FAIL case'), 'the file\'s own FAIL line is shown');
|
|
167
|
+
assert.equal(printed[printed.length - 1], ' ok c');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await t('a pooled TIMEOUT is re-run alone, and the alone run is the verdict', async () => {
|
|
171
|
+
const calls = [];
|
|
172
|
+
let inFlight = 0;
|
|
173
|
+
const run = async (file) => {
|
|
174
|
+
inFlight++;
|
|
175
|
+
const alone = inFlight === 1;
|
|
176
|
+
calls.push({ file, alone });
|
|
177
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
178
|
+
inFlight--;
|
|
179
|
+
// 'slow' only finishes when nothing else is running; 'hang' never finishes
|
|
180
|
+
const killed = file === 'hang' || (file === 'slow' && !calls.filter((c) => c.file === 'slow').at(-1).alone);
|
|
181
|
+
return { ok: !killed, status: killed ? null : 0, signal: killed ? 'SIGTERM' : null, killed, stdout: killed ? '' : '1 passed, 0 failed', stderr: '' };
|
|
182
|
+
};
|
|
183
|
+
const files = ['a', 'hang', 'slow', 'z'];
|
|
184
|
+
const printed = [];
|
|
185
|
+
const results = await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
|
|
186
|
+
assert.deepEqual(results.map((r) => r.outcome), ['ok', 'fail', 'ok', 'ok'], 'starved-only passes; a real hang still fails');
|
|
187
|
+
assert.equal(calls.filter((c) => c.file === 'slow').length, 2);
|
|
188
|
+
assert.equal(calls.filter((c) => c.file === 'slow').at(-1).alone, true, 'the re-run had nothing beside it');
|
|
189
|
+
assert.equal(calls.filter((c) => c.file === 'a').length, 1, 'a file that passed is not re-run');
|
|
190
|
+
assert.equal(printed[0], ' ok a');
|
|
191
|
+
assert.equal(printed[1], ' FAIL hang (TIMEOUT after 1s, SIGTERM)');
|
|
192
|
+
assert.match(printed[2], /re-run alone/);
|
|
193
|
+
assert.equal(printed.at(-1), ' ok z', 'still in entry order');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await t('an assertion failure in the pool is NOT re-run', async () => {
|
|
197
|
+
const { run, log } = fakeRun({});
|
|
198
|
+
await runEntries(['a', 'b!'].map(entry), { jobs: 2, perTestTimeout: 1000, run, print: () => {} });
|
|
199
|
+
assert.deepEqual(log.started.filter((f) => f === 'b!'), ['b!']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await t('a write inside the repo goes to the serial lane; a write into a temp dir does not', () => {
|
|
203
|
+
const why = 'writes inside the repo';
|
|
204
|
+
assert.equal(serialReason('x', "const probe = path.join(ROOT, 'modules', 'p');\nmkdirSync(probe);"), why);
|
|
205
|
+
assert.equal(serialReason('x', "fs.writeFileSync(path.join(REPO_ROOT, 'config', 'x.json'), '{}');"), why);
|
|
206
|
+
assert.equal(serialReason('x', "const target = new URL('../src/a.js', import.meta.url);\nfs.writeFileSync(target, s);"), why);
|
|
207
|
+
assert.equal(serialReason('x', "const tmp = fs.mkdtempSync(path.join(ROOT, '.tmp-'));"), why);
|
|
208
|
+
assert.equal(serialReason('x', "const src = path.join(ROOT, 'config', 'a.json');\nfs.readFileSync(src, 'utf8');"), null, 'reading the repo is fine');
|
|
209
|
+
assert.equal(serialReason('x', "const d = fs.mkdtempSync(path.join(os.tmpdir(), 'w-'));\nfs.writeFileSync(path.join(d, 'f'), 'x');"), null);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await t('the real suite: every repo-writer found by watching a pooled run is in the serial lane', () => {
|
|
213
|
+
// Each of these changed a file another test reads while the pool ran (task 1004926).
|
|
214
|
+
for (const name of ['prelaunch_gate', 'file_map', 'figma_adapter', 'repo_map', 'government_vocabulary', 'cloud_bongos_pack', 'search_ingest_nul']) {
|
|
215
|
+
const src = fs.readFileSync(path.join(ROOT, 'tests', `${name}.mjs`), 'utf8');
|
|
216
|
+
assert.equal(serialReason(name, src), 'writes inside the repo', name);
|
|
217
|
+
}
|
|
218
|
+
// agents_event_router times a handler at 50 ms of wall clock — a busy pool makes that a coin toss
|
|
219
|
+
assert.equal(serialReason('agents_event_router', ''), 'listed in SERIAL');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── verdict rendering (unchanged rules)─────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
await t('renderResult: timeout, skips and the libuv teardown abort read as before', () => {
|
|
225
|
+
const timeout = renderResult('slow', { ok: false, status: null, signal: 'SIGTERM', killed: true, stdout: '', stderr: '' }, 90_000);
|
|
226
|
+
assert.equal(timeout.outcome, 'fail');
|
|
227
|
+
assert.equal(timeout.lines[0], ' FAIL slow (TIMEOUT after 90s, SIGTERM)');
|
|
228
|
+
assert.match(timeout.lines[1], /no output at all/);
|
|
229
|
+
|
|
230
|
+
const skips = renderResult('sk', { ok: true, status: 0, signal: null, killed: false, stdout: ' SKIP one — no db\n1 passed, 0 failed, 1 skipped', stderr: '' }, 90_000);
|
|
231
|
+
assert.deepEqual(skips, { lines: [' ok sk (1 SKIPPED — not run here)'], outcome: 'ok', skips: 1 });
|
|
232
|
+
|
|
233
|
+
const abort = renderResult('ab', { ok: false, status: 127, signal: null, killed: false, stdout: '3 passed, 0 failed', stderr: 'Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)' }, 90_000);
|
|
234
|
+
assert.equal(abort.outcome, 'teardown');
|
|
235
|
+
assert.equal(abort.lines[0], ' ok ab (clean verdict, then libuv teardown abort, exit 127)');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ── the real child-process path ─────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'unit-runner-pool-'));
|
|
241
|
+
try {
|
|
242
|
+
const write = (name, body) => { const p = path.join(dir, name); fs.writeFileSync(p, body); return p; };
|
|
243
|
+
const pass = write('pass.mjs', "console.log(' PASS one'); console.log('1 passed, 0 failed');\n");
|
|
244
|
+
const fail = write('fail.mjs', "console.log(' FAIL one'); console.log('0 passed, 1 failed'); console.error('boom'); process.exit(3);\n");
|
|
245
|
+
const stdin = write('stdin.mjs', "process.stdin.resume(); process.stdin.on('end', () => { console.log('1 passed, 0 failed'); });\n");
|
|
246
|
+
const hang = write('hang.mjs', 'setInterval(() => {}, 1000);\n');
|
|
247
|
+
|
|
248
|
+
await t('runFile: a passing file resolves ok with its stdout', async () => {
|
|
249
|
+
const r = await runFile(pass, 20_000);
|
|
250
|
+
assert.equal(r.ok, true);
|
|
251
|
+
assert.equal(r.status, 0);
|
|
252
|
+
assert.match(r.stdout, /1 passed, 0 failed/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await t('runFile: a failing file resolves with its exit code, stdout and stderr', async () => {
|
|
256
|
+
const r = await runFile(fail, 20_000);
|
|
257
|
+
assert.equal(r.ok, false);
|
|
258
|
+
assert.equal(r.status, 3);
|
|
259
|
+
assert.equal(r.killed, false);
|
|
260
|
+
assert.match(r.stdout, /FAIL {2}one/);
|
|
261
|
+
assert.match(r.stderr, /boom/);
|
|
262
|
+
assert.equal(renderResult('fail', r, 20_000).lines[0], ' FAIL fail (exit 3)');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await t('runFile: stdin reads see end-of-input at once, as under the old ignored stdin', async () => {
|
|
266
|
+
const r = await runFile(stdin, 20_000);
|
|
267
|
+
assert.equal(r.ok, true, 'a suite that reads stdin must not wait for the whole budget');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await t('runFile: a hung file is killed at the budget and reads as a TIMEOUT', async () => {
|
|
271
|
+
const r = await runFile(hang, 1_500);
|
|
272
|
+
assert.equal(r.ok, false);
|
|
273
|
+
assert.equal(r.killed, true);
|
|
274
|
+
assert.match(renderResult('hang', r, 1_500).lines[0], /^ {2}FAIL {2}hang \(TIMEOUT after 1\.5s/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await t('runEntries end to end: real files, a pool of 3, sorted output and outcomes', async () => {
|
|
278
|
+
const entries = [
|
|
279
|
+
{ displayName: 'fail', file: fail, serial: null },
|
|
280
|
+
{ displayName: 'pass', file: pass, serial: 'listed in SERIAL' },
|
|
281
|
+
{ displayName: 'stdin', file: stdin, serial: null },
|
|
282
|
+
];
|
|
283
|
+
const printed = [];
|
|
284
|
+
const results = await runEntries(entries, { jobs: 3, perTestTimeout: 20_000, print: (l) => printed.push(l) });
|
|
285
|
+
assert.deepEqual(results.map((r) => r.outcome), ['fail', 'ok', 'ok']);
|
|
286
|
+
assert.equal(printed[0], ' FAIL fail (exit 3)');
|
|
287
|
+
assert.deepEqual(printed.slice(-2), [' ok pass', ' ok stdin']);
|
|
288
|
+
});
|
|
289
|
+
} finally {
|
|
290
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
|
|
70
293
|
console.log('');
|
|
71
294
|
console.log(`unit_runner_serial: ${passed} passed, ${failed} failed`);
|
|
72
295
|
process.exit(failed ? 1 : 0);
|
|
@@ -166,7 +166,7 @@ function closePool({ status = 'building', goalStatus = 'open', remaining = 0, st
|
|
|
166
166
|
return txPool((sql) => {
|
|
167
167
|
const q = String(sql).replace(/\s+/g, ' ');
|
|
168
168
|
if (/FROM versions WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: 'V1', status }] };
|
|
169
|
-
if (/FROM goals WHERE id = \$1 AND version_id = \$2 FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 'g', status: goalStatus }] };
|
|
169
|
+
if (/FROM goals WHERE id = ANY\(\$1::bigint\[\]\) AND version_id = \$2 ORDER BY id FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 'g', status: goalStatus }] };
|
|
170
170
|
if (/UPDATE tasks SET status = 'abandoned'/.test(q)) return { rows: [{ id: 11 }, { id: 12 }] };
|
|
171
171
|
// R17 (task 1003604) — the roll_forward writes: the successor INSERT, the
|
|
172
172
|
// membership copy, and the task re-point.
|
|
@@ -75,7 +75,7 @@ test('an unknown future prefix is caught too', () => {
|
|
|
75
75
|
|
|
76
76
|
// ---- what the guard must NOT catch ---------------------------------------
|
|
77
77
|
test('a COMMENT naming an old version is not a violation', () => {
|
|
78
|
-
//
|
|
78
|
+
// A script may legitimately say "PMS-V2 P6 not live yet" in a comment.
|
|
79
79
|
const r = scan({
|
|
80
80
|
'scripts/bongos/d.sh': '# TODO: once PMS-V2 P6 ships, drop this',
|
|
81
81
|
'scripts/bongos/e.js': "// version_id: 'GDS-V3' used to live here",
|
|
@@ -133,12 +133,10 @@ test('smoke-write.sh resolves its version instead of naming one', () => {
|
|
|
133
133
|
assert.match(sh, /exit 1/, 'and stops if it cannot');
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
test('
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
assert.match(sh, /'version_id': '\$\{VERSION_ID\}'/, `${f} uses the resolved id`);
|
|
141
|
-
}
|
|
136
|
+
test('the bash smoke writes the resolved id, not a named version', () => {
|
|
137
|
+
// smoke-write.sh is the one bash smoke left (task 1005002 deleted the other three).
|
|
138
|
+
const sh = read('scripts', 'bongos', 'smoke-write.sh');
|
|
139
|
+
assert.match(sh, /'version_id': '\$\{VERSION_ID\}'/, 'smoke-write.sh uses the resolved id');
|
|
142
140
|
});
|
|
143
141
|
|
|
144
142
|
test('capture.js no longer keeps its own copy of the rule', () => {
|
package/tests/welcome_boot.mjs
CHANGED
|
@@ -75,12 +75,23 @@ test('every light sits inside the painting, and every pulse climbs inside its to
|
|
|
75
75
|
assert.deepEqual(W.pulseRuns(W.TOWERS, 3, 7), runs, 'seeded: every visit draws the same picture');
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
test('the banner
|
|
79
|
-
assert.equal(W.bannerText
|
|
80
|
-
|
|
81
|
-
assert.
|
|
78
|
+
test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
|
|
79
|
+
assert.equal(W.bannerText, undefined, 'the "Working on the core of" banner is gone');
|
|
80
|
+
const harbor = W.logoHtml({ identity: { worldName: 'Harbor', productName: 'Cloud Bongos' } });
|
|
81
|
+
assert.match(harbor, /<svg class="wb__mark"/, 'no project logo: the Bongos mark');
|
|
82
|
+
assert.match(harbor, /class="wb__ring"[^>]*pathLength="100"/, 'the ring can draw in');
|
|
83
|
+
assert.match(harbor, /class="wb__moonlet"[^>]*fill="#fb9569"/, 'the salmon moon');
|
|
84
|
+
assert.match(harbor, /<span class="wb__name">Harbor<\/span>/);
|
|
85
|
+
assert.match(W.logoHtml(undefined), /<span class="wb__name">this project<\/span>/, 'no branding: still a name');
|
|
86
|
+
const own = W.logoHtml({ identity: { worldName: 'Harbor' }, theme: { logo: '/logo.png?a="b"' } });
|
|
87
|
+
assert.match(own, /<img class="wb__logo" src="\/logo\.png\?a="b""/, 'a project logo takes the mark\'s place, escaped');
|
|
88
|
+
assert.doesNotMatch(own, /wb__mark/);
|
|
82
89
|
const branding = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
|
|
83
|
-
assert.
|
|
90
|
+
assert.match(W.logoHtml(branding), new RegExp('>' + branding.identity.worldName + '<'));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the credit links to cloudbongos.com (task 1004953)', () => {
|
|
94
|
+
assert.deepEqual(W.BUILT_ON, { href: 'https://cloudbongos.com', label: 'cloudbongos.com' });
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
test('the owner\'s role mapping: destination and craft mode', () => {
|
|
@@ -297,17 +297,49 @@ test('the way out sinks and fades into the hall, which is usable at once', { ski
|
|
|
297
297
|
await ctx.close();
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
-
test('the banner
|
|
300
|
+
test('the banner is the animated logo, the motion is laid on, and the drawn tower and city are gone', { skip }, async () => {
|
|
301
301
|
const ctx = await hall();
|
|
302
302
|
const { page } = await open(ctx);
|
|
303
303
|
await signIn(page);
|
|
304
|
-
await page.waitForSelector('#welcome-boot .wb-motion');
|
|
304
|
+
await page.waitForSelector('#welcome-boot.wb--lit .wb-motion');
|
|
305
305
|
const s = await page.evaluate(() => ({
|
|
306
|
-
banner: document.querySelector('.wb__banner
|
|
306
|
+
banner: document.querySelector('.wb__banner').textContent,
|
|
307
|
+
name: document.querySelector('.wb__banner .wb__name').textContent,
|
|
308
|
+
mark: !!document.querySelector('.wb__banner svg.wb__mark .wb__moonlet'),
|
|
309
|
+
ringAnim: getComputedStyle(document.querySelector('.wb__ring')).animationName,
|
|
307
310
|
pulses: document.querySelectorAll('#wb-fx .wb-pulse').length,
|
|
308
311
|
cars: document.querySelectorAll('#wb-fx .wb-car').length,
|
|
309
312
|
old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-scene, #wb-beam, #wb-city').length,
|
|
310
313
|
}));
|
|
311
|
-
assert.
|
|
314
|
+
assert.doesNotMatch(s.banner, /Working on the core of/, 'task 1004953: the banner text is gone');
|
|
315
|
+
assert.deepEqual({ name: s.name, mark: s.mark, ringAnim: s.ringAnim, pulses: s.pulses, cars: s.cars, old: s.old },
|
|
316
|
+
{ name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 21, cars: 3, old: 0 });
|
|
312
317
|
await ctx.close();
|
|
313
318
|
});
|
|
319
|
+
|
|
320
|
+
test('the "Built on cloudbongos.com" credit sits bottom right, clear of the choices, at desktop and phone width', { skip }, async () => {
|
|
321
|
+
for (const viewport of [{ width: 1280, height: 800 }, { width: 390, height: 760 }]) {
|
|
322
|
+
const ctx = await hall();
|
|
323
|
+
const { page } = await open(ctx);
|
|
324
|
+
await page.setViewportSize(viewport);
|
|
325
|
+
await page.evaluate(() => window.OTBShell.applyAccess({ authed: false }));
|
|
326
|
+
await page.waitForSelector('#welcome-boot .wb__built');
|
|
327
|
+
await page.click('[data-wb-skip]');
|
|
328
|
+
const s = await page.evaluate(() => {
|
|
329
|
+
const a = document.querySelector('.wb__built').getBoundingClientRect();
|
|
330
|
+
const c = document.querySelector('.wb__choices').getBoundingClientRect();
|
|
331
|
+
const el = document.querySelector('.wb__built');
|
|
332
|
+
return {
|
|
333
|
+
text: el.textContent, href: el.getAttribute('href'),
|
|
334
|
+
right: Math.round(innerWidth - a.right), bottom: Math.round(innerHeight - a.bottom),
|
|
335
|
+
clear: a.top >= c.bottom || a.right <= c.left || a.left >= c.right, tall: a.height >= 24,
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
assert.equal(s.text, 'Built on cloudbongos.com');
|
|
339
|
+
assert.equal(s.href, 'https://cloudbongos.com');
|
|
340
|
+
assert.ok(s.right <= 24 && s.bottom <= 24, 'bottom-right corner at ' + viewport.width + 'w');
|
|
341
|
+
assert.ok(s.clear, 'does not cover the choices at ' + viewport.width + 'w');
|
|
342
|
+
assert.ok(s.tall, 'a 24px target');
|
|
343
|
+
await ctx.close();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Releasing the standalone `bongos` CLI
|
|
2
|
-
|
|
3
|
-
How the compiled `bongos` binaries get built, published, and installed — the
|
|
4
|
-
"no Node, no repo" on-ramp (task [#1248](https://example.com/builders#/task/1248), V4.R81).
|
|
5
|
-
The release flow: **CI builds, the operator publishes to a public releases repo, the server serves
|
|
6
|
-
branded redirects.** There is no cross-repo write token in CI — publishing is a
|
|
7
|
-
deliberate operator step.
|
|
8
|
-
|
|
9
|
-
## The moving parts
|
|
10
|
-
|
|
11
|
-
| Piece | Where | Role |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| Build wrapper | [`scripts/bongos/build-bongos.js`](../../scripts/bongos/build-bongos.js) | `bun build --compile` → per-platform binaries in `dist/` |
|
|
14
|
-
| CI | `.github/workflows/build-bongos-cli.yml` (instance-side) | runs the wrapper, uploads `bongos-cli-binaries` artifacts (no publish) |
|
|
15
|
-
| Release pointer | `config/bongos-cli.json` (instance-side) | `releases_repo` + `version` + per-target asset filenames |
|
|
16
|
-
| Redirects | [`src/bongos-downloads.js`](../../src/bongos-downloads.js) → `/downloads/bongos/*` in [`serve-internal.js`](../../src/bongos/serve-internal.js) | `/downloads/bongos/<target>` 302s to the release asset; `/version` + `/latest.json` for update-checks |
|
|
17
|
-
| Installer | `infra/bongos-install.sh` + `.ps1` (instance-side) | served (origin-pinned) at `/downloads/bongos/install.sh` / `.ps1` |
|
|
18
|
-
| Package managers | `packaging/homebrew/bongos.rb`, `packaging/scoop/bongos.json` (instance-side) | brew/scoop templates (fill sha256 at release) |
|
|
19
|
-
|
|
20
|
-
## What the user runs
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
# macOS / Linux — no Node required:
|
|
24
|
-
curl -fsSL https://example.com/downloads/bongos/install.sh | sh
|
|
25
|
-
# Windows (PowerShell):
|
|
26
|
-
irm https://example.com/downloads/bongos/install.ps1 | iex
|
|
27
|
-
# or, once the tap/bucket is published:
|
|
28
|
-
brew install example-owner/bongos/bongos # macOS/Linux
|
|
29
|
-
scoop bucket add bongos https://github.com/example-owner/scoop-bongos; scoop install bongos
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Then: `bongos login <instance-url>` → `bongos start`. Writing code needs a checkout of the
|
|
33
|
-
project repo, where the full CLI is installed.
|
|
34
|
-
|
|
35
|
-
## One-time owner setup (tracked as the open blocker on [#1248](https://example.com/builders#/task/1248))
|
|
36
|
-
|
|
37
|
-
1. **Create a PUBLIC releases repo** — `example-owner/example-bongos` (matches `config/bongos-cli.json`'s `releases_repo`). Public so GitHub's release CDN serves the downloads at $0; it holds ONLY the binaries, never source.
|
|
38
|
-
2. *(optional, for brew/scoop)* create the tap repo `example-owner/homebrew-bongos` and/or the bucket `lars689/scoop-bongos`.
|
|
39
|
-
3. *(optional)* point `get.bongos.dev` at the instance with a DNS CNAME → `example.com`, so the branded `curl -fsSL https://get.bongos.dev | sh` works (it just proxies `/downloads/bongos/install.sh`).
|
|
40
|
-
|
|
41
|
-
## Cutting a release
|
|
42
|
-
|
|
43
|
-
1. **Bump** `config/bongos-cli.json`: `version` + every `assets.*` filename to the new version. (Claim a task; this rides the normal ship flow.)
|
|
44
|
-
2. **Push** → `build-bongos-cli.yml` runs and produces the `bongos-cli-binaries` artifact. Wait for green.
|
|
45
|
-
3. **Download** the artifacts:
|
|
46
|
-
```sh
|
|
47
|
-
gh run download -R example-owner/example --name bongos-cli-binaries --dir dist
|
|
48
|
-
```
|
|
49
|
-
4. **Publish** them as a `v<version>` release on the public repo:
|
|
50
|
-
```sh
|
|
51
|
-
gh release create v0.1.0 -R example-owner/example-bongos --title "bongos v0.1.0" --notes "Standalone bongos CLI" dist/bongos-*
|
|
52
|
-
```
|
|
53
|
-
The tag and asset filenames MUST match `config/bongos-cli.json` exactly — the redirect is `releases/download/v<version>/<asset>`; a renamed asset 404s.
|
|
54
|
-
5. *(package managers)* compute `shasum -a 256 dist/bongos-*`, fill the `sha256`/`hash` fields in `packaging/homebrew/bongos.rb` + `packaging/scoop/bongos.json`, and copy them to the tap/bucket repos.
|
|
55
|
-
6. **Ship** the `config/bongos-cli.json` bump. The `/downloads/bongos/*` redirects follow within a minute of deploy.
|
|
56
|
-
|
|
57
|
-
Verify: `curl -fsSL https://example.com/downloads/bongos/version` shows the new version, and `curl -fsSL .../install.sh | sh` installs a working binary.
|
|
58
|
-
|
|
59
|
-
## The binary architecture (why some verbs aren't in it)
|
|
60
|
-
|
|
61
|
-
`bin/bongos.js` is one dispatcher with two runtimes ([task 1248](https://example.com/builders#/task/1248)):
|
|
62
|
-
|
|
63
|
-
- **repo / `npm i -g`** → runs under `node`; spawns each sibling `scripts/bongos/*.js` (unchanged).
|
|
64
|
-
- **compiled binary** → `process.execPath` is the `bongos` binary (no `node`, no scripts on disk), so it dispatches the **bundled on-ramp verbs in-process**: `login`/`setup`, `reauth`, `paste-token`, `api`, `init`, `onboard`. The heavy work verbs (`ship`/`claim`/`start`/`status`/`recall`/`cost`/`release`/`module`/`package-core`) and `doctor` are **not** bundled — they need a full repo + git to run, and pulling their dependency trees in would bloat the binary. Run those in a clone, where `npm i -g` gives the full CLI. A bare-binary user who invokes one gets a clear "get onto a real environment first" message.
|
|
65
|
-
|
|
66
|
-
Follow-ups (not blocking the first release): SHA-pin `oven-sh/setup-bun`, code-sign the mac/win binaries, and expand the bundled verb set if a self-host workflow needs more offline.
|