@bongos/core 1.18.0 → 1.18.18
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 +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -107,5 +107,81 @@ t('refusal carries the canonical reason code', () => {
|
|
|
107
107
|
assert.equal(d.reason, 'session_already_has_active_claim');
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// =========================================================================
|
|
111
|
+
// Worktree-keying fix — the real commingle boundary is the WORKING TREE, not
|
|
112
|
+
// the shared auth session. A builder's concurrent worktrees share ONE auth
|
|
113
|
+
// session_id (the CLI token lives in ~/.config), so keying on the session alone
|
|
114
|
+
// wrongly blocked the second parallel worktree. Refusal must scope to the SAME
|
|
115
|
+
// worktree_name; a different worktree is a different branch → allowed.
|
|
116
|
+
// =========================================================================
|
|
117
|
+
|
|
118
|
+
// (e) THE FIX: same auth session, DIFFERENT worktree → ALLOWED (the parallel
|
|
119
|
+
// worktrees the code's own comment promised "must keep working").
|
|
120
|
+
t('same session, different worktree → ALLOWED (parallel worktrees)', () => {
|
|
121
|
+
const d = shouldRefuseSessionClaim({
|
|
122
|
+
creatorSessionId: 'sess-1',
|
|
123
|
+
worktreeName: 'wt-feature-b',
|
|
124
|
+
activeClaimsForSession: [
|
|
125
|
+
{ claim_id: 40, task_id: 50, creator_session_id: 'sess-1', worktree_name: 'wt-feature-a', title: 'A' },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
assert.equal(d.refuse, false);
|
|
129
|
+
assert.equal(d.inFlightTaskId, null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// (f) Same auth session, SAME worktree → refused (a real commingle: one tree,
|
|
133
|
+
// one branch, two tasks).
|
|
134
|
+
t('same session, same worktree → refused', () => {
|
|
135
|
+
const d = shouldRefuseSessionClaim({
|
|
136
|
+
creatorSessionId: 'sess-1',
|
|
137
|
+
worktreeName: 'wt-feature-a',
|
|
138
|
+
activeClaimsForSession: [
|
|
139
|
+
{ claim_id: 41, task_id: 51, creator_session_id: 'sess-1', worktree_name: 'wt-feature-a', title: 'A' },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
assert.equal(d.refuse, true);
|
|
143
|
+
assert.equal(d.inFlightTaskId, 51);
|
|
144
|
+
assert.equal(d.reason, 'session_already_has_active_claim');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// (g) Both worktree-less (a plain clone, no --worktree): the single branch really
|
|
148
|
+
// would commingle → still refused (the safe default, unchanged behaviour).
|
|
149
|
+
t('same session, both worktree-less (null) → refused (shared clone)', () => {
|
|
150
|
+
const d = shouldRefuseSessionClaim({
|
|
151
|
+
creatorSessionId: 'sess-1',
|
|
152
|
+
worktreeName: null,
|
|
153
|
+
activeClaimsForSession: [
|
|
154
|
+
{ claim_id: 42, task_id: 52, creator_session_id: 'sess-1', worktree_name: null, title: 'A' },
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
assert.equal(d.refuse, true);
|
|
158
|
+
assert.equal(d.inFlightTaskId, 52);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// (h) A worktree-bound existing claim must NOT block a later worktree-less claim
|
|
162
|
+
// (they are genuinely different trees), and vice-versa.
|
|
163
|
+
t('worktree-bound claim does not block a worktree-less claim (different trees)', () => {
|
|
164
|
+
const held = { claim_id: 43, task_id: 53, creator_session_id: 'sess-1', worktree_name: 'wt-x', title: 'X' };
|
|
165
|
+
assert.equal(shouldRefuseSessionClaim({ creatorSessionId: 'sess-1', worktreeName: null, activeClaimsForSession: [held] }).refuse, false);
|
|
166
|
+
const heldNull = { claim_id: 44, task_id: 54, creator_session_id: 'sess-1', worktree_name: null, title: 'N' };
|
|
167
|
+
assert.equal(shouldRefuseSessionClaim({ creatorSessionId: 'sess-1', worktreeName: 'wt-y', activeClaimsForSession: [heldNull] }).refuse, false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// (i) Correct in-flight task is named when MULTIPLE claims exist across worktrees
|
|
171
|
+
// — the block points at the one in the SAME tree, not just the first row.
|
|
172
|
+
t('names the in-flight task in the MATCHING worktree, not the first row', () => {
|
|
173
|
+
const d = shouldRefuseSessionClaim({
|
|
174
|
+
creatorSessionId: 'sess-1',
|
|
175
|
+
worktreeName: 'wt-two',
|
|
176
|
+
activeClaimsForSession: [
|
|
177
|
+
{ claim_id: 45, task_id: 60, creator_session_id: 'sess-1', worktree_name: 'wt-one', title: 'One' },
|
|
178
|
+
{ claim_id: 46, task_id: 61, creator_session_id: 'sess-1', worktree_name: 'wt-two', title: 'Two' },
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
assert.equal(d.refuse, true);
|
|
182
|
+
assert.equal(d.inFlightTaskId, 61);
|
|
183
|
+
assert.equal(d.inFlightTitle, 'Two');
|
|
184
|
+
});
|
|
185
|
+
|
|
110
186
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
111
187
|
process.exit(failed ? 1 : 0);
|
|
@@ -95,15 +95,17 @@ await test('approve: tasks.shipped_by is attributed to the REQUESTER, not the Ar
|
|
|
95
95
|
'must not re-SELECT the requester row already locked by the UPDATE');
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
await test('approve:
|
|
98
|
+
await test('approve: ADR 0120 pay-on-land — NO credit at override-approval; the reward waits for the land', async () => {
|
|
99
99
|
const pool = fakePool({ requesterId: REQUESTER, decision: 'approve', task: TASK });
|
|
100
|
-
await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
assert.
|
|
100
|
+
const out = await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
|
|
101
|
+
|
|
102
|
+
// The override-approval flips completed→confirmed and attributes shipped_by to
|
|
103
|
+
// the requester (asserted above), but the per-task credit now lands at the
|
|
104
|
+
// confirmed→shipped flip (shipTask → awardLandingCredit), NOT here — so a
|
|
105
|
+
// stranded override-confirmed task pays nothing until its code is on `main`.
|
|
106
|
+
assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
|
|
107
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
|
|
108
|
+
'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
await test('approve: transaction commits (not rolls back)', async () => {
|
|
@@ -183,10 +183,14 @@ await test('approve: credit + tasks.shipped_by go to the CLAIM HOLDER, not the r
|
|
|
183
183
|
assert.deepEqual(taskUpdate.params, [TASK.id, WORKER],
|
|
184
184
|
`shipped_by must bind to the claim holder ${WORKER}, got ${JSON.stringify(taskUpdate.params)}`);
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
// ADR 0120 pay-on-land: the per-task credit no longer lands at
|
|
187
|
+
// override-approval — it lands at the confirmed→shipped flip (shipTask). The
|
|
188
|
+
// credit-theft guard this test exists for is still intact: creditedBuilderId +
|
|
189
|
+
// shipped_by are the CLAIM HOLDER (asserted above), so when the credit DOES
|
|
190
|
+
// land at ship it lands on the worker, never the requester/Archon.
|
|
191
|
+
assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
|
|
192
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
|
|
193
|
+
'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
|
|
190
194
|
});
|
|
191
195
|
|
|
192
196
|
await test('approve: the claim-holder lookup happens BEFORE the tasks UPDATE binds shipped_by', async () => {
|
package/tests/package_core.mjs
CHANGED
|
@@ -16,6 +16,9 @@ import { strict as assert } from 'node:assert';
|
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import zlib from 'node:zlib';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join, dirname } from 'node:path';
|
|
19
22
|
|
|
20
23
|
const require = createRequire(import.meta.url);
|
|
21
24
|
const m = require('../scripts/gds/package-core.js');
|
|
@@ -25,14 +28,22 @@ const {
|
|
|
25
28
|
ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
|
|
26
29
|
// task 1824 — the consumer-core file-classification layer
|
|
27
30
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
31
|
+
// idea 1000543 — npm-never-installed files dropped from the pin
|
|
32
|
+
isNpmStripped,
|
|
28
33
|
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
|
|
29
34
|
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
30
35
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
31
36
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
32
37
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
|
33
38
|
publishArgs,
|
|
39
|
+
// task 1002218 (audit H3) — build-time self-verify pin reproduction
|
|
40
|
+
reproducePinFromTree,
|
|
41
|
+
// task 1002220 (audit H6) — post-build leak scan
|
|
42
|
+
scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
|
|
43
|
+
// task 1002217 (audit H4) — the fixed neutral floor
|
|
44
|
+
PRIOR_INSTANCE_FLOOR,
|
|
34
45
|
} = m;
|
|
35
|
-
const { buildRedactionRules, collectSensitiveLiterals } = require('../scripts/gds/mirror-redact.js');
|
|
46
|
+
const { buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks } = require('../scripts/gds/mirror-redact.js');
|
|
36
47
|
|
|
37
48
|
// A synthetic instance brand for the classification tests (never the real OTB literals).
|
|
38
49
|
const TEST_BRANDING = {
|
|
@@ -485,5 +496,239 @@ t('prunePackageLock: on the real repo lockfile, drops the vast majority of packa
|
|
|
485
496
|
assert.ok(keptCount > 0);
|
|
486
497
|
});
|
|
487
498
|
|
|
499
|
+
// ---- task 1002218 (audit H3): build-time self-verify pin reproduction ----
|
|
500
|
+
// Build a tiny unpacked tree + a matching sidecar manifest (same hash fns the packager uses),
|
|
501
|
+
// then prove reproducePinFromTree accepts a faithful reproduction and rejects each tamper.
|
|
502
|
+
function makeTreeAndSidecar() {
|
|
503
|
+
const root = mkdtempSync(join(tmpdir(), 'pc-selfverify-'));
|
|
504
|
+
const files = [
|
|
505
|
+
{ path: 'src/a.js', content: 'module.exports = 1\n', mode: '0000644' },
|
|
506
|
+
{ path: 'sub/dir/b.json', content: '{"k":2}\n', mode: '0000644' },
|
|
507
|
+
{ path: 'bin/run.sh', content: '#!/bin/sh\necho hi\n', mode: '0000755' },
|
|
508
|
+
];
|
|
509
|
+
for (const f of files) {
|
|
510
|
+
const abs = join(root, ...f.path.split('/'));
|
|
511
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
512
|
+
writeFileSync(abs, f.content);
|
|
513
|
+
}
|
|
514
|
+
const meta = files
|
|
515
|
+
.map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: sha256hex(Buffer.from(f.content)) }))
|
|
516
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
517
|
+
const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(meta), files: meta, tarball: { sha256: 'abc123', bytes: 0 } };
|
|
518
|
+
return { root, sidecar, files };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
t('reproducePinFromTree: a faithful tree reproduces the pin (ok, no problems)', () => {
|
|
522
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
523
|
+
try {
|
|
524
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar }); // no tgzPath → skip the tarball-bytes check
|
|
525
|
+
assert.equal(r.ok, true, r.problems.join('; '));
|
|
526
|
+
assert.equal(r.fileCount, 3);
|
|
527
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
t('reproducePinFromTree: a tampered file content → not ok (content mismatch)', () => {
|
|
531
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
532
|
+
try {
|
|
533
|
+
writeFileSync(join(root, 'src', 'a.js'), 'module.exports = 666 // tampered\n');
|
|
534
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
535
|
+
assert.equal(r.ok, false);
|
|
536
|
+
assert.ok(r.problems.some((p) => /content mismatch: src\/a\.js/.test(p)), r.problems.join('; '));
|
|
537
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
t('reproducePinFromTree: a missing file → not ok (missing in artifact)', () => {
|
|
541
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
542
|
+
try {
|
|
543
|
+
rmSync(join(root, 'sub', 'dir', 'b.json'));
|
|
544
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
545
|
+
assert.equal(r.ok, false);
|
|
546
|
+
assert.ok(r.problems.some((p) => /missing in artifact: sub\/dir\/b\.json/.test(p)), r.problems.join('; '));
|
|
547
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
t('reproducePinFromTree: a manifest whose tree_sha256 lies → not ok (tree hash mismatch)', () => {
|
|
551
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
552
|
+
try {
|
|
553
|
+
const lying = { ...sidecar, tree_sha256: 'f'.repeat(64) }; // every file matches, but the recorded hash is wrong
|
|
554
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar: lying });
|
|
555
|
+
assert.equal(r.ok, false);
|
|
556
|
+
assert.ok(r.problems.some((p) => /recomputed tree_sha256/.test(p)), r.problems.join('; '));
|
|
557
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
t('reproducePinFromTree: a .tgz whose bytes differ from manifest.tarball.sha256 → not ok', () => {
|
|
561
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
562
|
+
const tgz = join(root, 'artifact.tgz');
|
|
563
|
+
try {
|
|
564
|
+
writeFileSync(tgz, Buffer.from('the actual tarball bytes'));
|
|
565
|
+
const withTb = { ...sidecar, tarball: { sha256: sha256hex(Buffer.from('DIFFERENT bytes')), bytes: 24 } };
|
|
566
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar: withTb, tgzPath: tgz });
|
|
567
|
+
assert.equal(r.ok, false);
|
|
568
|
+
assert.ok(r.problems.some((p) => /\.tgz sha256/.test(p)), r.problems.join('; '));
|
|
569
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ---- task 1002220 (audit H6): post-build leak scan ----
|
|
573
|
+
t('scanArtifactForLeaks: flags a prior-instance literal in any shipped file', () => {
|
|
574
|
+
const files = [
|
|
575
|
+
{ path: 'src/x.js', content: 'const base = "https://amazonprimea.com";\n' },
|
|
576
|
+
{ path: 'docs/readme.md', content: 'Owned by lars589.\n' },
|
|
577
|
+
{ path: 'src/clean.js', content: 'export const ok = true;\n' },
|
|
578
|
+
];
|
|
579
|
+
const r = scanArtifactForLeaks(files);
|
|
580
|
+
const hitPaths = r.literalHits.map((h) => h.path);
|
|
581
|
+
assert.ok(hitPaths.includes('src/x.js'), 'amazonprimea in a functional file must be flagged');
|
|
582
|
+
assert.ok(hitPaths.includes('docs/readme.md'), 'owner login in a doc must be flagged too (literal scan spans all files)');
|
|
583
|
+
assert.ok(!hitPaths.includes('src/clean.js'), 'a clean file must not be flagged');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
t('scanArtifactForLeaks: flags a public IP but not a private/localhost one', () => {
|
|
587
|
+
const r = scanArtifactForLeaks([
|
|
588
|
+
{ path: 'a.js', content: 'host = 104.236.254.243\n' }, // public → flagged
|
|
589
|
+
{ path: 'b.js', content: 'host = 127.0.0.1; lan = 10.0.0.5\n' }, // localhost + private → not
|
|
590
|
+
]);
|
|
591
|
+
const byPath = Object.fromEntries(r.literalHits.map((h) => [h.path, h.label]));
|
|
592
|
+
assert.ok(byPath['a.js'] === 'public-ip', `expected a public-ip hit on a.js, got ${JSON.stringify(r.literalHits)}`);
|
|
593
|
+
assert.ok(!('b.js' in byPath), 'localhost/private IPs must not be flagged');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
t('scanArtifactForLeaks: credential SHAPE warning fires on functional files, not docs', () => {
|
|
597
|
+
const key = `sk-ant-${'A1b2C3d4E5f6G7h8'}`; // a fake Anthropic-shaped token
|
|
598
|
+
const r = scanArtifactForLeaks([
|
|
599
|
+
{ path: 'src/leak.js', content: `const k = "${key}";\n` },
|
|
600
|
+
{ path: 'docs/guide.md', content: `example key: ${key}\n` }, // docs are redacted+gated elsewhere → not a shape warning here
|
|
601
|
+
]);
|
|
602
|
+
const warnPaths = r.secretWarnings.map((w) => w.path);
|
|
603
|
+
assert.ok(warnPaths.includes('src/leak.js'), 'a credential shape in a functional file must warn');
|
|
604
|
+
assert.ok(!warnPaths.includes('docs/guide.md'), 'docs are out of scope for the credential-shape warning');
|
|
605
|
+
const w = r.secretWarnings.find((x) => x.path === 'src/leak.js');
|
|
606
|
+
assert.ok(w.shapes.includes('anthropic-key'), `expected anthropic-key shape, got ${JSON.stringify(w.shapes)}`);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
t('scanArtifactForLeaks: ordinary long identifiers / base64 hashes do NOT warn (low false-positive)', () => {
|
|
610
|
+
const r = scanArtifactForLeaks([
|
|
611
|
+
{ path: 'src/normal.js', content: 'const integrityHash = "sha512-Abc123Def456Ghi789Jkl012Mno345Pqr678";\nfunction reallyLongDescriptiveFunctionName() {}\n' },
|
|
612
|
+
{ path: 'data.json', content: JSON.stringify({ sha256: 'a'.repeat(64), token: 'short' }) },
|
|
613
|
+
]);
|
|
614
|
+
assert.equal(r.secretWarnings.length, 0, `no credential shapes expected, got ${JSON.stringify(r.secretWarnings)}`);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
t('PRIOR_INSTANCE_LITERALS: carries the customer-zero tokens the neutral trunk must not ship', () => {
|
|
618
|
+
assert.ok(Array.isArray(PRIOR_INSTANCE_LITERALS) && PRIOR_INSTANCE_LITERALS.length >= 3);
|
|
619
|
+
// Case-insensitive: the floor tokens are lowercased for the redaction rules + findLeaks
|
|
620
|
+
// (both case-insensitive), so assert coverage, not exact case.
|
|
621
|
+
const lower = PRIOR_INSTANCE_LITERALS.map((s) => s.toLowerCase());
|
|
622
|
+
for (const lit of ['amazonprimea', 'off the boats', 'lars589']) {
|
|
623
|
+
assert.ok(lower.includes(lit), `floor list should cover ${lit}`);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ---- task 1002217 (audit H4): the neutral floor gate (fails closed with no branding.json) ----
|
|
628
|
+
t('neutral floor: with EMPTY branding + the floor as extra, docs de-brand and the gate covers the floor', () => {
|
|
629
|
+
// The F2 gap: with no branding.json, buildRedactionRules({})/collectSensitiveLiterals({}) are
|
|
630
|
+
// empty, so a doc keeps its prior-instance identity. Injecting PRIOR_INSTANCE_FLOOR as `extra`
|
|
631
|
+
// (exactly what package-core's main() now does) closes it.
|
|
632
|
+
const rules = buildRedactionRules({}, PRIOR_INSTANCE_FLOOR);
|
|
633
|
+
const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
|
|
634
|
+
const doc = 'See https://amazonprimea.com, owned by lars589 — the Off the Boats world.';
|
|
635
|
+
const red = redactText(doc, rules);
|
|
636
|
+
assert.ok(!/amazonprimea/i.test(red), `amazonprimea survived: ${red}`);
|
|
637
|
+
assert.ok(!/lars589/i.test(red), `owner login survived: ${red}`);
|
|
638
|
+
assert.ok(!/off the boats/i.test(red), `product name survived: ${red}`);
|
|
639
|
+
assert.deepEqual(findLeaks(red, lits), [], `gate still sees a floor leak after redaction: ${JSON.stringify(findLeaks(red, lits))}`);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
t('neutral floor: the fail-closed gate WOULD catch an un-redacted survivor (empty branding)', () => {
|
|
643
|
+
const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
|
|
644
|
+
// If a doc reached the gate still carrying a floor literal, findLeaks (the fail-closed detector)
|
|
645
|
+
// flags it — which aborts the build in package-core (atomic, produces nothing).
|
|
646
|
+
assert.ok(findLeaks('leftover amazonprimea reference', lits).length > 0, 'floor token must be gated');
|
|
647
|
+
assert.ok(findLeaks('signed off by lars589', lits).length > 0, 'floor owner must be gated');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
t('neutral floor: WITHOUT the floor, empty branding leaks (proves the floor is what closes F2)', () => {
|
|
651
|
+
// The counterfactual — the pre-H4 neutral-trunk behavior: empty branding + no floor = no rules,
|
|
652
|
+
// no gate literals, so the doc ships its prior-instance identity verbatim.
|
|
653
|
+
const rules = buildRedactionRules({});
|
|
654
|
+
const lits = collectSensitiveLiterals({});
|
|
655
|
+
const doc = 'amazonprimea + lars589';
|
|
656
|
+
assert.equal(redactText(doc, rules).includes('amazonprimea'), true, 'no-floor redaction is a no-op (the F2 gap)');
|
|
657
|
+
assert.equal(findLeaks(doc, lits).length, 0, 'no-floor gate sees nothing (the F2 gap)');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// ---- idea 1000543: npm-never-installed files are dropped from the pin ----
|
|
661
|
+
// npm strips .gitignore/.npmignore/.npmrc from an installed package, so a pin that
|
|
662
|
+
// LISTS one fails when a consumer recomputes the tree hash over node_modules/@bongos/core
|
|
663
|
+
// (verifyInstalledPin, upgrade.js) -> the vendored `bongos upgrade` auto-rolls-back.
|
|
664
|
+
t('isNpmStripped: matches .gitignore/.npmignore/.npmrc at root and any depth', () => {
|
|
665
|
+
assert.equal(isNpmStripped('.gitignore'), true);
|
|
666
|
+
assert.equal(isNpmStripped('.npmignore'), true);
|
|
667
|
+
assert.equal(isNpmStripped('.npmrc'), true);
|
|
668
|
+
assert.equal(isNpmStripped('modules/economy/.gitignore'), true); // npm strips nested ones too
|
|
669
|
+
assert.equal(isNpmStripped('a/b/c/.npmrc'), true);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
t('isNpmStripped: does NOT match ordinary files or basename lookalikes', () => {
|
|
673
|
+
assert.equal(isNpmStripped('src/index.js'), false);
|
|
674
|
+
assert.equal(isNpmStripped('package.json'), false);
|
|
675
|
+
assert.equal(isNpmStripped('docs/.gitignore.md'), false); // .gitignore is not the basename here
|
|
676
|
+
assert.equal(isNpmStripped('gitignore'), false); // no leading dot
|
|
677
|
+
assert.equal(isNpmStripped('src/.gitignoreignore'), false);
|
|
678
|
+
assert.equal(isNpmStripped(''), false);
|
|
679
|
+
assert.equal(isNpmStripped(null), false);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
t('the packager selection drops the stripped files from the pinned set (models main())', () => {
|
|
683
|
+
const treePaths = ['.gitignore', 'src/a.js', 'migrations/001.sql', 'modules/x/.npmrc', 'README.md'];
|
|
684
|
+
const kept = treePaths.filter((p) => !isNpmStripped(p)); // exactly main()'s `publishable` filter
|
|
685
|
+
assert.deepEqual(kept, ['src/a.js', 'migrations/001.sql', 'README.md']);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// End-to-end proof against the consume-side recompute. reproducePinFromTree and upgrade.js's
|
|
689
|
+
// verifyInstalledPin both iterate manifest.files[] and rebuild canonicalTreeHash, so a manifest
|
|
690
|
+
// that LISTS .gitignore fails when npm has stripped it (the bug); a manifest built through the
|
|
691
|
+
// isNpmStripped filter does not list it and reproduces cleanly (the fix).
|
|
692
|
+
function buildManifestAndNpmInstalledTree({ includeGitignore }) {
|
|
693
|
+
const root = mkdtempSync(join(tmpdir(), 'pc-npmstrip-'));
|
|
694
|
+
const treeFiles = [ // the FULL git tree the packager enumerates, incl. a tracked .gitignore
|
|
695
|
+
{ path: '.gitignore', content: 'dist/\nnode_modules/\n' },
|
|
696
|
+
{ path: 'src/a.js', content: 'module.exports = 1\n' },
|
|
697
|
+
{ path: 'package.json', content: '{"name":"@bongos/core"}\n' },
|
|
698
|
+
];
|
|
699
|
+
// What the packager PINS: pre-fix keeps everything; post-fix drops the npm-stripped files.
|
|
700
|
+
const pinned = treeFiles.filter((f) => (includeGitignore ? true : !isNpmStripped(f.path)));
|
|
701
|
+
const files = pinned
|
|
702
|
+
.map((f) => ({ path: f.path, mode: normalizeMode('100644'), sha256: sha256hex(Buffer.from(f.content)) }))
|
|
703
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
704
|
+
const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(files), files };
|
|
705
|
+
// What `npm install` ACTUALLY materializes: everything EXCEPT the files npm strips.
|
|
706
|
+
for (const f of treeFiles) {
|
|
707
|
+
if (isNpmStripped(f.path)) continue; // npm never writes .gitignore into the installed package
|
|
708
|
+
const abs = join(root, ...f.path.split('/'));
|
|
709
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
710
|
+
writeFileSync(abs, f.content);
|
|
711
|
+
}
|
|
712
|
+
return { root, sidecar };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
t('idea 1000543 (the bug): a manifest that PINS .gitignore FAILS against an npm-installed tree', () => {
|
|
716
|
+
const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: true });
|
|
717
|
+
try {
|
|
718
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
719
|
+
assert.equal(r.ok, false, 'pinning .gitignore must fail once npm has stripped it');
|
|
720
|
+
assert.ok(r.problems.some((p) => /\.gitignore/.test(p)), `expected a .gitignore problem, got: ${r.problems.join('; ')}`);
|
|
721
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
t('idea 1000543 (the fix): the isNpmStripped filter makes the pin reproduce against the npm-installed tree', () => {
|
|
725
|
+
const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: false });
|
|
726
|
+
try {
|
|
727
|
+
assert.ok(!sidecar.files.some((f) => f.path === '.gitignore'), 'the fixed manifest must not list .gitignore');
|
|
728
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
729
|
+
assert.equal(r.ok, true, `the fixed manifest must reproduce; problems: ${r.problems.join('; ')}`);
|
|
730
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
731
|
+
});
|
|
732
|
+
|
|
488
733
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
489
734
|
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// tests/parse_cookie_decode.mjs — regression for the post-login redirect landing
|
|
2
|
+
// on /gate/ instead of the requested page (task 1002241).
|
|
3
|
+
//
|
|
4
|
+
// The onboarding wizard's "Connect GitHub" button hits /auth/web/start?return=/create,
|
|
5
|
+
// which stores the return target via `res.cookie(RETURN_COOKIE, '/create', …)`. Express
|
|
6
|
+
// `res.cookie` percent-ENCODES the value, so the wire cookie is `gds_return=%2Fcreate`.
|
|
7
|
+
// parseCookie() must DECODE symmetrically on read — else the callback runs
|
|
8
|
+
// safeReturnTo('%2Fcreate'), which (not starting with '/') is parsed as a URL, throws,
|
|
9
|
+
// returns null, and the post-login redirect silently falls through to the default
|
|
10
|
+
// landing (cloudbongos.com → /gate/). Decoding is a no-op for hex/simple values, so it
|
|
11
|
+
// can't regress the state/session/repos cookies.
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { parseCookie } = require('../src/bongos/routes/_helpers.js');
|
|
15
|
+
|
|
16
|
+
let pass = 0, fail = 0;
|
|
17
|
+
const ok = (cond, msg) => { if (cond) { pass++; console.log(' ok ', msg); } else { fail++; console.log(' FAIL', msg); } };
|
|
18
|
+
const req = (cookie) => ({ headers: { cookie } });
|
|
19
|
+
|
|
20
|
+
console.log('parseCookie — decodes percent-encoded values (task 1002241):');
|
|
21
|
+
ok(parseCookie(req('gds_return=%2Fcreate'), 'gds_return') === '/create',
|
|
22
|
+
'"%2Fcreate" → "/create" (the Connect-GitHub return bug)');
|
|
23
|
+
ok(parseCookie(req('gds_return=https%3A%2F%2Fbuilders.cloudbongos.com%2Fx'), 'gds_return')
|
|
24
|
+
=== 'https://builders.cloudbongos.com/x',
|
|
25
|
+
'encoded full https URL round-trips (the #726 subdomain return, same latent bug)');
|
|
26
|
+
|
|
27
|
+
console.log('\nparseCookie — no-op / safe for the other cookies:');
|
|
28
|
+
ok(parseCookie(req('gds_oauth_state=03e92dd24cad3219c637e50fc9f115ef'), 'gds_oauth_state')
|
|
29
|
+
=== '03e92dd24cad3219c637e50fc9f115ef', 'hex state value unchanged');
|
|
30
|
+
ok(parseCookie(req('gds_session=' + 'a'.repeat(64)), 'gds_session') === 'a'.repeat(64),
|
|
31
|
+
'hex session token unchanged');
|
|
32
|
+
ok(parseCookie(req('gds_oauth_repos=1'), 'gds_oauth_repos') === '1', 'repos "1" unchanged');
|
|
33
|
+
|
|
34
|
+
console.log('\nparseCookie — misc:');
|
|
35
|
+
ok(parseCookie(req(''), 'gds_return') === null, 'no cookies → null');
|
|
36
|
+
ok(parseCookie(req('other=x; gds_return=%2Fcreate'), 'gds_return') === '/create',
|
|
37
|
+
'finds the named cookie among several');
|
|
38
|
+
ok(parseCookie(req('gds_return=%E0%A4%A'), 'gds_return') === '%E0%A4%A',
|
|
39
|
+
'malformed percent-escape falls back to the raw value (no throw)');
|
|
40
|
+
|
|
41
|
+
console.log(`\nparseCookie decode tests: ${pass} passed, ${fail} failed`);
|
|
42
|
+
if (fail) process.exit(1);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// tests/pay_on_land.mjs
|
|
2
|
+
//
|
|
3
|
+
// ADR 0120 part 1 (pay-on-land) — the per-task credit + idea-promotion bonus
|
|
4
|
+
// are booked at the confirmed→SHIPPED flip (db.shipTask → awardLandingCredit),
|
|
5
|
+
// NOT at confirm. This is the mirror of confirm_attribution.mjs / override_
|
|
6
|
+
// attribution.mjs / grade_attribution.mjs, which now assert NO credit at their
|
|
7
|
+
// (confirm-time) transition; here we assert the credit lands at ship.
|
|
8
|
+
//
|
|
9
|
+
// The contract this pins:
|
|
10
|
+
// • ship awards a positive per-task credit, reason='task.shipped'
|
|
11
|
+
// • the credit is attributed to the CLAIM HOLDER (the builder who did the
|
|
12
|
+
// work), even when a DIFFERENT builder (the reconciler / a supervisor)
|
|
13
|
+
// triggers the ship
|
|
14
|
+
// • idempotent on (task_id, 'task.shipped'): a re-ship / reconciler re-sweep
|
|
15
|
+
// books nothing a second time
|
|
16
|
+
// • credits_reward = 0 → no credit_log INSERT
|
|
17
|
+
// • a task not in 'confirmed' state throws TASK_NOT_CONFIRMED, awards nothing
|
|
18
|
+
//
|
|
19
|
+
// shipTask honors the #538 `deps.pool` seam under NODE_ENV==='test', so we drive
|
|
20
|
+
// it with a SQL-aware fakeClient — no live DB.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/pay_on_land.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
27
|
+
|
|
28
|
+
process.env.NODE_ENV = 'test';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
// shipTask resolves the `reward` port for the credit grant + achievement
|
|
32
|
+
// reconcile. Register it; stub the gamification hooks to no-ops so the test
|
|
33
|
+
// isolates the credit path (the real credits.js SQL still runs against the fake).
|
|
34
|
+
const achievements = require('../modules/economy/achievements.js');
|
|
35
|
+
achievements.evaluateAfterShip = async () => [];
|
|
36
|
+
achievements.evaluateAfterCreditAward = async () => [];
|
|
37
|
+
achievements.reconcileBuilderAchievements = async () => [];
|
|
38
|
+
const seams = require('../src/module-seams.js');
|
|
39
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
40
|
+
const db = require('../modules/lifecycle/db.js');
|
|
41
|
+
const { shipTask } = db;
|
|
42
|
+
|
|
43
|
+
const { test, summary } = makeRunner();
|
|
44
|
+
|
|
45
|
+
// SQL-aware fake covering shipTask's transaction. Knobs:
|
|
46
|
+
// claimHolderId → who mostRecentClaimHolderId resolves (null = none)
|
|
47
|
+
// shippedBy → tasks.shipped_by on the returned shipped row
|
|
48
|
+
// creditsReward, kind → the task's reward inputs
|
|
49
|
+
// confirmed=false → the UPDATE tasks matches nothing (TASK_NOT_CONFIRMED)
|
|
50
|
+
// priorShipCredit → a 'task.shipped' credit_log row already exists (idempotent)
|
|
51
|
+
function fakePool({ claimHolderId, shippedBy, creditsReward = 15, kind = 'feature', confirmed = true, priorShipCredit = false }) {
|
|
52
|
+
return makeSqlAwareClient((s) => {
|
|
53
|
+
if (/^UPDATE tasks/.test(s)) {
|
|
54
|
+
return {
|
|
55
|
+
rows: confirmed
|
|
56
|
+
? [{ id: TASK.id, title: TASK.title, status: 'shipped', shipped_at: new Date(0).toISOString(),
|
|
57
|
+
shipped_by: shippedBy, security_sensitive: false, value_summary: null,
|
|
58
|
+
credits_reward: creditsReward, kind }]
|
|
59
|
+
: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (/^UPDATE claims/.test(s)) return { rows: [] }; // no open claims to heal
|
|
63
|
+
if (/UPDATE builders SET first_ship_at/.test(s)) return { rows: [] };
|
|
64
|
+
if (/SELECT rank FROM builders/.test(s)) return { rows: [{ rank: 'thetes' }] }; // not xenos → no graduation
|
|
65
|
+
if (/SELECT builder_id FROM claims/.test(s)) {
|
|
66
|
+
return { rows: claimHolderId == null ? [] : [{ builder_id: claimHolderId }] };
|
|
67
|
+
}
|
|
68
|
+
if (/SELECT 1 FROM credit_log .* 'task\.shipped'/.test(s)) {
|
|
69
|
+
return { rows: priorShipCredit ? [{ '?column?': 1 }] : [] };
|
|
70
|
+
}
|
|
71
|
+
if (/SELECT 1 FROM credit_log .* 'idea_promotion_bonus'/.test(s)) return { rows: [] };
|
|
72
|
+
if (/FROM idea_inbox/.test(s)) return { rows: [] }; // no idea lineage
|
|
73
|
+
if (/INSERT INTO credit_log/.test(s)) return { rows: [] };
|
|
74
|
+
return { rows: [] }; // BEGIN/COMMIT/SAVEPOINT/etc.
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const find = (queries, re) => queries.find((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
|
|
79
|
+
const all = (queries, re) => queries.filter((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
|
|
80
|
+
|
|
81
|
+
const RECONCILER = 999; // the builder_id the reconciler passes to shipTask
|
|
82
|
+
const WORKER = 25; // the claim holder who actually did the work
|
|
83
|
+
const TASK = { id: 660, title: 'fixture task' };
|
|
84
|
+
|
|
85
|
+
// =========================================================================
|
|
86
|
+
await test('ship: the per-task credit lands at SHIP (reason=task.shipped), attributed to the claim holder', async () => {
|
|
87
|
+
// shipped_by was set to the claim holder at confirm; the reconciler triggers
|
|
88
|
+
// the ship with its own builderId — attribution must still be the worker.
|
|
89
|
+
const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 15, kind: 'feature' });
|
|
90
|
+
const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
|
|
91
|
+
|
|
92
|
+
assert.equal(out.status, 'shipped', 'task flips to shipped');
|
|
93
|
+
assert.equal(out.creditedBuilderId, WORKER, 'credit attributed to the claim holder, not the reconciler');
|
|
94
|
+
assert.ok(out.creditsAwarded > 0, 'a positive per-task credit must be awarded at ship');
|
|
95
|
+
|
|
96
|
+
const insert = find(pool._client.queries, /INSERT INTO credit_log/);
|
|
97
|
+
assert.ok(insert, 'a credit_log INSERT must run at ship');
|
|
98
|
+
assert.equal(insert.params[0], WORKER, 'credit_log.builder_id = claim holder');
|
|
99
|
+
assert.notEqual(insert.params[0], RECONCILER, 'credit must NOT go to the ship trigger');
|
|
100
|
+
assert.equal(insert.params[1], TASK.id, 'credit_log.task_id = the task');
|
|
101
|
+
assert.equal(insert.params[3], 'task.shipped', "reason must be 'task.shipped' (ADR 0120), not 'task.confirmed'");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await test('ship: idempotent — a prior task.shipped credit means no second INSERT', async () => {
|
|
105
|
+
const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, priorShipCredit: true });
|
|
106
|
+
const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
|
|
107
|
+
assert.equal(out.creditsAwarded, 0, 'no fresh credit on a re-ship / reconciler re-sweep');
|
|
108
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not double-insert credit');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await test('ship: credits_reward = 0 → no credit_log INSERT', async () => {
|
|
112
|
+
const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 0 });
|
|
113
|
+
const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
|
|
114
|
+
assert.equal(out.creditsAwarded, 0, 'a zero-reward task pays nothing');
|
|
115
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'no INSERT for a 0-reward task');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await test('ship: not in confirmed state → throws TASK_NOT_CONFIRMED, awards nothing', async () => {
|
|
119
|
+
const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, confirmed: false });
|
|
120
|
+
await assert.rejects(
|
|
121
|
+
() => shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool }),
|
|
122
|
+
(err) => err.code === 'TASK_NOT_CONFIRMED',
|
|
123
|
+
'must throw TASK_NOT_CONFIRMED when the UPDATE matches no confirmed row'
|
|
124
|
+
);
|
|
125
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not award credit on a failed ship');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
summary();
|