@bongos/core 1.18.3 → 1.18.19
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 +1576 -286
- 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/migrate.sh +99 -14
- 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 +124 -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/migrate_root_resolution.mjs +124 -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/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`);
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { strict as assert } from 'node:assert';
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, realpathSync,
|
|
22
|
+
} from 'node:fs';
|
|
21
23
|
import { tmpdir } from 'node:os';
|
|
22
24
|
import { fileURLToPath } from 'node:url';
|
|
23
25
|
import path from 'node:path';
|
|
@@ -93,5 +95,126 @@ t('CORE_ROOT is always the script-relative repo root, independent of the invocat
|
|
|
93
95
|
assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
|
|
94
96
|
});
|
|
95
97
|
|
|
98
|
+
// ==========================================================================
|
|
99
|
+
// A CORE ROOT WHOSE PATH CONTAINS A SPACE (task 1002611)
|
|
100
|
+
// ==========================================================================
|
|
101
|
+
// `for f in $MIGRATIONS` word-split the newline-separated migration list on IFS — which includes
|
|
102
|
+
// space — so a checkout at "/Users/.../Hermeslines Marketing/..." fragmented every path, the
|
|
103
|
+
// `[ -e "$f" ]` guard dropped all of them, and ZERO core migrations ran. Reproduced on
|
|
104
|
+
// @bongos/core@1.18.18: 117 files listed, 234 iterations, 234 -e failures, schema_migrations empty.
|
|
105
|
+
//
|
|
106
|
+
// These exercise migrate.sh's `--print-migrations` seam, which walks the REAL loop and reports each
|
|
107
|
+
// path instead of applying it (DB-free, like --print-roots). Reporting through the real loop is the
|
|
108
|
+
// point: a copy of the loop inside this test would not catch a revert of the loop in the script.
|
|
109
|
+
|
|
110
|
+
// A synthetic CORE root: `scripts/migrate.sh` SYMLINKED to the real script (so the fixture can never
|
|
111
|
+
// drift from it), a stub instance-config that names the fixture as both roots, and n migration files.
|
|
112
|
+
// `spaced` puts a space in the fixture's own directory name — the whole point of the fixture.
|
|
113
|
+
function makeCore({ spaced, files = ['001_a.sql', '002_b.sql', '006_pms_v2.sql', '019_c.sql'] } = {}) {
|
|
114
|
+
const prefix = spaced ? 'r89 spaced core-' : 'r89-plain-core-';
|
|
115
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), prefix)));
|
|
116
|
+
mkdirSync(path.join(dir, 'scripts'));
|
|
117
|
+
mkdirSync(path.join(dir, 'migrations'));
|
|
118
|
+
mkdirSync(path.join(dir, 'src'));
|
|
119
|
+
symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
|
|
120
|
+
// Pin both roots to the fixture explicitly, so the test does not lean on migrate.sh's
|
|
121
|
+
// node-unavailable fallback to decide what CORE_ROOT is.
|
|
122
|
+
writeFileSync(path.join(dir, 'src', 'instance-config.js'), [
|
|
123
|
+
"const path = require('node:path');",
|
|
124
|
+
"const ROOT = path.resolve(__dirname, '..');",
|
|
125
|
+
'module.exports = {',
|
|
126
|
+
' resolveCoreRoot: () => ROOT,',
|
|
127
|
+
' resolveInstanceRootExplicit: () => ROOT,',
|
|
128
|
+
'};',
|
|
129
|
+
'',
|
|
130
|
+
].join('\n'));
|
|
131
|
+
for (const f of files) writeFileSync(path.join(dir, 'migrations', f), '-- probe\n');
|
|
132
|
+
return dir;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Run `<core>/scripts/migrate.sh --print-migrations` and parse the seam's output.
|
|
136
|
+
// Returns { applied: [paths], listed, unreadable, status, stderr }.
|
|
137
|
+
function printMigrations(coreDir, { script = 'migrate.sh' } = {}) {
|
|
138
|
+
const env = { ...process.env };
|
|
139
|
+
for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
|
|
140
|
+
delete env.INIT_CWD;
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
let status = 0;
|
|
144
|
+
try {
|
|
145
|
+
stdout = execFileSync('bash', [path.join(coreDir, 'scripts', script), '--print-migrations'],
|
|
146
|
+
{ cwd: coreDir, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
147
|
+
} catch (e) {
|
|
148
|
+
status = e.status ?? 1;
|
|
149
|
+
stdout = e.stdout ?? '';
|
|
150
|
+
stderr = e.stderr ?? '';
|
|
151
|
+
}
|
|
152
|
+
const applied = [];
|
|
153
|
+
let listed = null;
|
|
154
|
+
let unreadable = null;
|
|
155
|
+
for (const line of stdout.split('\n')) {
|
|
156
|
+
const a = /^would-apply\t(.*)$/.exec(line);
|
|
157
|
+
if (a) { applied.push(a[1]); continue; }
|
|
158
|
+
const s = /^listed=(\d+) unreadable=(\d+)$/.exec(line);
|
|
159
|
+
if (s) { listed = Number(s[1]); unreadable = Number(s[2]); }
|
|
160
|
+
}
|
|
161
|
+
return { applied, listed, unreadable, status, stderr, stdout };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
t('core root containing a space: every migration is listed as ONE whole, readable path', () => {
|
|
165
|
+
const core = makeCore({ spaced: true });
|
|
166
|
+
assert.ok(core.includes(' '), 'fixture path must contain a space to be meaningful');
|
|
167
|
+
const r = printMigrations(core);
|
|
168
|
+
assert.equal(r.status, 0, `migrate.sh --print-migrations must succeed; stderr:\n${r.stderr}`);
|
|
169
|
+
assert.equal(r.listed, 4, 'all four migrations must be listed');
|
|
170
|
+
assert.equal(r.unreadable, 0, 'no listed path may fail the -e test (fragmentation)');
|
|
171
|
+
assert.equal(r.applied.length, 4, 'all four must reach apply_migration');
|
|
172
|
+
for (const f of r.applied) {
|
|
173
|
+
assert.ok(existsSync(f), `listed path must exist on disk, got fragment: ${f}`);
|
|
174
|
+
assert.ok(f.startsWith(core), `listed path must be whole and under the core root: ${f}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
t('core root containing a space: applies in basename order, unaffected by the space', () => {
|
|
179
|
+
const core = makeCore({ spaced: true });
|
|
180
|
+
const r = printMigrations(core);
|
|
181
|
+
assert.deepEqual(
|
|
182
|
+
r.applied.map((f) => path.basename(f)),
|
|
183
|
+
['001_a.sql', '002_b.sql', '006_pms_v2.sql', '019_c.sql'],
|
|
184
|
+
'the merge-and-sort-by-basename order must survive a spaced path',
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
t('a space-free core root behaves identically (the fixture itself is not what passes)', () => {
|
|
189
|
+
const spaced = printMigrations(makeCore({ spaced: true }));
|
|
190
|
+
const plain = printMigrations(makeCore({ spaced: false }));
|
|
191
|
+
assert.equal(plain.status, spaced.status);
|
|
192
|
+
assert.equal(plain.listed, spaced.listed);
|
|
193
|
+
assert.equal(plain.unreadable, spaced.unreadable);
|
|
194
|
+
assert.deepEqual(plain.applied.map((f) => path.basename(f)), spaced.applied.map((f) => path.basename(f)));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// The no-op guard, proven against the ACTUAL pre-fix code. Re-introducing the unquoted loop into a
|
|
198
|
+
// copy of the script must now FAIL LOUDLY rather than skip every core migration in silence and let
|
|
199
|
+
// the module loop below report a misleading `relation "achievements" does not exist`.
|
|
200
|
+
t('the pre-fix unquoted loop now fails loudly instead of silently skipping', () => {
|
|
201
|
+
const core = makeCore({ spaced: true });
|
|
202
|
+
const src = readFileSync(MIGRATE, 'utf8');
|
|
203
|
+
const mutant = src
|
|
204
|
+
.replace('while IFS= read -r f; do', 'for f in $MIGRATIONS; do')
|
|
205
|
+
.replace('done <<< "$MIGRATIONS"', 'done');
|
|
206
|
+
assert.notEqual(mutant, src, 'the mutation must apply — has the core loop been reshaped?');
|
|
207
|
+
assert.ok(mutant.includes('for f in $MIGRATIONS'), 'mutant must carry the unquoted loop');
|
|
208
|
+
writeFileSync(path.join(core, 'scripts', 'mutant.sh'), mutant);
|
|
209
|
+
|
|
210
|
+
const r = printMigrations(core, { script: 'mutant.sh' });
|
|
211
|
+
assert.notEqual(r.status, 0, 'the unquoted loop on a spaced path must NOT exit 0');
|
|
212
|
+
assert.match(r.stderr, /FATAL/, 'the failure must be loud');
|
|
213
|
+
assert.match(r.stderr, /applied 0 and found 0 already applied/, 'it must name the real cause');
|
|
214
|
+
assert.match(r.stderr, /Refusing to continue to module migrations/,
|
|
215
|
+
'it must say it stopped before the module loop — the misleading-error path');
|
|
216
|
+
assert.equal(r.applied.length, 0, 'the unquoted loop applies nothing (that was the bug)');
|
|
217
|
+
});
|
|
218
|
+
|
|
96
219
|
console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
|
|
97
220
|
process.exit(failed ? 1 : 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
|
+
}
|
|
@@ -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);
|