@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,253 @@
|
|
|
1
|
+
// tests/payout_approvals.mjs — the payout approval step (task 1004913, ADR 0385 D4).
|
|
2
|
+
// DB-free: the pure rules, then the real router over real HTTP with a fake store.
|
|
3
|
+
// Pins: a signature needs the person's own web session (never a CLI token, never a
|
|
4
|
+
// machine principal); a narrowing can never widen; one approval per request, no
|
|
5
|
+
// batch; a run pays only after its pause window, within its caps, unheld and as
|
|
6
|
+
// noticed; the permission gate is payout.approve on every route.
|
|
7
|
+
// Run: node tests/payout_approvals.mjs
|
|
8
|
+
import { strict as assert } from 'node:assert';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import http from 'node:http';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const express = require('express');
|
|
14
|
+
const lib = require('../src/bongos/payout-approvals');
|
|
15
|
+
const build = require('../src/bongos/routes/payout-approvals');
|
|
16
|
+
|
|
17
|
+
// The N10 payout gates, standing in as "everyone may be paid": this file tests
|
|
18
|
+
// the approval step; tests/payout_gates.mjs tests the gates and their wiring.
|
|
19
|
+
const passGates = {
|
|
20
|
+
gateOwedPay: async () => ({ ok: true, pay_to: 'self', needs_guardian_cosign: false }),
|
|
21
|
+
gateRecipient: async () => ({ ok: true, pay_to: 'self', needs_guardian_cosign: false }),
|
|
22
|
+
hasCosign: async () => true,
|
|
23
|
+
gateOwedPayMany: async (m) => new Map([...m.keys()].map((k) => [k, { ok: true, pay_to: 'self', needs_guardian_cosign: false }])),
|
|
24
|
+
cosignedInRun: async () => new Set(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let n = 0;
|
|
28
|
+
const ok = (name) => { n++; console.log(`ok - ${name}`); };
|
|
29
|
+
|
|
30
|
+
const TERMS = {
|
|
31
|
+
caps: { per_payout_cents: 50_000, per_run_cents: 200_000, per_month_cents: 500_000 },
|
|
32
|
+
currency: 'USD',
|
|
33
|
+
formula: { kind: 'task_pay_terms' },
|
|
34
|
+
schedule: 'weekly',
|
|
35
|
+
pause_hours: 48,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ── 1. pure rules ──────────────────────────────────────────────────────────
|
|
39
|
+
{
|
|
40
|
+
const v = lib.validateRuleTerms(TERMS);
|
|
41
|
+
assert.deepEqual(v.terms, TERMS); ok('a complete rule validates to exactly what was sent');
|
|
42
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, extra: 1 }).errors); ok('an unknown term is refused, not dropped');
|
|
43
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, caps: { per_payout_cents: 1.5, per_run_cents: 10 } }).errors); ok('fractional cents refused');
|
|
44
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, caps: { per_payout_cents: 300, per_run_cents: 200 } }).errors); ok('per-payout above per-run refused');
|
|
45
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, pause_hours: 0 }).errors); ok('a rule with no pause window is refused');
|
|
46
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, formula: { kind: 'credit_share' } }).errors); ok('a credit-share rule must name its credit row kinds (ADR 0387 D5)');
|
|
47
|
+
assert.ok(lib.validateRuleTerms({ ...TERMS, formula: { kind: 'credit_share', credit_row_kinds: ['session.token_reward'] } }).terms); ok('a credit-share rule naming its rows validates');
|
|
48
|
+
|
|
49
|
+
const tighter = { ...TERMS, caps: { per_payout_cents: 40_000, per_run_cents: 100_000, per_month_cents: 400_000 }, pause_hours: 72 };
|
|
50
|
+
assert.deepEqual(lib.wideningReasons(TERMS, tighter), []); ok('lower caps and a longer pause are a narrowing');
|
|
51
|
+
assert.ok(lib.wideningReasons(TERMS, { ...TERMS, caps: { ...TERMS.caps, per_run_cents: 300_000 } }).length); ok('a higher cap widens');
|
|
52
|
+
const { per_month_cents: _drop, ...noMonth } = TERMS.caps;
|
|
53
|
+
assert.ok(lib.wideningReasons(TERMS, { ...TERMS, caps: noMonth }).length); ok('removing a cap widens');
|
|
54
|
+
assert.ok(lib.wideningReasons(TERMS, { ...TERMS, pause_hours: 24 }).length); ok('a shorter pause widens');
|
|
55
|
+
assert.ok(lib.wideningReasons(TERMS, { ...TERMS, schedule: 'on_ship' }).length); ok('a different schedule is not a narrowing');
|
|
56
|
+
assert.ok(lib.wideningReasons(TERMS, { ...TERMS, formula: { kind: 'fixed_per_task', amount_cents: 1 } }).length); ok('a different formula is not a narrowing');
|
|
57
|
+
|
|
58
|
+
const web = { builder: { id: '7', rank: 'archon', system_role: null }, gdsSession: { source: 'web', session_id: '91' } };
|
|
59
|
+
assert.equal(lib.personalSession(web).ok, true); ok('a person on the website is personal');
|
|
60
|
+
assert.equal(lib.personalSession({ ...web, gdsSession: { source: 'cli', session_id: '91' } }).reason, 'not_a_web_session'); ok('a CLI (AI) session is not personal');
|
|
61
|
+
assert.equal(lib.personalSession({ ...web, gdsSession: { source: null, session_id: '91' } }).reason, 'not_a_web_session'); ok('a legacy sourceless session is not personal');
|
|
62
|
+
assert.equal(lib.personalSession({ ...web, builder: { ...web.builder, system_role: 'bfg' } }).reason, 'machine_principal'); ok('a machine principal is never personal');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 2. judging a run ───────────────────────────────────────────────────────
|
|
66
|
+
{
|
|
67
|
+
const at = (h) => new Date(Date.UTC(2026, 9, 10) + h * 3600_000).toISOString();
|
|
68
|
+
const signed = { id: '1', kind: 'rule_signed', terms: TERMS, actor_id: '7', created_at: at(0) };
|
|
69
|
+
const notice = (id, key, payouts, h) => ({ id, kind: 'run_noticed', rule_id: '1', run_key: key, amount_cents: payouts.reduce((s, p) => s + p.amount_cents, 0), currency: 'USD', terms: { payouts }, created_at: at(h) });
|
|
70
|
+
const P = [{ recipient_id: '8', amount_cents: 30_000 }, { recipient_id: '9', amount_cents: 20_000 }];
|
|
71
|
+
const events = [signed, notice('2', 'w1', P, 1)];
|
|
72
|
+
const rule = lib.foldRule(events);
|
|
73
|
+
const judge = (extra = {}) => lib.judgeRun({ rule, ruleEvents: events, runKey: 'w1', payouts: P, now: new Date(at(60)), ...extra });
|
|
74
|
+
|
|
75
|
+
assert.equal(judge().ok, true); ok('a noticed run inside its caps pays after the pause');
|
|
76
|
+
assert.equal(judge({ now: new Date(at(10)) }).reason, 'in_pause_window'); ok('it does not pay inside the pause window');
|
|
77
|
+
assert.equal(judge({ runKey: 'w2' }).reason, 'run_not_noticed'); ok('an unnoticed run never pays');
|
|
78
|
+
assert.equal(judge({ payouts: [{ recipient_id: '8', amount_cents: 50_000 }] }).reason, 'run_differs_from_notice'); ok('a run that differs from its notice does not pay');
|
|
79
|
+
assert.equal(judge({ payouts: [...P].reverse() }).ok, true); ok('order of payouts does not matter');
|
|
80
|
+
assert.equal(lib.judgeRun({ rule, ruleEvents: [...events, { id: '3', kind: 'run_held', rule_id: '1', run_key: 'w1', created_at: at(2) }], runKey: 'w1', payouts: P, now: new Date(at(60)) }).reason, 'run_held'); ok('a held run never pays');
|
|
81
|
+
const revokedEvents = [...events, { id: '3', kind: 'rule_revoked', rule_id: '1', actor_id: '7', created_at: at(2) }];
|
|
82
|
+
assert.equal(lib.judgeRun({ rule: lib.foldRule(revokedEvents), ruleEvents: revokedEvents, runKey: 'w1', payouts: P, now: new Date(at(60)) }).reason, 'rule_revoked'); ok('a revoked rule pays nothing, even a run noticed before');
|
|
83
|
+
const narrowed = [...events, { id: '3', kind: 'rule_narrowed', rule_id: '1', terms: { ...TERMS, caps: { ...TERMS.caps, per_payout_cents: 25_000 } }, created_at: at(2) }];
|
|
84
|
+
assert.equal(lib.judgeRun({ rule: lib.foldRule(narrowed), ruleEvents: narrowed, runKey: 'w1', payouts: P, now: new Date(at(60)) }).reason, 'over_per_payout_cap'); ok('a narrowing binds runs already noticed');
|
|
85
|
+
const big = [{ recipient_id: '8', amount_cents: 50_000 }, { recipient_id: '9', amount_cents: 50_000 }, { recipient_id: '10', amount_cents: 50_000 }];
|
|
86
|
+
const month = [signed, notice('2', 'a', big, 1), notice('3', 'b', big, 2), notice('4', 'c', big, 3), notice('5', 'd', big, 4)];
|
|
87
|
+
const mr = lib.foldRule(month);
|
|
88
|
+
assert.equal(lib.judgeRun({ rule: mr, ruleEvents: month, runKey: 'c', payouts: big, now: new Date(at(200)) }).ok, true); ok('runs within the month cap pay');
|
|
89
|
+
assert.equal(lib.judgeRun({ rule: mr, ruleEvents: month, runKey: 'd', payouts: big, now: new Date(at(200)) }).reason, 'over_per_month_cap'); ok('the run that crosses the month cap does not');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 3. the router over real HTTP ───────────────────────────────────────────
|
|
93
|
+
const rows = [];
|
|
94
|
+
let nextId = 1;
|
|
95
|
+
const store = {
|
|
96
|
+
...lib,
|
|
97
|
+
insertEvent: async (e) => {
|
|
98
|
+
const row = { id: String(nextId++), kind: e.kind, rule_id: e.rule_id ? String(e.rule_id) : null, run_key: e.run_key ?? null, actor_id: String(e.actor.id), session_source: e.actor.session_source, session_id: e.actor.session_id, recipient_id: e.recipient_id ?? null, amount_cents: e.amount_cents ?? null, currency: e.currency ?? null, terms: e.terms || {}, note: e.note ?? null, created_at: new Date().toISOString() };
|
|
99
|
+
rows.push(row);
|
|
100
|
+
return row;
|
|
101
|
+
},
|
|
102
|
+
readRule: async (id) => { const events = rows.filter((r) => r.id === String(id) || r.rule_id === String(id)); return { rule: lib.foldRule(events), events }; },
|
|
103
|
+
listRules: async () => [],
|
|
104
|
+
listLog: async () => [...rows].reverse(),
|
|
105
|
+
};
|
|
106
|
+
let who = { builder: { id: '7', rank: 'archon', system_role: null }, gdsSession: { source: 'web', session_id: '91' } };
|
|
107
|
+
const gated = [];
|
|
108
|
+
const app = express();
|
|
109
|
+
app.use(express.json());
|
|
110
|
+
app.use((req, res, next) => { res.fail = (code, { status = 400, message } = {}) => res.status(status).json({ error: code, message }); next(); });
|
|
111
|
+
app.use((req, res, next) => { req.builder = who.builder; req.gdsSession = who.gdsSession; next(); });
|
|
112
|
+
app.use('/', build({ store, gates: passGates, requireBuilder: (q, r, nx) => nx(), requirePermission: (...p) => { gated.push(p.join(',')); return (q, r, nx) => nx(); } }));
|
|
113
|
+
const server = http.createServer(app).listen(0);
|
|
114
|
+
const port = server.address().port;
|
|
115
|
+
const call = (method, path, body) => new Promise((resolve) => {
|
|
116
|
+
const r = http.request({ port, path, method, headers: { 'content-type': 'application/json' } }, (res) => {
|
|
117
|
+
let d = ''; res.on('data', (c) => (d += c)); res.on('end', () => { let b = null; try { b = JSON.parse(d); } catch { /* not json */ } resolve({ status: res.statusCode, body: b }); });
|
|
118
|
+
});
|
|
119
|
+
if (body) r.write(JSON.stringify(body));
|
|
120
|
+
r.end();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
assert.ok(gated.length >= 9 && gated.every((g) => g === 'payout.approve')); ok('every route gates on payout.approve');
|
|
125
|
+
|
|
126
|
+
let r = await call('POST', '/payouts/rules', { terms: TERMS });
|
|
127
|
+
assert.equal(r.status, 201); assert.equal(r.body.approval.session_source, 'web'); ok('a person signs a rule from the website');
|
|
128
|
+
const ruleId = r.body.approval.id;
|
|
129
|
+
|
|
130
|
+
who = { ...who, gdsSession: { source: 'cli', session_id: '92' } };
|
|
131
|
+
for (const [m, p, b] of [['POST', '/payouts/rules', { terms: TERMS }], ['POST', `/payouts/rules/${ruleId}/narrow`, { terms: TERMS }], ['POST', `/payouts/rules/${ruleId}/revoke`, {}], ['POST', '/payouts/approve', { recipient_id: 8, amount_cents: 100, currency: 'USD' }], ['POST', '/payouts/grants', { recipient_id: 8, terms: { title: 'Grant', summary: 'one grant' } }], ['POST', '/payouts/contracts', { recipient_id: 8, terms: { title: 'Contract', summary: 'one contract' } }]]) {
|
|
132
|
+
const before = rows.length;
|
|
133
|
+
r = await call(m, p, b);
|
|
134
|
+
assert.equal(r.status, 403, p); assert.equal(r.body.error, 'personal_approval_required'); assert.equal(rows.length, before);
|
|
135
|
+
}
|
|
136
|
+
ok('an AI (CLI) session can sign nothing, and nothing is written');
|
|
137
|
+
|
|
138
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs`, { run_key: 'w1', payouts: [{ recipient_id: 8, amount_cents: 30_000 }] });
|
|
139
|
+
assert.equal(r.status, 201); assert.ok(r.body.pays_from); ok('the runner may notice a run (it authorises nothing) and is told when it may pay');
|
|
140
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs`, { run_key: 'w2', payouts: [{ recipient_id: 8, amount_cents: 60_000 }] });
|
|
141
|
+
assert.equal(r.status, 409); assert.equal(r.body.error, 'over_cap'); ok('a run over the caps cannot even be noticed');
|
|
142
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs/w1/hold`, {});
|
|
143
|
+
assert.equal(r.status, 403); ok('holding a run is a person\'s act too');
|
|
144
|
+
|
|
145
|
+
who = { ...who, builder: { ...who.builder, system_role: 'bfg' }, gdsSession: { source: 'web', session_id: '93' } };
|
|
146
|
+
r = await call('POST', '/payouts/approve', { recipient_id: 8, amount_cents: 100, currency: 'USD' });
|
|
147
|
+
assert.equal(r.status, 403); ok('a machine principal is refused even on a web session');
|
|
148
|
+
|
|
149
|
+
who = { builder: { id: '7', rank: 'archon', system_role: null }, gdsSession: { source: 'web', session_id: '91' } };
|
|
150
|
+
r = await call('POST', `/payouts/rules/${ruleId}/narrow`, { terms: { ...TERMS, caps: { ...TERMS.caps, per_run_cents: 300_000 } } });
|
|
151
|
+
assert.equal(r.status, 409); assert.equal(r.body.error, 'would_widen'); ok('a narrowing that would widen is refused in words');
|
|
152
|
+
r = await call('POST', `/payouts/rules/${ruleId}/narrow`, { terms: { ...TERMS, pause_hours: 96 } });
|
|
153
|
+
assert.equal(r.status, 201); ok('a real narrowing is recorded');
|
|
154
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs/w1/hold`, { note: 'checking the numbers' });
|
|
155
|
+
assert.equal(r.status, 201); ok('a person holds a run in its pause');
|
|
156
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs/w1/hold`, {});
|
|
157
|
+
assert.equal(r.status, 409); ok('a run is held once');
|
|
158
|
+
|
|
159
|
+
r = await call('POST', '/payouts/approve', [{ recipient_id: 8, amount_cents: 100, currency: 'USD' }]);
|
|
160
|
+
assert.equal(r.status, 400); ok('a list of payouts is refused: no batch approve');
|
|
161
|
+
r = await call('POST', '/payouts/approve', { recipient_id: [8, 9], amount_cents: 100, currency: 'USD' });
|
|
162
|
+
assert.equal(r.status, 400); assert.equal(r.body.error, 'one_at_a_time'); ok('a list hidden in a field is refused too');
|
|
163
|
+
r = await call('POST', '/payouts/approve', { recipient_id: 8, amount_cents: 12_345, currency: 'USD' });
|
|
164
|
+
assert.equal(r.status, 201); assert.equal(r.body.approval.kind, 'payout_approved'); ok('one payout approved by one person');
|
|
165
|
+
r = await call('POST', '/payouts/grants', { recipient_id: 8, terms: { title: 'Profit interest', summary: 'as approved by counsel', secret: 1 } });
|
|
166
|
+
assert.equal(r.status, 400); ok('a grant with unknown terms is refused');
|
|
167
|
+
r = await call('POST', '/payouts/grants', { recipient_id: 8, terms: { title: 'Profit interest', summary: 'as approved by counsel' } });
|
|
168
|
+
assert.equal(r.status, 201); assert.equal(r.body.approval.kind, 'grant_signed'); ok('one equity grant, one signature');
|
|
169
|
+
|
|
170
|
+
r = await call('POST', `/payouts/rules/${ruleId}/revoke`, {});
|
|
171
|
+
assert.equal(r.status, 201); ok('a person revokes a rule');
|
|
172
|
+
r = await call('POST', `/payouts/rules/${ruleId}/runs`, { run_key: 'w3', payouts: [{ recipient_id: 8, amount_cents: 100 }] });
|
|
173
|
+
assert.equal(r.status, 409); assert.equal(r.body.error, 'rule_revoked'); ok('nothing runs under a revoked rule');
|
|
174
|
+
|
|
175
|
+
r = await call('GET', '/payouts/approvals');
|
|
176
|
+
assert.equal(r.status, 200); assert.equal(r.body.approvals[0].kind, 'rule_revoked'); ok('the log reads back newest first');
|
|
177
|
+
} finally {
|
|
178
|
+
server.close();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 4. the migration says what the code relies on ──────────────────────────
|
|
182
|
+
{
|
|
183
|
+
const sql = readFileSync(new URL('../migrations/core_283_payout_approvals.sql', import.meta.url), 'utf8');
|
|
184
|
+
assert.match(sql, /BEFORE UPDATE OR DELETE ON payout_approvals/); assert.match(sql, /BEFORE TRUNCATE ON payout_approvals/); ok('the table refuses edits, deletes and truncation by trigger');
|
|
185
|
+
assert.match(sql, /kind = 'run_noticed' OR \(session_source = 'web' AND session_id IS NOT NULL\)/); ok('the table demands a web session on every signature');
|
|
186
|
+
assert.match(sql, /system_role INTO machine/); ok('the table refuses a machine principal\'s signature');
|
|
187
|
+
for (const k of lib.SIGNED_KINDS.concat('run_noticed')) assert.ok(sql.includes(`'${k}'`), k);
|
|
188
|
+
ok('every kind the code writes is one the table accepts');
|
|
189
|
+
const grant = readFileSync(new URL('../modules/government/migrations/government_035_payout_approve.sql', import.meta.url), 'utf8');
|
|
190
|
+
assert.match(grant, /SELECT 'archon', unnest\(ARRAY\[\s*'payout.approve'/); ok('payout.approve is granted to archon');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── 5. checkApproval: the one-off check the paying tasks call ─────────────
|
|
194
|
+
{
|
|
195
|
+
const stored = {
|
|
196
|
+
5: { id: 5, kind: 'payout_approved', rule_id: null, actor_id: 7, recipient_id: 8, amount_cents: '12345', currency: 'USD', terms: {}, created_at: 'x' },
|
|
197
|
+
6: { id: 6, kind: 'grant_signed', rule_id: null, actor_id: 7, recipient_id: 8, amount_cents: null, currency: null, terms: { title: 'g', summary: 'g' }, created_at: 'x' },
|
|
198
|
+
};
|
|
199
|
+
const seen = [];
|
|
200
|
+
const pool = { query: async (sql, params) => { seen.push(params); return { rows: stored[params[0]] ? [stored[params[0]]] : [] }; } };
|
|
201
|
+
const chk = (a) => lib.checkApproval(a, { pool, gates: passGates });
|
|
202
|
+
assert.equal((await chk({ id: 5, recipientId: 8, amountCents: 12345, currency: 'USD' })).ok, true); ok('checkApproval: the exact payout it approved passes');
|
|
203
|
+
assert.equal((await chk({ id: 5, recipientId: '8', amountCents: 12345, currency: 'USD' })).ok, true); ok('checkApproval: ids compare as text, not by type');
|
|
204
|
+
assert.equal((await chk({ id: 5, recipientId: 9, amountCents: 12345, currency: 'USD' })).reason, 'different_recipient'); ok('checkApproval: another recipient is refused');
|
|
205
|
+
assert.equal((await chk({ id: 5, recipientId: 8, amountCents: 12346, currency: 'USD' })).reason, 'different_amount'); ok('checkApproval: another amount is refused');
|
|
206
|
+
assert.equal((await chk({ id: 5, recipientId: 8, amountCents: 12345, currency: 'EUR' })).reason, 'different_amount'); ok('checkApproval: another currency is refused');
|
|
207
|
+
assert.equal((await chk({ id: 404, recipientId: 8, amountCents: 1, currency: 'USD' })).reason, 'no_such_approval'); ok('checkApproval: an unknown id is refused');
|
|
208
|
+
const before = seen.length;
|
|
209
|
+
assert.equal((await chk({ id: 'drop table', recipientId: 8 })).reason, 'no_such_approval'); assert.equal(seen.length, before); ok('checkApproval: a malformed id never reaches the database');
|
|
210
|
+
assert.equal((await chk({ id: 6, recipientId: 8, amountCents: 1, currency: 'USD' })).reason, 'no_such_approval'); ok('checkApproval: a grant is not a payout approval');
|
|
211
|
+
assert.equal((await chk({ id: 6, kind: 'grant_signed', recipientId: 8 })).ok, true); ok('checkApproval: a grant checks as a grant, for its recipient');
|
|
212
|
+
assert.equal((await chk({ id: 5, kind: 'grant_signed', recipientId: 8 })).reason, 'no_such_approval'); ok('checkApproval: a payout approval is not a grant');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── 6. holding a run: allowed after the pause too, refused when nothing to hold ──
|
|
216
|
+
{
|
|
217
|
+
const r2 = [];
|
|
218
|
+
let id = 100;
|
|
219
|
+
const st = { ...store, insertEvent: async (e) => { const row = { id: String(id++), kind: e.kind, rule_id: e.rule_id ? String(e.rule_id) : null, run_key: e.run_key ?? null, actor_id: '7', terms: e.terms || {}, created_at: '2026-01-01T00:00:00.000Z' }; r2.push(row); return row; },
|
|
220
|
+
readRule: async (rid) => { const ev = r2.filter((r) => r.id === String(rid) || r.rule_id === String(rid)); return { rule: lib.foldRule(ev), events: ev }; } };
|
|
221
|
+
const app2 = express();
|
|
222
|
+
app2.use(express.json());
|
|
223
|
+
app2.use((req, res, next) => { res.fail = (code, { status = 400, message } = {}) => res.status(status).json({ error: code, message }); next(); });
|
|
224
|
+
app2.use((req, res, next) => { req.builder = { id: '7', rank: 'archon', system_role: null }; req.gdsSession = { source: 'web', session_id: '91' }; next(); });
|
|
225
|
+
app2.use('/', build({ store: st, gates: passGates, requireBuilder: (q, r, nx) => nx(), requirePermission: () => (q, r, nx) => nx() }));
|
|
226
|
+
const srv = http.createServer(app2).listen(0);
|
|
227
|
+
const p2 = srv.address().port;
|
|
228
|
+
const call2 = (method, path, body) => new Promise((resolve) => {
|
|
229
|
+
const r = http.request({ port: p2, path, method, headers: { 'content-type': 'application/json' } }, (res) => { let d = ''; res.on('data', (c) => (d += c)); res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(d || 'null') })); });
|
|
230
|
+
if (body) r.write(JSON.stringify(body));
|
|
231
|
+
r.end();
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
const rule = (await call2('POST', '/payouts/rules', { terms: TERMS })).body.approval.id;
|
|
235
|
+
let r = await call2('POST', `/payouts/rules/${rule}/runs/nope/hold`, {});
|
|
236
|
+
assert.equal(r.status, 404); assert.equal(r.body.error, 'run_not_noticed'); ok('a run never noticed cannot be held');
|
|
237
|
+
r = await call2('POST', '/payouts/rules/999/runs/w1/hold', {});
|
|
238
|
+
assert.equal(r.status, 404); assert.equal(r.body.error, 'no_such_rule'); ok('a run under no rule cannot be held');
|
|
239
|
+
await call2('POST', `/payouts/rules/${rule}/runs`, { run_key: 'old', payouts: [{ recipient_id: 8, amount_cents: 100 }] });
|
|
240
|
+
r = await call2('POST', `/payouts/rules/${rule}/runs/old/hold`, {});
|
|
241
|
+
assert.equal(r.status, 201); ok('a run whose pause has long passed can still be held before it is paid');
|
|
242
|
+
} finally { srv.close(); }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
const sql = readFileSync(new URL('../migrations/core_283_payout_approvals.sql', import.meta.url), 'utf8');
|
|
247
|
+
assert.match(sql, /payout_approvals_rules_idx[\s\S]*WHERE kind IN \('rule_signed', 'rule_narrowed', 'rule_revoked'\)/);
|
|
248
|
+
const src = readFileSync(new URL('../src/bongos/payout-approvals.js', import.meta.url), 'utf8');
|
|
249
|
+
assert.match(src, /WHERE kind IN \('rule_signed', 'rule_narrowed', 'rule_revoked'\)/);
|
|
250
|
+
ok('the rules list reads exactly the kinds its index covers');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(`\n${n} passed`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tests/payout_approvals_db.mjs — task 1004913 (ADR 0385 D4, ADR 0387 D2). Real DB.
|
|
2
|
+
// The approval log built from the real migration in a throwaway schema, proving the
|
|
3
|
+
// DATABASE (not the code that writes it) refuses: any UPDATE, DELETE or TRUNCATE; a
|
|
4
|
+
// signature with no web session; a machine principal's signature; a narrowing that
|
|
5
|
+
// widens; anything after a revocation. Then the store's checkRun over real rows.
|
|
6
|
+
// Self-skips without a reachable Postgres.
|
|
7
|
+
// Run: DATABASE_URL=... node tests/payout_approvals_db.mjs
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { Pool } = require('pg');
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL || undefined, max: 1 });
|
|
18
|
+
let client;
|
|
19
|
+
try { client = await pgPool.connect(); } catch (e) {
|
|
20
|
+
console.log(`payout_approvals_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
21
|
+
await pgPool.end().catch(() => {});
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const SCHEMA = `t1004913_${process.pid}`;
|
|
25
|
+
let pass = 0;
|
|
26
|
+
const ok = (c, m) => { assert.ok(c, m); pass++; console.log(` ok - ${m}`); };
|
|
27
|
+
const refused = async (sql, params, m) => {
|
|
28
|
+
await client.query('SAVEPOINT s');
|
|
29
|
+
let threw = false;
|
|
30
|
+
try { await client.query(sql, params); } catch { threw = true; }
|
|
31
|
+
await client.query('ROLLBACK TO SAVEPOINT s');
|
|
32
|
+
ok(threw, m);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const TERMS = { caps: { per_payout_cents: 500, per_run_cents: 1000, per_month_cents: 5000 }, currency: 'USD', formula: { kind: 'task_pay_terms' }, schedule: 'weekly', pause_hours: 48 };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await client.query(`CREATE SCHEMA ${SCHEMA}`);
|
|
39
|
+
await client.query(`SET search_path TO ${SCHEMA}`);
|
|
40
|
+
await client.query('CREATE TABLE builders (id bigserial PRIMARY KEY, system_role text)');
|
|
41
|
+
await client.query(readFileSync(join(here, '..', 'migrations', 'core_283_payout_approvals.sql'), 'utf8'));
|
|
42
|
+
await client.query("INSERT INTO builders (system_role) VALUES (NULL), (NULL), ('bfg')");
|
|
43
|
+
await client.query('BEGIN');
|
|
44
|
+
|
|
45
|
+
const lib = require('../src/bongos/payout-approvals.js');
|
|
46
|
+
const o = { pool: client };
|
|
47
|
+
// The N10 gates pass everyone here; tests/payout_gates_db.mjs runs them for real.
|
|
48
|
+
const og = { pool: client, gates: { gateOwedPay: async () => ({ ok: true, pay_to: 'self' }), gateRecipient: async () => ({ ok: true, pay_to: 'self' }), hasCosign: async () => true, gateOwedPayMany: async (m) => new Map([...m.keys()].map((k) => [k, { ok: true, pay_to: 'self' }])), cosignedInRun: async () => new Set() } };
|
|
49
|
+
const person = { id: 1, rank: 'archon', session_id: 11, session_source: 'web' };
|
|
50
|
+
|
|
51
|
+
const rule = await lib.insertEvent({ kind: 'rule_signed', actor: person, terms: TERMS }, o);
|
|
52
|
+
ok(rule.kind === 'rule_signed', 'a person signs a rule');
|
|
53
|
+
|
|
54
|
+
await refused('UPDATE payout_approvals SET note = $1 WHERE id = $2', ['x', rule.id], 'an UPDATE is refused by the database');
|
|
55
|
+
await refused('DELETE FROM payout_approvals WHERE id = $1', [rule.id], 'a DELETE is refused by the database');
|
|
56
|
+
await refused('TRUNCATE payout_approvals', [], 'a TRUNCATE is refused by the database');
|
|
57
|
+
|
|
58
|
+
const ins = "INSERT INTO payout_approvals (kind, actor_id, session_id, session_source, recipient_id, amount_cents, currency) VALUES ('payout_approved', $1, $2, $3, 2, 100, 'USD')";
|
|
59
|
+
await refused(ins, [1, 12, 'cli'], 'a signature from a CLI session is refused by the database');
|
|
60
|
+
await refused(ins, [1, null, 'web'], 'a signature naming no session is refused');
|
|
61
|
+
await refused(ins, [3, 13, 'web'], 'a machine principal cannot sign, even on a web session');
|
|
62
|
+
await client.query(ins, [1, 14, 'web']);
|
|
63
|
+
ok(true, 'a person on the website approves one payout');
|
|
64
|
+
|
|
65
|
+
const narrow = (terms) => client.query(
|
|
66
|
+
"INSERT INTO payout_approvals (kind, rule_id, actor_id, session_id, session_source, terms) VALUES ('rule_narrowed', $1, 1, 15, 'web', $2::jsonb)",
|
|
67
|
+
[rule.id, JSON.stringify(terms)],
|
|
68
|
+
);
|
|
69
|
+
const widen = async (terms, m) => { await client.query('SAVEPOINT s'); let threw = false; try { await narrow(terms); } catch { threw = true; } await client.query('ROLLBACK TO SAVEPOINT s'); ok(threw, m); };
|
|
70
|
+
await widen({ ...TERMS, caps: { ...TERMS.caps, per_run_cents: 2000 } }, 'the database refuses a narrowing that raises a cap');
|
|
71
|
+
await widen({ ...TERMS, caps: { per_payout_cents: 500, per_run_cents: 1000 } }, 'the database refuses a narrowing that removes a cap');
|
|
72
|
+
await widen({ ...TERMS, pause_hours: 1 }, 'the database refuses a narrowing that shortens the pause');
|
|
73
|
+
await widen({ ...TERMS, schedule: 'on_ship' }, 'the database refuses a narrowing that changes the schedule');
|
|
74
|
+
await narrow({ ...TERMS, caps: { ...TERMS.caps, per_payout_cents: 400 }, pause_hours: 72 });
|
|
75
|
+
ok(true, 'a real narrowing is accepted');
|
|
76
|
+
await widen({ ...TERMS, pause_hours: 48 }, 'the bar is the LATEST terms, not the signed ones');
|
|
77
|
+
|
|
78
|
+
const runner = { id: 3, session_id: null, session_source: 'cli' };
|
|
79
|
+
await lib.insertEvent({ kind: 'run_noticed', rule_id: rule.id, run_key: 'w1', actor: runner, amount_cents: 700, currency: 'USD', terms: { payouts: [{ recipient_id: '2', amount_cents: 400 }, { recipient_id: '1', amount_cents: 300 }] } }, o);
|
|
80
|
+
ok(true, 'a runner (even a machine) may notice a run: it authorises nothing');
|
|
81
|
+
const P = [{ recipient_id: '1', amount_cents: 300 }, { recipient_id: '2', amount_cents: 400 }];
|
|
82
|
+
ok((await lib.checkRun({ ruleId: rule.id, runKey: 'w1', payouts: P }, og)).reason === 'in_pause_window', 'a fresh run waits out its pause');
|
|
83
|
+
const later = new Date(Date.now() + 73 * 3600_000);
|
|
84
|
+
ok((await lib.checkRun({ ruleId: rule.id, runKey: 'w1', payouts: P, now: later }, og)).ok, 'after the pause it pays exactly as noticed');
|
|
85
|
+
|
|
86
|
+
await refused("INSERT INTO payout_approvals (kind, rule_id, run_key, actor_id, session_id, session_source) VALUES ('run_held', $1, 'nope', 1, 16, 'web')", [rule.id], 'holding a run never noticed is refused');
|
|
87
|
+
await lib.insertEvent({ kind: 'run_held', rule_id: rule.id, run_key: 'w1', actor: person }, o);
|
|
88
|
+
ok((await lib.checkRun({ ruleId: rule.id, runKey: 'w1', payouts: P, now: later }, og)).reason === 'run_held', 'a held run never pays');
|
|
89
|
+
|
|
90
|
+
await lib.insertEvent({ kind: 'rule_revoked', rule_id: rule.id, actor: person }, o);
|
|
91
|
+
await refused("INSERT INTO payout_approvals (kind, rule_id, run_key, actor_id, amount_cents, currency) VALUES ('run_noticed', $1, 'w2', 3, 100, 'USD')", [rule.id], 'nothing may follow a revocation');
|
|
92
|
+
ok((await lib.listRules(o))[0].active === false, 'the rule reads back revoked');
|
|
93
|
+
ok((await lib.listLog(o)).length >= 6, 'the log reads back');
|
|
94
|
+
|
|
95
|
+
const one = await lib.insertEvent({ kind: 'payout_approved', actor: person, recipient_id: 2, amount_cents: 250, currency: 'USD' }, o);
|
|
96
|
+
ok((await lib.checkApproval({ id: one.id, recipientId: 2, amountCents: 250, currency: 'USD' }, og)).ok, 'checkApproval passes the exact stored approval');
|
|
97
|
+
ok((await lib.checkApproval({ id: one.id, recipientId: 1, amountCents: 250, currency: 'USD' }, og)).reason === 'different_recipient', 'checkApproval refuses another recipient against real rows');
|
|
98
|
+
ok((await lib.checkApproval({ id: one.id, recipientId: 2, amountCents: 251, currency: 'USD' }, og)).reason === 'different_amount', 'checkApproval refuses another amount against real rows');
|
|
99
|
+
ok((await lib.checkApproval({ id: rule.id, recipientId: 2, amountCents: 250, currency: 'USD' }, og)).reason === 'no_such_approval', 'a rule is not a one-off approval');
|
|
100
|
+
await client.query('ROLLBACK');
|
|
101
|
+
} finally {
|
|
102
|
+
await client.query(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => {});
|
|
103
|
+
client.release();
|
|
104
|
+
await pgPool.end();
|
|
105
|
+
}
|
|
106
|
+
console.log(`payout_approvals_db.mjs: ${pass} passed`);
|