@bongos/core 1.18.3 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1575 -285
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -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.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// tests/reward_mode_cost_plus_only.mjs — task 1002270 / ADR 0146 (real DB).
|
|
2
|
+
//
|
|
3
|
+
// End-to-end proof that the per-instance reward MODE actually flips the LIVE
|
|
4
|
+
// ship path — not just the pure helper. With branding config
|
|
5
|
+
// reward.mode='cost-plus-only', db.shipTask awards NO per-task estimate credit
|
|
6
|
+
// (no 'task.shipped' credit_log row) and no idea-promotion slice, while the
|
|
7
|
+
// task's credits_reward stays on the row (shown at claim time + the no-zero
|
|
8
|
+
// claim gate). The cost-plus session reward is a separate path (the sessions
|
|
9
|
+
// upload) and is unaffected here.
|
|
10
|
+
//
|
|
11
|
+
// This is the integration seam the DB-free credit_grant.mjs cases can't reach:
|
|
12
|
+
// the REAL awardLandingCredit → reward port → rewardConfig() → branding() wire.
|
|
13
|
+
//
|
|
14
|
+
// Real-DB (self-skips without Postgres; in the INTEGRATION set so the DB-free
|
|
15
|
+
// lane never runs it).
|
|
16
|
+
//
|
|
17
|
+
// Run: DATABASE_URL=... node tests/reward_mode_cost_plus_only.mjs
|
|
18
|
+
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
// The branding override MUST be set before anything calls the memoized
|
|
26
|
+
// branding() — so before the first app require below. A partial instance file
|
|
27
|
+
// deep-merges over neutral, so we override only the reward block. PID-suffixed
|
|
28
|
+
// so a parallel process can't collide (Date.now/random are unavailable here).
|
|
29
|
+
const brandingFile = path.join(os.tmpdir(), `bongos-reward-cpo-${process.pid}.json`);
|
|
30
|
+
fs.writeFileSync(brandingFile, JSON.stringify({ reward: { mode: 'cost-plus-only', marginPct: 20, usdPeg: 1 } }));
|
|
31
|
+
process.env.GDS_BRANDING_FILE = brandingFile;
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const { Pool } = require('pg');
|
|
35
|
+
|
|
36
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
37
|
+
try {
|
|
38
|
+
await pool.query('SELECT 1');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.log(`reward_mode_cost_plus_only.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
41
|
+
fs.rmSync(brandingFile, { force: true });
|
|
42
|
+
await pool.end().catch(() => {});
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const seams = require('../src/module-seams.js');
|
|
47
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
48
|
+
const credits = require('../modules/economy/credits.js');
|
|
49
|
+
const db = require('../modules/lifecycle/db.js');
|
|
50
|
+
|
|
51
|
+
const VER = 'ZZZ-RM', WORKER = 90000301, TASK = 90000310;
|
|
52
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
53
|
+
|
|
54
|
+
async function cleanup() {
|
|
55
|
+
await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
|
|
56
|
+
await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id=$2`, [WORKER, TASK]).catch(() => {});
|
|
57
|
+
await q(`DELETE FROM claims WHERE task_id=$1`, [TASK]).catch(() => {});
|
|
58
|
+
await q(`DELETE FROM tasks WHERE id=$1`, [TASK]).catch(() => {});
|
|
59
|
+
await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
|
|
60
|
+
await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let pass = 0;
|
|
64
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Precondition: the branding override actually resolved to cost-plus-only. If
|
|
68
|
+
// this fails, the env/cache ordering broke and the rest is meaningless — so we
|
|
69
|
+
// assert it up front rather than let a silent default mask a real regression.
|
|
70
|
+
ok(credits.rewardConfig().mode === 'cost-plus-only',
|
|
71
|
+
'branding override resolved (GDS_BRANDING_FILE read before branding() memoized)');
|
|
72
|
+
ok(credits.rewardConfig().perTaskCredit === false,
|
|
73
|
+
'cost-plus-only ⇒ perTaskCredit is off');
|
|
74
|
+
|
|
75
|
+
await cleanup();
|
|
76
|
+
await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'rm','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
|
|
77
|
+
// Archon so no rank/newcomer gate interferes with the ship.
|
|
78
|
+
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'rm-w','rm-worker','RM Worker','archon')`, [WORKER]);
|
|
79
|
+
// A confirmed task owned by WORKER with a real estimate. In DEFAULT mode this
|
|
80
|
+
// would pay 60 × 1.0 (feature) at ship; in cost-plus-only it must pay nothing.
|
|
81
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES ($1,$2,'rm task','confirmed',60,'feature',$3)`, [TASK, VER, WORKER]);
|
|
82
|
+
await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [TASK, WORKER]);
|
|
83
|
+
|
|
84
|
+
// Drive the REAL ship transition (the same call the reconciler/ship.js use).
|
|
85
|
+
await db.shipTask({ taskId: String(TASK), builderId: String(WORKER) });
|
|
86
|
+
|
|
87
|
+
const status = (await q(`SELECT status FROM tasks WHERE id=$1`, [TASK]))[0].status;
|
|
88
|
+
ok(status === 'shipped', `the task shipped (status=${status})`);
|
|
89
|
+
|
|
90
|
+
const shippedCredit = await q(
|
|
91
|
+
`SELECT delta FROM credit_log WHERE task_id=$1 AND reason='task.shipped'`, [TASK]);
|
|
92
|
+
ok(shippedCredit.length === 0,
|
|
93
|
+
'cost-plus-only: NO per-task estimate credit_log row was written at ship');
|
|
94
|
+
|
|
95
|
+
// Belt-and-suspenders: no per-task estimate row under EITHER pay-on-land reason,
|
|
96
|
+
// and no idea-promotion slice (it cascades from the 0 base credit). Achievement
|
|
97
|
+
// rewards are a separate stream and intentionally out of scope (not asserted).
|
|
98
|
+
const anyTaskCredit = await q(
|
|
99
|
+
`SELECT reason FROM credit_log
|
|
100
|
+
WHERE task_id=$1 AND reason IN ('task.shipped','task.confirmed','idea_promotion_bonus')`, [TASK]);
|
|
101
|
+
ok(anyTaskCredit.length === 0,
|
|
102
|
+
'no per-task estimate credit nor idea-promotion slice was paid');
|
|
103
|
+
|
|
104
|
+
const est = (await q(`SELECT credits_reward FROM tasks WHERE id=$1`, [TASK]))[0].credits_reward;
|
|
105
|
+
ok(Number(est) === 60,
|
|
106
|
+
'the estimate (credits_reward=60) stays on the row — shown at claim time, just not paid');
|
|
107
|
+
|
|
108
|
+
console.log(`\nreward_mode_cost_plus_only (task 1002270 / ADR 0146): ${pass} passed, 0 failed`);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('reward_mode_cost_plus_only.mjs FAILED:', e.stack || e.message);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
} finally {
|
|
113
|
+
await cleanup();
|
|
114
|
+
fs.rmSync(brandingFile, { force: true });
|
|
115
|
+
await pool.end();
|
|
116
|
+
}
|