@bongos/core 1.23.22 → 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 +902 -407
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -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/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 +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- 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 +1 -0
- package/docs/module-api-changelog.md +5 -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/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +34 -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 +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +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/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-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +220 -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 +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +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/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// tests/project_settings_spend_cap_route.mjs — PATCH /project-settings/spend-cap,
|
|
2
|
+
// executed for real (task 1004985, goal 1000094).
|
|
3
|
+
//
|
|
4
|
+
// The spend cap is the project's only monthly ceiling (task 1004712, ADR 0379), and
|
|
5
|
+
// until this file no test performed a write through its route: the neighbours pin
|
|
6
|
+
// refusal sentences and the act-alone inventory, never what the handler WRITES. So
|
|
7
|
+
// every case here asserts two things — the answer the caller got, and exactly what
|
|
8
|
+
// reached setSpendCap (nothing, on every refusal).
|
|
9
|
+
//
|
|
10
|
+
// What is real: the router (modules/lifecycle/routes/project-settings.js), the
|
|
11
|
+
// kernel's requirePermission (auth.js) and requireActAlone (act-alone.js), the
|
|
12
|
+
// body validator, the error envelope, and the pure parser + describer from
|
|
13
|
+
// src/bongos/project-settings.js. What is faked: who the caller is (requireBuilder),
|
|
14
|
+
// the two calls that would reach Postgres (setSpendCap records, resolveSpendCap
|
|
15
|
+
// reads back what was recorded), and the government port — the one place both
|
|
16
|
+
// gates ask "may this builder do this" — so a case can grant or withhold the atom
|
|
17
|
+
// and let the Treasury act alone or send it to a sitting.
|
|
18
|
+
//
|
|
19
|
+
// The doorway is stubbed in require.cache BEFORE the route loads (the
|
|
20
|
+
// tests/access_requests_invite.mjs pattern).
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test tests/project_settings_spend_cap_route.mjs
|
|
23
|
+
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const express = require('express');
|
|
31
|
+
|
|
32
|
+
const apiPath = require.resolve('../src/module-api.js');
|
|
33
|
+
const realApi = require(apiPath);
|
|
34
|
+
const realSettings = require('../src/bongos/project-settings.js');
|
|
35
|
+
const seams = require('../src/module-seams.js');
|
|
36
|
+
|
|
37
|
+
const ATOM = 'project.settings.manage';
|
|
38
|
+
const CALLER = 42;
|
|
39
|
+
|
|
40
|
+
// ── the world each case arranges ─────────────────────────────────────────────
|
|
41
|
+
let held = [ATOM]; // what the government port says the caller holds
|
|
42
|
+
let actAlone = { ok: true }; // what checkActAlone answers
|
|
43
|
+
let setThrows = null; // an Error setSpendCap raises, or null
|
|
44
|
+
let writes = []; // every setSpendCap(usd, builderId) call
|
|
45
|
+
let parses = []; // every parseSpendCap(raw) call
|
|
46
|
+
let actAloneAsked = []; // every checkActAlone(area, builderId) call
|
|
47
|
+
let stored = null; // what resolveSpendCap reads back
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
held = [ATOM];
|
|
51
|
+
actAlone = { ok: true };
|
|
52
|
+
setThrows = null;
|
|
53
|
+
writes = [];
|
|
54
|
+
parses = [];
|
|
55
|
+
actAloneAsked = [];
|
|
56
|
+
stored = null;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The government port, as both gates reach it (seams.resolveOptional('government')).
|
|
60
|
+
// It is registered before any request and removed afterwards, so it cannot leak
|
|
61
|
+
// into another file's process — each test file runs in its own node.
|
|
62
|
+
const government = {
|
|
63
|
+
async resolveBuilderPermissions(builderId) { return builderId === CALLER ? held : []; },
|
|
64
|
+
async checkActAlone(area, builderId) {
|
|
65
|
+
actAloneAsked.push({ area, builderId });
|
|
66
|
+
if (actAlone instanceof Error) throw actAlone;
|
|
67
|
+
return actAlone;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
require.cache[apiPath].exports = {
|
|
72
|
+
...realApi,
|
|
73
|
+
requireBuilder: (req, _res, next) => { req.builder = { id: CALLER, github_login: 'treasurer' }; next(); },
|
|
74
|
+
projectSettings: {
|
|
75
|
+
...realApi.projectSettings,
|
|
76
|
+
// The REAL parser, recorded, so "the parser's own reason" is the parser's own words.
|
|
77
|
+
parseSpendCap: (raw) => { parses.push(raw); return realSettings.parseSpendCap(raw); },
|
|
78
|
+
setSpendCap: async (usd, builderId) => {
|
|
79
|
+
writes.push({ usd, builderId });
|
|
80
|
+
if (setThrows) throw setThrows;
|
|
81
|
+
stored = usd;
|
|
82
|
+
return usd;
|
|
83
|
+
},
|
|
84
|
+
resolveSpendCap: async () => ({ on: stored != null, usd: stored, source: stored == null ? 'default' : 'project_settings', invalid: null }),
|
|
85
|
+
describeSpendCap: realSettings.describeSpendCap,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const buildRouter = require('../modules/lifecycle/routes/project-settings.js');
|
|
90
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
91
|
+
|
|
92
|
+
const app = express();
|
|
93
|
+
app.use(express.json());
|
|
94
|
+
app.use(attachFail);
|
|
95
|
+
app.use('/api/gds', buildRouter());
|
|
96
|
+
|
|
97
|
+
let server, port;
|
|
98
|
+
before(async () => {
|
|
99
|
+
seams.registerProvider('government', government);
|
|
100
|
+
server = http.createServer(app);
|
|
101
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
102
|
+
port = server.address().port;
|
|
103
|
+
});
|
|
104
|
+
after(async () => {
|
|
105
|
+
await new Promise((r) => server.close(r));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function patch(body) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const payload = JSON.stringify(body);
|
|
111
|
+
const req = http.request({
|
|
112
|
+
host: '127.0.0.1', port, path: '/api/gds/project-settings/spend-cap', method: 'PATCH',
|
|
113
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) },
|
|
114
|
+
}, (res) => {
|
|
115
|
+
let data = '';
|
|
116
|
+
res.on('data', (c) => { data += c; });
|
|
117
|
+
res.on('end', () => resolve({ status: res.statusCode, json: data ? JSON.parse(data) : null }));
|
|
118
|
+
});
|
|
119
|
+
req.on('error', reject);
|
|
120
|
+
req.write(payload);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── setting and clearing ─────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
test('{usd: 50} sets the cap for the caller and answers it back as on', async () => {
|
|
128
|
+
const r = await patch({ usd: 50 });
|
|
129
|
+
assert.equal(r.status, 200);
|
|
130
|
+
assert.deepEqual(writes, [{ usd: 50, builderId: CALLER }], 'one write, the parsed dollars, by the caller');
|
|
131
|
+
assert.equal(r.json.ok, true);
|
|
132
|
+
assert.equal(r.json.on, true);
|
|
133
|
+
assert.equal(r.json.usd, 50);
|
|
134
|
+
assert.equal(r.json.sentence, realSettings.describeSpendCap({ on: true, usd: 50 }), 'the describer’s own sentence, not a re-typed one');
|
|
135
|
+
assert.deepEqual(parses, [50], 'a number goes through the shared parser');
|
|
136
|
+
assert.deepEqual(actAloneAsked, [{ area: 'treasury', builderId: CALLER }], 'the Treasury’s row decides, asked about this caller');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('{usd: null} turns the cap off — setSpendCap(null), and the answer says off', async () => {
|
|
140
|
+
stored = 50;
|
|
141
|
+
const r = await patch({ usd: null });
|
|
142
|
+
assert.equal(r.status, 200);
|
|
143
|
+
assert.deepEqual(writes, [{ usd: null, builderId: CALLER }]);
|
|
144
|
+
assert.equal(r.json.on, false);
|
|
145
|
+
assert.equal(r.json.usd, null);
|
|
146
|
+
assert.equal(r.json.sentence, realSettings.describeSpendCap({ on: false, usd: null }));
|
|
147
|
+
assert.deepEqual(parses, [], 'null is the off switch, never handed to the parser');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── bad bodies: refused, nothing written ─────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
test('{} → 400 bad_spend_cap and no write', async () => {
|
|
153
|
+
const r = await patch({});
|
|
154
|
+
assert.equal(r.status, 400);
|
|
155
|
+
assert.equal(r.json.error.code, 'bad_spend_cap');
|
|
156
|
+
// The route's own sentence, not the parser's: a missing usd is refused before any
|
|
157
|
+
// parse, so an absent field can never be read as "turn the cap off".
|
|
158
|
+
assert.equal(r.json.error.message, 'Send a number of dollars a month, or nothing to turn the cap off.');
|
|
159
|
+
assert.deepEqual(parses, [], 'refused before the parser is consulted');
|
|
160
|
+
assert.deepEqual(writes, []);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("{usd: 'lots'} → 400 carrying the parser's own reason, and no write", async () => {
|
|
164
|
+
const r = await patch({ usd: 'lots' });
|
|
165
|
+
assert.equal(r.status, 400);
|
|
166
|
+
assert.equal(r.json.error.code, 'bad_spend_cap');
|
|
167
|
+
assert.deepEqual(parses, ['lots']);
|
|
168
|
+
const own = realSettings.parseSpendCap('lots').reason;
|
|
169
|
+
assert.ok(own, 'the parser refuses it');
|
|
170
|
+
assert.equal(r.json.error.message, own);
|
|
171
|
+
assert.deepEqual(writes, []);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── who may do it ────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
test('a caller without project.settings.manage → 403 and no write, set or clear', async () => {
|
|
177
|
+
held = ['some.other.atom'];
|
|
178
|
+
for (const body of [{ usd: 50 }, { usd: null }]) {
|
|
179
|
+
const r = await patch(body);
|
|
180
|
+
assert.equal(r.status, 403, JSON.stringify(body));
|
|
181
|
+
assert.equal(r.json.error.code, 'permission_forbidden');
|
|
182
|
+
assert.match(r.json.error.message, /project\.settings\.manage/);
|
|
183
|
+
}
|
|
184
|
+
assert.deepEqual(writes, []);
|
|
185
|
+
assert.deepEqual(actAloneAsked, [], 'the atom gate refuses before the Treasury is even asked');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('authority that cannot be established (the resolver throws) → 403 and no write', async () => {
|
|
189
|
+
held = null;
|
|
190
|
+
government.resolveBuilderPermissions = async () => { throw new Error('resolver down'); };
|
|
191
|
+
try {
|
|
192
|
+
const r = await patch({ usd: 50 });
|
|
193
|
+
assert.equal(r.status, 403);
|
|
194
|
+
assert.equal(r.json.error.code, 'permission_forbidden');
|
|
195
|
+
assert.deepEqual(writes, []);
|
|
196
|
+
} finally {
|
|
197
|
+
government.resolveBuilderPermissions = async (builderId) => (builderId === CALLER ? held : []);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("requireActAlone('treasury') sending it to a sitting → its 409 refusal and no write", async () => {
|
|
202
|
+
actAlone = {
|
|
203
|
+
ok: false, reason: 'decided_by_sitting', area: 'treasury', rule: 'sitting',
|
|
204
|
+
who: 'the Treasury’s sitting', what_this_means: 'put it to a vote', open_with: '/government/treasury',
|
|
205
|
+
};
|
|
206
|
+
for (const body of [{ usd: 50 }, { usd: null }]) {
|
|
207
|
+
const r = await patch(body);
|
|
208
|
+
assert.equal(r.status, 409, JSON.stringify(body));
|
|
209
|
+
assert.equal(r.json.error.code, 'decided_by_sitting');
|
|
210
|
+
assert.equal(r.json.error.details.area, 'treasury');
|
|
211
|
+
assert.equal(r.json.error.details.open_with, '/government/treasury', 'the gate’s own refusal, passed through');
|
|
212
|
+
}
|
|
213
|
+
assert.deepEqual(writes, []);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('a Treasury rule that cannot be read → 503 and no write (fail closed)', async () => {
|
|
217
|
+
actAlone = new Error('rule table unreadable');
|
|
218
|
+
const r = await patch({ usd: 50 });
|
|
219
|
+
assert.equal(r.status, 503);
|
|
220
|
+
assert.equal(r.json.error.code, 'decision_rule_unreadable');
|
|
221
|
+
assert.deepEqual(writes, []);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── the write failing ────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
test('setSpendCap throwing → 500 project_settings_write_failed, the error’s text kept off the wire', async () => {
|
|
227
|
+
setThrows = new Error('relation project_settings: connection reset SECRET-ISH-DETAIL');
|
|
228
|
+
const r = await patch({ usd: 50 });
|
|
229
|
+
assert.equal(r.status, 500);
|
|
230
|
+
assert.equal(r.json.error.code, 'project_settings_write_failed');
|
|
231
|
+
assert.equal(r.json.error.message, 'internal error');
|
|
232
|
+
assert.doesNotMatch(JSON.stringify(r.json), /SECRET-ISH-DETAIL|connection reset/, 'the database error never reaches the caller');
|
|
233
|
+
assert.equal(r.json.ok, undefined, 'no success shape rides on a failure');
|
|
234
|
+
assert.deepEqual(writes, [{ usd: 50, builderId: CALLER }], 'it was attempted once, not retried');
|
|
235
|
+
});
|
|
@@ -240,7 +240,35 @@ await test('a throwing pool is reported, not propagated (a sweep can never crash
|
|
|
240
240
|
await test('the sweep runs on the tick, fire-and-forget like its siblings', async () => {
|
|
241
241
|
const src = readFileSync(new URL('../modules/lifecycle/publish-reconciler.js', import.meta.url), 'utf8')
|
|
242
242
|
.replace(/\r\n/g, '\n');
|
|
243
|
-
|
|
243
|
+
// task 1004924: the tick runs a guarded list (buildTick skips a sweep still in
|
|
244
|
+
// flight), so the wiring is an entry in SWEEPS, beside runCriterionSweep.
|
|
245
|
+
// task 1004965: the promotion sweep runs HOURLY; the criterion sweep keeps the tick.
|
|
246
|
+
assert.match(src, /\['promotion sweep', runPromotionSweep, HOURLY_MS\]/, 'wired into the tick, hourly');
|
|
247
|
+
assert.match(src, /\['criterion sweep', runCriterionSweep\]/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await test('cadence per sweep: publish, cost and criterion every tick; promotion and craft backfill hourly (task 1004965)', async () => {
|
|
251
|
+
const byName = new Map(reconciler.SWEEPS.map(([name, , everyMs]) => [name, everyMs]));
|
|
252
|
+
assert.equal(reconciler.HOURLY_MS, 60 * 60_000);
|
|
253
|
+
for (const name of ['sweep', 'cost sweep', 'criterion sweep']) assert.equal(byName.get(name), undefined, `${name} keeps the tick`);
|
|
254
|
+
for (const name of ['promotion sweep', 'craft achievement backfill']) assert.equal(byName.get(name), reconciler.HOURLY_MS, `${name} is hourly`);
|
|
255
|
+
|
|
256
|
+
// Drive buildTick through 2 hours of 5-minute ticks on a fake clock.
|
|
257
|
+
const runs = { fast: 0, hourlyAt: [] };
|
|
258
|
+
let t = 0;
|
|
259
|
+
let i = 0;
|
|
260
|
+
const sweeps = [
|
|
261
|
+
['fast', async () => { runs.fast += 1; }],
|
|
262
|
+
['hourly', async () => { runs.hourlyAt.push(i); }, reconciler.HOURLY_MS],
|
|
263
|
+
];
|
|
264
|
+
const tick = reconciler.buildTick({ logFn: () => {}, errFn: () => {} }, sweeps, { now: () => t, tickMs: 5 * 60_000 });
|
|
265
|
+
for (i = 0; i < 24; i++) {
|
|
266
|
+
tick();
|
|
267
|
+
await new Promise((r) => setImmediate(r)); // let the fire-and-forget runs settle
|
|
268
|
+
t += 5 * 60_000 - (i % 2 ? 3 : 0); // a few ms early on some ticks: timer jitter
|
|
269
|
+
}
|
|
270
|
+
assert.equal(runs.fast, 24, 'every tick');
|
|
271
|
+
assert.deepEqual(runs.hourlyAt, [0, 12], 'on the first tick and exactly 12 ticks (an hour) later — jitter never costs it a tick');
|
|
244
272
|
});
|
|
245
273
|
|
|
246
274
|
// ---------- Summary ----------
|
package/tests/provision.mjs
CHANGED
|
@@ -1628,7 +1628,8 @@ t('webEnvBody writes the BARE publish-credential vars, pem flattened to one env
|
|
|
1628
1628
|
});
|
|
1629
1629
|
// BARE names (github-push.js reads process.env.GITHUB_APP_ID, no instance prefix)
|
|
1630
1630
|
assert.match(body, /^GITHUB_APP_ID=42$/m);
|
|
1631
|
-
|
|
1631
|
+
// double-quoted so systemd keeps the \n (task 1005009; tests/github_one_trip.mjs parses it as systemd does)
|
|
1632
|
+
assert.match(body, /^GITHUB_APP_PRIVATE_KEY="-----BEGIN KEY-----\\nAAA\\nBBB\\n-----END KEY-----\\n"$/m);
|
|
1632
1633
|
assert.doesNotMatch(body, /GITHUB_APP_INSTALLATION_ID/, 'installation id is discovered at runtime, never placed');
|
|
1633
1634
|
// pem missing (older Apps) → no half-written credential
|
|
1634
1635
|
const noPem = P.webEnvBody(inst, { clientId: 'Iv1.abc123def456ghi7', clientSecret: 's'.repeat(40), appId: 42, appPem: null });
|
|
@@ -1683,7 +1684,7 @@ await ta('placeManifestCreds (--apply): converts, rewrites web.env with the cred
|
|
|
1683
1684
|
assert.match(env.body, /_GITHUB_CLIENT_SECRET=z{40}/);
|
|
1684
1685
|
// …and the publish credential (task 1003049): App id + one-line pem, bare names
|
|
1685
1686
|
assert.match(env.body, /^GITHUB_APP_ID=9$/m, 'the App id is placed as the publish credential');
|
|
1686
|
-
assert.match(env.body, /^GITHUB_APP_PRIVATE_KEY
|
|
1687
|
+
assert.match(env.body, /^GITHUB_APP_PRIVATE_KEY="-----BEGIN KEY-----\\nAAA\\n-----END KEY-----\\n"$/m, 'the pem is placed env-flattened, double-quoted (task 1005009)');
|
|
1687
1688
|
// the unit was restarted, and /healthz was probed
|
|
1688
1689
|
assert.ok(cmds.some((c) => /systemctl restart mercury/.test(c)), 'restarts the unit');
|
|
1689
1690
|
assert.ok(cmds.some((c) => /\/healthz/.test(c)), 'probes healthz');
|
|
@@ -219,6 +219,24 @@ await ta('the knocks run a few at a time, never all at once and never one by one
|
|
|
219
219
|
assert.deepEqual(out, { checked: 12, live: 12 });
|
|
220
220
|
assert.equal(peak, 5);
|
|
221
221
|
});
|
|
222
|
+
await ta('cadence: every 5 minutes, not every minute (task 1004960)', () => {
|
|
223
|
+
assert.equal(liveness.INTERVAL_MS, 5 * 60_000);
|
|
224
|
+
});
|
|
225
|
+
await ta('a run that starts while the previous sweep is in flight is skipped (task 1004960)', async () => {
|
|
226
|
+
let reads = 0;
|
|
227
|
+
let release;
|
|
228
|
+
const gate = new Promise((r) => { release = r; });
|
|
229
|
+
const db = { query: async () => { reads++; await gate; return { rows: [] }; } };
|
|
230
|
+
const first = liveness.tick({ pool: db, log: () => {} });
|
|
231
|
+
const secondRun = liveness.tick({ pool: db, log: () => {} }); // fires while the first is still reading
|
|
232
|
+
release();
|
|
233
|
+
const [firstOut, second] = await Promise.all([first, secondRun]);
|
|
234
|
+
assert.deepEqual(second, { skipped: true });
|
|
235
|
+
assert.deepEqual(firstOut, { checked: 0, live: 0 });
|
|
236
|
+
assert.equal(reads, 1, 'the overlapping run never touched the database');
|
|
237
|
+
assert.deepEqual(await liveness.tick({ pool: { query: async () => ({ rows: [] }) }, log: () => {} }), { checked: 0, live: 0 },
|
|
238
|
+
'once the first finished, the next run goes ahead');
|
|
239
|
+
});
|
|
222
240
|
await ta('the module starts the poller', () => {
|
|
223
241
|
const manifest = JSON.parse(readFileSync(new URL('../modules/provisioning/module.json', import.meta.url), 'utf8'));
|
|
224
242
|
assert.ok(manifest.contributes.pollers.includes('app-liveness'));
|
|
@@ -130,6 +130,59 @@ await ta('GitHub unreachable → ADMITTED, matching the runner: an outage must n
|
|
|
130
130
|
assert.equal(await adoptCredentialFault(adopt(), deps({ throws: 'github_error' })), null);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
// ── pre-launch (stealth) keeps project code private (task 1004998) ──────────────
|
|
134
|
+
|
|
135
|
+
await ta('pre-launch ON: a PUBLIC repo is refused 409 public_repo_prelaunch, saying how to proceed', async () => {
|
|
136
|
+
const d = { ...deps({ repo: { private: false, admin: true } }), prelaunchActive: () => true };
|
|
137
|
+
const fault = await adoptCredentialFault(adopt(), d);
|
|
138
|
+
assert.equal(fault && fault.code, 'public_repo_prelaunch');
|
|
139
|
+
assert.equal(fault.status, 409);
|
|
140
|
+
assert.match(fault.message, /octo-owner\/demo is public/);
|
|
141
|
+
assert.match(fault.message, /private on GitHub first/, 'names the way forward');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await ta('pre-launch ON: a PRIVATE repo with admin still adopts, exactly as today', async () => {
|
|
145
|
+
const d = { ...deps({ repo: { private: true, admin: true } }), prelaunchActive: () => true };
|
|
146
|
+
assert.equal(await adoptCredentialFault(adopt(), d), null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await ta('pre-launch ON: the admin refusal stops offering "make it public" as the way out', async () => {
|
|
150
|
+
const d = { ...deps({ repo: { private: true, admin: false } }), prelaunchActive: () => true };
|
|
151
|
+
const fault = await adoptCredentialFault(adopt(), d);
|
|
152
|
+
assert.equal(fault && fault.code, 'repo_admin_required');
|
|
153
|
+
assert.doesNotMatch(fault.message, /make the repository public/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await ta('pre-launch ON: a GitHub hiccup still ADMITS — no positive read that the repo is public', async () => {
|
|
157
|
+
const d = { ...deps({ throws: 'github_error' }), prelaunchActive: () => true };
|
|
158
|
+
assert.equal(await adoptCredentialFault(adopt(), d), null);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await ta('pre-launch OFF (or not wired): a public repo adopts as before', async () => {
|
|
162
|
+
assert.equal(await adoptCredentialFault(adopt(), { ...deps(), prelaunchActive: () => false }), null);
|
|
163
|
+
assert.equal(await adoptCredentialFault(adopt(), deps()), null);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await ta('the doorway hands the module the REAL gate switch, read from the env each call', async () => {
|
|
167
|
+
const api = require('../src/module-api.js');
|
|
168
|
+
const saved = [process.env.PRELAUNCH_PASSWORD, process.env.STEALTH_PASSWORD];
|
|
169
|
+
try {
|
|
170
|
+
delete process.env.PRELAUNCH_PASSWORD; delete process.env.STEALTH_PASSWORD;
|
|
171
|
+
assert.equal(api.prelaunchActive(), false);
|
|
172
|
+
process.env.STEALTH_PASSWORD = 'x'; // the deprecated alias live still sets
|
|
173
|
+
assert.equal(api.prelaunchActive(), true);
|
|
174
|
+
} finally {
|
|
175
|
+
if (saved[0] === undefined) delete process.env.PRELAUNCH_PASSWORD; else process.env.PRELAUNCH_PASSWORD = saved[0];
|
|
176
|
+
if (saved[1] === undefined) delete process.env.STEALTH_PASSWORD; else process.env.STEALTH_PASSWORD = saved[1];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await ta('the create route WIRES the switch in — without it the check above is never asked', async () => {
|
|
181
|
+
const src = require('node:fs').readFileSync(require.resolve('../modules/provisioning/routes/provisioning.js'), 'utf8');
|
|
182
|
+
const call = src.slice(src.indexOf('await adoptCredentialFault('), src.indexOf('if (credFault)'));
|
|
183
|
+
assert.match(call, /prelaunchActive:\s*api\.prelaunchActive/);
|
|
184
|
+
});
|
|
185
|
+
|
|
133
186
|
// ── the parse must not drift from the runner's ──────────────────────────────────
|
|
134
187
|
|
|
135
188
|
await ta('parseTargetRef accepts every form the runner accepts, and rejects what it rejects', async () => {
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// tests/provisioning_env_manifest_edit_delete.mjs — PATCH and DELETE
|
|
2
|
+
// /provisioning/instances/:id/env-manifest/:varId, executed for real (task 1004986,
|
|
3
|
+
// goal 1000094). The POST path is tests/provisioning_env_manifest.mjs's.
|
|
4
|
+
//
|
|
5
|
+
// The two write routes after a declaration exists had no test driving them, and each
|
|
6
|
+
// carries a promise a regression would break quietly:
|
|
7
|
+
// - a VALUE is refused out loud, never stored (the manifest holds names only);
|
|
8
|
+
// - a variable id that belongs to ANOTHER project answers exactly like an id that
|
|
9
|
+
// does not exist, so a guessed id never confirms what someone else declared;
|
|
10
|
+
// - a builder who neither owns the project nor is an Archon is turned away before
|
|
11
|
+
// any data call, with the same 404 a missing project gets;
|
|
12
|
+
// - the project the data call is scoped to comes from the owner gate's loaded row,
|
|
13
|
+
// never from anything the caller put in the body.
|
|
14
|
+
// Every refusal below asserts that NO data call was made.
|
|
15
|
+
//
|
|
16
|
+
// What is real: the router (modules/provisioning/routes/env-manifest.js), the owner
|
|
17
|
+
// gate (route-helpers.js requireProjectOwner), parseVarId, the value refusal, the
|
|
18
|
+
// strict body validator and the kernel error envelope. What is faked: who the caller
|
|
19
|
+
// is (requireBuilder), the project row (getInstanceById: project 1, owned by builder
|
|
20
|
+
// 7), and the two data calls, which RECORD their arguments and answer the way the SQL
|
|
21
|
+
// does — a row only when the declaration's instance_id matches the scoped project.
|
|
22
|
+
//
|
|
23
|
+
// Run: node --test tests/provisioning_env_manifest_edit_delete.mjs
|
|
24
|
+
|
|
25
|
+
import assert from 'node:assert/strict';
|
|
26
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
27
|
+
import http from 'node:http';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const express = require('express');
|
|
32
|
+
|
|
33
|
+
const api = require('../src/module-api.js');
|
|
34
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
35
|
+
const envManifest = require('../modules/provisioning/env-manifest.js');
|
|
36
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
37
|
+
|
|
38
|
+
// ── the cast ─────────────────────────────────────────────────────────────────
|
|
39
|
+
const OWNER = { id: 7, rank: 'xenos', github_login: 'owner' };
|
|
40
|
+
const STRANGER = { id: 8, rank: 'metic', github_login: 'stranger' };
|
|
41
|
+
const ARCHON = { id: 9, rank: 'archon', github_login: 'archon' };
|
|
42
|
+
const PROJECT_1 = Object.freeze({ id: 1, owner_builder_id: 7, slug: 'lantern', status: 'active', hosting_shape: 'byo-host' });
|
|
43
|
+
|
|
44
|
+
// Declarations as the table holds them: 5 is project 1's, 6 is project 2's.
|
|
45
|
+
const DECLARED = new Map([
|
|
46
|
+
['5', { id: 5, instance_id: 1, name: 'DATABASE_URL', description: 'old text', environments: ['production'], required: true, created_at: 'c', updated_at: 'u' }],
|
|
47
|
+
['6', { id: 6, instance_id: 2, name: 'OTHER_PROJECTS_VAR', description: 'theirs', environments: ['production'], required: true, created_at: 'c', updated_at: 'u' }],
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
let caller = OWNER;
|
|
51
|
+
let loads = [];
|
|
52
|
+
let updates = [];
|
|
53
|
+
let removes = [];
|
|
54
|
+
|
|
55
|
+
beforeEach(() => { caller = OWNER; loads = []; updates = []; removes = []; });
|
|
56
|
+
|
|
57
|
+
api.requireBuilder = (req, _res, next) => { req.builder = caller; next(); };
|
|
58
|
+
provisioning.getInstanceById = async (_db, id) => {
|
|
59
|
+
loads.push(String(id));
|
|
60
|
+
return String(id) === '1' ? { ...PROJECT_1 } : null;
|
|
61
|
+
};
|
|
62
|
+
// The WHERE instance_id = $1 AND id = $2 the real SQL applies, applied here.
|
|
63
|
+
const scoped = (instanceId, varId) => {
|
|
64
|
+
const row = DECLARED.get(String(varId));
|
|
65
|
+
return row && String(row.instance_id) === String(instanceId) ? row : null;
|
|
66
|
+
};
|
|
67
|
+
envManifest.updateEnvVar = async (db, instanceId, varId, patch) => {
|
|
68
|
+
updates.push({ db, instanceId, varId, patch: { ...patch } });
|
|
69
|
+
const row = scoped(instanceId, varId);
|
|
70
|
+
return row ? envManifest.envVarRow({ ...row, ...patch }) : null;
|
|
71
|
+
};
|
|
72
|
+
envManifest.removeEnvVar = async (db, instanceId, varId) => {
|
|
73
|
+
removes.push({ db, instanceId, varId });
|
|
74
|
+
return !!scoped(instanceId, varId);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const buildRouter = require('../modules/provisioning/routes/env-manifest.js');
|
|
78
|
+
const app = express();
|
|
79
|
+
app.use(express.json());
|
|
80
|
+
app.use(attachFail);
|
|
81
|
+
app.use('/api/bongos', buildRouter());
|
|
82
|
+
|
|
83
|
+
let server, base;
|
|
84
|
+
before(async () => {
|
|
85
|
+
server = http.createServer(app);
|
|
86
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
87
|
+
base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
88
|
+
});
|
|
89
|
+
after(async () => { await new Promise((r) => server.close(r)); });
|
|
90
|
+
|
|
91
|
+
function call(method, path, body) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const payload = body === undefined ? null : JSON.stringify(body);
|
|
94
|
+
const headers = payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } : {};
|
|
95
|
+
const req = http.request(`${base}${path}`, { method, headers }, (res) => {
|
|
96
|
+
let raw = '';
|
|
97
|
+
res.on('data', (c) => { raw += c; });
|
|
98
|
+
res.on('end', () => { let json = null; try { json = JSON.parse(raw); } catch { /* not JSON */ } resolve({ status: res.statusCode, json, raw }); });
|
|
99
|
+
});
|
|
100
|
+
req.on('error', reject);
|
|
101
|
+
if (payload) req.write(payload);
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const varUrl = (varId, project = 1) => `/provisioning/instances/${project}/env-manifest/${varId}`;
|
|
106
|
+
|
|
107
|
+
// Obviously fake: the shape of a provider key, none of its substance.
|
|
108
|
+
const FAKE_SECRET = 'sk-FAKE-not-a-real-key-000000';
|
|
109
|
+
|
|
110
|
+
// ── PATCH ────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
test('PATCH {value} → 400 value_not_accepted, the value never echoed, no updateEnvVar call', async () => {
|
|
113
|
+
const r = await call('PATCH', varUrl(5), { value: FAKE_SECRET });
|
|
114
|
+
assert.equal(r.status, 400);
|
|
115
|
+
assert.equal(r.json.error.code, 'value_not_accepted', 'the value refusal speaks, not the generic validator');
|
|
116
|
+
assert.doesNotMatch(r.raw, /sk-FAKE/, 'the secret is not reflected back');
|
|
117
|
+
assert.deepEqual(updates, []);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('PATCH carrying a value under every spelling the manifest knows, beside a legal edit → refused, no call', async () => {
|
|
121
|
+
for (const field of envManifest.VALUE_FIELDS) {
|
|
122
|
+
const r = await call('PATCH', varUrl(5), { description: 'fine', [field]: FAKE_SECRET });
|
|
123
|
+
assert.equal(r.status, 400, field);
|
|
124
|
+
assert.equal(r.json.error.code, 'value_not_accepted', field);
|
|
125
|
+
}
|
|
126
|
+
assert.deepEqual(updates, []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("PATCH another project's varId → 404 env_var_not_found, byte-identical to a varId that does not exist", async () => {
|
|
130
|
+
const foreign = await call('PATCH', varUrl(6), { description: 'mine now' });
|
|
131
|
+
const missing = await call('PATCH', varUrl(999), { description: 'mine now' });
|
|
132
|
+
assert.equal(foreign.status, 404);
|
|
133
|
+
assert.equal(foreign.json.error.code, 'env_var_not_found');
|
|
134
|
+
assert.equal(foreign.raw, missing.raw, 'a guessed id confirms nothing about another project');
|
|
135
|
+
assert.doesNotMatch(foreign.raw, /OTHER_PROJECTS_VAR|theirs/);
|
|
136
|
+
assert.deepEqual(updates.map((u) => u.instanceId), [1, 1], 'both scoped to the caller’s project, never to the id’s owner');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('PATCH as builder 8 (not owner, not Archon) → 404 instance_not_found and no updateEnvVar call', async () => {
|
|
140
|
+
caller = STRANGER;
|
|
141
|
+
const r = await call('PATCH', varUrl(5), { description: 'x' });
|
|
142
|
+
assert.equal(r.status, 404);
|
|
143
|
+
assert.equal(r.json.error.code, 'instance_not_found');
|
|
144
|
+
assert.deepEqual(updates, []);
|
|
145
|
+
// …and byte-identical to a project that does not exist, so the id is not confirmed.
|
|
146
|
+
const none = await call('PATCH', varUrl(5, 77), { description: 'x' });
|
|
147
|
+
assert.equal(r.raw, none.raw);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('PATCH as an Archon on project 1 → 200, scoped to project 1', async () => {
|
|
151
|
+
caller = ARCHON;
|
|
152
|
+
const r = await call('PATCH', varUrl(5), { description: 'archon fixed it' });
|
|
153
|
+
assert.equal(r.status, 200, r.raw);
|
|
154
|
+
assert.equal(r.json.env_var.description, 'archon fixed it');
|
|
155
|
+
assert.equal(r.json.values_held, false);
|
|
156
|
+
assert.equal(updates.length, 1);
|
|
157
|
+
assert.equal(updates[0].instanceId, 1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('PATCH with only description → environments (and the rest) absent from the update call', async () => {
|
|
161
|
+
const r = await call('PATCH', varUrl(5), { description: 'new text' });
|
|
162
|
+
assert.equal(r.status, 200, r.raw);
|
|
163
|
+
assert.equal(updates.length, 1);
|
|
164
|
+
const u = updates[0];
|
|
165
|
+
assert.equal(u.db, api.pool, 'the module pool, not something the request supplied');
|
|
166
|
+
assert.equal(u.instanceId, 1);
|
|
167
|
+
assert.equal(u.varId, '5');
|
|
168
|
+
assert.deepEqual(Object.keys(u.patch), ['description']);
|
|
169
|
+
assert.equal(Object.prototype.hasOwnProperty.call(u.patch, 'environments'), false, 'an omitted field is left alone (merge-patch)');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('PATCH a malformed varId → 400 bad_id and no call', async () => {
|
|
173
|
+
const r = await call('PATCH', varUrl('5abc'), { description: 'x' });
|
|
174
|
+
assert.equal(r.status, 400);
|
|
175
|
+
assert.equal(r.json.error.code, 'bad_id');
|
|
176
|
+
assert.deepEqual(updates, []);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── DELETE ───────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
test('DELETE → removeEnvVar(pool, 1, varId): the project id from the gate, never the body', async () => {
|
|
182
|
+
const r = await call('DELETE', varUrl(5), { instance_id: 2, instanceId: 2, id: 2 });
|
|
183
|
+
assert.equal(r.status, 200, r.raw);
|
|
184
|
+
assert.equal(r.json.removed, true);
|
|
185
|
+
assert.equal(removes.length, 1);
|
|
186
|
+
assert.equal(removes[0].db, api.pool);
|
|
187
|
+
assert.equal(removes[0].instanceId, 1, 'the loaded row’s id, not the body’s 2');
|
|
188
|
+
assert.equal(removes[0].varId, '5');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("DELETE another project's varId → 404 env_var_not_found, byte-identical to a missing one", async () => {
|
|
192
|
+
const foreign = await call('DELETE', varUrl(6));
|
|
193
|
+
const missing = await call('DELETE', varUrl(999));
|
|
194
|
+
assert.equal(foreign.status, 404);
|
|
195
|
+
assert.equal(foreign.json.error.code, 'env_var_not_found');
|
|
196
|
+
assert.equal(foreign.raw, missing.raw);
|
|
197
|
+
assert.deepEqual(removes.map((x) => x.instanceId), [1, 1]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('DELETE as a non-owner → 404 instance_not_found before any call', async () => {
|
|
201
|
+
caller = STRANGER;
|
|
202
|
+
const r = await call('DELETE', varUrl(5));
|
|
203
|
+
assert.equal(r.status, 404);
|
|
204
|
+
assert.equal(r.json.error.code, 'instance_not_found');
|
|
205
|
+
assert.deepEqual(removes, []);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('DELETE as an Archon on project 1 → 200, scoped to project 1', async () => {
|
|
209
|
+
caller = ARCHON;
|
|
210
|
+
const r = await call('DELETE', varUrl(5));
|
|
211
|
+
assert.equal(r.status, 200, r.raw);
|
|
212
|
+
assert.deepEqual(removes.map((x) => x.instanceId), [1]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('DELETE a malformed varId → 400 bad_id from parseVarId and no call', async () => {
|
|
216
|
+
for (const bad of ['abc', '5;DROP', '-1']) {
|
|
217
|
+
const r = await call('DELETE', varUrl(encodeURIComponent(bad)));
|
|
218
|
+
assert.equal(r.status, 400, bad);
|
|
219
|
+
assert.equal(r.json.error.code, 'bad_id', bad);
|
|
220
|
+
assert.match(r.json.error.message, /varId/, 'parseVarId’s own sentence, naming the field');
|
|
221
|
+
}
|
|
222
|
+
assert.deepEqual(removes, []);
|
|
223
|
+
});
|
|
@@ -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();
|