@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,205 @@
|
|
|
1
|
+
// tests/goal_achievement_reward.mjs — the karma a goal's contributors earn when it
|
|
2
|
+
// is achieved (task 1001693 / BV1.P5.3, ADR 0101 Part 3).
|
|
3
|
+
//
|
|
4
|
+
// Three layers: the pure split (contribution-weighted, conserving, owner share,
|
|
5
|
+
// nothing for a goal that shipped nothing); the booking against a recording client
|
|
6
|
+
// (one guarded INSERT on the goal's reason key, so a re-achieve pays nothing); and
|
|
7
|
+
// the lifecycle hook (only the statement that actually flipped a goal awards, once
|
|
8
|
+
// per flipped goal, and a reward failure never fails the caller).
|
|
9
|
+
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const {
|
|
16
|
+
goalRewardSplit, awardGoalAchievementKarma,
|
|
17
|
+
GOAL_ACHIEVEMENT_KARMA_POOL, GOAL_REASON_PREFIX,
|
|
18
|
+
} = require('../modules/economy/goal-reward.js');
|
|
19
|
+
const seams = require('../src/module-seams.js');
|
|
20
|
+
const reward = require('../modules/economy/reward.js');
|
|
21
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', reward);
|
|
22
|
+
const { autoSatisfyShippedCriteria } = require('../modules/lifecycle/done-when.js');
|
|
23
|
+
|
|
24
|
+
const sum = (rows) => rows.reduce((t, r) => t + r.delta, 0);
|
|
25
|
+
const byId = (rows) => Object.fromEntries(rows.map((r) => [String(r.builderId), r]));
|
|
26
|
+
|
|
27
|
+
// ── the pure split ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('the split follows shipped work and conserves the pool', () => {
|
|
30
|
+
const rows = goalRewardSplit({
|
|
31
|
+
contributions: [{ builderId: 1, shipped: 3 }, { builderId: 2, shipped: 1 }],
|
|
32
|
+
ownerId: null, pool: 8, ownerShare: 0,
|
|
33
|
+
});
|
|
34
|
+
assert.equal(sum(rows), 8);
|
|
35
|
+
assert.equal(byId(rows)['1'].delta, 6);
|
|
36
|
+
assert.equal(byId(rows)['2'].delta, 2);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('rounding still conserves the pool and is deterministic', () => {
|
|
40
|
+
const contributions = [{ builderId: 3, shipped: 1 }, { builderId: 1, shipped: 1 }, { builderId: 2, shipped: 1 }];
|
|
41
|
+
const a = goalRewardSplit({ contributions, ownerId: null, pool: 10, ownerShare: 0 });
|
|
42
|
+
const b = goalRewardSplit({ contributions: [...contributions].reverse(), ownerId: null, pool: 10, ownerShare: 0 });
|
|
43
|
+
assert.equal(sum(a), 10);
|
|
44
|
+
assert.deepEqual(byId(a), byId(b));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('joining a goal earns nothing: only builders who shipped are paid', () => {
|
|
48
|
+
const rows = goalRewardSplit({
|
|
49
|
+
contributions: [{ builderId: 1, shipped: 2 }, { builderId: 2, shipped: 0 }],
|
|
50
|
+
ownerId: null, ownerShare: 0,
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(rows.map((r) => String(r.builderId)), ['1']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('the owner takes a coordination share, folded into one row when they also shipped', () => {
|
|
56
|
+
const asOwnerOnly = goalRewardSplit({ contributions: [{ builderId: 1, shipped: 1 }], ownerId: 9, pool: 10, ownerShare: 0.1 });
|
|
57
|
+
assert.equal(byId(asOwnerOnly)['9'].delta, 1);
|
|
58
|
+
assert.equal(byId(asOwnerOnly)['9'].role, 'owner');
|
|
59
|
+
assert.equal(sum(asOwnerOnly), 10);
|
|
60
|
+
|
|
61
|
+
const asBoth = goalRewardSplit({ contributions: [{ builderId: 9, shipped: 1 }, { builderId: 2, shipped: 1 }], ownerId: 9, pool: 10, ownerShare: 0.1 });
|
|
62
|
+
assert.equal(asBoth.filter((r) => String(r.builderId) === '9').length, 1, 'one row per builder');
|
|
63
|
+
assert.equal(byId(asBoth)['9'].role, 'owner_contributor');
|
|
64
|
+
assert.equal(sum(asBoth), 10);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a goal that shipped nothing earns nothing, the owner share included', () => {
|
|
68
|
+
assert.deepEqual(goalRewardSplit({ contributions: [], ownerId: 9 }), []);
|
|
69
|
+
assert.deepEqual(goalRewardSplit({ contributions: [{ builderId: 1, shipped: 0 }], ownerId: 9 }), []);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the default pool is karma-sized, not a money figure', () => {
|
|
73
|
+
assert.ok(GOAL_ACHIEVEMENT_KARMA_POOL > 0 && GOAL_ACHIEVEMENT_KARMA_POOL <= 50);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── the booking ──────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function recordingExec({ ownerId = 9, shippers = [{ builder_id: 1, shipped: 2 }], alreadyAwarded = false,
|
|
79
|
+
insertFails = false, inTx = true } = {}) {
|
|
80
|
+
const calls = [];
|
|
81
|
+
return {
|
|
82
|
+
calls,
|
|
83
|
+
async query(sql, params) {
|
|
84
|
+
calls.push({ sql, params });
|
|
85
|
+
if (/SAVEPOINT/.test(sql)) {
|
|
86
|
+
if (!inTx) throw new Error('SAVEPOINT can only be used in transaction blocks');
|
|
87
|
+
return { rows: [] };
|
|
88
|
+
}
|
|
89
|
+
if (/^SELECT created_by FROM goals/.test(sql)) return { rows: ownerId === undefined ? [] : [{ created_by: ownerId }] };
|
|
90
|
+
if (/FROM tasks/.test(sql)) return { rows: shippers };
|
|
91
|
+
if (/INSERT INTO karma_log/.test(sql)) {
|
|
92
|
+
if (insertFails) throw new Error('karma_log is unwritable');
|
|
93
|
+
return { rowCount: alreadyAwarded ? 0 : params[0].length };
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`unexpected SQL: ${sql}`);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test('booking writes karma, never credits, in one statement guarded on the goal reason', async () => {
|
|
101
|
+
const exec = recordingExec();
|
|
102
|
+
const r = await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
103
|
+
assert.equal(r.reason, 'awarded');
|
|
104
|
+
assert.equal(r.awarded, GOAL_ACHIEVEMENT_KARMA_POOL);
|
|
105
|
+
const inserts = exec.calls.filter((c) => /INSERT/.test(c.sql));
|
|
106
|
+
assert.equal(inserts.length, 1);
|
|
107
|
+
assert.match(inserts[0].sql, /INSERT INTO karma_log/);
|
|
108
|
+
assert.ok(!exec.calls.some((c) => /credit_log/.test(c.sql)), 'no credit is booked');
|
|
109
|
+
assert.match(inserts[0].sql.replace(/\s+/g, ' '),
|
|
110
|
+
/NOT EXISTS \(SELECT 1 FROM karma_log WHERE builder_id = ANY\(\$4::bigint\[\]\) AND reason = \$3\)/,
|
|
111
|
+
'builder-scoped so idx_karma_log_builder serves it, never a scan of all karma');
|
|
112
|
+
assert.equal(inserts[0].params[2], `${GOAL_REASON_PREFIX}42`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('the once-per-goal guard always includes the goal owner, even when they shipped nothing', async () => {
|
|
116
|
+
// Owner 9 shipped nothing; builder 1 shipped. An earlier award paid the owner,
|
|
117
|
+
// so the owner must be in the guard set for a re-achieve to be refused.
|
|
118
|
+
const exec = recordingExec({ ownerId: 9, shippers: [{ builder_id: 1, shipped: 2 }] });
|
|
119
|
+
await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
120
|
+
const insert = exec.calls.find((c) => /INSERT INTO karma_log/.test(c.sql));
|
|
121
|
+
assert.deepEqual([...insert.params[3]].sort(), ['1', '9']);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('only shipped, non-smoke tasks of THIS goal count as contribution', async () => {
|
|
125
|
+
const exec = recordingExec();
|
|
126
|
+
await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
127
|
+
const read = exec.calls.find((c) => /FROM tasks/.test(c.sql));
|
|
128
|
+
assert.match(read.sql, /goal_id = \$1/);
|
|
129
|
+
assert.match(read.sql, /status = 'shipped'/);
|
|
130
|
+
assert.match(read.sql, /smoke/);
|
|
131
|
+
assert.deepEqual(read.params, [42]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('a re-achieved goal pays nothing the second time', async () => {
|
|
135
|
+
const r = await awardGoalAchievementKarma(recordingExec({ alreadyAwarded: true }), { goalId: 42 });
|
|
136
|
+
assert.equal(r.awarded, 0);
|
|
137
|
+
assert.equal(r.reason, 'already_awarded');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a goal that shipped nothing writes nothing', async () => {
|
|
141
|
+
const exec = recordingExec({ shippers: [] });
|
|
142
|
+
const r = await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
143
|
+
assert.equal(r.reason, 'nothing_shipped');
|
|
144
|
+
assert.ok(!exec.calls.some((c) => /INSERT/.test(c.sql)));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a failed karma write rolls back to its savepoint and never throws into the ship', async () => {
|
|
148
|
+
const exec = recordingExec({ insertFails: true });
|
|
149
|
+
const r = await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
150
|
+
assert.equal(r.reason, 'failed');
|
|
151
|
+
assert.equal(r.awarded, 0);
|
|
152
|
+
const sqls = exec.calls.map((c) => c.sql);
|
|
153
|
+
assert.equal(sqls[0], 'SAVEPOINT goal_achievement_karma');
|
|
154
|
+
assert.ok(sqls.includes('ROLLBACK TO SAVEPOINT goal_achievement_karma'));
|
|
155
|
+
assert.ok(!sqls.includes('RELEASE SAVEPOINT goal_achievement_karma'));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('on the pool (no transaction) the booking still runs, without a savepoint', async () => {
|
|
159
|
+
const exec = recordingExec({ inTx: false });
|
|
160
|
+
const r = await awardGoalAchievementKarma(exec, { goalId: 42 });
|
|
161
|
+
assert.equal(r.reason, 'awarded');
|
|
162
|
+
assert.ok(!exec.calls.some((c) => /RELEASE|ROLLBACK/.test(c.sql)));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── the lifecycle hook ───────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
// Drives the REAL ship-time entry, autoSatisfyShippedCriteria scoped to a task:
|
|
168
|
+
// no criterion closes, so it re-checks the shipping task's own goal through the
|
|
169
|
+
// one flip statement. `flipped` is what that UPDATE ... RETURNING hands back.
|
|
170
|
+
function lifecycleExec(flipped) {
|
|
171
|
+
return {
|
|
172
|
+
async query(sql) {
|
|
173
|
+
if (/^\s*UPDATE goals g/.test(sql)) return { rows: flipped.map((id) => ({ id, title: 'g', version_id: 'V', status: 'achieved' })) };
|
|
174
|
+
if (/SELECT goal_id FROM tasks WHERE id/.test(sql)) return { rows: [{ goal_id: 5 }] };
|
|
175
|
+
return { rows: [], rowCount: 0 };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const shipFlip = (exec) => autoSatisfyShippedCriteria(exec, { taskId: 77 }).then((r) => r.goals);
|
|
180
|
+
|
|
181
|
+
async function withStubbedAward(fn, stub) {
|
|
182
|
+
const original = reward.awardGoalAchievementKarma;
|
|
183
|
+
reward.awardGoalAchievementKarma = stub;
|
|
184
|
+
try { return await fn(); } finally { reward.awardGoalAchievementKarma = original; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test('the goal a ship flipped is rewarded once, on the ship executor', async () => {
|
|
188
|
+
const seen = [];
|
|
189
|
+
const exec = lifecycleExec([5]);
|
|
190
|
+
await withStubbedAward(() => shipFlip(exec),
|
|
191
|
+
async (e, { goalId }) => { seen.push({ same: e === exec, goalId: Number(goalId) }); return { awarded: 1 }; });
|
|
192
|
+
assert.deepEqual(seen, [{ same: true, goalId: 5 }]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('no flip, no reward: a goal still open or already achieved earns nothing here', async () => {
|
|
196
|
+
let called = 0;
|
|
197
|
+
await withStubbedAward(() => shipFlip(lifecycleExec([])), async () => { called += 1; });
|
|
198
|
+
assert.equal(called, 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('a reward provider that throws never fails the achievement', async () => {
|
|
202
|
+
const goals = await withStubbedAward(() => shipFlip(lifecycleExec([5])),
|
|
203
|
+
async () => { throw new Error('karma write failed'); });
|
|
204
|
+
assert.deepEqual(goals.map((g) => g.id), [5]);
|
|
205
|
+
});
|
|
@@ -101,7 +101,8 @@ await test('the t.* views are rebuilt WITH the column — the core_190 lesson',
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
await test('createTask appends createdBy at the end of the bind list (the goals convention)', () => {
|
|
104
|
-
|
|
104
|
+
// later columns append after it (task 1004950 added `needs`), never before
|
|
105
|
+
assert.match(DB_TASKS, /goal_id, created_by, promoted_at[,)]/,
|
|
105
106
|
'new columns go on the end so pinned bind orders survive');
|
|
106
107
|
});
|
|
107
108
|
|
package/tests/goal_tier.mjs
CHANGED
|
@@ -174,10 +174,20 @@ await test('setGoalStatus: UPDATE sets status + bumps updated_at, returns the ro
|
|
|
174
174
|
const out = await setGoalStatus({ goalId: 7, status: 'achieved' }, { pool });
|
|
175
175
|
assert.equal(out.status, 'achieved');
|
|
176
176
|
const upd = find(pool.queries, /^UPDATE goals/);
|
|
177
|
-
|
|
177
|
+
// The third bind is the optional fromStatus guard (task 1001693): null = unconditional.
|
|
178
|
+
assert.deepEqual(upd.params, [7, 'achieved', null]);
|
|
178
179
|
assert.match(upd.sql, /updated_at = now\(\)/, 'must bump updated_at');
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
await test('setGoalStatus: fromStatus makes the move conditional, and a lost race returns null', async () => {
|
|
183
|
+
const pool = fakePool((s) => (/^UPDATE goals/.test(s) ? { rows: [] } : null));
|
|
184
|
+
const out = await setGoalStatus({ goalId: 7, status: 'achieved', fromStatus: 'open' }, { pool });
|
|
185
|
+
assert.equal(out, null);
|
|
186
|
+
const upd = find(pool.queries, /^UPDATE goals/);
|
|
187
|
+
assert.deepEqual(upd.params, [7, 'achieved', 'open']);
|
|
188
|
+
assert.match(upd.sql, /status = \$3::text/, 'the guard is in the WHERE, not checked afterwards');
|
|
189
|
+
});
|
|
190
|
+
|
|
181
191
|
await test('setGoalStatus: unknown goal returns null', async () => {
|
|
182
192
|
const pool = fakePool(() => ({ rows: [] }));
|
|
183
193
|
assert.equal(await setGoalStatus({ goalId: 404, status: 'archived' }, { pool }), null);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// tests/google_fonts_nonblocking.mjs — every served page that loads the Google Fonts
|
|
2
|
+
// stylesheet warms both font origins first and asks for font-display: swap
|
|
3
|
+
// (task 1004930, from the front-end load-speed audit).
|
|
4
|
+
//
|
|
5
|
+
// The stylesheet lives on fonts.googleapis.com and the font files on
|
|
6
|
+
// fonts.gstatic.com: two extra origins, each a DNS + TLS round trip before text can
|
|
7
|
+
// paint. A <link rel="preconnect"> to both starts those handshakes while the HTML is
|
|
8
|
+
// still parsing, and display=swap paints text in the fallback face at once instead
|
|
9
|
+
// of holding it invisible until the font arrives. A page that drops either pays the
|
|
10
|
+
// whole wait on first paint, silently; this sweep makes a new page that forgets it
|
|
11
|
+
// fail here rather than load slowly in production.
|
|
12
|
+
//
|
|
13
|
+
// The page set is discovered from git, not listed, so a page added later is covered
|
|
14
|
+
// without anyone remembering to add it. Design mocks are exempt (never served).
|
|
15
|
+
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
|
|
25
|
+
/** The served page sources: tracked .html/.js under modules/ and src/, minus mocks and tests. */
|
|
26
|
+
function servedPageSources() {
|
|
27
|
+
const out = execFileSync('git', ['ls-files', '-z', '--', 'modules', 'src'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
28
|
+
return out.split('\0').filter((f) => /\.(html|js)$/.test(f) && !/\/(mocks|tests)\//.test(f));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Every <link ...> tag in the text, as a map of its attributes (order-independent). */
|
|
32
|
+
function linkTags(text) {
|
|
33
|
+
const tags = [];
|
|
34
|
+
for (const m of text.matchAll(/<link\b([^>]*)>/gi)) {
|
|
35
|
+
const attrs = {};
|
|
36
|
+
for (const a of m[1].matchAll(/([a-z-]+)(?:\s*=\s*"([^"]*)")?/gi)) attrs[a[1].toLowerCase()] = a[2] ?? true;
|
|
37
|
+
tags.push(attrs);
|
|
38
|
+
}
|
|
39
|
+
return tags;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The Google Fonts verdict for one page's text: null when it loads no Google Fonts stylesheet. */
|
|
43
|
+
export function fontsVerdict(text) {
|
|
44
|
+
const links = linkTags(text);
|
|
45
|
+
const sheets = links.filter((l) => l.rel === 'stylesheet' && typeof l.href === 'string' && l.href.startsWith('https://fonts.googleapis.com/css'));
|
|
46
|
+
if (sheets.length === 0) return null;
|
|
47
|
+
const pre = (origin) => links.find((l) => l.rel === 'preconnect' && l.href === origin);
|
|
48
|
+
const gstatic = pre('https://fonts.gstatic.com');
|
|
49
|
+
return {
|
|
50
|
+
preconnectApi: Boolean(pre('https://fonts.googleapis.com')),
|
|
51
|
+
preconnectGstatic: Boolean(gstatic),
|
|
52
|
+
// Font files are fetched in CORS mode, so a preconnect without crossorigin opens
|
|
53
|
+
// a connection the font request cannot reuse.
|
|
54
|
+
gstaticCrossorigin: Boolean(gstatic && 'crossorigin' in gstatic),
|
|
55
|
+
swap: sheets.every((s) => /[?&]display=swap(&|$)/.test(s.href.replace(/&/g, '&'))),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('the verdict reads the tags, not their order or spelling', () => {
|
|
60
|
+
const good = '<link href="https://fonts.googleapis.com/css2?family=X&display=swap" rel="stylesheet">'
|
|
61
|
+
+ '<link rel="preconnect" href="https://fonts.googleapis.com"><link crossorigin rel="preconnect" href="https://fonts.gstatic.com">';
|
|
62
|
+
assert.deepEqual(fontsVerdict(good), { preconnectApi: true, preconnectGstatic: true, gstaticCrossorigin: true, swap: true });
|
|
63
|
+
assert.equal(fontsVerdict('<link rel="stylesheet" href="/assets/site.css">'), null, 'a page with no Google Fonts is out of scope');
|
|
64
|
+
const bare = '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=X">';
|
|
65
|
+
assert.deepEqual(fontsVerdict(bare), { preconnectApi: false, preconnectGstatic: false, gstaticCrossorigin: false, swap: false });
|
|
66
|
+
const noCors = good.replace('crossorigin ', '');
|
|
67
|
+
assert.equal(fontsVerdict(noCors).gstaticCrossorigin, false, 'a gstatic preconnect without crossorigin is not reusable');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('every served page loading Google Fonts preconnects both origins and asks for swap', () => {
|
|
71
|
+
const pages = servedPageSources();
|
|
72
|
+
assert.ok(pages.length > 50, `found the page sources (${pages.length})`);
|
|
73
|
+
let checked = 0;
|
|
74
|
+
const bad = [];
|
|
75
|
+
for (const rel of pages) {
|
|
76
|
+
const v = fontsVerdict(fs.readFileSync(path.join(ROOT, rel), 'utf8'));
|
|
77
|
+
if (!v) continue;
|
|
78
|
+
checked++;
|
|
79
|
+
const missing = Object.entries(v).filter(([, ok]) => !ok).map(([k]) => k);
|
|
80
|
+
if (missing.length) bad.push(`${rel}: ${missing.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
assert.ok(checked >= 12,`the sweep reached the pages that load Google Fonts (${checked})`);
|
|
83
|
+
assert.deepEqual(bad, [], 'pages loading Google Fonts without both preconnects and display=swap');
|
|
84
|
+
});
|