@bongos/core 1.18.3 → 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 +1575 -285
- 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 +4 -2
- 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 +6 -1
- 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/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 +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- 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 +8 -0
- 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/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/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/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/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/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/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 +51 -58
- 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 -27
- 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/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- 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 +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- 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 +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -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 +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -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 +12 -0
- 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/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 +93 -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/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/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 +132 -7
- 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 +80 -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 +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -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`);
|
|
@@ -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
|
+
}
|
|
@@ -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 () => {
|
|
@@ -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);
|