@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// tests/publish_reconciler_overlap.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004924 — the reconciler's interval tick fired five sweeps every 5 minutes
|
|
4
|
+
// with no check that the previous round had finished, so a slow DB or GitHub call
|
|
5
|
+
// stacked rounds and held several pool connections at once. buildTick now keeps a
|
|
6
|
+
// per-sweep in-flight flag: a sweep still running from the last tick is skipped.
|
|
7
|
+
//
|
|
8
|
+
// Its own file on purpose: tests/publish_reconciler.mjs exits on a fixed 100 ms
|
|
9
|
+
// timer, so a case appended there that waits on a few event-loop turns is cut off
|
|
10
|
+
// before it reports — it neither passes nor fails, it just vanishes.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/publish_reconciler_overlap.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { makeRunner } from './helpers.mjs';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const reconciler = require('../modules/lifecycle/publish-reconciler.js');
|
|
20
|
+
const { test, summary } = makeRunner();
|
|
21
|
+
|
|
22
|
+
const settle = () => new Promise((r) => setImmediate(r));
|
|
23
|
+
|
|
24
|
+
await test('buildTick skips a sweep still in flight, and runs it again once it settles', async () => {
|
|
25
|
+
const calls = { slow: 0, fast: 0, failing: 0 };
|
|
26
|
+
let releaseSlow;
|
|
27
|
+
const sweeps = [
|
|
28
|
+
['slow', () => { calls.slow++; return new Promise((r) => { releaseSlow = r; }); }],
|
|
29
|
+
['fast', async () => { calls.fast++; }],
|
|
30
|
+
['failing', async () => { calls.failing++; throw new Error('boom'); }],
|
|
31
|
+
];
|
|
32
|
+
const logs = [];
|
|
33
|
+
const errs = [];
|
|
34
|
+
const tick = reconciler.buildTick({ log: (m) => logs.push(m), error: (m) => errs.push(m) }, sweeps);
|
|
35
|
+
|
|
36
|
+
tick(); await settle();
|
|
37
|
+
tick(); await settle();
|
|
38
|
+
tick(); await settle();
|
|
39
|
+
assert.equal(calls.slow, 1, 'the slow sweep is never stacked while its first run is pending');
|
|
40
|
+
assert.equal(calls.fast, 3, 'a slow sweep never starves the others');
|
|
41
|
+
assert.equal(calls.failing, 3, 'a rejected run clears its flag too');
|
|
42
|
+
assert.equal(logs.filter((m) => /slow still running/.test(m)).length, 2, 'each skip is logged');
|
|
43
|
+
|
|
44
|
+
releaseSlow(); await settle();
|
|
45
|
+
tick(); await settle();
|
|
46
|
+
assert.equal(calls.slow, 2, 'once the run settles the next tick runs it again');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await test('the default tick covers all five sweeps', () => {
|
|
50
|
+
// A sweep left out of the guarded list would simply never run again.
|
|
51
|
+
const src = require('node:fs').readFileSync(require.resolve('../modules/lifecycle/publish-reconciler.js'), 'utf8');
|
|
52
|
+
// task 1004965: read the exported list, since an entry may now carry a cadence.
|
|
53
|
+
const fns = reconciler.SWEEPS.map(([, fn]) => fn);
|
|
54
|
+
for (const fn of ['runSweep', 'runCostSweep', 'runCriterionSweep', 'runPromotionSweep', 'runCraftAchievementBackfill']) {
|
|
55
|
+
assert.ok(fns.includes(reconciler[fn]), `${fn} is in the tick`);
|
|
56
|
+
}
|
|
57
|
+
assert.match(src, /const tick = buildTick\(deps\);/, 'start() uses the guarded tick');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
summary();
|
|
@@ -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 = [];
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import { strict as assert } from 'node:assert';
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
19
22
|
import { requireHostModule } from './helpers.mjs';
|
|
20
23
|
|
|
21
24
|
const require = createRequire(import.meta.url);
|
|
22
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
decideFreshness, planFetch, viewNote, staleViewLine, lastMainFetchMs, FETCH_FRESH_MS, FETCH_OLD_NOTE_MS,
|
|
27
|
+
} = requireHostModule(require, '../.claude/hooks/session-start.js');
|
|
23
28
|
|
|
24
29
|
let passed = 0;
|
|
25
30
|
let failed = 0;
|
|
@@ -120,5 +125,65 @@ test('dirty and ahead → still just warn (no pull)', () => {
|
|
|
120
125
|
assert.equal(d.action, 'warn');
|
|
121
126
|
});
|
|
122
127
|
|
|
128
|
+
// ---------- the fetch is off the critical path (task 1004928) ----------
|
|
129
|
+
// The hook used to block on `git fetch` (4 s timeout). It now judges against the local
|
|
130
|
+
// origin/main, fetches in the background when that view is old, and says how old it is.
|
|
131
|
+
|
|
132
|
+
const NOW = Date.parse('2026-10-28T12:00:00Z');
|
|
133
|
+
const MIN = 60000;
|
|
134
|
+
|
|
135
|
+
test('planFetch: a view fetched under 10 min ago is used as-is, no fetch', () => {
|
|
136
|
+
const p = planFetch({ lastFetchMs: NOW - 9 * MIN, nowMs: NOW });
|
|
137
|
+
assert.equal(p.background, false);
|
|
138
|
+
assert.equal(p.ageMs, 9 * MIN);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('planFetch: an older view starts a background fetch', () => {
|
|
142
|
+
assert.equal(planFetch({ lastFetchMs: NOW - FETCH_FRESH_MS, nowMs: NOW }).background, true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('planFetch: never fetched (null) → background fetch, age unknown', () => {
|
|
146
|
+
const p = planFetch({ lastFetchMs: null, nowMs: NOW });
|
|
147
|
+
assert.equal(p.background, true);
|
|
148
|
+
assert.equal(p.ageMs, null);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('viewNote: the behind-count names the age of its origin/main', () => {
|
|
152
|
+
assert.equal(viewNote({ ageMs: 4 * MIN, background: false }), 'as fetched 4m ago');
|
|
153
|
+
assert.equal(viewNote({ ageMs: 3 * 60 * MIN, background: true }), 'as fetched 3h ago; a fresh fetch is running in the background');
|
|
154
|
+
assert.match(viewNote({ ageMs: null, background: true }), /time unknown/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('staleViewLine: silent when the view is fresh or merely minutes old', () => {
|
|
158
|
+
assert.equal(staleViewLine({ ageMs: 2 * MIN, background: false }), null);
|
|
159
|
+
assert.equal(staleViewLine({ ageMs: 20 * MIN, background: true }), null);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('staleViewLine: an old or unknown view says "up to date" is unconfirmed instead of staying silent', () => {
|
|
163
|
+
assert.match(staleViewLine({ ageMs: FETCH_OLD_NOTE_MS, background: true }), /last fetched 1h ago.*unconfirmed/);
|
|
164
|
+
assert.match(staleViewLine({ ageMs: null, background: true }), /never been fetched/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('lastMainFetchMs: the newest FETCH_HEAD across worktrees that recorded main wins', () => {
|
|
168
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ss-fetch-'));
|
|
169
|
+
try {
|
|
170
|
+
fs.mkdirSync(path.join(dir, 'worktrees', 'a'), { recursive: true });
|
|
171
|
+
fs.mkdirSync(path.join(dir, 'worktrees', 'b'), { recursive: true });
|
|
172
|
+
const main = "abc\t\tbranch 'main' of https://example.invalid/repo\n";
|
|
173
|
+
const other = "def\t\tbranch 'feature' of https://example.invalid/repo\n";
|
|
174
|
+
const put = (rel, body, t) => { const f = path.join(dir, rel); fs.writeFileSync(f, body); fs.utimesSync(f, t / 1000, t / 1000); };
|
|
175
|
+
put('FETCH_HEAD', main, NOW - 60 * MIN);
|
|
176
|
+
put(path.join('worktrees', 'a', 'FETCH_HEAD'), main, NOW - 5 * MIN);
|
|
177
|
+
put(path.join('worktrees', 'b', 'FETCH_HEAD'), other, NOW - 1 * MIN); // newest, but not main
|
|
178
|
+
assert.equal(Math.round(lastMainFetchMs(dir) / 1000), Math.round((NOW - 5 * MIN) / 1000));
|
|
179
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('lastMainFetchMs: no FETCH_HEAD anywhere → null (never fetched)', () => {
|
|
183
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ss-fetch-'));
|
|
184
|
+
try { assert.equal(lastMainFetchMs(dir), null); } finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
185
|
+
assert.equal(lastMainFetchMs(null), null);
|
|
186
|
+
});
|
|
187
|
+
|
|
123
188
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
124
189
|
process.exit(failed === 0 ? 0 : 1);
|