@bongos/core 1.18.0 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
package/src/platform-server.js
CHANGED
|
@@ -29,10 +29,17 @@
|
|
|
29
29
|
// by ABSENCE (the game middleware is simply never mounted), not by a runtime
|
|
30
30
|
// check, exactly as preview-server.js enforces "no internal API" by absence.
|
|
31
31
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
32
|
+
// BACKGROUND SERVICES — started here, at the bottom of this file (task 1002419).
|
|
33
|
+
// R51 deferred "which background services an instance runs" to the feature-module
|
|
34
|
+
// system. That system shipped: a module DECLARES contributes.pollers in its
|
|
35
|
+
// manifest and the instance's enabled-module set decides what actually runs
|
|
36
|
+
// (ADR 0083 / 0093 §1). So the policy question is already answered — the enabled
|
|
37
|
+
// set IS the policy — and this entrypoint just has to honour it, exactly as it
|
|
38
|
+
// already honours contributes.routes (mountModuleRoutes) and contributes.
|
|
39
|
+
// webSurfaces (moduleWebSurfaces). Until this was wired, startModulePollers had
|
|
40
|
+
// ZERO callers post-split — its only caller was ever the retired game entrypoint
|
|
41
|
+
// server.js — so on a standalone instance EVERY module poller was dead: the
|
|
42
|
+
// confirmed→shipped sweep never fired and shipped tasks stranded at 'confirmed'.
|
|
36
43
|
|
|
37
44
|
const http = require('http');
|
|
38
45
|
const fs = require('fs');
|
|
@@ -40,6 +47,8 @@ const path = require('path');
|
|
|
40
47
|
const express = require('express');
|
|
41
48
|
const { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal } = require('./bongos/serve-internal');
|
|
42
49
|
const { isApiShapedPath, apiNotFoundBody } = require('./bongos/api-path-404');
|
|
50
|
+
const moduleLoader = require('./module-loader/loader');
|
|
51
|
+
const { isModuleEnabled } = require('./modules');
|
|
43
52
|
|
|
44
53
|
const PORT = Number(process.env.PORT) || 3000;
|
|
45
54
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
@@ -63,6 +72,19 @@ function landingPage(file) {
|
|
|
63
72
|
return _landingPages.get(file);
|
|
64
73
|
}
|
|
65
74
|
function landingHtml() { return landingPage('index.html'); }
|
|
75
|
+
// The interactive landing gate (public-landing/gate/, ported from amazonprimea
|
|
76
|
+
// in task 1002407). When an instance ships it, it is the apex front door
|
|
77
|
+
// (task 1002408). Unlike the placeholder landing it is a self-contained baked
|
|
78
|
+
// design that reads no window.__BRANDING__, so it is served RAW, not injected.
|
|
79
|
+
// Memoized: absolute path to gate/index.html, or '' when the instance ships no gate.
|
|
80
|
+
let _gateIndexPath; // undefined = unresolved, '' = absent, string = present
|
|
81
|
+
function gateIndexPath() {
|
|
82
|
+
if (_gateIndexPath === undefined) {
|
|
83
|
+
const p = path.join(LANDING_DIR, 'gate', 'index.html');
|
|
84
|
+
_gateIndexPath = fs.existsSync(p) ? p : '';
|
|
85
|
+
}
|
|
86
|
+
return _gateIndexPath;
|
|
87
|
+
}
|
|
66
88
|
function isApexHost(req) {
|
|
67
89
|
return !isStatusHost(req) && !isBuildersHost(req);
|
|
68
90
|
}
|
|
@@ -81,16 +103,38 @@ function buildPlatformApp() {
|
|
|
81
103
|
// The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
|
|
82
104
|
mountInternalSurfaces(app);
|
|
83
105
|
|
|
84
|
-
// Apex landing. On the apex host (not status./builders.)
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
106
|
+
// Apex landing. On the apex host (not status./builders.):
|
|
107
|
+
// * if this instance ships the interactive landing gate (public-landing/gate/),
|
|
108
|
+
// the gate is the front door — GET / 302-redirects to /gate/ (mirrors the
|
|
109
|
+
// amazonprimea Caddy rule this replaces, task 1002408). The redirect (not a
|
|
110
|
+
// direct serve at /) is required so the browser base URL becomes /gate/ and
|
|
111
|
+
// the gate's relative asset paths (sprites/…, ../world, ../assets) resolve.
|
|
112
|
+
// * otherwise GET / serves the branding-injected minimal placeholder.
|
|
113
|
+
// Both fall through to the 404 tail when neither is present, and to the landing
|
|
114
|
+
// asset handler below for non-/ paths (e.g. the mascot image).
|
|
88
115
|
app.get('/', (req, res, next) => {
|
|
89
116
|
if (!isApexHost(req)) return next();
|
|
117
|
+
if (gateIndexPath()) return res.redirect(302, '/gate/');
|
|
90
118
|
const html = landingHtml();
|
|
91
119
|
if (!html) return next();
|
|
92
120
|
res.type('html').send(html);
|
|
93
121
|
});
|
|
122
|
+
// /gate → /gate/ (so the gate's relative asset paths resolve against the dir),
|
|
123
|
+
// and /gate/ serves the gate's own index RAW — a self-contained baked design,
|
|
124
|
+
// so no window.__BRANDING__ injection. The gate's sub-assets (/gate/*, /world/*,
|
|
125
|
+
// /assets/*) are served by the express.static(LANDING_DIR) handler below with no
|
|
126
|
+
// extra rule, which also closes the amazonprimea Caddy gap where /assets/anim/idle/*
|
|
127
|
+
// (the buddha idle frames) fell through to the app instead of the static server.
|
|
128
|
+
// One handler for both forms — Express's non-strict routing matches /gate and
|
|
129
|
+
// /gate/ interchangeably, so branch on the real req.path: /gate 301→ /gate/
|
|
130
|
+
// (so relative asset paths resolve against the dir), /gate/ serves the index.
|
|
131
|
+
app.get(['/gate', '/gate/'], (req, res, next) => {
|
|
132
|
+
if (!isApexHost(req)) return next();
|
|
133
|
+
const p = gateIndexPath();
|
|
134
|
+
if (!p) return next();
|
|
135
|
+
if (!req.path.endsWith('/')) return res.redirect(301, '/gate/');
|
|
136
|
+
return res.type('html').sendFile(p);
|
|
137
|
+
});
|
|
94
138
|
// The branded project-creation experience (goal 35, task 2081) — a sibling
|
|
95
139
|
// landing page at a clean /create URL, branding-injected like the landing.
|
|
96
140
|
app.get('/create', (req, res, next) => {
|
|
@@ -122,6 +166,28 @@ function buildPlatformApp() {
|
|
|
122
166
|
return app;
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
// Start each ENABLED module's background pollers through the loader — module-
|
|
170
|
+
// agnostic: it requires modules/<key>/pollers/<name>.js and fires start(). Same
|
|
171
|
+
// `{ isEnabled: isModuleEnabled }` gate mountModuleRoutes uses, so a module that
|
|
172
|
+
// is off contributes no poller, exactly as it contributes no route. Non-blocking
|
|
173
|
+
// + failure-isolated by the loader, and every poller is independently idempotent,
|
|
174
|
+
// unref'd and self-gating (PUBLISH_RECONCILER_DISABLED, ROUTINE_TIMERS_DISABLED,
|
|
175
|
+
// DISCORD_BOT_ENABLED), so a poller problem can never take the platform down.
|
|
176
|
+
//
|
|
177
|
+
// Safe to start unconditionally — no cross-process lease needed — because this
|
|
178
|
+
// entrypoint runs as a SINGLE process (docker-entrypoint.sh `exec`s it; there is
|
|
179
|
+
// no cluster/replica topology). The one poller with a hard single-runner
|
|
180
|
+
// requirement, the Discord gateway, is double-gated off anyway: its module is
|
|
181
|
+
// default:false AND it needs DISCORD_BOT_ENABLED=true. If an instance ever runs
|
|
182
|
+
// more than one web process, this is the line that needs a lease
|
|
183
|
+
// (modules/lifecycle/merge-lock.js has the pattern).
|
|
184
|
+
//
|
|
185
|
+
// Deliberately OUTSIDE buildPlatformApp(): importing this module (tests) must
|
|
186
|
+
// bind no port and start no timers. Exported so a test can drive it directly.
|
|
187
|
+
function startBackgroundServices() {
|
|
188
|
+
return moduleLoader.startModulePollers({}, { isEnabled: isModuleEnabled });
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
// Start only when run as the entry (`node src/platform-server.js`). Importing the
|
|
126
192
|
// module (the smoke test) must NOT bind a port.
|
|
127
193
|
if (require.main === module) {
|
|
@@ -129,7 +195,18 @@ if (require.main === module) {
|
|
|
129
195
|
const httpServer = http.createServer(app);
|
|
130
196
|
httpServer.listen(PORT, HOST, () => {
|
|
131
197
|
console.log(`Cloud Bongos platform (GDS-only, no game; ADR 0062 R51) listening on ${HOST}:${PORT}`);
|
|
198
|
+
startBackgroundServices();
|
|
132
199
|
});
|
|
200
|
+
|
|
201
|
+
// Graceful shutdown — stop each module poller (e.g. close the Discord gateway
|
|
202
|
+
// socket cleanly on a systemd restart / Ctrl-C so no zombie session is left for
|
|
203
|
+
// Discord to reap). stopModulePollers awaits each stop and isolates errors, so
|
|
204
|
+
// a stuck poller never blocks the exit.
|
|
205
|
+
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
206
|
+
process.on(sig, () => {
|
|
207
|
+
moduleLoader.stopModulePollers().finally(() => process.exit(0));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
133
210
|
}
|
|
134
211
|
|
|
135
|
-
module.exports = { buildPlatformApp };
|
|
212
|
+
module.exports = { buildPlatformApp, startBackgroundServices };
|
package/tests/abandon_task.mjs
CHANGED
|
@@ -30,7 +30,7 @@ process.env.NODE_ENV = 'test';
|
|
|
30
30
|
|
|
31
31
|
const require = createRequire(import.meta.url);
|
|
32
32
|
const db = require('../modules/lifecycle/db.js');
|
|
33
|
-
const { abandonTask } = db;
|
|
33
|
+
const { abandonTask, restoreTask } = db;
|
|
34
34
|
|
|
35
35
|
const { test, summary } = makeRunner();
|
|
36
36
|
|
|
@@ -39,7 +39,8 @@ const { test, summary } = makeRunner();
|
|
|
39
39
|
// notFound → the FOR UPDATE select matches no row
|
|
40
40
|
// status → the row's current status
|
|
41
41
|
// hasActiveClaim → the claims lookup returns an unreleased claim
|
|
42
|
-
|
|
42
|
+
// updatedStatus → what the UPDATE ... RETURNING reports (restore returns 'backlog')
|
|
43
|
+
function fakePool({ status = 'ready', notFound = false, hasActiveClaim = false, updatedStatus = 'abandoned' }) {
|
|
43
44
|
return makeSqlAwareClient((s, params) => {
|
|
44
45
|
if (/^SELECT id, title, status FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
|
|
45
46
|
return { rows: notFound ? [] : [{ id: params[0], title: 'fixture task', status }] };
|
|
@@ -48,7 +49,7 @@ function fakePool({ status = 'ready', notFound = false, hasActiveClaim = false }
|
|
|
48
49
|
return { rows: hasActiveClaim ? [{ id: 999 }] : [] };
|
|
49
50
|
}
|
|
50
51
|
if (/^UPDATE tasks/.test(s)) {
|
|
51
|
-
return { rows: [{ id: params[0], title: 'fixture task', status:
|
|
52
|
+
return { rows: [{ id: params[0], title: 'fixture task', status: updatedStatus }] };
|
|
52
53
|
}
|
|
53
54
|
return { rows: [] }; // BEGIN / COMMIT / ROLLBACK
|
|
54
55
|
});
|
|
@@ -134,5 +135,60 @@ await test('abandon: a missing task throws TASK_NOT_FOUND and rolls back', async
|
|
|
134
135
|
assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
|
|
135
136
|
});
|
|
136
137
|
|
|
138
|
+
// =========================================================================
|
|
139
|
+
// restoreTask (ADR 0152) — the undo that had to exist before abandon could be
|
|
140
|
+
// handed to Metics. Same txn shape as abandonTask, inverted.
|
|
141
|
+
|
|
142
|
+
await test('restore: an abandoned task flips to backlog, COMMITs, reports previousStatus', async () => {
|
|
143
|
+
const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
|
|
144
|
+
const out = await restoreTask({ taskId: TASK }, { pool });
|
|
145
|
+
|
|
146
|
+
assert.equal(out.taskStatus, 'backlog', 'restores to backlog, never straight to ready');
|
|
147
|
+
assert.equal(out.previousStatus, 'abandoned');
|
|
148
|
+
assert.ok(find(pool._client.queries, /^UPDATE tasks/), 'the tasks UPDATE must run');
|
|
149
|
+
assert.ok(find(pool._client.queries, /^COMMIT/), 'must COMMIT');
|
|
150
|
+
assert.ok(!find(pool._client.queries, /^ROLLBACK/), 'no ROLLBACK on the happy path');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await test("restore: clears only an 'Abandoned: …' value_summary, leaving any other note", async () => {
|
|
154
|
+
const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
|
|
155
|
+
await restoreTask({ taskId: TASK }, { pool });
|
|
156
|
+
const upd = find(pool._client.queries, /^UPDATE tasks/);
|
|
157
|
+
const sql = String(upd.sql).replace(/\s+/g, ' ');
|
|
158
|
+
assert.match(sql, /status = 'backlog'/);
|
|
159
|
+
assert.match(sql, /value_summary LIKE 'Abandoned: %' THEN NULL ELSE value_summary END/,
|
|
160
|
+
'the retire stamp is cleared conditionally — an unrelated summary survives');
|
|
161
|
+
assert.equal(upd.params.length, 1, 'only the task id is bound');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await test('restore: a task that is not abandoned is refused and rolls back', async () => {
|
|
165
|
+
for (const status of ['ready', 'backlog', 'shipped']) {
|
|
166
|
+
const pool = fakePool({ status });
|
|
167
|
+
await assert.rejects(
|
|
168
|
+
() => restoreTask({ taskId: TASK }, { pool }),
|
|
169
|
+
(err) => err.code === 'TASK_NOT_ABANDONED',
|
|
170
|
+
`${status} must not be restorable`,
|
|
171
|
+
);
|
|
172
|
+
assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0, `must not UPDATE a ${status} task`);
|
|
173
|
+
assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await test('restore: a missing task throws TASK_NOT_FOUND and rolls back', async () => {
|
|
178
|
+
const pool = fakePool({ notFound: true });
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
() => restoreTask({ taskId: 424242 }, { pool }),
|
|
181
|
+
(err) => err.code === 'TASK_NOT_FOUND',
|
|
182
|
+
);
|
|
183
|
+
assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
|
|
184
|
+
assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await test('restore: never consults claims (an abandoned task has none by construction)', async () => {
|
|
188
|
+
const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
|
|
189
|
+
await restoreTask({ taskId: TASK }, { pool });
|
|
190
|
+
assert.equal(all(pool._client.queries, /SELECT id FROM claims/).length, 0);
|
|
191
|
+
});
|
|
192
|
+
|
|
137
193
|
// -------------------------------------------------------------------------
|
|
138
194
|
summary();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// tests/api_cli_flags.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for two PURE helpers exported from scripts/gds/cli-lib.js in support
|
|
4
|
+
// of scripts/gds/api.js's CLI flags (task 1002475):
|
|
5
|
+
// - stripBom(s) → drop a leading UTF-8 BOM (U+FEFF); pass clean input through
|
|
6
|
+
// - resolveQuiet(args, env) → true when --quiet or GDS_QUIET should suppress
|
|
7
|
+
// api.js's stderr status banner
|
|
8
|
+
// Extracted out of api.js's own top-level IIFE (which has no exports and would
|
|
9
|
+
// run on require()) so this behavior is unit-testable without spawning a
|
|
10
|
+
// process or a network call. Deterministic, no DB/network/filesystem/live
|
|
11
|
+
// server. Run: node tests/api_cli_flags.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const cli = require('../scripts/gds/cli-lib.js');
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
function test(name, fn) {
|
|
22
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
23
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('stripBom: removes a leading U+FEFF', () => {
|
|
27
|
+
assert.equal(cli.stripBom('{"a":1}'), '{"a":1}');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('stripBom: clean input (no BOM) passes through untouched', () => {
|
|
31
|
+
assert.equal(cli.stripBom('{"a":1}'), '{"a":1}');
|
|
32
|
+
assert.equal(cli.stripBom(''), '');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('stripBom: non-string input passes through untouched (edge)', () => {
|
|
36
|
+
assert.equal(cli.stripBom(null), null);
|
|
37
|
+
assert.equal(cli.stripBom(undefined), undefined);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('resolveQuiet: --quiet flag present → true', () => {
|
|
41
|
+
assert.equal(cli.resolveQuiet(['GET', '/api/gds/me', '--quiet'], {}), true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('resolveQuiet: GDS_QUIET env var set → true', () => {
|
|
45
|
+
assert.equal(cli.resolveQuiet(['GET', '/api/gds/me'], { GDS_QUIET: '1' }), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('resolveQuiet: neither flag nor env → false (default off)', () => {
|
|
49
|
+
assert.equal(cli.resolveQuiet(['GET', '/api/gds/me'], {}), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(`\napi.js CLI-flag helpers (task 1002475) tests: ${passed} passed, ${failed} failed`);
|
|
53
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/tests/auth_page_gate.mjs
CHANGED
|
@@ -29,6 +29,17 @@ db.lookupSession = async () => {
|
|
|
29
29
|
};
|
|
30
30
|
const auth = require('../src/bongos/auth.js');
|
|
31
31
|
|
|
32
|
+
// BV1.R107: the page gate now also resolves a permission (page.view.public) through
|
|
33
|
+
// the `governance` kernel port, so an authenticated request needs the port stubbed —
|
|
34
|
+
// exactly as tests/governance_require_permission.mjs stubs it for requirePermission.
|
|
35
|
+
// The audience is unchanged; tests/governance_page_gates.mjs is the parity proof.
|
|
36
|
+
const seams = require('../src/module-seams.js');
|
|
37
|
+
const catalog = require('../modules/governance/catalog.js');
|
|
38
|
+
seams._reset();
|
|
39
|
+
seams.registerProvider('governance', {
|
|
40
|
+
resolveBuilderPermissions: async () => catalog.seedForRank('xenos'),
|
|
41
|
+
});
|
|
42
|
+
|
|
32
43
|
// Minimal res double capturing redirect target + status.
|
|
33
44
|
function fakeRes() {
|
|
34
45
|
return {
|
|
@@ -88,5 +99,7 @@ await t('session lookup error → fails CLOSED (redirect, next() NOT called)', a
|
|
|
88
99
|
'must redirect to sign-in on a lookup error (fail closed)');
|
|
89
100
|
});
|
|
90
101
|
|
|
102
|
+
seams._reset(); // leave the seam registry as we found it (the governance_* suites' pattern)
|
|
103
|
+
|
|
91
104
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
92
105
|
process.exit(failed ? 1 : 0);
|
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
14
17
|
|
|
15
18
|
const require = createRequire(import.meta.url);
|
|
16
19
|
const bf = require('../scripts/gds/backfill-sessions.js');
|
|
20
|
+
const { encodeProjectPath } = require('../scripts/gds/ship.js');
|
|
21
|
+
const { looksLikeGraderTranscript } = require('../scripts/gds/session-cost.js');
|
|
17
22
|
|
|
18
23
|
let passed = 0;
|
|
19
24
|
let failed = 0;
|
|
@@ -58,7 +63,7 @@ test('extracts a release marker', () => {
|
|
|
58
63
|
test('does NOT false-positive on CLAUDE.md markdown #N links', () => {
|
|
59
64
|
// The exact shape that saturates CLAUDE.md / the injected context.
|
|
60
65
|
const claudeMd = asst(
|
|
61
|
-
'See [#435](https://
|
|
66
|
+
'See [#435](https://example.com/builders#/task/435) and ADR 0024 ([#395](x), [#410](y)). ' +
|
|
62
67
|
'Phase 2.2 [#421] shipped. (#607) mirrors to Discord.'
|
|
63
68
|
);
|
|
64
69
|
assert.deepEqual(bf.extractTaskIds([claudeMd]), []);
|
|
@@ -97,5 +102,126 @@ test('inScope matches every historical project-path family', () => {
|
|
|
97
102
|
assert.equal(bf.inScope('-Users-x-Desktop-SomeOtherProject'), false);
|
|
98
103
|
});
|
|
99
104
|
|
|
105
|
+
// --- regressions: the tool was a silent no-op on any non-legacy checkout -----
|
|
106
|
+
|
|
107
|
+
test('inScope matches THIS checkout, not just the four legacy names', () => {
|
|
108
|
+
// The bug: PROJECT_TOKENS was a hardcoded list of the owner's old project
|
|
109
|
+
// names, so every other clone skipped every folder and reported "0 transcripts"
|
|
110
|
+
// while exiting 0. The scope token must be derived from the repo itself.
|
|
111
|
+
const derived = encodeProjectPath(path.resolve('.'));
|
|
112
|
+
assert.ok(bf.inScope(derived), 'the current checkout must be in scope');
|
|
113
|
+
assert.ok(
|
|
114
|
+
bf.inScope(`${derived}--claude-worktrees-task-1002503`),
|
|
115
|
+
'a per-task worktree dir must be in scope too'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a Windows cwd encodes to the dir name Claude Code actually creates', () => {
|
|
120
|
+
// Claude Code replaces every non-alphanumeric char, so a Windows cwd (drive
|
|
121
|
+
// colon, backslashes, spaces) encodes to one long dashed name. This is the
|
|
122
|
+
// shape the old [/.] encoder no-op'd on, which is why no transcript was found.
|
|
123
|
+
assert.equal(
|
|
124
|
+
encodeProjectPath('C:\\Users\\rocke\\OneDrive\\Desktop\\Cloud Bongos\\cloud-bongos'),
|
|
125
|
+
'C--Users-rocke-OneDrive-Desktop-Cloud-Bongos-cloud-bongos'
|
|
126
|
+
);
|
|
127
|
+
// And the matching loop accepts it when it IS the derived token — asserted
|
|
128
|
+
// against inScope itself, machine-independently, via the real first token.
|
|
129
|
+
const derived = bf.PROJECT_TOKENS[0];
|
|
130
|
+
assert.ok(bf.inScope(derived), 'the derived token must match itself through inScope');
|
|
131
|
+
assert.equal(bf.inScope('C--Users-someone-else-Desktop-OtherProject'), false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('buildUploadPayload sends model_usage at the TOP level', () => {
|
|
135
|
+
// The route reads body.model_usage and never looks inside metrics; dropping it
|
|
136
|
+
// silently under-pays via the opus cache_read fallback. Guard the field set.
|
|
137
|
+
const digest = {
|
|
138
|
+
totalTokens: 135734889,
|
|
139
|
+
durationSeconds: 6407,
|
|
140
|
+
startedAt: '2026-07-26T21:18:16.356Z',
|
|
141
|
+
endedAt: '2026-07-26T23:05:03.560Z',
|
|
142
|
+
detail: [{ i: 0, role: 'assistant' }],
|
|
143
|
+
metrics: { assistant_turns: 1, model_usage: { 'claude-opus-5': { input: 1, output: 2 } } },
|
|
144
|
+
};
|
|
145
|
+
const payload = bf.buildUploadPayload({ sessionId: 'abc', taskIds: [1002471], digest });
|
|
146
|
+
assert.deepEqual(payload.model_usage, { 'claude-opus-5': { input: 1, output: 2 } });
|
|
147
|
+
assert.equal(payload.session_id, 'abc');
|
|
148
|
+
assert.deepEqual(payload.task_ids, [1002471]);
|
|
149
|
+
assert.equal(payload.total_tokens, 135734889);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('buildUploadPayload defaults model_usage to {} rather than undefined', () => {
|
|
153
|
+
// An unpriceable digest must still upload (the server prices from total_tokens);
|
|
154
|
+
// an undefined field would fail the route's object validator.
|
|
155
|
+
const payload = bf.buildUploadPayload({ sessionId: 'x', taskIds: [], digest: { metrics: {} } });
|
|
156
|
+
assert.deepEqual(payload.model_usage, {});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('grader detection matches a NEW worker persona, not just the known four', () => {
|
|
160
|
+
// A closed name list would let a future grader worker be uploaded as a builder
|
|
161
|
+
// session and earn credit for spend the ship path never pays.
|
|
162
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backfill-grader2-'));
|
|
163
|
+
const write = (name, text) => {
|
|
164
|
+
const f = path.join(dir, `${name}.jsonl`);
|
|
165
|
+
fs.writeFileSync(f, JSON.stringify({ type: 'user', message: { role: 'user', content: text } }) + '\n');
|
|
166
|
+
return f;
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
for (const [persona, joiner] of [['Narc', '— a specialist'], ['Quality', 'specialist — an'],
|
|
170
|
+
['Efficiency Monger', '— a specialist'], ['Cartographer', '— a specialist']]) {
|
|
171
|
+
const f = write(persona.replace(/\s/g, ''), `You are the **${persona}** ${joiner} adversarial subagent for the project.`);
|
|
172
|
+
assert.ok(looksLikeGraderTranscript(f), `${persona} must be detected as a grader worker`);
|
|
173
|
+
}
|
|
174
|
+
// A real session that merely mentions the panel must NOT be skipped.
|
|
175
|
+
const real = write('real', 'The adversarial subagent panel flagged my diff — help me fix it.');
|
|
176
|
+
assert.equal(looksLikeGraderTranscript(real), false, 'a builder session must not be skipped');
|
|
177
|
+
} finally {
|
|
178
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('extracts a 7-digit task id from a ship.js invocation', () => {
|
|
183
|
+
// {1,4} matched nothing on a live instance, so no modern session could ever pay.
|
|
184
|
+
const ids = bf.extractTaskIds([toolUse('Bash', { command: 'node scripts/gds/ship.js 1002471' })]);
|
|
185
|
+
assert.deepEqual(ids, [1002471]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('extracts 7-digit ids from claim/release/Claimed markers', () => {
|
|
189
|
+
assert.deepEqual(bf.extractTaskIds([asst('Running /builder-claim 1002503 now')]), [1002503]);
|
|
190
|
+
assert.deepEqual(bf.extractTaskIds([asst('builder-release 1002446 — handing it back')]), [1002446]);
|
|
191
|
+
assert.deepEqual(bf.extractTaskIds([asst('Claimed task #1002475.')]), [1002475]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('7-digit ids survive extractTaskIds (the n < 10000 clamp is gone)', () => {
|
|
195
|
+
// Widening TASK_MARKERS alone was not enough: the clamp dropped them again.
|
|
196
|
+
const entries = [
|
|
197
|
+
toolUse('Bash', { command: 'node scripts/gds/ship.js 1002471' }),
|
|
198
|
+
asst('Claimed #284.'), // legacy 3-digit ids must still work
|
|
199
|
+
];
|
|
200
|
+
assert.deepEqual(bf.extractTaskIds(entries), [284, 1002471]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('grader worker transcripts are detectable and would otherwise be paid', () => {
|
|
204
|
+
// The grader panel writes each worker as its OWN transcript in the same project
|
|
205
|
+
// dir. A worker quotes the diff, so it carries real ship markers — uploading one
|
|
206
|
+
// would book a cost-plus reward the live ship path never pays.
|
|
207
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backfill-grader-'));
|
|
208
|
+
const file = path.join(dir, 'aaaaaaaa-0000-0000-0000-000000000000.jsonl');
|
|
209
|
+
const graderPrompt =
|
|
210
|
+
'You are the **Narc** — a specialist adversarial subagent for this project. ' +
|
|
211
|
+
'Review the diff shipped by node scripts/gds/ship.js 1002471 --summary "x".';
|
|
212
|
+
try {
|
|
213
|
+
fs.writeFileSync(file, JSON.stringify({ type: 'user', message: { role: 'user', content: graderPrompt } }) + '\n');
|
|
214
|
+
assert.ok(looksLikeGraderTranscript(file), 'the grader worker prompt must be detected');
|
|
215
|
+
// Load-bearing: without the skip, this transcript yields a real shipped id.
|
|
216
|
+
assert.deepEqual(bf.extractTaskIds([asst(graderPrompt)]), [1002471]);
|
|
217
|
+
|
|
218
|
+
const normal = path.join(dir, 'bbbbbbbb-0000-0000-0000-000000000000.jsonl');
|
|
219
|
+
fs.writeFileSync(normal, JSON.stringify({ type: 'user', message: { role: 'user', content: 'fix the backfill script' } }) + '\n');
|
|
220
|
+
assert.equal(looksLikeGraderTranscript(normal), false, 'a real builder session must NOT be skipped');
|
|
221
|
+
} finally {
|
|
222
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
100
226
|
console.log(`\nbackfill_sessions: ${passed} passed, ${failed} failed`);
|
|
101
227
|
process.exit(failed ? 1 : 0);
|
package/tests/backup_cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Run: node tests/backup_cli.mjs
|
|
11
11
|
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
13
14
|
|
|
14
15
|
let passed = 0;
|
|
15
16
|
let failed = 0;
|
|
@@ -94,6 +95,59 @@ t('trigger response: 409 when already running', () => {
|
|
|
94
95
|
assert.equal(response.error, 'backup_already_running');
|
|
95
96
|
});
|
|
96
97
|
|
|
98
|
+
// ---- staleness detection (task 1002479) -------------------------------------
|
|
99
|
+
// These import the REAL implementations rather than re-implementing them, so a
|
|
100
|
+
// regression in the shipped code fails here instead of passing against a copy.
|
|
101
|
+
const backupRoute = createRequire(import.meta.url)('../src/bongos/routes/backup.js');
|
|
102
|
+
const { parseDumpTimestamp, dumpAgeHours, isStaleDump } = backupRoute;
|
|
103
|
+
|
|
104
|
+
const NOW = Date.parse('2026-07-28T23:00:00Z');
|
|
105
|
+
|
|
106
|
+
t('parseDumpTimestamp: the dump stamp is NOT plain ISO — colons must be reinserted', () => {
|
|
107
|
+
// Regression guard: Date.parse on the raw stamp returns NaN. If someone
|
|
108
|
+
// "simplifies" this to Date.parse(stamp), staleness silently becomes unknown
|
|
109
|
+
// → every dump reads stale (or, with a sloppier fallback, every dump reads fresh).
|
|
110
|
+
assert.ok(Number.isNaN(Date.parse('2026-07-28T230421Z')), 'raw stamp should not parse as ISO');
|
|
111
|
+
assert.equal(parseDumpTimestamp('2026-07-28T230421Z'), Date.parse('2026-07-28T23:04:21Z'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
t('parseDumpTimestamp: rejects garbage rather than guessing', () => {
|
|
115
|
+
for (const bad of ['', null, undefined, 'nope', '2026-07-28', '2026-07-28T2304Z']) {
|
|
116
|
+
assert.equal(parseDumpTimestamp(bad), null, `should reject ${JSON.stringify(bad)}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t('dumpAgeHours: computes hours, floors at 0 for a future stamp', () => {
|
|
121
|
+
assert.equal(dumpAgeHours('2026-07-28T220000Z', NOW), 1);
|
|
122
|
+
assert.equal(dumpAgeHours('2026-07-27T230000Z', NOW), 24);
|
|
123
|
+
assert.equal(dumpAgeHours('2026-07-29T230000Z', NOW), 0, 'future stamp must not go negative');
|
|
124
|
+
assert.equal(dumpAgeHours('garbage', NOW), null);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
t('isStaleDump: fresh dump is not stale', () => {
|
|
128
|
+
assert.equal(isStaleDump('2026-07-28T230421Z', NOW), false);
|
|
129
|
+
assert.equal(isStaleDump('2026-07-27T140000Z', NOW), false, '33h is inside the 36h threshold');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
t('isStaleDump: the real 13-day cloudbongos outage is detected', () => {
|
|
133
|
+
// The actual last good dump before the 203/EXEC failure was 2026-07-15.
|
|
134
|
+
assert.equal(isStaleDump('2026-07-15T040302Z', NOW), true);
|
|
135
|
+
assert.ok(dumpAgeHours('2026-07-15T040302Z', NOW) > 300, 'should be ~13 days of hours');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
t('isStaleDump: an undateable stamp fails LOUD (stale), never silently healthy', () => {
|
|
139
|
+
assert.equal(isStaleDump('garbage', NOW), true);
|
|
140
|
+
assert.equal(isStaleDump('', NOW), true);
|
|
141
|
+
assert.equal(isStaleDump(undefined, NOW), true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
t('isStaleDump: threshold boundary is exclusive', () => {
|
|
145
|
+
const at36h = '2026-07-27T110000Z'; // exactly 36h before NOW
|
|
146
|
+
assert.equal(dumpAgeHours(at36h, NOW), 36);
|
|
147
|
+
assert.equal(isStaleDump(at36h, NOW, 36), false, 'exactly at threshold is not yet stale');
|
|
148
|
+
assert.equal(isStaleDump('2026-07-27T105900Z', NOW, 36), true, 'one minute past is stale');
|
|
149
|
+
});
|
|
150
|
+
|
|
97
151
|
// ---- summary ----------------------------------------------------------------
|
|
98
152
|
console.log('');
|
|
99
153
|
if (failed === 0) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// helpers are exercised. Picked up by scripts/gds/run-unit-tests.js (the `unit` gate).
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
5
|
import { test } from 'node:test';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
6
7
|
import lib from '../scripts/gds/box-connect-lib.js';
|
|
7
8
|
|
|
8
9
|
const HOST = 'box-alice.dev.amazonprimea.com';
|
|
@@ -63,6 +64,62 @@ test('buildKnownHostsUpsert drops the stale line for the host, keeps others, app
|
|
|
63
64
|
assert.ok(next.includes(`${HOST} ssh-rsa AAAARSANEW`), 'appends every reported key');
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// task 1002509 — the upsert used to compare field 1 to the hostname with `!==`, so
|
|
68
|
+
// the two forms OpenSSH actually writes survived the "replace" and stale lines
|
|
69
|
+
// ACCUMULATED: the client kept verifying against the first, now-wrong entry
|
|
70
|
+
// ("Host denied (verification failed)") on a box that had just re-reported keys.
|
|
71
|
+
test('buildKnownHostsUpsert drops a host,ip entry for this box (the accumulation bug)', () => {
|
|
72
|
+
const existing = `${HOST},64.225.49.206 ssh-ed25519 STALE\n`;
|
|
73
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
|
|
74
|
+
assert.ok(!next.includes('STALE'), 'a `host,ip` field-1 names this host and must be replaced');
|
|
75
|
+
assert.ok(next.includes(`${HOST} ssh-ed25519 AAAANEW`));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('buildKnownHostsUpsert drops a HASHED entry for this box, keeps another host, keeps comments', () => {
|
|
79
|
+
const salt = crypto.randomBytes(20);
|
|
80
|
+
const hashFor = (h) => `|1|${salt.toString('base64')}|${crypto.createHmac('sha1', salt).update(h).digest('base64')}`;
|
|
81
|
+
const existing = [
|
|
82
|
+
'# hand-written note',
|
|
83
|
+
`${hashFor(HOST)} ssh-ed25519 STALEHASHED`,
|
|
84
|
+
`${hashFor('other.example.com')} ssh-ed25519 KEEPHASHED`,
|
|
85
|
+
].join('\n') + '\n';
|
|
86
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
|
|
87
|
+
assert.ok(!next.includes('STALEHASHED'), 'HashKnownHosts entries must be matched, not stepped over');
|
|
88
|
+
assert.ok(next.includes('KEEPHASHED'), 'another host hashed with the same salt stays');
|
|
89
|
+
assert.ok(next.includes('# hand-written note'), 'comments survive');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('buildKnownHostsUpsert drops an IP-keyed line when the box IP is supplied', () => {
|
|
93
|
+
const existing = '203.0.113.7 ssh-ed25519 STALEIP\nunrelated.example.com ssh-ed25519 KEEP\n';
|
|
94
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW', '203.0.113.7');
|
|
95
|
+
assert.ok(!next.includes('STALEIP'), 'DNS is repointed on every wake — an old IP line is stale too');
|
|
96
|
+
assert.ok(next.includes('KEEP'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('buildKnownHostsUpsert is idempotent — re-running never grows the file', () => {
|
|
100
|
+
const first = lib.buildKnownHostsUpsert('', HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
|
|
101
|
+
const second = lib.buildKnownHostsUpsert(first, HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
|
|
102
|
+
assert.equal(second, first);
|
|
103
|
+
assert.equal(second.split('\n').filter(Boolean).length, 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// task 1002509 — the old inline logic FAILED OPEN: no fingerprint (ssh-keygen absent
|
|
107
|
+
// from a GUI process's leaner PATH) or an unreadable key list meant "skip", so a
|
|
108
|
+
// builder whose key was never on the server got a bare "Permission denied". A
|
|
109
|
+
// redundant POST is free (the server upserts by fingerprint), uncertainty is not.
|
|
110
|
+
test('decideKeyRegistration registers on uncertainty and skips only on proof', () => {
|
|
111
|
+
const FP = 'SHA256:abc';
|
|
112
|
+
assert.deepEqual(lib.decideKeyRegistration({ created: true }), { register: true, why: 'created' });
|
|
113
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: FP }] }).register, false);
|
|
114
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: 'SHA256:other' }] }).register, true);
|
|
115
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [] }).register, true);
|
|
116
|
+
// the two fail-open holes this closes:
|
|
117
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [] }).register, true);
|
|
118
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [{ fingerprint: FP }] }).register, true);
|
|
119
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listFailed: true }).register, true);
|
|
120
|
+
assert.equal(lib.decideKeyRegistration({}).register, true, 'no information at all → register');
|
|
121
|
+
});
|
|
122
|
+
|
|
66
123
|
test('buildRemoteShellCommand forces claude only on first run and always lands in /workspace', () => {
|
|
67
124
|
const first = lib.buildRemoteShellCommand({ firstRun: true });
|
|
68
125
|
const later = lib.buildRemoteShellCommand({ firstRun: false });
|
|
@@ -74,6 +131,26 @@ test('buildRemoteShellCommand forces claude only on first run and always lands i
|
|
|
74
131
|
assert.match(later, /exec bash -l/);
|
|
75
132
|
});
|
|
76
133
|
|
|
134
|
+
test('buildRemoteShellCommand re-scopes on connect ONLY with sync:true (BV1.R101)', () => {
|
|
135
|
+
const noSync = lib.buildRemoteShellCommand({ firstRun: false, sync: false });
|
|
136
|
+
const withSync = lib.buildRemoteShellCommand({ firstRun: false, sync: true });
|
|
137
|
+
assert.ok(!/box-sync/.test(noSync), 'default (sync:false) never re-scopes');
|
|
138
|
+
assert.match(withSync, /node scripts\/gds\/box-sync\.js \|\| true/);
|
|
139
|
+
// order: cd → re-scope → shell, so the builder lands in the scoped folder.
|
|
140
|
+
assert.ok(withSync.indexOf('cd /workspace') < withSync.indexOf('box-sync'), 'cd before sync');
|
|
141
|
+
assert.ok(withSync.indexOf('box-sync') < withSync.indexOf('exec bash -l'), 'sync before the shell');
|
|
142
|
+
// sync + firstRun: re-scope THEN the one-time claude /login handoff.
|
|
143
|
+
const both = lib.buildRemoteShellCommand({ firstRun: true, sync: true });
|
|
144
|
+
assert.ok(both.indexOf('box-sync') < both.indexOf('claude || true'), 'sync before the first-run login');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('buildRemoteResyncCommand is a cd + re-scope one-shot (bongos code path) — no shell (BV1.R101)', () => {
|
|
148
|
+
const cmd = lib.buildRemoteResyncCommand();
|
|
149
|
+
assert.match(cmd, /cd \/workspace/);
|
|
150
|
+
assert.match(cmd, /node scripts\/gds\/box-sync\.js \|\| true/);
|
|
151
|
+
assert.ok(!/exec bash/.test(cmd), 'one-shot — never spawns a login shell');
|
|
152
|
+
});
|
|
153
|
+
|
|
77
154
|
test('remoteAuthority builds a VS Code Remote-SSH authority', () => {
|
|
78
155
|
assert.equal(lib.remoteAuthority(HOST), `ssh-remote+${HOST}`);
|
|
79
156
|
});
|