@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
|
@@ -74,3 +74,68 @@ test('resolveDocsRoot: the explicit instance root when set, else coreRoot (no-op
|
|
|
74
74
|
assert.equal(resolveDocsRoot({ env: { CLOUDBONGOS_INSTANCE_ROOT: '/srv/consumer' } }), path.resolve('/srv/consumer'), 'consumer writes into its own repo');
|
|
75
75
|
assert.equal(resolveDocsRoot({ env: {} }), resolveCoreRoot(), 'single checkout: coincides with coreRoot');
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// resolveApiBase — the ONE session-aware CLI base resolution (task 1002427).
|
|
80
|
+
//
|
|
81
|
+
// Regression fixture for a bug that locked a builder out of re-authenticating.
|
|
82
|
+
// Three call sites resolved the API base three different ways and none reached
|
|
83
|
+
// the instance the builder was actually signed into:
|
|
84
|
+
// * paste-token.js hardcoded 'https://amazonprimea.com' (a straggler the task
|
|
85
|
+
// 1002201 sweep missed), so a token minted by cloudbongos.com was verified
|
|
86
|
+
// against the founder's host and rejected 401 — a VALID token, wrong host.
|
|
87
|
+
// * reauth.js used the module-level branding default, so it printed a
|
|
88
|
+
// localhost:3000 click-path to a builder signed into cloudbongos.com.
|
|
89
|
+
// * cliClient() alone had it right (env -> session.api_base -> default).
|
|
90
|
+
// The session file knew the answer the whole time. This locks the order in.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const BRAND_B = { domains: { publicOrigin: 'https://instance-b.example' } };
|
|
94
|
+
|
|
95
|
+
test('resolveApiBase: the persisted session beats the branding default', () => {
|
|
96
|
+
// THE regression. A builder authenticated against instance A must keep hitting
|
|
97
|
+
// A even when this checkout's own branding names B.
|
|
98
|
+
assert.equal(
|
|
99
|
+
ic.resolveApiBase({
|
|
100
|
+
session: { api_base: 'https://instance-a.example' },
|
|
101
|
+
env: {},
|
|
102
|
+
b: BRAND_B,
|
|
103
|
+
}),
|
|
104
|
+
'https://instance-a.example'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('resolveApiBase: an explicit env override beats the session', () => {
|
|
109
|
+
assert.equal(
|
|
110
|
+
ic.resolveApiBase({
|
|
111
|
+
session: { api_base: 'https://instance-a.example' },
|
|
112
|
+
env: { CLOUDBONGOS_API_BASE: 'http://localhost:3000' },
|
|
113
|
+
b: BRAND_B,
|
|
114
|
+
}),
|
|
115
|
+
'http://localhost:3000'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('resolveApiBase: falls back to the branding default with no env and no session', () => {
|
|
120
|
+
assert.equal(
|
|
121
|
+
ic.resolveApiBase({ session: null, env: {}, b: BRAND_B }),
|
|
122
|
+
'https://instance-b.example'
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('resolveApiBase: a session without a usable api_base falls back, never returns empty', () => {
|
|
127
|
+
for (const session of [{}, { api_base: '' }, { api_base: ' ' }, { api_base: 42 }]) {
|
|
128
|
+
assert.equal(
|
|
129
|
+
ic.resolveApiBase({ session, env: {}, b: BRAND_B }),
|
|
130
|
+
'https://instance-b.example',
|
|
131
|
+
`session ${JSON.stringify(session)} must fall back to the branding default`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('resolveApiBase: session:null does not read the session file from disk', () => {
|
|
137
|
+
// Passing null explicitly means "no session" — the resolver must honour that
|
|
138
|
+
// rather than falling back to a disk read, so this test stays pure/hermetic
|
|
139
|
+
// and callers that already know there is no session get a deterministic answer.
|
|
140
|
+
assert.equal(ic.resolveApiBase({ session: null, env: {}, b: BRAND_B }), 'https://instance-b.example');
|
|
141
|
+
});
|
|
@@ -27,6 +27,8 @@ function test(name, fn) {
|
|
|
27
27
|
const DEMO = { domains: { publicOrigin: 'https://demo.cloudbongos.com' } };
|
|
28
28
|
const OTB = { domains: { publicOrigin: 'https://amazonprimea.com' } };
|
|
29
29
|
const noEnv = {}; // no <PREFIX>_API_BASE override in scope
|
|
30
|
+
const branded = () => true; // simulate a checkout that HAS config/branding.json (a declared instance)
|
|
31
|
+
const neutral = () => false; // simulate a NEUTRAL checkout (portable core — no config/branding.json)
|
|
30
32
|
|
|
31
33
|
test('instanceOriginHost: extracts host, tolerates trailing slash', () => {
|
|
32
34
|
assert.equal(cli.instanceOriginHost('https://amazonprimea.com'), 'amazonprimea.com');
|
|
@@ -41,24 +43,24 @@ test('instanceOriginHost: unparseable input → null (edge)', () => {
|
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
test('match: same-host session + branding → no mismatch (the OTB/normal case)', () => {
|
|
44
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: OTB, env: noEnv });
|
|
46
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: OTB, env: noEnv, hasInstanceConfig: branded });
|
|
45
47
|
assert.equal(r.mismatch, false);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('match: scheme/trailing-slash differences still match on host', () => {
|
|
49
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com/' }, brand: OTB, env: noEnv });
|
|
51
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com/' }, brand: OTB, env: noEnv, hasInstanceConfig: branded });
|
|
50
52
|
assert.equal(r.mismatch, false);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
test('mismatch: demo repo + founder amazonprimea session → REFUSE (the reported failure)', () => {
|
|
54
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: noEnv });
|
|
56
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: noEnv, hasInstanceConfig: branded });
|
|
55
57
|
assert.equal(r.mismatch, true);
|
|
56
58
|
assert.equal(r.expected, 'https://demo.cloudbongos.com');
|
|
57
59
|
assert.equal(r.actual, 'https://amazonprimea.com');
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test('mismatch: demo repo + cloudbongos.com session (sibling instance) → REFUSE', () => {
|
|
61
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: DEMO, env: noEnv });
|
|
63
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: DEMO, env: noEnv, hasInstanceConfig: branded });
|
|
62
64
|
assert.equal(r.mismatch, true);
|
|
63
65
|
});
|
|
64
66
|
|
|
@@ -71,8 +73,23 @@ test('env-override: an explicit *_API_BASE override wins (dev/staging) → no mi
|
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
test('no-branding: repo declares no publicOrigin → cannot assert a mismatch → no mismatch', () => {
|
|
74
|
-
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: {}, env: noEnv }).mismatch, false);
|
|
75
|
-
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: { domains: {} }, env: noEnv }).mismatch, false);
|
|
76
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: {}, env: noEnv, hasInstanceConfig: branded }).mismatch, false);
|
|
77
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: { domains: {} }, env: noEnv, hasInstanceConfig: branded }).mismatch, false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('NEUTRAL checkout (no config/branding.json) → trust the session, NO mismatch (task 1002399)', () => {
|
|
81
|
+
// A neutral core checkout signed into a real instance: even though the resolved
|
|
82
|
+
// brand host would differ from the session host, there is no declared instance to
|
|
83
|
+
// enforce, so the guard must NOT fire. This is exactly what lets a neutral checkout
|
|
84
|
+
// run /builder-* with no <PREFIX>_API_BASE prefix.
|
|
85
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: OTB, env: noEnv, hasInstanceConfig: neutral });
|
|
86
|
+
assert.equal(r.mismatch, false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('neutral guard is precedence-safe: env override still wins even when neutral', () => {
|
|
90
|
+
// env override short-circuits before the neutral check — order-independent.
|
|
91
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: { GDS_API_BASE: 'http://localhost:3000' }, hasInstanceConfig: neutral });
|
|
92
|
+
assert.equal(r.mismatch, false);
|
|
76
93
|
});
|
|
77
94
|
|
|
78
95
|
test('no-session: nothing to borrow → no mismatch (the no-session/401 path handles it)', () => {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// tests/instance_release_config.mjs
|
|
2
|
+
//
|
|
3
|
+
// serve-internal mounts /downloads/devbox/* and /downloads/bongos/* only when the
|
|
4
|
+
// instance ships the matching RELEASE config (config/devbox-app.json,
|
|
5
|
+
// config/bongos-cli.json). Those configs are boundary-EXCLUDED from the packaged core,
|
|
6
|
+
// so they live at the INSTANCE root — which on a vendored instance (@bongos/core in
|
|
7
|
+
// node_modules, ADR 0108) is a DIFFERENT directory from the core package dir
|
|
8
|
+
// serve-internal.js sits in. The old core-relative `require('../../config/…')` looked
|
|
9
|
+
// only in node_modules/@bongos/core/config/, so a vendored instance's Dev Box download
|
|
10
|
+
// routes never mounted (task 1002371; ADR 0145 papered over it by copying the config
|
|
11
|
+
// into node_modules — lost on the next npm ci). Same class as ADR 0144's vendored
|
|
12
|
+
// infra/ path bug.
|
|
13
|
+
//
|
|
14
|
+
// The first test is THE regression: a config present ONLY at the instance root must be
|
|
15
|
+
// found. It fails against the old resolution (which could not see the instance root at
|
|
16
|
+
// all) and passes against the instance-root-first ladder.
|
|
17
|
+
//
|
|
18
|
+
// serve-internal is DB-free (the pg pool connects lazily), so the end-to-end case mounts
|
|
19
|
+
// it on a throwaway express app over loopback — same pattern as tests/docs_openapi_route.mjs.
|
|
20
|
+
//
|
|
21
|
+
// Run: node --test tests/instance_release_config.mjs
|
|
22
|
+
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
// A minimal but VALID release config — buildDevboxDownloads rejects a bad
|
|
33
|
+
// releases_repo/version at boot, so the end-to-end mount below proves real parsing.
|
|
34
|
+
const DEVBOX_CFG = {
|
|
35
|
+
releases_repo: 'example-owner/example-devbox',
|
|
36
|
+
version: '9.9.9',
|
|
37
|
+
assets: { 'mac-arm64': 'Example-Dev-Box-9.9.9-arm64.dmg' },
|
|
38
|
+
aliases: { mac: 'mac-arm64' },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Write config/<name> under a fresh temp dir and return the dir (a stand-in root).
|
|
42
|
+
function makeRoot(name, contents) {
|
|
43
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-root-'));
|
|
44
|
+
if (name) {
|
|
45
|
+
fs.mkdirSync(path.join(dir, 'config'), { recursive: true });
|
|
46
|
+
fs.writeFileSync(path.join(dir, 'config', name), contents);
|
|
47
|
+
}
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Point the process at a temp INSTANCE root BEFORE serve-internal loads, so the default
|
|
52
|
+
// (uninjected) resolution runs the vendored shape: config at the instance root, absent
|
|
53
|
+
// from the core package dir. This checkout ships no config/devbox-app.json — it is
|
|
54
|
+
// instance-only — so the core tier genuinely has nothing to find, exactly like a
|
|
55
|
+
// freshly-vendored box.
|
|
56
|
+
const INSTANCE_ROOT = makeRoot('devbox-app.json', JSON.stringify(DEVBOX_CFG));
|
|
57
|
+
process.env.CLOUDBONGOS_INSTANCE_ROOT = INSTANCE_ROOT;
|
|
58
|
+
const { tryLoadInstanceConfig, mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
59
|
+
|
|
60
|
+
test('finds a release config that exists ONLY at the instance root (the vendored bug)', () => {
|
|
61
|
+
const instance = makeRoot('devbox-app.json', '{"version":"1.0.0"}');
|
|
62
|
+
const core = makeRoot(null);
|
|
63
|
+
assert.deepEqual(tryLoadInstanceConfig('devbox-app.json', [instance, core]), { version: '1.0.0' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the instance root wins over the core package dir when both carry a copy', () => {
|
|
67
|
+
const instance = makeRoot('devbox-app.json', '{"from":"instance"}');
|
|
68
|
+
const core = makeRoot('devbox-app.json', '{"from":"core"}');
|
|
69
|
+
assert.equal(tryLoadInstanceConfig('devbox-app.json', [instance, core]).from, 'instance');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('falls back to the core package dir (single checkout + the copied-in workaround)', () => {
|
|
73
|
+
const instance = makeRoot(null);
|
|
74
|
+
const core = makeRoot('bongos-cli.json', '{"from":"core"}');
|
|
75
|
+
assert.equal(tryLoadInstanceConfig('bongos-cli.json', [instance, core]).from, 'core');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('absent from every root → null, so the download surface just does not mount', () => {
|
|
79
|
+
assert.equal(tryLoadInstanceConfig('devbox-app.json', [makeRoot(null), makeRoot(null)]), null);
|
|
80
|
+
assert.equal(tryLoadInstanceConfig('devbox-app.json', []), null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a MALFORMED config still throws — only a MISSING file is tolerated', () => {
|
|
84
|
+
const bad = makeRoot('devbox-app.json', '{ this is not valid json ');
|
|
85
|
+
assert.throws(() => tryLoadInstanceConfig('devbox-app.json', [bad]), SyntaxError);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('/downloads/devbox/version mounts from an INSTANCE-root config (end to end)', async () => {
|
|
89
|
+
const express = require('express');
|
|
90
|
+
const app = express();
|
|
91
|
+
mountInternalSurfaces(app);
|
|
92
|
+
const server = await new Promise((resolve) => {
|
|
93
|
+
const s = app.listen(0, () => resolve(s));
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`http://127.0.0.1:${server.address().port}/downloads/devbox/version`);
|
|
97
|
+
assert.equal(res.status, 200, 'the route must mount when only the INSTANCE root has the config');
|
|
98
|
+
const body = await res.json();
|
|
99
|
+
assert.equal(body.version, DEVBOX_CFG.version);
|
|
100
|
+
assert.match(body.assets['mac-arm64'], /^https:\/\/github\.com\/example-owner\/example-devbox\//);
|
|
101
|
+
} finally {
|
|
102
|
+
await new Promise((r) => server.close(r));
|
|
103
|
+
}
|
|
104
|
+
});
|
package/tests/linkify_refs.mjs
CHANGED
|
@@ -252,12 +252,25 @@ t('a known idea link is never unwrapped by the past-mislink pass', () => {
|
|
|
252
252
|
assert.equal(runIds(s, ids), s);
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
-
t('discoverDocs finds CLAUDE.md
|
|
255
|
+
t('discoverDocs finds CLAUDE.md, recurses docs/ subdirs (only .md), skips the generated ideas file', () => {
|
|
256
256
|
const docs = discoverDocs();
|
|
257
257
|
assert.ok(docs.includes('CLAUDE.md'), 'includes CLAUDE.md');
|
|
258
|
-
assert.ok(docs.some((d) => d.startsWith('docs/session-logs/')), 'includes session logs');
|
|
259
258
|
assert.ok(docs.every((d) => d.endsWith('.md')), 'only markdown files');
|
|
260
259
|
assert.ok(!docs.includes('generative-ideas.md'), 'excludes the generated ideas file');
|
|
260
|
+
// Recurses into docs/ subdirectories. docs/adr/*.md is the always-present nested
|
|
261
|
+
// case; assert recursion against it (task 1002234). Session logs are the canonical
|
|
262
|
+
// nested example the linkifier targets, but the neutral core carries none (they're
|
|
263
|
+
// instance-specific / not committed), so that stricter check is conditional — it
|
|
264
|
+
// still verifies them on an instance whose docs/session-logs/ is populated.
|
|
265
|
+
assert.ok(docs.some((d) => d.startsWith('docs/') && d.split('/').length >= 3),
|
|
266
|
+
'recurses into docs/ subdirectories (e.g. docs/adr/*.md)');
|
|
267
|
+
const fs = require('node:fs');
|
|
268
|
+
const path = require('node:path');
|
|
269
|
+
const logsDir = path.resolve(process.cwd(), 'docs/session-logs');
|
|
270
|
+
const hasSessionLogMd = fs.existsSync(logsDir) && fs.readdirSync(logsDir).some((f) => f.endsWith('.md'));
|
|
271
|
+
if (hasSessionLogMd) {
|
|
272
|
+
assert.ok(docs.some((d) => d.startsWith('docs/session-logs/')), 'includes session logs when present');
|
|
273
|
+
}
|
|
261
274
|
});
|
|
262
275
|
|
|
263
276
|
console.log(`\nlinkify_refs: ${passed} passed, ${failed} failed`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// tests/migrations_forward_safe.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002223 / audit H10 (finding F6) — the FAIL-CLOSED forward-safety gate for core migrations.
|
|
4
|
+
// `bongos upgrade` auto-rollback reverts the code pin but NOT the DB, so a core migration must be
|
|
5
|
+
// forward-safe (old code still runs on the new schema) — or explicitly acknowledge the exception
|
|
6
|
+
// with a `-- forward-unsafe: <reason>` pragma. This runs in the DB-free unit gate every PR: add a
|
|
7
|
+
// DROP COLUMN / RENAME / ALTER TYPE without the pragma and this REDs, long before a field rollback
|
|
8
|
+
// strands old code on new schema. (The migrate.sh notice is advisory; THIS is the enforcement.)
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/migrations_forward_safe.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const lint = require('../scripts/gds/lint-migrations.js');
|
|
19
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
let passed = 0, failed = 0;
|
|
22
|
+
function t(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- THE GATE: the committed core migrations must be forward-safe (or acknowledged) ----
|
|
28
|
+
t('core migrations: zero UNACKNOWLEDGED forward-unsafe operations', () => {
|
|
29
|
+
const { violations, acknowledged, scanned } = lint.scanMigrationsDir(join(REPO_ROOT, 'migrations'));
|
|
30
|
+
assert.ok(scanned > 0, 'expected to scan some core migrations');
|
|
31
|
+
assert.equal(violations.length, 0,
|
|
32
|
+
`unacknowledged forward-unsafe migration(s): ${violations.map((v) => `${v.file} [${v.unsafe.join(',')}]`).join('; ')}\n`
|
|
33
|
+
+ ' Add a top-of-file `-- forward-unsafe: <reason>` comment if the operation is intentional + safe here.');
|
|
34
|
+
// sanity: the two known historical exceptions stay acknowledged (not silently dropped)
|
|
35
|
+
assert.ok(acknowledged.some((a) => a.file.startsWith('039_')), 'expected 039 to remain an acknowledged exception');
|
|
36
|
+
assert.ok(acknowledged.some((a) => a.file.startsWith('102_')), 'expected 102 to remain an acknowledged exception');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ---- the scanner classifies operations correctly ----
|
|
40
|
+
t('scanMigrationSql: flags each genuinely forward-unsafe operation', () => {
|
|
41
|
+
const cases = {
|
|
42
|
+
'drop-table': 'DROP TABLE widgets;',
|
|
43
|
+
'drop-column': 'ALTER TABLE t DROP COLUMN c;',
|
|
44
|
+
'rename-column': 'ALTER TABLE t RENAME COLUMN a TO b;',
|
|
45
|
+
'rename-table': 'ALTER TABLE t RENAME TO t2;',
|
|
46
|
+
'alter-column-type': 'ALTER TABLE t ALTER COLUMN c TYPE bigint;',
|
|
47
|
+
'set-not-null': 'ALTER TABLE t ALTER COLUMN c SET NOT NULL;',
|
|
48
|
+
};
|
|
49
|
+
for (const [expected, sql] of Object.entries(cases)) {
|
|
50
|
+
const r = lint.scanMigrationSql(sql);
|
|
51
|
+
assert.ok(r.unsafe.includes(expected), `expected ${expected} for: ${sql} — got ${JSON.stringify(r.unsafe)}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('scanMigrationSql: does NOT flag additive / recreate-in-place operations', () => {
|
|
56
|
+
const safe = [
|
|
57
|
+
'CREATE TABLE t (id int);',
|
|
58
|
+
'ALTER TABLE t ADD COLUMN c text;',
|
|
59
|
+
'CREATE INDEX idx ON t (c);',
|
|
60
|
+
'DROP INDEX IF EXISTS idx; CREATE INDEX idx ON t (c);', // recreate-in-place
|
|
61
|
+
'DROP TRIGGER IF EXISTS trg ON t; CREATE TRIGGER trg BEFORE INSERT ON t ...;',
|
|
62
|
+
'CREATE OR REPLACE VIEW v AS SELECT 1;',
|
|
63
|
+
'DROP VIEW IF EXISTS v; CREATE VIEW v AS SELECT 1;',
|
|
64
|
+
'DROP FUNCTION IF EXISTS f(); CREATE FUNCTION f() RETURNS void ...;',
|
|
65
|
+
"INSERT INTO t (id) VALUES (1);",
|
|
66
|
+
];
|
|
67
|
+
for (const sql of safe) {
|
|
68
|
+
const r = lint.scanMigrationSql(sql);
|
|
69
|
+
assert.equal(r.unsafe.length, 0, `false positive on safe SQL: ${sql} — got ${JSON.stringify(r.unsafe)}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('scanMigrationSql: the acknowledgement pragma flips an unsafe op to acknowledged', () => {
|
|
74
|
+
const r = lint.scanMigrationSql('-- forward-unsafe: intentional, two-step deploy\nALTER TABLE t DROP COLUMN c;');
|
|
75
|
+
assert.ok(r.unsafe.includes('drop-column'));
|
|
76
|
+
assert.equal(r.acknowledged, true);
|
|
77
|
+
assert.match(r.ackReason, /two-step/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
t('scanMigrationSql: an unsafe keyword only inside a comment does NOT count', () => {
|
|
81
|
+
const r = lint.scanMigrationSql('-- this migration does NOT drop table anything\nCREATE TABLE t (id int);');
|
|
82
|
+
assert.equal(r.unsafe.length, 0, `comment text must not trip the scan — got ${JSON.stringify(r.unsafe)}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log(`\nmigrations_forward_safe: ${passed} passed, ${failed} failed`);
|
|
86
|
+
process.exit(failed ? 1 : 0);
|
package/tests/module_api.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
|
20
20
|
// was added; no enabled module at its pinned coreVersion lost a capability.
|
|
21
21
|
const PUBLISHED_SURFACE = [
|
|
22
22
|
'CORE_VERSION',
|
|
23
|
-
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope',
|
|
23
|
+
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
|
|
24
24
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
25
25
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
26
26
|
'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
|
|
@@ -146,3 +146,58 @@ test('settings-panel seam: ABSENT → empty; REGISTERED → present, sorted by o
|
|
|
146
146
|
assert.throws(() => OTB.contributeSettingsPanel({ id: 'x' }), /requires id \+ mount/);
|
|
147
147
|
delete global.window;
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
// --- hall-widget placement governance (task 2177) ----------------------------
|
|
151
|
+
// Home grew unbounded once; contributeHallWidget now refuses the registrations
|
|
152
|
+
// that let it. Rejections are console.error + null (NOT a throw) so one bad
|
|
153
|
+
// third-party widget can't blank the hall — so these assert the RETURN VALUE and
|
|
154
|
+
// that the widget never reached the registry.
|
|
155
|
+
|
|
156
|
+
test('hall-widget registry: placement rules reject bad registrations, accept good ones', () => {
|
|
157
|
+
global.window = {};
|
|
158
|
+
// The settings-panel test above already required dom-utils.js, and the IIFE
|
|
159
|
+
// assigns window.OTB exactly once at load — so re-requiring a cached module
|
|
160
|
+
// would leave window.OTB undefined. Evict it to get a fresh registry.
|
|
161
|
+
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
162
|
+
delete require.cache[require.resolve(domUtils)];
|
|
163
|
+
require(domUtils);
|
|
164
|
+
const OTB = global.window.OTB;
|
|
165
|
+
|
|
166
|
+
const errors = [];
|
|
167
|
+
const realError = console.error;
|
|
168
|
+
console.error = (msg) => errors.push(String(msg));
|
|
169
|
+
const reg = (o) => OTB.contributeHallWidget({ label: 'L', mount() {}, ...o });
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// the structural contract still THROWS (first-party programmer error)
|
|
173
|
+
assert.throws(() => reg({ label: 'no id' }), /requires id \+ label \+ mount/);
|
|
174
|
+
|
|
175
|
+
// unknown zone / unknown view — a typo used to mount the widget on Home
|
|
176
|
+
// silently, which read as "my widget vanished"
|
|
177
|
+
assert.equal(reg({ id: 'a', view: 'home', zone: 'home-sidebar' }), null);
|
|
178
|
+
assert.match(errors.at(-1), /unknown zone "home-sidebar"/);
|
|
179
|
+
assert.equal(reg({ id: 'b', view: 'dashboard', zone: 'home-main', summary: true }), null);
|
|
180
|
+
assert.match(errors.at(-1), /unknown view "dashboard"/);
|
|
181
|
+
|
|
182
|
+
// a Home widget must NAME its zone — there is no home-main fallback any more
|
|
183
|
+
assert.equal(reg({ id: 'c' }), null);
|
|
184
|
+
assert.match(errors.at(-1), /must declare a zone/);
|
|
185
|
+
|
|
186
|
+
// a non-core home-main widget must be summary or collapsed
|
|
187
|
+
assert.equal(reg({ id: 'd', view: 'home', zone: 'home-main' }), null);
|
|
188
|
+
assert.match(errors.at(-1), /summary:true .* or collapsedByDefault:true/);
|
|
189
|
+
|
|
190
|
+
// …and the escapes from that rule work
|
|
191
|
+
assert.ok(reg({ id: 'e', view: 'home', zone: 'home-main', summary: true }));
|
|
192
|
+
assert.ok(reg({ id: 'f', view: 'home', zone: 'home-main', collapsedByDefault: true }));
|
|
193
|
+
assert.ok(reg({ id: 'g', view: 'home', zone: 'home-main', core: true }));
|
|
194
|
+
// a non-Home view needs no zone (Standing widgets carry `group` instead)
|
|
195
|
+
assert.ok(reg({ id: 'h', view: 'standing', group: 'work' }));
|
|
196
|
+
|
|
197
|
+
// only the accepted four reached the registry
|
|
198
|
+
assert.deepEqual(OTB.hallWidgetIds(), ['e', 'f', 'g', 'h']);
|
|
199
|
+
} finally {
|
|
200
|
+
console.error = realError;
|
|
201
|
+
delete global.window;
|
|
202
|
+
}
|
|
203
|
+
});
|
package/tests/module_loader.mjs
CHANGED
|
@@ -168,13 +168,18 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
168
168
|
// onboarding (R81) + sessions (R82) + autonomy (R83 — conductor/architect/gate
|
|
169
169
|
// + the routine-timers poller) are the tranche-2 core-domain carves (ADR 0093),
|
|
170
170
|
// then status-ui (R84) + hall-ui (R85) the two web-surface carves; and
|
|
171
|
-
// provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting)
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
171
|
+
// provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting);
|
|
172
|
+
// platform-identity (ADR 0141) the hub IdP; and governance (ADR 0151 / BV1.R94) the
|
|
173
|
+
// configurable roles/permissions module. All are discovered; none has validation
|
|
174
|
+
// errors or coreVersion mismatches. Order is the readdir sort.
|
|
175
|
+
//
|
|
176
|
+
// NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
|
|
177
|
+
// instance/content-excluded from the published core (ADR 0098) so they are not
|
|
178
|
+
// present here — the discovered set is the 17 always-shipped core-domain + feature
|
|
179
|
+
// modules below.
|
|
175
180
|
loader._reset();
|
|
176
181
|
const snap = loader.loaderSnapshot();
|
|
177
|
-
assert.deepEqual(snap.loaded, ['
|
|
182
|
+
assert.deepEqual(snap.loaded, ['autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
178
183
|
assert.deepEqual(snap.errors, []);
|
|
179
184
|
assert.deepEqual(snap.skipped, []);
|
|
180
185
|
loader._reset();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// tests/module_scope_active_claims.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit guard for db.moduleScopeForActiveClaims (goal 1000051 / R92, ADR 0148 §2) —
|
|
4
|
+
// the lifecycle-port resolver the dev-box source-access path (R93/R94) reads to
|
|
5
|
+
// derive a builder's TASK-scoped sparse-checkout: the union of module KEYS across
|
|
6
|
+
// the builder's active claims' goals (claims → tasks → goals.scope_modules).
|
|
7
|
+
//
|
|
8
|
+
// DB-free, same posture as goal_achievement.mjs: the function honours the #538
|
|
9
|
+
// deps.pool injection seam (ONLY when NODE_ENV==='test'), so a SQL-aware fake
|
|
10
|
+
// client drives it with no live Postgres. We assert the query SHAPE (joins
|
|
11
|
+
// claims→tasks→goals, unnests scope_modules, filters ACTIVE claims), the bound
|
|
12
|
+
// params, the rows→keys mapping, and the numeric-input guard. The actual
|
|
13
|
+
// set-union/dedup is Postgres's DISTINCT (exercised end-to-end once box-access is
|
|
14
|
+
// wired — R95); here we prove the port method's contract.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/module_scope_active_claims.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
21
|
+
|
|
22
|
+
process.env.NODE_ENV = 'test';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { moduleScopeForActiveClaims } = require('../modules/lifecycle/db.js');
|
|
26
|
+
|
|
27
|
+
const { test, summary } = makeRunner();
|
|
28
|
+
|
|
29
|
+
function fakePool(handler) {
|
|
30
|
+
const c = makeSqlAwareClient(handler);
|
|
31
|
+
return { query: (sql, params) => c._client.query(sql, params), connect: c.connect, queries: c._client.queries };
|
|
32
|
+
}
|
|
33
|
+
const norm = (s) => String(s).replace(/\s+/g, ' ').trim();
|
|
34
|
+
const find = (queries, re) => queries.find((q) => re.test(norm(q.sql)));
|
|
35
|
+
|
|
36
|
+
// The one read this function issues: claims → tasks → goals, unnest scope_modules,
|
|
37
|
+
// active claims only. The fake returns whatever module-key rows we hand it (the
|
|
38
|
+
// DISTINCT/ORDER BY is Postgres's job — here we just prove the plumbing).
|
|
39
|
+
function scopeHandler(rows) {
|
|
40
|
+
return (s) => {
|
|
41
|
+
if (/FROM claims c/.test(s) && /JOIN goals g/.test(s) && /unnest\(g\.scope_modules\)/.test(s)) {
|
|
42
|
+
return { rows };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =========================================================================
|
|
49
|
+
// Happy path — active claims put N goals' scope_modules in scope
|
|
50
|
+
// =========================================================================
|
|
51
|
+
await test('returns the module keys the active claims scope, in DB order', async () => {
|
|
52
|
+
const pool = fakePool(scopeHandler([{ module_key: 'dev-box' }, { module_key: 'kernel' }, { module_key: 'lifecycle' }]));
|
|
53
|
+
const keys = await moduleScopeForActiveClaims(3, { pool });
|
|
54
|
+
assert.deepEqual(keys, ['dev-box', 'kernel', 'lifecycle'], 'maps rows → module_key list');
|
|
55
|
+
|
|
56
|
+
const q = find(pool.queries, /FROM claims c JOIN tasks t .* JOIN goals g/);
|
|
57
|
+
assert.ok(q, 'issues the claims→tasks→goals scope read');
|
|
58
|
+
assert.match(norm(q.sql), /c\.builder_id = \$1 AND c\.released_at IS NULL/, 'active claims only, keyed on builder');
|
|
59
|
+
assert.match(norm(q.sql), /unnest\(g\.scope_modules\)/, 'unnests the goal scope_modules array');
|
|
60
|
+
assert.match(norm(q.sql), /SELECT DISTINCT/, 'DISTINCT dedupes the union across claims');
|
|
61
|
+
assert.deepEqual(q.params, [3], 'binds the builder id');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// =========================================================================
|
|
65
|
+
// No active claim (or no scoped goal) → base-only signal ([])
|
|
66
|
+
// =========================================================================
|
|
67
|
+
await test('no active claim / no scoped goal → empty array (box falls back to BASE-only)', async () => {
|
|
68
|
+
const pool = fakePool(scopeHandler([]));
|
|
69
|
+
const keys = await moduleScopeForActiveClaims(3, { pool });
|
|
70
|
+
assert.deepEqual(keys, [], 'zero rows → []');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// =========================================================================
|
|
74
|
+
// Input guard — a non-numeric builder id never reaches SQL
|
|
75
|
+
// =========================================================================
|
|
76
|
+
await test('non-numeric builderId throws before any query', async () => {
|
|
77
|
+
const pool = fakePool(scopeHandler([{ module_key: 'dev-box' }]));
|
|
78
|
+
await assert.rejects(() => moduleScopeForActiveClaims('not-a-number', { pool }), /must be numeric/);
|
|
79
|
+
await assert.rejects(() => moduleScopeForActiveClaims(NaN, { pool }), /must be numeric/);
|
|
80
|
+
assert.equal(pool.queries.length, 0, 'guard trips before the pool is touched');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// =========================================================================
|
|
84
|
+
// The #538 deps.pool test seam is actually wired (we ARE in NODE_ENV==='test')
|
|
85
|
+
// =========================================================================
|
|
86
|
+
await test('honours the deps.pool test seam', async () => {
|
|
87
|
+
let touched = false;
|
|
88
|
+
const pool = fakePool((s) => { touched = true; return /unnest/.test(s) ? { rows: [{ module_key: 'game' }] } : null; });
|
|
89
|
+
const keys = await moduleScopeForActiveClaims(7, { pool });
|
|
90
|
+
assert.ok(touched, 'the injected pool was used');
|
|
91
|
+
assert.deepEqual(keys, ['game']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
summary();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// tests/needs_rebase.mjs — ADR 0120 part 2 (real DB).
|
|
2
|
+
//
|
|
3
|
+
// Exercises db.flagTaskNeedsRebase + the flag-clearing on ship against a live
|
|
4
|
+
// Postgres (DATABASE_URL). The fake-client harness can't cover the actual
|
|
5
|
+
// WHERE-clause guards (status='confirmed' AND needs_rebase=false) or the
|
|
6
|
+
// migration-093 columns, so this is a real-DB integration test. It SELF-SKIPS
|
|
7
|
+
// (exit 0) when no DB is reachable, so it is safe on a bare worktree; it is also
|
|
8
|
+
// listed in run-unit-tests.js's INTEGRATION set so the default DB-free lane
|
|
9
|
+
// never runs it.
|
|
10
|
+
//
|
|
11
|
+
// Run: DATABASE_URL=... node tests/needs_rebase.mjs
|
|
12
|
+
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { Pool } = require('pg');
|
|
17
|
+
|
|
18
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
19
|
+
|
|
20
|
+
// Reachability probe — skip cleanly if there's no local Postgres.
|
|
21
|
+
try {
|
|
22
|
+
await pool.query('SELECT 1');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.log(`needs_rebase.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
25
|
+
await pool.end().catch(() => {});
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const seams = require('../src/module-seams.js');
|
|
30
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
31
|
+
const db = require('../modules/lifecycle/db.js');
|
|
32
|
+
|
|
33
|
+
const VER = 'ZZZ-NR', WORKER = 90000101, TASK = 90000101;
|
|
34
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
35
|
+
const taskRow = async () => (await q(`SELECT status, needs_rebase, needs_rebase_reason, needs_rebase_at FROM tasks WHERE id=$1`, [TASK]))[0];
|
|
36
|
+
|
|
37
|
+
async function cleanup() {
|
|
38
|
+
await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
|
|
39
|
+
await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id=$2`, [WORKER, TASK]).catch(() => {});
|
|
40
|
+
await q(`DELETE FROM claims WHERE task_id=$1`, [TASK]).catch(() => {});
|
|
41
|
+
await q(`DELETE FROM tasks WHERE id=$1`, [TASK]).catch(() => {});
|
|
42
|
+
await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
|
|
43
|
+
await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let pass = 0;
|
|
47
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await cleanup();
|
|
51
|
+
await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'nr','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
|
|
52
|
+
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'nr-w','nr-worker','NR Worker','thetes')`, [WORKER]);
|
|
53
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES ($1,$2,'nr fixture','confirmed',10,'feature',$3)`, [TASK, VER, WORKER]);
|
|
54
|
+
await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2,now())`, [TASK, WORKER]);
|
|
55
|
+
|
|
56
|
+
// 1) flag a CONFIRMED task → flagged, reason + at set, returns true.
|
|
57
|
+
const first = await db.flagTaskNeedsRebase(String(TASK), 'strand:no_branch_no_tip');
|
|
58
|
+
ok(first === true, 'flagTaskNeedsRebase returns true when it newly flags a confirmed task');
|
|
59
|
+
let t = await taskRow();
|
|
60
|
+
ok(t.needs_rebase === true, 'tasks.needs_rebase is true after flagging');
|
|
61
|
+
ok(t.needs_rebase_reason === 'strand:no_branch_no_tip', 'reason recorded');
|
|
62
|
+
ok(t.needs_rebase_at != null, 'needs_rebase_at timestamp set');
|
|
63
|
+
|
|
64
|
+
// 2) idempotent — a second flag is a no-op (returns false, at unchanged).
|
|
65
|
+
const atWas = t.needs_rebase_at;
|
|
66
|
+
const second = await db.flagTaskNeedsRebase(String(TASK), 'strand:different');
|
|
67
|
+
ok(second === false, 'a re-flag of an already-flagged task returns false (idempotent)');
|
|
68
|
+
t = await taskRow();
|
|
69
|
+
ok(t.needs_rebase_reason === 'strand:no_branch_no_tip', 'reason NOT overwritten on re-flag');
|
|
70
|
+
ok(String(t.needs_rebase_at.getTime?.() ?? t.needs_rebase_at) === String(atWas.getTime?.() ?? atWas), 'needs_rebase_at unchanged on re-flag');
|
|
71
|
+
|
|
72
|
+
// 3) shipping the task CLEARS the flag (the land completed).
|
|
73
|
+
await db.shipTask({ taskId: String(TASK), builderId: String(WORKER) });
|
|
74
|
+
t = await taskRow();
|
|
75
|
+
ok(t.status === 'shipped', 'task shipped');
|
|
76
|
+
ok(t.needs_rebase === false, 'needs_rebase cleared on ship');
|
|
77
|
+
ok(t.needs_rebase_reason === null && t.needs_rebase_at === null, 'reason + at cleared on ship');
|
|
78
|
+
|
|
79
|
+
// 4) a NON-confirmed task cannot be flagged (guard: WHERE status='confirmed').
|
|
80
|
+
const onShipped = await db.flagTaskNeedsRebase(String(TASK), 'should-not-apply');
|
|
81
|
+
ok(onShipped === false, 'flagTaskNeedsRebase is a no-op on a non-confirmed (shipped) task');
|
|
82
|
+
t = await taskRow();
|
|
83
|
+
ok(t.needs_rebase === false, 'a shipped task stays unflagged');
|
|
84
|
+
|
|
85
|
+
console.log(`\nneeds_rebase (ADR 0120 part 2): ${pass} passed, 0 failed`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('needs_rebase.mjs FAILED:', e.message);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
} finally {
|
|
90
|
+
await cleanup();
|
|
91
|
+
await pool.end();
|
|
92
|
+
}
|