@bongos/core 1.23.22 → 1.24.2
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 +933 -413
- 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 +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -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 +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +57 -7
- package/docs/page-readings.json +1210 -1027
- 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/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -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 +158 -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/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- 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 +38 -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 +230 -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/government-walk.js +3 -0
- 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 +7 -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/access_holders.mjs +214 -0
- 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_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- 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,161 @@
|
|
|
1
|
+
// tests/account_logins_db.mjs — the ways a person signs in, against real SQL
|
|
2
|
+
// (task 1004992, ADR 0393). The DB-free sibling is tests/account_logins.mjs; this
|
|
3
|
+
// file proves what a fake pool cannot execute: the UNIQUE constraints, the
|
|
4
|
+
// sequence, the trigger, the backfill and a real race.
|
|
5
|
+
//
|
|
6
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the INTEGRATION set
|
|
7
|
+
// of scripts/bongos/run-unit-tests.js.
|
|
8
|
+
//
|
|
9
|
+
// Run: DATABASE_URL=... node tests/account_logins_db.mjs
|
|
10
|
+
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { Pool } = require('pg');
|
|
18
|
+
const logins = require('../modules/platform-identity/account-logins.js');
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
22
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
23
|
+
|
|
24
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
25
|
+
try {
|
|
26
|
+
await pool.query('SELECT 1');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log(`account_logins_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
29
|
+
await pool.end().catch(() => {});
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const GH = 90499201; // a GitHub-born account
|
|
34
|
+
const GH_OLD = 90499202; // an account from before migration 034
|
|
35
|
+
const SUB = (s) => `t1004992-${s}`;
|
|
36
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
37
|
+
|
|
38
|
+
async function cleanup() {
|
|
39
|
+
await q(
|
|
40
|
+
`DELETE FROM platform_identity_accounts
|
|
41
|
+
WHERE github_id = ANY($1::bigint[])
|
|
42
|
+
OR id IN (SELECT account_id FROM platform_identity_account_logins WHERE subject LIKE 't1004992-%')`,
|
|
43
|
+
[[GH, GH_OLD]],
|
|
44
|
+
).catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
const loginRows = async (accountId) => (await q(
|
|
47
|
+
`SELECT provider, subject, email FROM platform_identity_account_logins WHERE account_id = $1 ORDER BY provider`,
|
|
48
|
+
[accountId],
|
|
49
|
+
)).rows;
|
|
50
|
+
const accountRow = async (key) => (await q(
|
|
51
|
+
`SELECT id, github_id::text AS key, github_login, handle, profile_state FROM platform_identity_accounts WHERE github_id = $1`,
|
|
52
|
+
[key],
|
|
53
|
+
)).rows[0];
|
|
54
|
+
const code = async (p) => { try { await p; return null; } catch (e) { return e.code; } };
|
|
55
|
+
|
|
56
|
+
let pass = 0;
|
|
57
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
58
|
+
const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
for (const f of [
|
|
62
|
+
'platform_identity_001_tables.sql', 'platform_identity_007_profile_consent.sql',
|
|
63
|
+
'platform_identity_008_handle.sql', 'platform_identity_009_profile_fields.sql',
|
|
64
|
+
'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
|
|
65
|
+
'platform_identity_024_terms_grandfathered.sql',
|
|
66
|
+
]) await q(migrationSql(f));
|
|
67
|
+
await cleanup();
|
|
68
|
+
|
|
69
|
+
// An account from before this change: created, then its login row removed, as
|
|
70
|
+
// if 034 had not run yet. Re-running 034 must backfill it — and re-running is safe.
|
|
71
|
+
await q(`INSERT INTO platform_identity_accounts (github_id, github_login) VALUES ($1, 'old-octo')`, [GH_OLD]).catch(() => {});
|
|
72
|
+
await q(migrationSql('platform_identity_034_account_logins.sql'));
|
|
73
|
+
const old = await accountRow(GH_OLD);
|
|
74
|
+
await q(`DELETE FROM platform_identity_account_logins WHERE account_id = $1`, [old.id]);
|
|
75
|
+
await q(migrationSql('platform_identity_034_account_logins.sql'));
|
|
76
|
+
same(await loginRows(old.id), [{ provider: 'github', subject: String(GH_OLD), email: null }],
|
|
77
|
+
'every existing account gets its GitHub way in, and the migration re-runs safely');
|
|
78
|
+
|
|
79
|
+
// 1 — a GitHub-born account gets its login row from the trigger, with no writer change.
|
|
80
|
+
await q(`INSERT INTO platform_identity_accounts (github_id, github_login) VALUES ($1, 'octo')`, [GH]);
|
|
81
|
+
const gh = await accountRow(GH);
|
|
82
|
+
same(await loginRows(gh.id), [{ provider: 'github', subject: String(GH), email: null }],
|
|
83
|
+
'an account born through GitHub has its GitHub way in, written by the trigger');
|
|
84
|
+
|
|
85
|
+
// 2 — a person arriving through Google: a minted negative key, no GitHub login.
|
|
86
|
+
const jane = await logins.createAccountForLogin(
|
|
87
|
+
{ provider: 'google', subject: SUB('jane'), email: 'jane@example.com', displayName: 'Jane Doe' }, { pool });
|
|
88
|
+
ok(jane.created && logins.isMintedPersonKey(jane.personKey), `a Google arrival gets a minted negative key (${jane.personKey})`);
|
|
89
|
+
const janeRow = await accountRow(jane.personKey);
|
|
90
|
+
same([janeRow.github_login, janeRow.profile_state], [null, 'provisional'], 'no GitHub login is invented, and the account is provisional');
|
|
91
|
+
ok(/^janedoe(-\d+)?$/.test(janeRow.handle), `the handle grows from the name (${janeRow.handle})`);
|
|
92
|
+
same(await loginRows(janeRow.id), [{ provider: 'google', subject: SUB('jane'), email: 'jane@example.com' }],
|
|
93
|
+
'only the Google way in — the trigger writes no GitHub row for a minted key');
|
|
94
|
+
|
|
95
|
+
// 3 — the same Google identity again reaches the same person.
|
|
96
|
+
const again = await logins.createAccountForLogin({ provider: 'google', subject: SUB('jane') }, { pool });
|
|
97
|
+
same([again.personKey, again.created], [jane.personKey, false], 'signing up twice with one Google account is one person');
|
|
98
|
+
same(await logins.findPersonByLogin({ provider: 'google', subject: SUB('jane') }, { pool }),
|
|
99
|
+
{ accountId: janeRow.id, personKey: jane.personKey }, 'the sign-in lookup finds them');
|
|
100
|
+
|
|
101
|
+
// 4 — a real race: two sign-ups for one new identity at once make one person.
|
|
102
|
+
const [r1, r2] = await Promise.all([
|
|
103
|
+
logins.createAccountForLogin({ provider: 'google', subject: SUB('race') }, { pool }),
|
|
104
|
+
logins.createAccountForLogin({ provider: 'google', subject: SUB('race') }, { pool }),
|
|
105
|
+
]);
|
|
106
|
+
same(r1.personKey, r2.personKey, 'racing sign-ups land on one key');
|
|
107
|
+
const raced = (await q(
|
|
108
|
+
`SELECT count(*)::int AS n FROM platform_identity_account_logins WHERE provider = 'google' AND subject = $1`, [SUB('race')],
|
|
109
|
+
)).rows[0].n;
|
|
110
|
+
same(raced, 1, 'and one way in');
|
|
111
|
+
const orphans = (await q(
|
|
112
|
+
`SELECT count(*)::int AS n FROM platform_identity_accounts a
|
|
113
|
+
WHERE a.github_id < 0 AND NOT EXISTS (SELECT 1 FROM platform_identity_account_logins l WHERE l.account_id = a.id)`,
|
|
114
|
+
)).rows[0].n;
|
|
115
|
+
same(orphans, 0, 'the losing request left no account without a way in');
|
|
116
|
+
|
|
117
|
+
// 5 — the database refuses a second owner, and a second way in of one kind.
|
|
118
|
+
same(await code(q(
|
|
119
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'google', $2)`, [gh.id, SUB('jane')],
|
|
120
|
+
)), '23505', 'a Google identity can belong to only one person');
|
|
121
|
+
await q(`INSERT INTO platform_identity_account_logins (account_id, provider, subject, email) VALUES ($1, 'google', $2, 'octo@example.com')`,
|
|
122
|
+
[gh.id, SUB('octo')]);
|
|
123
|
+
same(await code(q(
|
|
124
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'google', $2)`, [gh.id, SUB('octo2')],
|
|
125
|
+
)), '23505', 'a person holds at most one Google way in');
|
|
126
|
+
same(await code(q(
|
|
127
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'email', 'a@b.c')`, [gh.id],
|
|
128
|
+
)), '23514', 'an unknown provider is refused by the database');
|
|
129
|
+
|
|
130
|
+
// 6 — the own-scope read lists the person's ways in, GitHub first, and no ids.
|
|
131
|
+
const listed = await logins.listLoginsForPerson(String(GH), { pool });
|
|
132
|
+
same(listed.map((m) => [m.provider, m.email]), [['github', null], ['google', 'octo@example.com']],
|
|
133
|
+
'a person with both ways in sees both, GitHub first');
|
|
134
|
+
ok(listed.every((m) => !('subject' in m)), 'provider ids never leave the read');
|
|
135
|
+
same((await logins.listLoginsForPerson(jane.personKey, { pool })).map((m) => m.provider), ['google'],
|
|
136
|
+
'a minted key reads its own ways in');
|
|
137
|
+
|
|
138
|
+
// 7 — a minted key stays the key: the sequence never hands out a GitHub-shaped id.
|
|
139
|
+
const keys = (await q(`SELECT github_id FROM platform_identity_accounts WHERE id IN (
|
|
140
|
+
SELECT account_id FROM platform_identity_account_logins WHERE subject LIKE 't1004992-%' AND provider = 'google')
|
|
141
|
+
AND github_id < 0`)).rows;
|
|
142
|
+
ok(keys.length >= 2, 'every Google-born fixture holds a negative key');
|
|
143
|
+
|
|
144
|
+
// 8 — signing in with Google (task 1004993, ADR 0396): the same person every time,
|
|
145
|
+
// found by Google's id; and the hub's own sign-in never writes the core stand-in
|
|
146
|
+
// (or any login) onto a minted key.
|
|
147
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
148
|
+
const first = await logins.signInWithLogin({ provider: 'google', subject: SUB('signin'), email: 'one@example.com', displayName: 'Sign In' }, { pool });
|
|
149
|
+
ok(first.created && /^-\d+$/.test(first.personKey), 'a first Google sign-in creates a person with a minted key');
|
|
150
|
+
const second = await logins.signInWithLogin({ provider: 'google', subject: SUB('signin'), email: 'two@example.com' }, { pool });
|
|
151
|
+
same([second.personKey, second.created], [first.personKey, false], 'signing in again reaches the same person');
|
|
152
|
+
same((await logins.listLoginsForPerson(first.personKey, { pool }))[0].email, 'two@example.com', 'the shown email follows the latest sign-in');
|
|
153
|
+
await pi.upsertAccount({ githubId: first.personKey, githubLogin: '~signin', displayName: 'Sign In', activate: true }, { pool });
|
|
154
|
+
const after = await accountRow(first.personKey);
|
|
155
|
+
same([after.github_login, after.profile_state], [null, 'active'], 'the hub sign-in activates the person and writes no login');
|
|
156
|
+
|
|
157
|
+
console.log(`\naccount_logins_db.mjs: ${pass} passed`);
|
|
158
|
+
} finally {
|
|
159
|
+
await cleanup();
|
|
160
|
+
await pool.end();
|
|
161
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// tests/ai_studio_billing.mjs — what an AI generation costs, and the order
|
|
2
|
+
// money moves in (task 1004920, goal 1000136, ADR 0392):
|
|
3
|
+
// - price = provider cost x 1.08, in integer micro-dollars, never shaved;
|
|
4
|
+
// - a top-up adds the processor's fee so the asked-for amount arrives whole;
|
|
5
|
+
// - the full price is HELD before the call, a short balance refuses and the
|
|
6
|
+
// call never runs, the real price is captured after, and a provider failure
|
|
7
|
+
// releases the whole hold;
|
|
8
|
+
// - the default backend refuses money entirely, and the ledger is append-only.
|
|
9
|
+
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const P = require('../modules/ai-studio/pricing.js');
|
|
18
|
+
const payments = require('../modules/ai-studio/payments.js');
|
|
19
|
+
const { runPaid, BillingRefusal } = require('../modules/ai-studio/billing.js');
|
|
20
|
+
const S = require('../modules/ai-studio/studio.js');
|
|
21
|
+
|
|
22
|
+
const USD = P.MICRO_PER_USD;
|
|
23
|
+
|
|
24
|
+
// ---- pricing arithmetic ----------------------------------------------------
|
|
25
|
+
|
|
26
|
+
test('the multiplier is the owner\'s 1.08, and nothing else in the module spells it', () => {
|
|
27
|
+
assert.equal(P.AI_PRICE_MULTIPLIER, 1.08);
|
|
28
|
+
for (const f of ['billing.js', 'payments.js', 'keys.js', 'db.js', 'routes/ai-studio.js', 'generate.js', 'studio.js', 'openrouter.js', 'context.js', 'gemini.js']) {
|
|
29
|
+
const src = fs.readFileSync(new URL(`../modules/ai-studio/${f}`, import.meta.url), 'utf8');
|
|
30
|
+
assert.doesNotMatch(src, /1\.08|\b108\b/, `${f} re-spells the multiplier`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('price = cost x 1.08: provider cost and markup apart, rounded up, never down', () => {
|
|
35
|
+
assert.deepEqual(P.priceFor(1 * USD), { provider_cost_micro: 1000000, markup_micro: 80000, price_micro: 1080000 });
|
|
36
|
+
assert.deepEqual(P.priceFor(0), { provider_cost_micro: 0, markup_micro: 0, price_micro: 0 });
|
|
37
|
+
// A sub-cent call: 0.0437 cents -> 437 micro x 1.08 = 471.96 -> 472 (up).
|
|
38
|
+
assert.deepEqual(P.priceFor(437), { provider_cost_micro: 437, markup_micro: 35, price_micro: 472 });
|
|
39
|
+
for (const c of [1, 7, 99, 12345, 987654321]) {
|
|
40
|
+
const p = P.priceFor(c);
|
|
41
|
+
assert.ok(p.price_micro >= c * 1.08, `price for ${c} fell below cost x 1.08`);
|
|
42
|
+
assert.ok(p.price_micro - c * 1.08 < 1, `price for ${c} rounded up by more than one micro-dollar`);
|
|
43
|
+
assert.equal(p.provider_cost_micro + p.markup_micro, p.price_micro);
|
|
44
|
+
}
|
|
45
|
+
assert.throws(() => P.priceFor(-1), TypeError);
|
|
46
|
+
assert.throws(() => P.priceFor(1.5), TypeError);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a top-up charges enough that, after the processor\'s fee, the full amount arrives', () => {
|
|
50
|
+
const q = P.topUpCharge(10 * USD);
|
|
51
|
+
// $10 net: (10 + 0.30) / 0.971 = 10.6076... -> $10.61; fee = round(10.61 x 2.9%) + 0.30 = 0.61.
|
|
52
|
+
assert.deepEqual(q, { charge_micro: 10610000, processor_fee_micro: 610000, credited_micro: 10000000 });
|
|
53
|
+
for (const usd of [5, 7.33, 20, 99.99, 500]) {
|
|
54
|
+
const want = Math.round(usd * USD);
|
|
55
|
+
const r = P.topUpCharge(want);
|
|
56
|
+
const chargeCents = r.charge_micro / 10000;
|
|
57
|
+
assert.ok(r.charge_micro - P.processorFeeCents(chargeCents) * 10000 >= r.credited_micro, `$${usd}: the fee ate into the balance`);
|
|
58
|
+
assert.ok(r.credited_micro >= want, `$${usd}: credited less than asked`);
|
|
59
|
+
// And it is the SMALLEST such charge: one cent less would not cover it.
|
|
60
|
+
assert.ok((chargeCents - 1) - P.processorFeeCents(chargeCents - 1) < r.credited_micro / 10000, `$${usd}: overcharged`);
|
|
61
|
+
}
|
|
62
|
+
assert.throws(() => P.topUpCharge(1 * USD), RangeError);
|
|
63
|
+
assert.throws(() => P.topUpCharge(501 * USD), RangeError);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('formatUsd shows a sub-cent cost instead of a rounded zero', () => {
|
|
67
|
+
assert.equal(P.formatUsd(472), '$0.0005');
|
|
68
|
+
assert.equal(P.formatUsd(10610000), '$10.61');
|
|
69
|
+
assert.equal(P.formatUsd(0), '$0.00');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---- hold, run, capture / release -----------------------------------------
|
|
73
|
+
|
|
74
|
+
function harness(startUsd) {
|
|
75
|
+
const b = payments.createFakeBackend();
|
|
76
|
+
const rows = [];
|
|
77
|
+
const ledger = async (row) => { rows.push(row); };
|
|
78
|
+
return { b, rows, ledger, fund: () => b.topUp(1, { chargeMicro: startUsd * USD + 1, creditedMicro: startUsd * USD }) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test('THE PAID PATH: hold the most it can cost, run, capture the real price, return the rest', async () => {
|
|
82
|
+
const h = harness(10); await h.fund();
|
|
83
|
+
let sawHeld = null;
|
|
84
|
+
const out = await runPaid({
|
|
85
|
+
builderId: 1, requestId: 'r1', maxCostMicro: 2 * USD, payments: h.b, ledger: h.ledger,
|
|
86
|
+
run: async () => { sawHeld = await h.b.balance(1); return { costMicro: 500000, result: 'pictures' }; },
|
|
87
|
+
});
|
|
88
|
+
// While the call ran, the whole ceiling (2 x 1.08) was already held.
|
|
89
|
+
assert.deepEqual(sawHeld, { available_micro: 10 * USD - 2160000, held_micro: 2160000 });
|
|
90
|
+
assert.equal(out.result, 'pictures');
|
|
91
|
+
assert.equal(out.charge.price_micro, 540000);
|
|
92
|
+
assert.deepEqual(await h.b.balance(1), { available_micro: 10 * USD - 540000, held_micro: 0 });
|
|
93
|
+
assert.deepEqual(h.rows.map((r) => [r.kind, r.amountMicro]), [['hold', 2160000], ['capture', 540000]]);
|
|
94
|
+
const cap = h.rows[1];
|
|
95
|
+
assert.equal(cap.providerCostMicro, 500000);
|
|
96
|
+
assert.equal(cap.markupMicro, 40000);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a short balance refuses BEFORE the provider is called', async () => {
|
|
100
|
+
const h = harness(5); await h.fund();
|
|
101
|
+
let ran = false;
|
|
102
|
+
await assert.rejects(
|
|
103
|
+
runPaid({ builderId: 1, requestId: 'r2', maxCostMicro: 6 * USD, payments: h.b, ledger: h.ledger, run: async () => { ran = true; return { costMicro: 1 }; } }),
|
|
104
|
+
(err) => err instanceof BillingRefusal && err.code === 'insufficient_funds' && err.needed_micro === 6480000);
|
|
105
|
+
assert.equal(ran, false, 'the generation ran without its funds secured');
|
|
106
|
+
assert.deepEqual(await h.b.balance(1), { available_micro: 5 * USD, held_micro: 0 });
|
|
107
|
+
assert.equal(h.rows.length, 0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a provider failure releases the WHOLE hold and records it', async () => {
|
|
111
|
+
const h = harness(10); await h.fund();
|
|
112
|
+
await assert.rejects(
|
|
113
|
+
runPaid({ builderId: 1, requestId: 'r3', maxCostMicro: 3 * USD, payments: h.b, ledger: h.ledger, run: async () => { throw new Error('provider 500'); } }),
|
|
114
|
+
/provider 500/);
|
|
115
|
+
assert.deepEqual(await h.b.balance(1), { available_micro: 10 * USD, held_micro: 0 }, 'money was kept after a failed call');
|
|
116
|
+
assert.deepEqual(h.rows.map((r) => [r.kind, r.amountMicro]), [['hold', 3240000], ['release', 3240000]]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a provider reporting more than its ceiling is charged at most the ceiling', async () => {
|
|
120
|
+
const h = harness(10); await h.fund();
|
|
121
|
+
const out = await runPaid({ builderId: 1, requestId: 'r4', maxCostMicro: 1 * USD, payments: h.b, ledger: h.ledger, run: async () => ({ costMicro: 9 * USD }) });
|
|
122
|
+
assert.equal(out.charge.price_micro, 1080000);
|
|
123
|
+
assert.deepEqual(await h.b.balance(1), { available_micro: 10 * USD - 1080000, held_micro: 0 });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a ledger write failing after money moved does not charge twice or throw', async () => {
|
|
127
|
+
const h = harness(10); await h.fund();
|
|
128
|
+
const errors = [];
|
|
129
|
+
const out = await runPaid({
|
|
130
|
+
builderId: 1, requestId: 'r5', maxCostMicro: 1 * USD, payments: h.b,
|
|
131
|
+
ledger: async () => { throw new Error('db down'); }, log: { error: (o) => errors.push(o) },
|
|
132
|
+
run: async () => ({ costMicro: 100000 }),
|
|
133
|
+
});
|
|
134
|
+
assert.equal(out.charge.price_micro, 108000);
|
|
135
|
+
assert.equal(errors.length, 2);
|
|
136
|
+
assert.deepEqual(await h.b.balance(1), { available_micro: 10 * USD - 108000, held_micro: 0 });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('with payments not set up (the default), nothing can be held or topped up', async () => {
|
|
140
|
+
const saved = process.env.AI_STUDIO_PAYMENTS;
|
|
141
|
+
for (const v of [undefined, '', 'stripe-typo', 'FAKEISH']) {
|
|
142
|
+
if (v === undefined) delete process.env.AI_STUDIO_PAYMENTS; else process.env.AI_STUDIO_PAYMENTS = v;
|
|
143
|
+
payments._setBackend(null);
|
|
144
|
+
assert.equal(payments.backend().name, 'none', `"${v}" opened a backend`);
|
|
145
|
+
}
|
|
146
|
+
if (saved === undefined) delete process.env.AI_STUDIO_PAYMENTS; else process.env.AI_STUDIO_PAYMENTS = saved;
|
|
147
|
+
let ran = false;
|
|
148
|
+
await assert.rejects(
|
|
149
|
+
runPaid({ builderId: 1, requestId: 'r6', maxCostMicro: 1, payments: payments.NONE, ledger: async () => {}, run: async () => { ran = true; } }),
|
|
150
|
+
(err) => err.code === 'payments_not_configured');
|
|
151
|
+
assert.equal(ran, false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('the ledger table is append-only and has no balance column', () => {
|
|
155
|
+
const sql = fs.readFileSync(new URL('../modules/ai-studio/migrations/ai_studio_002_ledger.sql', import.meta.url), 'utf8');
|
|
156
|
+
assert.match(sql, /BEFORE UPDATE OR DELETE ON ai_studio_ledger/);
|
|
157
|
+
assert.match(sql, /RAISE EXCEPTION 'ai_studio_ledger is append-only'/);
|
|
158
|
+
assert.match(sql, /provider_cost_micro\s+BIGINT/);
|
|
159
|
+
assert.match(sql, /markup_micro\s+BIGINT/);
|
|
160
|
+
assert.match(sql, /processor_fee_micro\s+BIGINT/);
|
|
161
|
+
assert.doesNotMatch(sql.replace(/--.*$/gm, ''), /\bbalance\b/i, 'a balance column would be Bongos holding user money (ADR 0385)');
|
|
162
|
+
const db = fs.readFileSync(new URL('../modules/ai-studio/db.js', import.meta.url), 'utf8');
|
|
163
|
+
assert.doesNotMatch(db, /UPDATE ai_studio_ledger|DELETE FROM ai_studio_ledger/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ---- the routes ------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
const db = require('../modules/ai-studio/db.js');
|
|
169
|
+
const api = require('../src/module-api.js');
|
|
170
|
+
const ledgerRows = [];
|
|
171
|
+
db.appendLedger = async (row) => { ledgerRows.push({ ...row, id: ledgerRows.length + 1, created_at: 'now' }); return {}; };
|
|
172
|
+
db.recentLedger = async (builderId) => ledgerRows.filter((r) => r.builderId === builderId).map((r) => ({
|
|
173
|
+
id: r.id, kind: r.kind, request_id: r.requestId || null, amount_micro: r.amountMicro || 0, provider_cost_micro: r.providerCostMicro || 0,
|
|
174
|
+
markup_micro: r.markupMicro || 0, processor_fee_micro: r.processorFeeMicro || 0, backend: r.backend, note: r.note || '', created_at: r.created_at, ref: r.ref,
|
|
175
|
+
})).reverse();
|
|
176
|
+
let viewer = { id: 7, rank: 'xenos' };
|
|
177
|
+
for (const [name, value] of Object.entries({
|
|
178
|
+
requireBuilder: (req, res, next) => { req.builder = viewer; next(); },
|
|
179
|
+
requireRank: () => (req, res, next) => next(),
|
|
180
|
+
})) Object.defineProperty(api, name, { value, configurable: true, writable: true });
|
|
181
|
+
|
|
182
|
+
const express = require('express');
|
|
183
|
+
const app = express();
|
|
184
|
+
app.use((req, res, next) => {
|
|
185
|
+
res.fail = (code, opts) => res.status(typeof opts === 'number' ? opts : (opts && opts.status) || 400).json({ error: { code, message: opts && opts.message } });
|
|
186
|
+
next();
|
|
187
|
+
});
|
|
188
|
+
app.use(express.json());
|
|
189
|
+
app.use('/api/bongos', require('../modules/ai-studio/routes/ai-studio.js')());
|
|
190
|
+
const server = app.listen(0);
|
|
191
|
+
await new Promise((r) => server.once('listening', r));
|
|
192
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
193
|
+
test.after(() => server.close());
|
|
194
|
+
|
|
195
|
+
function call(method, path, body) {
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const data = body === undefined ? null : JSON.stringify(body);
|
|
198
|
+
const req = http.request(`${base}${path}`, { method, headers: data ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } : {} }, (res) => {
|
|
199
|
+
const chunks = [];
|
|
200
|
+
res.on('data', (c) => chunks.push(c));
|
|
201
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString()) }));
|
|
202
|
+
});
|
|
203
|
+
req.on('error', reject);
|
|
204
|
+
if (data) req.write(data);
|
|
205
|
+
req.end();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
test('routes: top up on the test backend, then read the balance and a ledger with every amount apart', async () => {
|
|
210
|
+
payments._setBackend(payments.createFakeBackend());
|
|
211
|
+
const up = await call('POST', '/ai-studio/topup', { amount_usd: 10 });
|
|
212
|
+
assert.equal(up.status, 201);
|
|
213
|
+
assert.deepEqual(up.body.topup, { charge_micro: 10610000, processor_fee_micro: 610000, credited_micro: 10000000 });
|
|
214
|
+
const bal = await call('GET', '/ai-studio/balance');
|
|
215
|
+
assert.equal(bal.body.payments, 'fake');
|
|
216
|
+
assert.equal(bal.body.test_mode, true);
|
|
217
|
+
assert.equal(bal.body.price_multiplier, 1.08);
|
|
218
|
+
assert.deepEqual(bal.body.balance, { available_micro: 10000000, held_micro: 0 });
|
|
219
|
+
assert.equal(bal.body.ledger[0].kind, 'topup');
|
|
220
|
+
assert.equal(bal.body.ledger[0].processor_fee_micro, 610000);
|
|
221
|
+
assert.ok(!('ref' in bal.body.ledger[0]), 'the processor reference is internal');
|
|
222
|
+
assert.equal((await call('POST', '/ai-studio/topup', { amount_usd: 1 })).status, 400);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Task 1004921: the Studio says "your card is charged $20.91" BEFORE anything is
|
|
226
|
+
// paid, and how much a run sets aside, from these two reads, so neither the fee
|
|
227
|
+
// nor the ceiling is spelled in the browser.
|
|
228
|
+
test('routes: a top-up quote charges what the top-up itself would, and writes nothing', async () => {
|
|
229
|
+
payments._setBackend(payments.createFakeBackend());
|
|
230
|
+
const before = ledgerRows.length;
|
|
231
|
+
const q = await call('GET', '/ai-studio/topup/quote?amount_usd=10');
|
|
232
|
+
assert.equal(q.status, 200);
|
|
233
|
+
assert.deepEqual(q.body.quote, P.topUpCharge(10 * USD));
|
|
234
|
+
assert.equal(q.body.quote.charge_micro, 10610000);
|
|
235
|
+
assert.equal(q.body.min_micro, P.MIN_TOPUP_MICRO);
|
|
236
|
+
assert.equal(q.body.max_micro, P.MAX_TOPUP_MICRO);
|
|
237
|
+
assert.equal((await call('GET', '/ai-studio/topup/quote?amount_usd=20')).body.quote.charge_micro, 20910000);
|
|
238
|
+
assert.equal((await call('GET', '/ai-studio/topup/quote?amount_usd=2')).status, 400);
|
|
239
|
+
assert.equal((await call('GET', '/ai-studio/topup/quote?amount_usd=abc')).status, 400);
|
|
240
|
+
assert.equal(ledgerRows.length, before, 'a quote must not move money or write the ledger');
|
|
241
|
+
const bal = await call('GET', '/ai-studio/balance');
|
|
242
|
+
assert.equal(bal.body.max_cost_micro, S.MAX_COST_MICRO);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('routes: with payments not set up, a top-up is refused and no row is written', async () => {
|
|
246
|
+
payments._setBackend(payments.NONE);
|
|
247
|
+
const before = ledgerRows.length;
|
|
248
|
+
const r = await call('POST', '/ai-studio/topup', { amount_usd: 10 });
|
|
249
|
+
assert.equal(r.status, 409);
|
|
250
|
+
assert.equal(r.body.error.code, 'payments_not_configured');
|
|
251
|
+
assert.equal(ledgerRows.length, before);
|
|
252
|
+
assert.equal((await call('GET', '/ai-studio/balance')).body.balance, null);
|
|
253
|
+
});
|