@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,82 @@
|
|
|
1
|
+
// tests/integration_fixture_ids.mjs — no two real-Postgres tests hand-pick the same
|
|
2
|
+
// fixture id (task 1005008).
|
|
3
|
+
//
|
|
4
|
+
// The integration tests share ONE database and seed rows at fixed ids in the
|
|
5
|
+
// 9xxxxxxx range. Task 1004950's task_needs_db.mjs picked builder ids 90000401-403,
|
|
6
|
+
// which page_tweak_no_block.mjs already used; that test failed part-way and its
|
|
7
|
+
// cleanup could not delete its builders (rows its claims had written still pointed
|
|
8
|
+
// at them), so the next file's INSERT hit a duplicate key and main's integration
|
|
9
|
+
// job went red, blocking every release. An id two files share is a collision
|
|
10
|
+
// waiting for one of them to leave a row behind.
|
|
11
|
+
//
|
|
12
|
+
// The ids already shared when this guard landed are frozen below. The list may
|
|
13
|
+
// only shrink: give a new test ids nobody else uses (grep the range first).
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/integration_fixture_ids.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import url from 'node:url';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const { INTEGRATION, INTEGRATION_PARKED } = require('../scripts/bongos/run-unit-tests.js');
|
|
25
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
// Shared before task 1005008, each by tests that have not collided in practice.
|
|
28
|
+
const KNOWN_SHARED = new Set([
|
|
29
|
+
'90000301', '90000302', '90000303', // claim_gate_open_blocker, clawback_confirmed_backlog, reward_mode_cost_plus_only
|
|
30
|
+
'90000401', // page_tweak_no_block, goal_spec_task_db
|
|
31
|
+
'90000411', // page_tweak_no_block, task_criteria_unlink_db, session_cost_row_raise
|
|
32
|
+
'90000801', '90000802', // acclaim_feed_evidence_db, blocker_hall_live_proof
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function sharedIds() {
|
|
36
|
+
const owners = new Map();
|
|
37
|
+
let scanned = 0;
|
|
38
|
+
for (const name of [...INTEGRATION, ...INTEGRATION_PARKED]) {
|
|
39
|
+
const file = path.join(HERE, `${name}.mjs`);
|
|
40
|
+
if (!fs.existsSync(file)) continue;
|
|
41
|
+
scanned++;
|
|
42
|
+
for (const id of new Set(fs.readFileSync(file, 'utf8').match(/\b9\d{7}\b/g) || [])) {
|
|
43
|
+
if (!owners.has(id)) owners.set(id, []);
|
|
44
|
+
owners.get(id).push(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { scanned, shared: [...owners].filter(([, names]) => names.length > 1) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let passed = 0, failed = 0;
|
|
51
|
+
function t(name, fn) {
|
|
52
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
53
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { scanned, shared } = sharedIds();
|
|
57
|
+
|
|
58
|
+
t('the scan actually read the integration tests', () => {
|
|
59
|
+
assert.ok(scanned >= 40, `expected the integration set on disk, read ${scanned} file(s)`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
t('no new fixture id is shared between two integration tests', () => {
|
|
63
|
+
const fresh = shared.filter(([id]) => !KNOWN_SHARED.has(id));
|
|
64
|
+
assert.deepEqual(fresh.map(([id, names]) => `${id}: ${names.join(', ')}`), [],
|
|
65
|
+
'pick an unused id range for the newer test');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('the frozen list only names ids that are still shared (shrink it when one is fixed)', () => {
|
|
69
|
+
const still = new Set(shared.map(([id]) => id));
|
|
70
|
+
assert.deepEqual([...KNOWN_SHARED].filter((id) => !still.has(id)), []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('task_needs_db uses its own ids (the collision that reddened main)', () => {
|
|
74
|
+
const src = fs.readFileSync(path.join(HERE, 'task_needs_db.mjs'), 'utf8');
|
|
75
|
+
const pt = fs.readFileSync(path.join(HERE, 'page_tweak_no_block.mjs'), 'utf8');
|
|
76
|
+
const ids = (s) => new Set(s.match(/\b9\d{7}\b/g) || []);
|
|
77
|
+
const overlap = [...ids(src)].filter((id) => ids(pt).has(id));
|
|
78
|
+
assert.deepEqual(overlap, []);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(`\nintegration_fixture_ids (task 1005008): ${passed} passed, ${failed} failed`);
|
|
82
|
+
if (failed) process.exitCode = 1;
|
|
@@ -110,7 +110,7 @@ try {
|
|
|
110
110
|
await q(`INSERT INTO builders (id, github_login) VALUES (42, 'owner')`);
|
|
111
111
|
for (const f of ['060_access_requests', 'core_230_access_requests_applicant_vouch',
|
|
112
112
|
'core_254_access_requests_applicant_handle', 'core_255_access_requests_invite_kind',
|
|
113
|
-
'core_266_access_requests_invited_login_idx']) await q(coreSql(f));
|
|
113
|
+
'core_266_access_requests_invited_login_idx', 'core_282_builders_lower_login_idx']) await q(coreSql(f));
|
|
114
114
|
|
|
115
115
|
// ── The instance: the door, the queues, the probe ────────────────────────
|
|
116
116
|
console.log('the instance:');
|
|
@@ -135,6 +135,18 @@ try {
|
|
|
135
135
|
await q('RESET enable_seqscan');
|
|
136
136
|
ok(/idx_access_requests_invited_login/.test(plan), 'the sign-in gate can read invited rows through core_266\'s index');
|
|
137
137
|
|
|
138
|
+
// core_282 (task 1004923): the onboarding routes' "already a member?" probe
|
|
139
|
+
// compares lower(github_login), which only an expression index can serve.
|
|
140
|
+
await q('SET enable_seqscan = off');
|
|
141
|
+
const memberPlan = (await q('EXPLAIN SELECT 1 FROM builders WHERE lower(github_login) = lower($1) LIMIT 1', ['Owner']))
|
|
142
|
+
.rows.map((r) => r['QUERY PLAN']).join('\n');
|
|
143
|
+
await q('RESET enable_seqscan');
|
|
144
|
+
ok(/idx_builders_lower_login/.test(memberPlan), 'the member-by-login probe reads builders through core_282\'s index');
|
|
145
|
+
ok((await q('SELECT 1 FROM builders WHERE lower(github_login) = lower($1)', ['OWNER'])).rowCount === 1,
|
|
146
|
+
'and still matches a login case-insensitively');
|
|
147
|
+
await q(coreSql('core_282_builders_lower_login_idx'));
|
|
148
|
+
ok(true, 'core_282 re-runs cleanly (IF NOT EXISTS, ON CONFLICT DO NOTHING)');
|
|
149
|
+
|
|
138
150
|
const hist = Object.fromEntries((await call('GET', '/api/gds/access-requests?status=invited')).json.requests.map((r) => [r.github_login, r]));
|
|
139
151
|
ok(hist.fay.expired === true && hist.eve.expired === false && hist.cy.expired === false, 'the queue marks only the expired invite');
|
|
140
152
|
ok(hist.cy.expires_at === null && new Date(hist.fay.expires_at) <= new Date() && new Date(hist.eve.expires_at) > new Date(),
|
|
@@ -39,10 +39,6 @@ const fitness = require(path.join(GDS, 'fitness.js'));
|
|
|
39
39
|
const SMOKES = [
|
|
40
40
|
'lib.sh',
|
|
41
41
|
'smoke-write.sh',
|
|
42
|
-
'smoke-batch-claim.sh',
|
|
43
|
-
'smoke-batch-release.sh',
|
|
44
|
-
'smoke-claim-consistency.sh',
|
|
45
|
-
'smoke-dependencies.sh',
|
|
46
42
|
'portability-smoke-memory.sh',
|
|
47
43
|
];
|
|
48
44
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// tests/market_watch_data.mjs — the Market Watch data layer without a database (task
|
|
2
|
+
// 1004936, goal 1000137): the seed built from the committed research files, the hub-only
|
|
3
|
+
// and idempotent seed write, the pure derivations behind levels, streaks, sparklines and
|
|
4
|
+
// the settings gauge, the read queries' project scoping, and the migration's own rules.
|
|
5
|
+
//
|
|
6
|
+
// The SQL itself runs against a real Postgres in tests/market_watch_data_db.mjs (the
|
|
7
|
+
// INTEGRATION set); this file is the DB-free half and runs in the unit lane.
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const seed = require('../modules/market-watch/seed.js');
|
|
15
|
+
const db = require('../modules/market-watch/db.js');
|
|
16
|
+
const hubSeed = require('../modules/market-watch/pollers/hub-seed.js');
|
|
17
|
+
const MIGRATION = readFileSync(new URL('../modules/market-watch/migrations/market_watch_001_tables.sql', import.meta.url), 'utf8');
|
|
18
|
+
|
|
19
|
+
const market = seed.readSeed('market-data.json');
|
|
20
|
+
const partners = seed.readSeed('partners-data.json');
|
|
21
|
+
const built = seed.buildSeed({ market, partners });
|
|
22
|
+
|
|
23
|
+
// A pool that records every statement and answers each SELECT … id with one row.
|
|
24
|
+
function fakePool({ failOn = null } = {}) {
|
|
25
|
+
const calls = [];
|
|
26
|
+
let id = 0;
|
|
27
|
+
const client = {
|
|
28
|
+
async query(sql, params = []) {
|
|
29
|
+
calls.push({ sql, params });
|
|
30
|
+
if (failOn && failOn.test(sql)) throw new Error('boom');
|
|
31
|
+
if (/^\s*SELECT id FROM/.test(sql)) return { rows: [{ id: ++id }], rowCount: 1 };
|
|
32
|
+
return { rows: [], rowCount: 1 };
|
|
33
|
+
},
|
|
34
|
+
release() { calls.push({ sql: 'RELEASE' }); },
|
|
35
|
+
};
|
|
36
|
+
return { calls, pool: { connect: async () => client, query: client.query } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('the seed carries all 36 researched companies and all 24 partners, scored and sourced', () => {
|
|
40
|
+
assert.equal(market.length, 36);
|
|
41
|
+
assert.equal(partners.length, 24);
|
|
42
|
+
const comps = built.items.filter((i) => i.kind === 'competitor');
|
|
43
|
+
const parts = built.items.filter((i) => i.kind === 'partner');
|
|
44
|
+
assert.equal(comps.length, 36);
|
|
45
|
+
assert.equal(parts.length, 24);
|
|
46
|
+
for (const it of built.items) {
|
|
47
|
+
const rubricKeys = seed.RUBRIC[it.kind].map((d) => d.key);
|
|
48
|
+
assert.deepEqual(Object.keys(it.scores).sort(), [...rubricKeys, 'overall'].sort(), `${it.name}: five rubric dimensions plus overall`);
|
|
49
|
+
for (const v of Object.values(it.scores)) assert.ok(v >= 0 && v <= 10, `${it.name}: ${v}`);
|
|
50
|
+
assert.ok(it.entries.length >= 1, `${it.name} has a case file`);
|
|
51
|
+
for (const e of it.entries) {
|
|
52
|
+
assert.match(e.sourceUrl, /^https?:\/\//, `${it.name}: every entry has a source`);
|
|
53
|
+
assert.ok([1, 2, 3].includes(e.confidence));
|
|
54
|
+
assert.ok(['insight', 'news', 'datapoint'].includes(e.type));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// The item key the migration enforces is unique across the seed, so no row is lost to it.
|
|
58
|
+
assert.equal(new Set(built.items.map((i) => `${i.kind}|${i.url}|${i.name}`)).size, 60);
|
|
59
|
+
// An unverified claim is never seeded as better than low confidence.
|
|
60
|
+
const unverified = built.items.flatMap((i) => i.entries).filter((e) => /unverified/i.test(e.takeaway));
|
|
61
|
+
assert.ok(unverified.length > 0);
|
|
62
|
+
for (const e of unverified) assert.equal(e.confidence, 1, e.takeaway);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the rules seed as version 1 with both rubrics, defined', () => {
|
|
66
|
+
assert.equal(built.rules.version, 1);
|
|
67
|
+
for (const kind of ['competitor', 'partner']) {
|
|
68
|
+
assert.equal(built.rules.rubric[kind].length, 5);
|
|
69
|
+
for (const d of built.rules.rubric[kind]) assert.ok(d.label && d.definition && d.weight > 0, JSON.stringify(d));
|
|
70
|
+
}
|
|
71
|
+
assert.ok(built.rules.searchQueries.length > 0 && built.rules.sources.length > 0 && built.rules.instructions.length > 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('projectKey is the public origin, normalised, and refuses anything else', () => {
|
|
75
|
+
assert.equal(seed.projectKey('https://CloudBongos.com/'), 'https://cloudbongos.com');
|
|
76
|
+
assert.equal(seed.projectKey('http://localhost:3000'), 'http://localhost:3000');
|
|
77
|
+
for (const bad of ['', null, 'cloudbongos.com', 'https://x.com/path', 'ftp://x.com']) assert.equal(seed.projectKey(bad), null, String(bad));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the seed writes nothing off the hub, or with no origin', async () => {
|
|
81
|
+
const { pool, calls } = fakePool();
|
|
82
|
+
assert.deepEqual(await seed.seedHub({ pool, isHub: false, project: 'https://hub.example' }), { seeded: false, skipped: 'not_hub' });
|
|
83
|
+
assert.deepEqual(await seed.seedHub({ pool, isHub: 'true', project: 'https://hub.example' }), { seeded: false, skipped: 'not_hub' });
|
|
84
|
+
assert.deepEqual(await seed.seedHub({ pool, isHub: true, project: 'not an origin' }), { seeded: false, skipped: 'no_project_origin' });
|
|
85
|
+
assert.equal(calls.length, 0, 'not one statement');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('on the hub it writes in one transaction, every insert idempotent and scoped to the project', async () => {
|
|
89
|
+
const { pool, calls } = fakePool();
|
|
90
|
+
const r = await seed.seedHub({ pool, isHub: true, project: 'https://Hub.example/' });
|
|
91
|
+
assert.equal(r.seeded, true);
|
|
92
|
+
assert.equal(r.project, 'https://hub.example');
|
|
93
|
+
assert.equal(calls[0].sql, 'BEGIN');
|
|
94
|
+
assert.equal(calls.at(-2).sql, 'COMMIT');
|
|
95
|
+
assert.equal(calls.at(-1).sql, 'RELEASE');
|
|
96
|
+
const inserts = calls.filter((c) => /^\s*INSERT/.test(c.sql));
|
|
97
|
+
assert.equal(inserts.length, 1 + 1 + 60 + 360 + built.items.reduce((a, i) => a + i.entries.length, 0));
|
|
98
|
+
for (const c of inserts) assert.match(c.sql, /ON CONFLICT .* DO NOTHING/s, c.sql);
|
|
99
|
+
for (const c of calls.filter((x) => /market_watch_(items|runs|rule_versions)/.test(x.sql) && x.params)) {
|
|
100
|
+
assert.equal(c.params[0], 'https://hub.example', 'project is $1 on every project-owned row');
|
|
101
|
+
}
|
|
102
|
+
// No person is invented: no seed statement writes an author, approver or adder.
|
|
103
|
+
for (const c of inserts) assert.doesNotMatch(c.sql, /author_id|approved_by|added_by|proposed_by/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a failed write rolls back and releases', async () => {
|
|
107
|
+
const { pool, calls } = fakePool({ failOn: /INSERT INTO market_watch_score_snapshots/ });
|
|
108
|
+
await assert.rejects(seed.seedHub({ pool, isHub: true, project: 'https://hub.example' }), /boom/);
|
|
109
|
+
assert.ok(calls.some((c) => c.sql === 'ROLLBACK'));
|
|
110
|
+
assert.ok(!calls.some((c) => c.sql === 'COMMIT'));
|
|
111
|
+
assert.equal(calls.at(-1).sql, 'RELEASE');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('the boot poller never throws into the server and counts a failure', async () => {
|
|
115
|
+
let reported = 0;
|
|
116
|
+
const r = await hubSeed.runOnce({ seedHub: async () => { throw new Error('db down'); }, reportError: () => { reported += 1; } });
|
|
117
|
+
assert.equal(r.seeded, false);
|
|
118
|
+
assert.equal(reported, 1);
|
|
119
|
+
const ok = await hubSeed.runOnce({ seedHub: async () => ({ seeded: false, skipped: 'not_hub' }) });
|
|
120
|
+
assert.equal(ok.skipped, 'not_hub');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---- the derivations ------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
const RUNS = [
|
|
126
|
+
{ id: 4, status: 'done', created_at: '2026-11-11T00:00:00Z' },
|
|
127
|
+
{ id: 1, status: 'done', created_at: '2026-10-28T00:00:00Z' },
|
|
128
|
+
{ id: 3, status: 'skipped', created_at: '2026-11-07T00:00:00Z' },
|
|
129
|
+
{ id: 2, status: 'partial', created_at: '2026-11-04T00:00:00Z' },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
test('runs are numbered oldest first, and a run that never ran takes no number', () => {
|
|
133
|
+
const n = db.numberRuns(RUNS);
|
|
134
|
+
assert.deepEqual(n.map((r) => [r.id, r.n]), [[1, 1], [2, 2], [3, null], [4, 3]]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('level counts the runs that found something; streak counts the latest ones in a row', () => {
|
|
138
|
+
const n = db.numberRuns(RUNS);
|
|
139
|
+
assert.deepEqual(db.progress(n, []), { level: 1, streak: 0 }, 'a tracked item is level 1');
|
|
140
|
+
assert.deepEqual(db.progress(n, [1, 2, 4]), { level: 3, streak: 3 }, 'a skipped run does not break a streak');
|
|
141
|
+
assert.deepEqual(db.progress(n, [1, 4]), { level: 2, streak: 1 });
|
|
142
|
+
assert.deepEqual(db.progress(n, [1, 2]), { level: 2, streak: 0 }, 'nothing in the latest run');
|
|
143
|
+
assert.deepEqual(db.progress(n, ['4']), { level: 1, streak: 1 }, 'ids compare as strings (pg bigint)');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('sparklines: one point per run that scored the item, null where a dimension was not scored', () => {
|
|
147
|
+
const n = db.numberRuns(RUNS);
|
|
148
|
+
const s = db.sparklines([
|
|
149
|
+
{ run_id: 1, dimension: 'overall', value: '5.0' }, { run_id: 1, dimension: 'human_gates', value: '6' },
|
|
150
|
+
{ run_id: 4, dimension: 'overall', value: '6.0' },
|
|
151
|
+
{ run_id: 99, dimension: 'overall', value: '9' },
|
|
152
|
+
], n);
|
|
153
|
+
assert.deepEqual(s.runs.map((r) => r.n), [1, 3]);
|
|
154
|
+
assert.deepEqual(s.series, { overall: [5, 6], human_gates: [6, null] });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the ask streak counts answered weeks back from the newest; an open unanswered week does not break it', () => {
|
|
158
|
+
assert.equal(db.weekStreak([{ id: 'w4', open: true }, { id: 'w3' }, { id: 'w2' }, { id: 'w1' }], ['w3', 'w2']), 2);
|
|
159
|
+
assert.equal(db.weekStreak([{ id: 'w4', open: false }, { id: 'w3' }], ['w3']), 0, 'a closed missed week breaks it');
|
|
160
|
+
assert.equal(db.weekStreak([{ id: 'w4', open: true }, { id: 'w3' }], ['w4', 'w3']), 2);
|
|
161
|
+
assert.equal(db.weekStreak([], []), 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('the next run is due one cadence after the last; no run yet means due now', () => {
|
|
165
|
+
assert.equal(db.nextRunDue(null, 'weekly'), null);
|
|
166
|
+
assert.equal(db.nextRunDue('2026-11-01T09:00:00Z', 'weekly').toISOString(), '2026-11-08T09:00:00.000Z');
|
|
167
|
+
assert.equal(db.nextRunDue('2026-11-01T09:00:00Z', 'biweekly').toISOString(), '2026-11-15T09:00:00.000Z');
|
|
168
|
+
assert.throws(() => db.nextRunDue('2026-11-01T09:00:00Z', 'daily'), /unknown cadence/);
|
|
169
|
+
const [a, b] = db.monthBounds(new Date('2026-12-31T23:59:00Z'));
|
|
170
|
+
assert.equal(a.toISOString(), '2026-12-01T00:00:00.000Z');
|
|
171
|
+
assert.equal(b.toISOString(), '2027-01-01T00:00:00.000Z');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ---- the reads are project-scoped ----------------------------------------
|
|
175
|
+
|
|
176
|
+
test('every read refuses a missing project and passes it as $1', async () => {
|
|
177
|
+
const seen = [];
|
|
178
|
+
const pool = {
|
|
179
|
+
async query(sql, params) {
|
|
180
|
+
seen.push({ sql, params });
|
|
181
|
+
if (/sum\(ri\.cost_usd\), 0\) AS spent/.test(sql)) return { rows: [{ spent: '0' }] };
|
|
182
|
+
return { rows: [] };
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const P = 'https://hub.example';
|
|
186
|
+
const reads = [
|
|
187
|
+
() => db.listItems(pool, { project: P }),
|
|
188
|
+
() => db.getItem(pool, { project: P, id: 1 }),
|
|
189
|
+
() => db.caseFile(pool, { project: P, itemId: 1 }),
|
|
190
|
+
() => db.scoreHistory(pool, { project: P, itemId: 1 }),
|
|
191
|
+
() => db.rules(pool, { project: P }),
|
|
192
|
+
() => db.proposals(pool, { project: P }),
|
|
193
|
+
() => db.weeklyAsk(pool, { project: P, weekOf: '2026-11-02' }),
|
|
194
|
+
() => db.askStreak(pool, { project: P, builderId: 1 }),
|
|
195
|
+
() => db.settingsView(pool, { project: P }),
|
|
196
|
+
() => db.remainingItems(pool, { project: P, runId: 1 }),
|
|
197
|
+
];
|
|
198
|
+
for (const r of reads) await r();
|
|
199
|
+
assert.ok(seen.length >= reads.length);
|
|
200
|
+
for (const s of seen) {
|
|
201
|
+
assert.equal(s.params[0], P, s.sql);
|
|
202
|
+
assert.match(s.sql, /project = \$1/, s.sql);
|
|
203
|
+
}
|
|
204
|
+
for (const fn of ['listItems', 'caseFile', 'scoreHistory', 'rules', 'proposals', 'weeklyAsk', 'settingsView', 'remainingItems']) {
|
|
205
|
+
await assert.rejects(db[fn](pool, {}), /project key is required/, fn);
|
|
206
|
+
}
|
|
207
|
+
const v = await db.settingsView(pool, { project: P });
|
|
208
|
+
assert.deepEqual(v.settings, { ...db.DEFAULT_SETTINGS }, 'an unconfigured project reads the watch as off');
|
|
209
|
+
assert.equal(v.nextRunDue, null);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ---- the migration's own rules -------------------------------------------
|
|
213
|
+
|
|
214
|
+
test('the migration is additive, namespaced, idempotent, transactional and ASCII', () => {
|
|
215
|
+
assert.match(MIGRATION, /^\s*BEGIN;/m);
|
|
216
|
+
assert.match(MIGRATION, /^\s*COMMIT;/m);
|
|
217
|
+
const code = MIGRATION.replace(/--[^\n]*/g, ''); // the prose may say "never a DELETE"
|
|
218
|
+
assert.doesNotMatch(code, /\bDROP\b|\bTRUNCATE\b|\bDELETE\b/i);
|
|
219
|
+
assert.ok(/^[\x00-\x7F]*$/.test(MIGRATION), 'ASCII only');
|
|
220
|
+
const tables = [...MIGRATION.matchAll(/CREATE TABLE (IF NOT EXISTS )?(\w+)/g)];
|
|
221
|
+
assert.equal(tables.length, 10);
|
|
222
|
+
for (const t of tables) {
|
|
223
|
+
assert.ok(t[1], `${t[2]} is IF NOT EXISTS`);
|
|
224
|
+
assert.match(t[2], /^market_watch_/);
|
|
225
|
+
}
|
|
226
|
+
for (const i of MIGRATION.matchAll(/CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?(\w+)/g)) assert.ok(i[2], `${i[3]} is IF NOT EXISTS`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('every seed ON CONFLICT target is a unique key the migration declares', () => {
|
|
230
|
+
const src = readFileSync(new URL('../modules/market-watch/seed.js', import.meta.url), 'utf8');
|
|
231
|
+
const want = {
|
|
232
|
+
'(project, version)': /UNIQUE \(project, version\)/,
|
|
233
|
+
'(project) WHERE trigger = \'seed\'': /market_watch_runs_one_seed\s+ON market_watch_runs \(project\) WHERE trigger = 'seed'/,
|
|
234
|
+
'(project, kind, url, name)': /UNIQUE \(project, kind, url, name\)/,
|
|
235
|
+
'(item_id, run_id, dimension)': /UNIQUE \(item_id, run_id, dimension\)/,
|
|
236
|
+
'(item_id, run_id, md5(takeaway))': /market_watch_log_entries_once\s+ON market_watch_log_entries \(item_id, run_id, md5\(takeaway\)\)/,
|
|
237
|
+
};
|
|
238
|
+
const targets = [...src.matchAll(/ON CONFLICT (.+?) DO NOTHING/g)].map((m) => m[1]);
|
|
239
|
+
assert.equal(targets.length, Object.keys(want).length);
|
|
240
|
+
for (const t of targets) {
|
|
241
|
+
assert.ok(want[t], `unexpected conflict target ${t}`);
|
|
242
|
+
assert.match(MIGRATION, want[t], t);
|
|
243
|
+
}
|
|
244
|
+
});
|