@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
|
@@ -225,6 +225,47 @@ await t('getPublishStatus: no PR for the head → none', async () => {
|
|
|
225
225
|
assert.equal(s.deploy_status, 'none');
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
// task 1002472 — an instance with NO deploy-prod.yml must not wait on it forever.
|
|
229
|
+
// The 404 is unambiguous: the /pulls fetch above already threw if the repo were
|
|
230
|
+
// unreachable, so a 404 HERE means the workflow file is absent — permanent, not
|
|
231
|
+
// transient. Anything else (5xx/403/network) stays 'pending' so a real deploy is
|
|
232
|
+
// still waited on.
|
|
233
|
+
await t('getPublishStatus: merged + workflow-runs 404 → unconfigured (no deploy-prod.yml)', async () => {
|
|
234
|
+
const mergeSha = 'd'.repeat(40);
|
|
235
|
+
const fetchImpl = async (url) => {
|
|
236
|
+
if (url.includes('/pulls?head=')) return jsonRes(200, [{ state: 'closed', merged_at: '2026-07-26T00:00:00Z', merge_commit_sha: mergeSha, html_url: 'http://pr/1' }]);
|
|
237
|
+
if (url.includes('deploy-prod.yml/runs')) return jsonRes(404, { message: 'Not Found' });
|
|
238
|
+
throw new Error('unexpected ' + url);
|
|
239
|
+
};
|
|
240
|
+
const s = await gp.getPublishStatus({ branch: 'claude/x' }, { token: 'TT', repoInfo: REPO, fetchImpl });
|
|
241
|
+
assert.equal(s.merged, true);
|
|
242
|
+
assert.equal(s.deploy_status, 'unconfigured', 'an absent workflow is permanent, not pending');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await t('getPublishStatus: merged + workflow-runs 5xx/403 → still pending (transient stays transient)', async () => {
|
|
246
|
+
const mergeSha = 'e'.repeat(40);
|
|
247
|
+
const mk = (code) => async (url) => {
|
|
248
|
+
if (url.includes('/pulls?head=')) return jsonRes(200, [{ state: 'closed', merged_at: '2026-07-26T00:00:00Z', merge_commit_sha: mergeSha, html_url: 'http://pr/1' }]);
|
|
249
|
+
if (url.includes('deploy-prod.yml/runs')) return jsonRes(code, { message: 'nope' });
|
|
250
|
+
throw new Error('unexpected ' + url);
|
|
251
|
+
};
|
|
252
|
+
for (const code of [500, 502, 403]) {
|
|
253
|
+
const s = await gp.getPublishStatus({ branch: 'claude/x' }, { token: 'TT', repoInfo: REPO, fetchImpl: mk(code) });
|
|
254
|
+
assert.equal(s.deploy_status, 'pending', `${code} must NOT be read as "no workflow"`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await t('getPublishStatus: merged, workflow EXISTS but no successful run yet → pending', async () => {
|
|
259
|
+
const mergeSha = 'f'.repeat(40);
|
|
260
|
+
const fetchImpl = async (url) => {
|
|
261
|
+
if (url.includes('/pulls?head=')) return jsonRes(200, [{ state: 'closed', merged_at: '2026-07-26T00:00:00Z', merge_commit_sha: mergeSha, html_url: 'http://pr/1' }]);
|
|
262
|
+
if (url.includes('deploy-prod.yml/runs')) return jsonRes(200, { workflow_runs: [{ head_sha: mergeSha, status: 'in_progress', conclusion: null }] });
|
|
263
|
+
throw new Error('unexpected ' + url);
|
|
264
|
+
};
|
|
265
|
+
const s = await gp.getPublishStatus({ branch: 'claude/x' }, { token: 'TT', repoInfo: REPO, fetchImpl });
|
|
266
|
+
assert.equal(s.deploy_status, 'pending', 'a real deploy in flight is still waited on');
|
|
267
|
+
});
|
|
268
|
+
|
|
228
269
|
// ===========================================================================
|
|
229
270
|
// Layer 2 — STRUCTURAL guard: the publish routes are requireBuilder and carry
|
|
230
271
|
// NO allowBoxScope (so a box-scoped session is denied — #919 regression guard).
|
|
@@ -418,5 +459,17 @@ await t('landConfirmed: true ONLY when merged AND deploy-prod succeeded', async
|
|
|
418
459
|
assert.equal(gp.landConfirmed({}), false);
|
|
419
460
|
});
|
|
420
461
|
|
|
462
|
+
// task 1002472 — an instance that deploys by pin-bump (package-core → bongos
|
|
463
|
+
// upgrade, ADR 0108) has no deploy-prod.yml to succeed. Waiting on one pins every
|
|
464
|
+
// merged PR at 'pending' forever. MERGED is still mandatory: 'unconfigured' relaxes
|
|
465
|
+
// only the deploy half, and landProvenForTask still has to prove the land is THIS
|
|
466
|
+
// task's before anything ships.
|
|
467
|
+
await t('landConfirmed: unconfigured deploy counts as landed, but ONLY when merged', async () => {
|
|
468
|
+
assert.equal(gp.landConfirmed({ merged: true, deploy_status: 'unconfigured' }), true, 'no deploy workflow → the merge IS the land');
|
|
469
|
+
assert.equal(gp.landConfirmed({ merged: false, deploy_status: 'unconfigured' }), false, 'not merged → never a land');
|
|
470
|
+
assert.equal(gp.landConfirmed({ pr_state: 'open', merged: false, deploy_status: 'unconfigured' }), false);
|
|
471
|
+
assert.equal(gp.landConfirmed({ merged: true, deploy_status: 'none' }), false, 'none ≠ unconfigured — no PR at all is not a land');
|
|
472
|
+
});
|
|
473
|
+
|
|
421
474
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
422
475
|
process.exit(fail === 0 ? 0 : 1);
|
|
@@ -58,11 +58,14 @@ function mkDeps({
|
|
|
58
58
|
refreshStaleImpl,
|
|
59
59
|
healAttempts = new Map(),
|
|
60
60
|
} = {}) {
|
|
61
|
-
const calls = { ship: [], broadcast: [], status: [], merge: [], ancestry: [], resolve: [], refresh: [], logs: [], errors: [] };
|
|
62
|
-
const realLandConfirmed = (s) => !!s && s.merged === true && s.deploy_status === 'success';
|
|
61
|
+
const calls = { ship: [], broadcast: [], status: [], merge: [], ancestry: [], resolve: [], refresh: [], flag: [], logs: [], errors: [] };
|
|
63
62
|
// isSafeBranch mirrors github-push's pattern enough for the test (rejects
|
|
64
63
|
// option-like / traversal refs); we use the REAL module so the contract can't drift.
|
|
65
64
|
const githubPush = require('../modules/lifecycle/github-push.js');
|
|
65
|
+
// task 1002472: this was a hand-rolled copy of landConfirmed's rule, so a change to
|
|
66
|
+
// the real gate could not fail this suite. Use the real one — the whole point of a
|
|
67
|
+
// sweep test is that it exercises the sweep's ACTUAL land decision.
|
|
68
|
+
const realLandConfirmed = githubPush.landConfirmed;
|
|
66
69
|
return {
|
|
67
70
|
db: {
|
|
68
71
|
listConfirmedTasksForReconcile: async () => rows,
|
|
@@ -70,6 +73,12 @@ function mkDeps({
|
|
|
70
73
|
calls.ship.push({ taskId, builderId });
|
|
71
74
|
return { id: taskId, value_summary: `task ${taskId}`, security_sensitive: false };
|
|
72
75
|
}),
|
|
76
|
+
// ADR 0120 part 2: the reconciler flags an unlandable confirmed task for
|
|
77
|
+
// its builder. Recording spy — returns true (newly flagged) by default.
|
|
78
|
+
flagTaskNeedsRebase: async (taskId, reason) => {
|
|
79
|
+
calls.flag.push({ taskId, reason });
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
73
82
|
},
|
|
74
83
|
githubPush: {
|
|
75
84
|
isConfigured: () => configured,
|
|
@@ -175,6 +184,29 @@ test('a landed branch → shipTask + broadcast fire, reconciled=1, attributed to
|
|
|
175
184
|
assert.deepEqual(deps._calls.broadcast, [1096]);
|
|
176
185
|
});
|
|
177
186
|
|
|
187
|
+
test('ADR 0120: an aged-out unprovable strand → flagged needs_rebase for its builder, NOT shipped', async () => {
|
|
188
|
+
// No branch, no published tip, confirmed long ago → unprovable + stale.
|
|
189
|
+
const deps = mkDeps({
|
|
190
|
+
rows: [{ task_id: 700, builder_id: 25, worktree_name: null, published_head_sha: null,
|
|
191
|
+
confirmed_at: '2020-01-01T00:00:00Z' }],
|
|
192
|
+
});
|
|
193
|
+
const r = await reconciler.runSweep(deps);
|
|
194
|
+
assert.equal(r.reconciled, 0, 'a strand does not ship');
|
|
195
|
+
assert.equal(deps._calls.ship.length, 0, 'shipTask must not run for an unlandable strand');
|
|
196
|
+
assert.equal(deps._calls.flag.length, 1, 'the strand is flagged needs_rebase exactly once');
|
|
197
|
+
assert.equal(deps._calls.flag[0].taskId, 700, 'flagged the right task');
|
|
198
|
+
assert.match(deps._calls.flag[0].reason, /^strand:/, "reason records it as a strand (attribution is shipped_by = claim holder)");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('ADR 0120: a recent unprovable confirmed task is NOT yet flagged (still within the land grace)', async () => {
|
|
202
|
+
const deps = mkDeps({
|
|
203
|
+
rows: [{ task_id: 701, builder_id: 25, worktree_name: null, published_head_sha: null,
|
|
204
|
+
confirmed_at: new Date().toISOString() }],
|
|
205
|
+
});
|
|
206
|
+
const r = await reconciler.runSweep(deps);
|
|
207
|
+
assert.equal(deps._calls.flag.length, 0, 'a fresh confirmed task is given its grace before needs_rebase');
|
|
208
|
+
});
|
|
209
|
+
|
|
178
210
|
test('a not-yet-landed branch (open PR / pending deploy) → no ship', async () => {
|
|
179
211
|
const deps = mkDeps({
|
|
180
212
|
rows: [{ task_id: 5, builder_id: 1, worktree_name: 'wt-open' }],
|
|
@@ -195,6 +227,86 @@ test('merged but deploy NOT yet success → no ship (landConfirmed requires both
|
|
|
195
227
|
assert.equal(r.reconciled, 0);
|
|
196
228
|
});
|
|
197
229
|
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// task 1002472 — the sweep's give-up path must never be silent again.
|
|
232
|
+
//
|
|
233
|
+
// The `continue` that fired when a PR existed but had not fully landed sat BEFORE
|
|
234
|
+
// the unprovable counter, the STALE WARN, and flagTaskNeedsRebase — so a merged-PR
|
|
235
|
+
// task strand produced no log line, no counter, and no flag. That is why tasks
|
|
236
|
+
// 1002427/1002428 sat 50h at 'confirmed' reporting needs_rebase: false.
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
test('1002472 an un-landed PR is COUNTED in unprovable, not silently skipped', async () => {
|
|
239
|
+
const deps = mkDeps({
|
|
240
|
+
rows: [{ task_id: 60, builder_id: 1, worktree_name: 'wt-openpr',
|
|
241
|
+
confirmed_at: new Date().toISOString() }],
|
|
242
|
+
statusByBranch: { 'claude/wt-openpr': PENDING },
|
|
243
|
+
});
|
|
244
|
+
const r = await reconciler.runSweep(deps);
|
|
245
|
+
assert.equal(r.reconciled, 0, 'still must not ship an open PR');
|
|
246
|
+
assert.equal(deps._calls.ship.length, 0);
|
|
247
|
+
assert.equal(r.unprovable, 1, 'the miss is counted — this was the silent continue');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('1002472 an AGED un-landed PR gets the STALE WARN + needs_rebase flag', async () => {
|
|
251
|
+
const old = new Date(Date.now() - 50 * 3_600_000).toISOString(); // 50h, like 1002427/1002428
|
|
252
|
+
const deps = mkDeps({
|
|
253
|
+
rows: [{ task_id: 61, builder_id: 7, worktree_name: 'wt-stale-pr', confirmed_at: old }],
|
|
254
|
+
statusByBranch: { 'claude/wt-stale-pr': { ...LANDED, deploy_status: 'pending' } },
|
|
255
|
+
});
|
|
256
|
+
const r = await reconciler.runSweep(deps);
|
|
257
|
+
assert.equal(r.reconciled, 0, 'a pending deploy still never ships');
|
|
258
|
+
assert.equal(r.staleStrands, 1, 'an aged merged-but-unlanded task is a strand');
|
|
259
|
+
assert.deepEqual(deps._calls.flag, [{ taskId: 61, reason: 'strand:pr_not_landed' }]);
|
|
260
|
+
assert.ok(
|
|
261
|
+
deps._calls.errors.some((e) => /STRAND task 61/.test(e)),
|
|
262
|
+
'the strand must be WARN\'d, not swallowed'
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('1002472 an un-landed PR does NOT ship even when the PR body references the task', async () => {
|
|
267
|
+
// The guard that matters: falling through to the observability block must not let
|
|
268
|
+
// landProvenForTask's no-published-tip fallback ship a PR that is still OPEN.
|
|
269
|
+
const deps = mkDeps({
|
|
270
|
+
rows: [{ task_id: 62, builder_id: 1, worktree_name: 'wt-open-selfref',
|
|
271
|
+
published_head_sha: null, confirmed_at: new Date().toISOString() }],
|
|
272
|
+
statusByBranch: {
|
|
273
|
+
'claude/wt-open-selfref': { ...PENDING, pr_number: 9, pr_title: 'task 62: x', pr_body: 'for task 62' },
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const r = await reconciler.runSweep(deps);
|
|
277
|
+
assert.equal(deps._calls.ship.length, 0, 'an OPEN PR referencing this task is still NOT a land');
|
|
278
|
+
assert.equal(r.unprovable, 1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// task 1002472 — an instance with no deploy-prod.yml (deploy_status 'unconfigured')
|
|
283
|
+
// lands on the merge alone, but STILL only with per-task proof (task 1547).
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
const LANDED_NO_WORKFLOW = { ...LANDED, deploy_status: 'unconfigured' };
|
|
286
|
+
|
|
287
|
+
test('1002472 merged + deploy unconfigured + published tip in main → ships', async () => {
|
|
288
|
+
const deps = mkDeps({
|
|
289
|
+
rows: [{ task_id: 63, builder_id: 4, worktree_name: 'wt-nowf', published_head_sha: ON_MAIN }],
|
|
290
|
+
statusByBranch: { 'claude/wt-nowf': LANDED_NO_WORKFLOW },
|
|
291
|
+
ancestryBySha: { [ON_MAIN]: true },
|
|
292
|
+
});
|
|
293
|
+
const r = await reconciler.runSweep(deps);
|
|
294
|
+
assert.equal(r.reconciled, 1, 'no deploy workflow must not pin a real land at confirmed forever');
|
|
295
|
+
assert.deepEqual(deps._calls.ship, [{ taskId: 63, builderId: 4 }]);
|
|
296
|
+
assert.deepEqual(deps._calls.broadcast, [63]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('1002472 merged + deploy unconfigured but tip NOT in main → NO ship (1547 proof intact)', async () => {
|
|
300
|
+
const deps = mkDeps({
|
|
301
|
+
rows: [{ task_id: 64, builder_id: 4, worktree_name: 'wt-nowf-reuse', published_head_sha: NOT_ON_MAIN }],
|
|
302
|
+
statusByBranch: { 'claude/wt-nowf-reuse': LANDED_NO_WORKFLOW },
|
|
303
|
+
ancestryBySha: { [NOT_ON_MAIN]: false },
|
|
304
|
+
});
|
|
305
|
+
const r = await reconciler.runSweep(deps);
|
|
306
|
+
assert.equal(r.reconciled, 0, 'relaxing the deploy gate must NOT relax the branch-reuse proof');
|
|
307
|
+
assert.equal(deps._calls.ship.length, 0);
|
|
308
|
+
});
|
|
309
|
+
|
|
198
310
|
test('getPublishStatus throws (transient) → swallowed, sweep continues to the next task', async () => {
|
|
199
311
|
const deps = mkDeps({
|
|
200
312
|
rows: [
|
|
@@ -721,6 +833,79 @@ test('1915 the heal path never runs when the module lacks refreshStaleGenerated
|
|
|
721
833
|
assert.equal(r.refreshedStale, 0, 'a missing heal fn is a clean no-op, not a crash');
|
|
722
834
|
});
|
|
723
835
|
|
|
836
|
+
// ---------------------------------------------------------------------------
|
|
837
|
+
// task 1002461 — the silent-poller cure.
|
|
838
|
+
//
|
|
839
|
+
// runSweep's no-credential path returned {skipped:'unconfigured'} with NO log,
|
|
840
|
+
// while start() unconditionally logged "started — sweeping confirmed→shipped
|
|
841
|
+
// every Ns". The journal therefore showed a healthy poller that could never do
|
|
842
|
+
// anything, and the stranding went unnoticed for 6+ days. Two fixes: say so
|
|
843
|
+
// (once — per-tick would be spam), and name the RESOLVED owner/repo so a WRONG
|
|
844
|
+
// repo bind is visible without owner access.
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
const REPO_OK = { owner: 'lars589', repo: 'cloud-bongos', branch: 'main' };
|
|
847
|
+
const REPO_ERR = { error: 'no_repo_info', detail: 'git remote origin not parseable' };
|
|
848
|
+
|
|
849
|
+
test('1002461 describeTarget names the resolved owner/repo and the credential state', () => {
|
|
850
|
+
const s = reconciler.describeTarget({ configured: true, repo: REPO_OK });
|
|
851
|
+
assert.match(s, /lars589\/cloud-bongos/, 'the resolved repo must be visible');
|
|
852
|
+
assert.match(s, /main/);
|
|
853
|
+
assert.match(s, /credential present/i);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test('1002461 describeTarget shouts when there is no credential', () => {
|
|
857
|
+
const s = reconciler.describeTarget({ configured: false, repo: REPO_OK });
|
|
858
|
+
assert.match(s, /NO PUSH CREDENTIAL/, 'the missing credential must be unmissable');
|
|
859
|
+
assert.match(s, /lars589\/cloud-bongos/, 'still names the repo — a wrong bind and a missing credential are separate faults');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test('1002461 describeTarget reports an unresolved repo instead of pretending', () => {
|
|
863
|
+
const s = reconciler.describeTarget({ configured: true, repo: REPO_ERR });
|
|
864
|
+
assert.match(s, /UNRESOLVED/);
|
|
865
|
+
assert.match(s, /git remote origin not parseable/, 'the detail is what makes it diagnosable');
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// warnState is injected per test for the same reason healAttempts is: these tests
|
|
869
|
+
// run concurrently, so a shared module-level latch would leak across them.
|
|
870
|
+
test('1002461 an unconfigured sweep WARNs — the skip is no longer silent', async () => {
|
|
871
|
+
const deps = mkDeps({ configured: false, rows: [{ task_id: 1, builder_id: 9, worktree_name: 'wt' }] });
|
|
872
|
+
Object.assign(deps, { repoInfo: REPO_OK, warnState: { unconfigured: false } });
|
|
873
|
+
const r = await reconciler.runSweep(deps);
|
|
874
|
+
assert.equal(r.skipped, 'unconfigured', 'still a no-op — this task only adds the log');
|
|
875
|
+
assert.equal(deps._calls.errors.length, 1, 'the skip must say so');
|
|
876
|
+
assert.match(deps._calls.errors[0], /NO PUSH CREDENTIAL/);
|
|
877
|
+
assert.match(deps._calls.errors[0], /lars589\/cloud-bongos/, 'names the repo it would have used');
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test('1002461 the unconfigured WARN fires ONCE, not once per tick (no log spam)', async () => {
|
|
881
|
+
const deps = mkDeps({ configured: false, rows: [] });
|
|
882
|
+
Object.assign(deps, { repoInfo: REPO_OK, warnState: { unconfigured: false } });
|
|
883
|
+
await reconciler.runSweep(deps);
|
|
884
|
+
await reconciler.runSweep(deps);
|
|
885
|
+
await reconciler.runSweep(deps);
|
|
886
|
+
assert.equal(deps._calls.errors.length, 1, '3 sweeps → 1 WARN; a 5-min poller would otherwise spam the journal');
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('1002461 the WARN re-arms once a credential appears and then disappears again', async () => {
|
|
890
|
+
const shared = { unconfigured: false }; // one latch across the three sweeps, as in production
|
|
891
|
+
const bad = mkDeps({ configured: false, rows: [] });
|
|
892
|
+
Object.assign(bad, { repoInfo: REPO_OK, warnState: shared });
|
|
893
|
+
await reconciler.runSweep(bad);
|
|
894
|
+
assert.equal(bad._calls.errors.length, 1);
|
|
895
|
+
|
|
896
|
+
// credential arrives → a sweep proceeds; the warn must re-arm so a LATER loss
|
|
897
|
+
// is reported instead of being swallowed by the first one.
|
|
898
|
+
const good = mkDeps({ configured: true, rows: [] });
|
|
899
|
+
Object.assign(good, { repoInfo: REPO_OK, warnState: shared });
|
|
900
|
+
await reconciler.runSweep(good);
|
|
901
|
+
assert.equal(shared.unconfigured, false, 'a healthy sweep re-arms the latch');
|
|
902
|
+
|
|
903
|
+
const badAgain = mkDeps({ configured: false, rows: [] });
|
|
904
|
+
Object.assign(badAgain, { repoInfo: REPO_OK, warnState: shared });
|
|
905
|
+
await reconciler.runSweep(badAgain);
|
|
906
|
+
assert.equal(badAgain._calls.errors.length, 1, 'a credential LOSS after a healthy period must warn again');
|
|
907
|
+
});
|
|
908
|
+
|
|
724
909
|
// Drain the microtask queue, then report + exit with the right code.
|
|
725
910
|
setTimeout(() => {
|
|
726
911
|
console.log(`\npublish reconciler (task 1134 + 1915) tests: ${passed} passed, ${failed} failed`);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// tests/repo_info_binding.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for src/bongos/repo-info.js — which GitHub repo this instance binds.
|
|
4
|
+
//
|
|
5
|
+
// loadFromGit ran `git remote get-url origin` with cwd = path.resolve(__dirname,'..','..').
|
|
6
|
+
// In the core dev checkout that is the repo root and the answer is right. On a live
|
|
7
|
+
// instance under ADR 0140 the hub runs from node_modules/@bongos/core, which is NOT a git
|
|
8
|
+
// repo, so git discovery walked UP the filesystem, found the INSTANCE repo, and returned
|
|
9
|
+
// its origin — silently binding a repo whose every API call 404s.
|
|
10
|
+
//
|
|
11
|
+
// This file had NO predecessor — repo-info.js shipped with zero test coverage, which is
|
|
12
|
+
// how the misbind survived. Every case injects the exec seam, so they run with no git and
|
|
13
|
+
// no network; the one end-to-end canary at the bottom is explicitly guarded and skips
|
|
14
|
+
// when git is unavailable (a container / git-less CI runner).
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/repo_info_binding.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ri = require('../src/bongos/repo-info.js');
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
function test(name, fn) {
|
|
30
|
+
try {
|
|
31
|
+
fn();
|
|
32
|
+
passed++;
|
|
33
|
+
console.log(` ok ${name}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
failed++;
|
|
36
|
+
console.log(` FAIL ${name}`);
|
|
37
|
+
console.log(` ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Neutral fixture identity. Implementation code under tests/ must not hardcode any real
|
|
42
|
+
// owner's identity (V3.R12, enforced by scripts/gds/audit-authorship.sh) — and the
|
|
43
|
+
// assertions care only that the parsed pair round-trips, so a synthetic pair is strictly
|
|
44
|
+
// better.
|
|
45
|
+
const FIXTURE_OWNER = 'testowner';
|
|
46
|
+
const FIXTURE_REPO = 'test-repo';
|
|
47
|
+
const REMOTE = `https://github.com/${FIXTURE_OWNER}/${FIXTURE_REPO}.git\n`;
|
|
48
|
+
|
|
49
|
+
// A scripted `git` — returns whatever the scenario says for each subcommand, and
|
|
50
|
+
// records the cwd every call was made with so we can prove the probe is anchored.
|
|
51
|
+
function mkExec({ toplevel, remote = REMOTE, throwOn = null }) {
|
|
52
|
+
const calls = [];
|
|
53
|
+
const exec = (cmd, opts) => {
|
|
54
|
+
calls.push({ cmd, cwd: opts && opts.cwd });
|
|
55
|
+
if (throwOn && cmd.includes(throwOn)) throw new Error('git not available');
|
|
56
|
+
if (cmd.includes('rev-parse')) {
|
|
57
|
+
if (toplevel == null) throw new Error('not a git repository');
|
|
58
|
+
return `${toplevel}\n`;
|
|
59
|
+
}
|
|
60
|
+
if (cmd.includes('remote get-url')) return remote;
|
|
61
|
+
throw new Error(`unexpected git call: ${cmd}`);
|
|
62
|
+
};
|
|
63
|
+
exec.calls = calls;
|
|
64
|
+
return exec;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- the case that WORKS today and must not regress ------------------------
|
|
68
|
+
|
|
69
|
+
test('package root IS the repo root → binds that remote (the core dev checkout)', () => {
|
|
70
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
71
|
+
const execSync = mkExec({ toplevel: root });
|
|
72
|
+
assert.deepEqual(ri.loadFromGit({ execSync, packageRoot: root }), {
|
|
73
|
+
owner: FIXTURE_OWNER,
|
|
74
|
+
repo: FIXTURE_REPO,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('a git worktree of the core checkout still binds (toplevel == the probed root)', () => {
|
|
79
|
+
const root = path.resolve('/srv/cloud-bongos/.claude/worktrees/task-1');
|
|
80
|
+
const execSync = mkExec({ toplevel: root });
|
|
81
|
+
assert.ok(ri.loadFromGit({ execSync, packageRoot: root }), 'a worktree must keep working');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---- the bug ---------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
test('B2: toplevel is an ANCESTOR of the probed root → REFUSES to bind', () => {
|
|
87
|
+
// The live shape: probing node_modules/@bongos/core, git walks up to the instance repo.
|
|
88
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
89
|
+
const execSync = mkExec({ toplevel: path.resolve('/srv/instance') });
|
|
90
|
+
assert.equal(
|
|
91
|
+
ri.loadFromGit({ execSync, packageRoot: root }),
|
|
92
|
+
null,
|
|
93
|
+
'a repo found by walking OUT of our own package is not our repo'
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
!execSync.calls.some((c) => c.cmd.includes('remote get-url')),
|
|
97
|
+
'must not even ask for the remote once the anchor check fails'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('B2: the probe is anchored at the package root, not process.cwd()', () => {
|
|
102
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
103
|
+
const execSync = mkExec({ toplevel: root });
|
|
104
|
+
ri.loadFromGit({ execSync, packageRoot: root });
|
|
105
|
+
assert.ok(execSync.calls.length > 0);
|
|
106
|
+
for (const c of execSync.calls) {
|
|
107
|
+
assert.equal(c.cwd, root, 'every git call must be pinned to the probed root');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---- path normalization ----------------------------------------------------
|
|
112
|
+
|
|
113
|
+
test('git\'s forward slashes match a Windows backslash root (same directory)', () => {
|
|
114
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
115
|
+
// git prints POSIX separators even on Windows.
|
|
116
|
+
const execSync = mkExec({ toplevel: root.split(path.sep).join('/') });
|
|
117
|
+
assert.ok(
|
|
118
|
+
ri.loadFromGit({ execSync, packageRoot: root }),
|
|
119
|
+
'separator style must not be read as a different directory'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a trailing newline/whitespace from git does not break the comparison', () => {
|
|
124
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
125
|
+
const execSync = (cmd) => {
|
|
126
|
+
if (cmd.includes('rev-parse')) return ` ${root} \n\n`;
|
|
127
|
+
return REMOTE;
|
|
128
|
+
};
|
|
129
|
+
assert.ok(ri.loadFromGit({ execSync, packageRoot: root }));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (process.platform === 'win32') {
|
|
133
|
+
test('win32: drive-letter/case differences still match (case-insensitive FS)', () => {
|
|
134
|
+
const root = path.resolve('C:/Srv/Cloud-Bongos');
|
|
135
|
+
const execSync = mkExec({ toplevel: 'c:/srv/cloud-bongos' });
|
|
136
|
+
assert.ok(ri.loadFromGit({ execSync, packageRoot: root }), 'win32 paths are case-insensitive');
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
test('posix: case differences are DIFFERENT directories (case-sensitive FS)', () => {
|
|
140
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
141
|
+
const execSync = mkExec({ toplevel: '/srv/Cloud-Bongos' });
|
|
142
|
+
assert.equal(ri.loadFromGit({ execSync, packageRoot: root }), null);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- failure modes that must stay unchanged --------------------------------
|
|
147
|
+
|
|
148
|
+
test('git absent / rev-parse throws → null (unchanged, caught)', () => {
|
|
149
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
150
|
+
assert.equal(ri.loadFromGit({ execSync: mkExec({ toplevel: null }), packageRoot: root }), null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('remote get-url throws after a good anchor → null (unchanged, caught)', () => {
|
|
154
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
155
|
+
const execSync = mkExec({ toplevel: root, throwOn: 'remote get-url' });
|
|
156
|
+
assert.equal(ri.loadFromGit({ execSync, packageRoot: root }), null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('an unparseable remote → null', () => {
|
|
160
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
161
|
+
const execSync = mkExec({ toplevel: root, remote: 'not-a-remote\n' });
|
|
162
|
+
assert.equal(ri.loadFromGit({ execSync, packageRoot: root }), null);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---- loadRepoInfo: precedence + the diagnosable failure --------------------
|
|
166
|
+
|
|
167
|
+
test('the env pin still WINS over git (unchanged precedence)', () => {
|
|
168
|
+
ri._resetCacheForTests();
|
|
169
|
+
process.env.OTB_GITHUB_OWNER = 'envowner';
|
|
170
|
+
process.env.OTB_GITHUB_REPO = 'envrepo';
|
|
171
|
+
try {
|
|
172
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
173
|
+
const info = ri.loadRepoInfo({ execSync: mkExec({ toplevel: root }), packageRoot: root });
|
|
174
|
+
assert.equal(info.owner, 'envowner');
|
|
175
|
+
assert.equal(info.repo, 'envrepo');
|
|
176
|
+
} finally {
|
|
177
|
+
delete process.env.OTB_GITHUB_OWNER;
|
|
178
|
+
delete process.env.OTB_GITHUB_REPO;
|
|
179
|
+
ri._resetCacheForTests();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('B2: a packaged instance with no env pin reports an ERROR, not a wrong repo', () => {
|
|
184
|
+
ri._resetCacheForTests();
|
|
185
|
+
try {
|
|
186
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
187
|
+
const info = ri.loadRepoInfo({
|
|
188
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }),
|
|
189
|
+
packageRoot: root,
|
|
190
|
+
});
|
|
191
|
+
assert.equal(info.error, 'no_repo_info', 'an explicit error is diagnosable; a wrong repo 404s silently');
|
|
192
|
+
assert.equal(info.owner, undefined, 'must not hand back the instance repo it walked up into');
|
|
193
|
+
} finally {
|
|
194
|
+
ri._resetCacheForTests();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('a good bind still produces the blob/tree bases callers rely on', () => {
|
|
199
|
+
ri._resetCacheForTests();
|
|
200
|
+
try {
|
|
201
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
202
|
+
const info = ri.loadRepoInfo({ execSync: mkExec({ toplevel: root }), packageRoot: root });
|
|
203
|
+
const base = `https://github.com/${FIXTURE_OWNER}/${FIXTURE_REPO}`;
|
|
204
|
+
assert.equal(info.blob_base, `${base}/blob/main`);
|
|
205
|
+
assert.equal(info.tree_base, `${base}/tree/main`);
|
|
206
|
+
} finally {
|
|
207
|
+
ri._resetCacheForTests();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---- end-to-end canary: the regression this must never cause ---------------
|
|
212
|
+
//
|
|
213
|
+
// The only case that touches real git. It earns its place — an anchor rule that is too
|
|
214
|
+
// TIGHT would bind nothing at all, and every hermetic case above would still pass. But it
|
|
215
|
+
// cannot be unconditional: a git-less CI runner or container has nothing to check, so it
|
|
216
|
+
// is guarded and skips rather than failing for an unrelated reason.
|
|
217
|
+
|
|
218
|
+
function gitAvailableHere() {
|
|
219
|
+
try {
|
|
220
|
+
execSync('git rev-parse --show-toplevel', {
|
|
221
|
+
cwd: path.resolve(fileURLToPath(import.meta.url), '..', '..'),
|
|
222
|
+
encoding: 'utf8',
|
|
223
|
+
timeout: 2000,
|
|
224
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
225
|
+
});
|
|
226
|
+
return true;
|
|
227
|
+
} catch (_) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (gitAvailableHere()) {
|
|
233
|
+
test('CANARY (real git): this checkout still binds itself — the rule is not over-tight', () => {
|
|
234
|
+
ri._resetCacheForTests();
|
|
235
|
+
try {
|
|
236
|
+
const info = ri.loadRepoInfo();
|
|
237
|
+
assert.ok(!info.error, `the core checkout must still resolve, got ${JSON.stringify(info)}`);
|
|
238
|
+
assert.ok(info.owner && info.repo, 'owner/repo must be present in a normal dev checkout');
|
|
239
|
+
} finally {
|
|
240
|
+
ri._resetCacheForTests();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
console.log(' skip CANARY (real git): no git available here — hermetic cases above still cover the rule');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(`\nrepo-info binding tests: ${passed} passed, ${failed} failed`);
|
|
248
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/tests/reward_gate.mjs
CHANGED
|
@@ -138,7 +138,12 @@ const migration178 = readFileSync(join(root, 'migrations/178_reward_gate_auto_as
|
|
|
138
138
|
t('promote route AUTO-ASSIGNS then proceeds (no 409 refusal for missing reward)', () => {
|
|
139
139
|
const start = tasksRouteSrc.indexOf("router.post('/tasks/:id/promote'");
|
|
140
140
|
assert.ok(start >= 0, 'promote route not found');
|
|
141
|
-
|
|
141
|
+
// Slice to the START OF THE NEXT ROUTE, not a fixed byte window: /promote grew a
|
|
142
|
+
// second source state (abandoned → backlog, ADR 0152) and a fixed 1600-char slice
|
|
143
|
+
// silently cut the ready flip out of view, failing the ordering assert below for a
|
|
144
|
+
// reason that had nothing to do with the reward gate.
|
|
145
|
+
const nextRoute = tasksRouteSrc.indexOf('router.post(', start + 1);
|
|
146
|
+
const body = tasksRouteSrc.slice(start, nextRoute > start ? nextRoute : undefined);
|
|
142
147
|
assert.match(body, /rewardGateAssignment/, 'promote route must consult the auto-assign decision');
|
|
143
148
|
assert.match(body, /setCreditsReward/, 'promote route must persist the assigned reward');
|
|
144
149
|
// it must assign BEFORE flipping to ready, and still flip to ready.
|
|
@@ -154,7 +159,11 @@ t('promote route AUTO-ASSIGNS then proceeds (no 409 refusal for missing reward)'
|
|
|
154
159
|
t('claim path AUTO-ASSIGNS inside the FOR UPDATE transaction (never refuses)', () => {
|
|
155
160
|
const start = dbSrc.indexOf('async function claimTask');
|
|
156
161
|
assert.ok(start >= 0, 'claimTask not found');
|
|
157
|
-
|
|
162
|
+
// The window is a slice heuristic, not a contract: it only has to cover the
|
|
163
|
+
// pre-flight half of claimTask (which spans ~13k chars). Widened 5000 -> 6000 in
|
|
164
|
+
// BV1.R106, when the permission-expressed claim gate pushed the reward-gate block
|
|
165
|
+
// past the old edge. Keep it comfortably below the claimTasksBatch boundary.
|
|
166
|
+
const body = dbSrc.slice(start, start + 6000);
|
|
158
167
|
assert.match(body, /rewardGateAssignment\(task\)/, 'claimTask must consult the auto-assign decision');
|
|
159
168
|
assert.match(body, /UPDATE tasks SET credits_reward/, 'claimTask must persist the assigned reward in-tx');
|
|
160
169
|
// it must SELECT the columns the gate needs.
|