@bongos/core 1.18.3 → 1.18.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1576 -286
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/migrate.sh +99 -14
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +124 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrate_root_resolution.mjs +124 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
// devbox-app/test/unit.mjs — hermetic unit tests for the app's pure logic
|
|
2
|
+
// (no Electron, no network, no fs side effects beyond a temp dir).
|
|
3
|
+
//
|
|
4
|
+
// Run: npm test (inside devbox-app/), or node devbox-app/test/unit.mjs.
|
|
5
|
+
// NOTE: lives under devbox-app/ (not the root tests/) on purpose — the root
|
|
6
|
+
// run-unit-tests gate runs from the droplet-bound dependency set; this
|
|
7
|
+
// subproject's tests ride its own CI job (.github/workflows/build-devbox-app.yml).
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { deriveStatus, pollDelayMs, transitionNotification, terminalAuthUrl, TERMINAL_USER, shouldHeartbeat } = require('../src/box-controller.js');
|
|
14
|
+
const { mergeKnownHosts, settingsUpToDate, scanComplete, hostKeyLinesFor, knownHostsHasKey, verifyKnownHosts } = require('../src/ssh-setup.js');
|
|
15
|
+
const { createPairing } = require('../src/pairing.js');
|
|
16
|
+
const { apiBase } = require('../src/config.js');
|
|
17
|
+
const brand = require('../src/brand.js');
|
|
18
|
+
const { compareVersions, isUpdateAvailable, resolveTarget, updateBannerText } = require('../src/updater.js');
|
|
19
|
+
const { createClient: createGdsClient, GdsError } = require('../src/gds-client.js');
|
|
20
|
+
const { shapeClaimableTasks, boxScopeMessage, claimErrorMessage } = require('../src/task-picker.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
function t(name, fn) {
|
|
25
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
26
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
27
|
+
}
|
|
28
|
+
async function ta(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('deriveStatus:');
|
|
34
|
+
|
|
35
|
+
t('no box → none; active box → active', () => {
|
|
36
|
+
assert.equal(deriveStatus(null), 'none');
|
|
37
|
+
assert.equal(deriveStatus({ box: { state: 'none' }, open_intent: null }), 'none');
|
|
38
|
+
assert.equal(deriveStatus({ box: { state: 'active' }, open_intent: null }), 'active');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
t('open intents win over raw state', () => {
|
|
42
|
+
assert.equal(deriveStatus({ box: { state: 'none' }, open_intent: { action: 'provision', state: 'pending' } }), 'provisioning');
|
|
43
|
+
assert.equal(deriveStatus({ box: { state: 'active' }, open_intent: { action: 'deprovision', state: 'pending' } }), 'closing');
|
|
44
|
+
assert.equal(deriveStatus({ box: { state: 'parked' }, open_intent: { action: 'wake', state: 'running' } }), 'provisioning');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t('parked reads as off (the switch wakes it); error surfaces', () => {
|
|
48
|
+
assert.equal(deriveStatus({ box: { state: 'parked' }, open_intent: null }), 'none');
|
|
49
|
+
assert.equal(deriveStatus({ box: { state: 'error' }, open_intent: null }), 'error');
|
|
50
|
+
assert.equal(deriveStatus({ box: { state: 'waking' }, open_intent: null }), 'provisioning');
|
|
51
|
+
assert.equal(deriveStatus({ box: { state: 'parking' }, open_intent: null }), 'closing');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('pollDelayMs:');
|
|
55
|
+
|
|
56
|
+
t('transitions poll fast, settled states slow', () => {
|
|
57
|
+
assert.ok(pollDelayMs('provisioning') <= 5000);
|
|
58
|
+
assert.ok(pollDelayMs('closing') <= 5000);
|
|
59
|
+
assert.ok(pollDelayMs('active') >= 30000);
|
|
60
|
+
assert.ok(pollDelayMs('none') >= 60000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log('shouldHeartbeat (leg 3 — connect-window keep-alive):');
|
|
64
|
+
|
|
65
|
+
t('only an ACTIVE box ever heartbeats', () => {
|
|
66
|
+
const now = 1_000_000;
|
|
67
|
+
for (const status of ['none', 'provisioning', 'closing', 'error', 'signed_out']) {
|
|
68
|
+
assert.equal(shouldHeartbeat({ status, panelVisible: true, lastConnectActionAt: now, now }), false, status);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('active + panel visible → heartbeat (the mid-connect lockout window)', () => {
|
|
73
|
+
const now = 1_000_000;
|
|
74
|
+
assert.equal(shouldHeartbeat({ status: 'active', panelVisible: true, lastConnectActionAt: 0, now }), true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
t('active + panel hidden but within the connect window → heartbeat', () => {
|
|
78
|
+
const now = 1_000_000;
|
|
79
|
+
assert.equal(shouldHeartbeat({ status: 'active', panelVisible: false, lastConnectActionAt: now - 60_000, now }), true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
t('active + panel hidden + connect window expired → NO heartbeat (idle box still reaps)', () => {
|
|
83
|
+
const now = 1_000_000;
|
|
84
|
+
// 11 min > the 10-min window
|
|
85
|
+
assert.equal(shouldHeartbeat({ status: 'active', panelVisible: false, lastConnectActionAt: now - 11 * 60_000, now }), false);
|
|
86
|
+
// never connected, panel hidden → also no beat
|
|
87
|
+
assert.equal(shouldHeartbeat({ status: 'active', panelVisible: false, lastConnectActionAt: 0, now }), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log('terminalAuthUrl:');
|
|
91
|
+
|
|
92
|
+
t('embeds otb:<credential> userinfo in the terminal URL', () => {
|
|
93
|
+
assert.equal(
|
|
94
|
+
terminalAuthUrl({ url: 'https://term-lars589.amazonprimea.com', credential: 'abc123' }),
|
|
95
|
+
`https://${TERMINAL_USER}:abc123@term-lars589.amazonprimea.com/`
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t('no terminal / no url → null; no credential → bare url; bad url → bare url', () => {
|
|
100
|
+
assert.equal(terminalAuthUrl(null), null);
|
|
101
|
+
assert.equal(terminalAuthUrl({ credential: 'x' }), null);
|
|
102
|
+
assert.equal(terminalAuthUrl({ url: 'https://t.example.com' }), 'https://t.example.com');
|
|
103
|
+
assert.equal(terminalAuthUrl({ url: 'not a url', credential: 'x' }), 'not a url');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
t('percent-encodes credentials that need it', () => {
|
|
107
|
+
const out = terminalAuthUrl({ url: 'https://t.example.com', credential: 'p@ss/wor:d' });
|
|
108
|
+
assert.ok(out.startsWith(`https://${TERMINAL_USER}:p%40ss%2Fwor%3Ad@t.example.com`));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log('transitionNotification:');
|
|
112
|
+
|
|
113
|
+
t('ready / idle-close notify; no-change does not', () => {
|
|
114
|
+
assert.equal(transitionNotification('active', 'active'), null);
|
|
115
|
+
assert.ok(transitionNotification('provisioning', 'active', { envName: 'X' }).title.includes('ready'));
|
|
116
|
+
assert.ok(transitionNotification('active', 'none').title.includes('closed'));
|
|
117
|
+
assert.equal(transitionNotification('none', 'provisioning'), null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t('ready notification names the Desktop environment when settings installed', () => {
|
|
121
|
+
const n = transitionNotification('provisioning', 'active', { envName: 'Env Name', settingsInstalled: true });
|
|
122
|
+
assert.ok(n.body.includes('Env Name'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log('mergeKnownHosts:');
|
|
126
|
+
|
|
127
|
+
t('replaces stale lines for the hostname, keeps everything else', () => {
|
|
128
|
+
const existing = [
|
|
129
|
+
'github.com ssh-ed25519 AAAA1',
|
|
130
|
+
'box-lars.dev.example.com ssh-ed25519 OLDKEY',
|
|
131
|
+
'box-lars.dev.example.com,1.2.3.4 ecdsa-sha2 OLDKEY2',
|
|
132
|
+
'# a comment',
|
|
133
|
+
'other.host ssh-rsa BBBB',
|
|
134
|
+
].join('\n');
|
|
135
|
+
const out = mergeKnownHosts(existing, 'box-lars.dev.example.com', [
|
|
136
|
+
'box-lars.dev.example.com ssh-ed25519 NEWKEY',
|
|
137
|
+
]);
|
|
138
|
+
assert.ok(!out.includes('OLDKEY'), 'old keys dropped');
|
|
139
|
+
assert.ok(out.includes('NEWKEY'), 'new key added');
|
|
140
|
+
assert.ok(out.includes('github.com ssh-ed25519 AAAA1'), 'unrelated host kept');
|
|
141
|
+
assert.ok(out.includes('# a comment'), 'comments kept');
|
|
142
|
+
assert.ok(out.endsWith('\n'), 'trailing newline');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
t('works against an empty/missing known_hosts', () => {
|
|
146
|
+
const out = mergeKnownHosts('', 'h.example.com', ['h.example.com ssh-ed25519 K']);
|
|
147
|
+
assert.equal(out, 'h.example.com ssh-ed25519 K\n');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log('scanComplete:');
|
|
151
|
+
|
|
152
|
+
t('a scan with an ed25519 line is complete', () => {
|
|
153
|
+
assert.equal(scanComplete([
|
|
154
|
+
'h.example.com ssh-rsa AAAA',
|
|
155
|
+
'h.example.com ssh-ed25519 BBBB',
|
|
156
|
+
]), true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
t('an rsa-only scan is NOT complete (#925 — Desktop ssh2 negotiates ed25519)', () => {
|
|
160
|
+
assert.equal(scanComplete(['h.example.com ssh-rsa AAAA']), false);
|
|
161
|
+
assert.equal(scanComplete(['h.example.com ecdsa-sha2-nistp256 CCCC']), false);
|
|
162
|
+
assert.equal(scanComplete([]), false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log('hostKeyLinesFor (task 1187 / idea 235 — authoritative host keys):');
|
|
166
|
+
|
|
167
|
+
t('builds <host> <type> <base64> lines from the reported keys, ed25519 present', () => {
|
|
168
|
+
const r = hostKeyLinesFor('box-x.dev.amazonprimea.com', 'ssh-ed25519 AAAA\necdsa-sha2-nistp256 BBBB');
|
|
169
|
+
assert.equal(r.ok, true);
|
|
170
|
+
assert.deepEqual(r.lines, [
|
|
171
|
+
'box-x.dev.amazonprimea.com ssh-ed25519 AAAA',
|
|
172
|
+
'box-x.dev.amazonprimea.com ecdsa-sha2-nistp256 BBBB',
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
t('these lines feed mergeKnownHosts as a clean replace-by-host', () => {
|
|
177
|
+
const r = hostKeyLinesFor('h.example.com', 'ssh-ed25519 NEW');
|
|
178
|
+
const merged = mergeKnownHosts('h.example.com ssh-ed25519 OLD\nother ssh-rsa K', 'h.example.com', r.lines);
|
|
179
|
+
assert.ok(merged.includes('other ssh-rsa K'), 'other host preserved');
|
|
180
|
+
assert.ok(merged.includes('h.example.com ssh-ed25519 NEW'), 'fresh key written');
|
|
181
|
+
assert.ok(!merged.includes('OLD'), 'stale key for our host removed');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
t('an rsa-only report is rejected (no ed25519 — the #925 Desktop failure)', () => {
|
|
185
|
+
const r = hostKeyLinesFor('h', 'ssh-rsa AAAA');
|
|
186
|
+
assert.equal(r.ok, false);
|
|
187
|
+
assert.equal(r.reason, 'no_ed25519');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
t('empty report / missing hostname are rejected', () => {
|
|
191
|
+
assert.equal(hostKeyLinesFor('h', '').ok, false);
|
|
192
|
+
assert.equal(hostKeyLinesFor('h', ' \n ').reason, 'no_reported_keys');
|
|
193
|
+
assert.equal(hostKeyLinesFor('', 'ssh-ed25519 AAAA').reason, 'no_hostname');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log('knownHostsHasKey + verifyKnownHosts (idea 264 — self-verify the on-disk pin):');
|
|
197
|
+
|
|
198
|
+
const HKEY = 'AAAAC3NzaC1lZDI1NTE5AAAAIBh0r5JKa722SY0UXjqHv2'; // sample ed25519 blob
|
|
199
|
+
const EXPECTED = hostKeyLinesFor('box-x.dev.amazonprimea.com', `ssh-ed25519 ${HKEY}\necdsa-sha2-nistp256 BBBB`).lines;
|
|
200
|
+
|
|
201
|
+
t('authoritative key present in file → true', () => {
|
|
202
|
+
const file = `box-x.dev.amazonprimea.com ssh-ed25519 ${HKEY}\nother.example.com ssh-rsa K`;
|
|
203
|
+
assert.equal(knownHostsHasKey(file, 'box-x.dev.amazonprimea.com', EXPECTED), true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
t('STALE ed25519 (rebuild minted a new key) → false, even though a host line exists', () => {
|
|
207
|
+
const file = 'box-x.dev.amazonprimea.com ssh-ed25519 OLDOLDOLD';
|
|
208
|
+
assert.equal(knownHostsHasKey(file, 'box-x.dev.amazonprimea.com', EXPECTED), false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
t('MISSING (ssh-keygen -R removed it / empty file) → false', () => {
|
|
212
|
+
assert.equal(knownHostsHasKey('unrelated.example.com ssh-ed25519 ZZZZ', 'box-x.dev.amazonprimea.com', EXPECTED), false);
|
|
213
|
+
assert.equal(knownHostsHasKey('', 'box-x.dev.amazonprimea.com', EXPECTED), false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
t('no authoritative key → any ed25519 line for the host counts (keyscan fallback)', () => {
|
|
217
|
+
assert.equal(knownHostsHasKey('box-x.dev.amazonprimea.com ssh-ed25519 WHATEVER', 'box-x.dev.amazonprimea.com', []), true);
|
|
218
|
+
// ...but an rsa-only pin (no ed25519, the algo Desktop verifies) is NOT enough.
|
|
219
|
+
assert.equal(knownHostsHasKey('box-x.dev.amazonprimea.com ssh-rsa RRRR', 'box-x.dev.amazonprimea.com', []), false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
t('host,ip combined first field + comments/blanks are handled', () => {
|
|
223
|
+
const file = `# a comment\n\nbox-x.dev.amazonprimea.com,1.2.3.4 ssh-ed25519 ${HKEY}`;
|
|
224
|
+
assert.equal(knownHostsHasKey(file, 'box-x.dev.amazonprimea.com', EXPECTED), true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
t('no hostname → false (fail-closed)', () => {
|
|
228
|
+
assert.equal(knownHostsHasKey(`box-x ssh-ed25519 ${HKEY}`, '', EXPECTED), false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
t('verifyKnownHosts: missing hostname is a clean not-ok with no file IO', () => {
|
|
232
|
+
const r = verifyKnownHosts('', 'ssh-ed25519 AAAA');
|
|
233
|
+
assert.equal(r.ok, false);
|
|
234
|
+
assert.equal(r.reason, 'no_hostname');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
console.log('settingsUpToDate:');
|
|
238
|
+
|
|
239
|
+
t('content equality is structural, missing file is false', () => {
|
|
240
|
+
assert.equal(settingsUpToDate('/nonexistent/path.json', '{"a":1}'), false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
console.log('pairing:');
|
|
244
|
+
|
|
245
|
+
await ta('pairing run: opens browser, polls to approval, returns the grant', async () => {
|
|
246
|
+
const calls = [];
|
|
247
|
+
let polls = 0;
|
|
248
|
+
const client = {
|
|
249
|
+
pairStart: async () => ({ pair_code: 'ABCD-EFGH', poll_token: 'pt', verify_url: 'https://x/pair?code=ABCD-EFGH', expires_in: 600, interval: 0.01 }),
|
|
250
|
+
pairPoll: async () => (++polls < 3 ? { status: 'pending' } : { status: 'approved', token: 'tok', builder: { github_login: 'l' }, expires_at: 'soon' }),
|
|
251
|
+
};
|
|
252
|
+
const pairing = createPairing({ client, openExternal: async (u) => calls.push(u), sleep: () => Promise.resolve() });
|
|
253
|
+
const updates = [];
|
|
254
|
+
const grant = await pairing.run((u) => updates.push(u));
|
|
255
|
+
assert.deepEqual(calls, ['https://x/pair?code=ABCD-EFGH']);
|
|
256
|
+
assert.equal(updates[0].phase, 'waiting');
|
|
257
|
+
assert.equal(grant.token, 'tok');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await ta('pairing run: expired poll throws', async () => {
|
|
261
|
+
const client = {
|
|
262
|
+
pairStart: async () => ({ pair_code: 'A', poll_token: 'pt', verify_url: 'https://x', expires_in: 600, interval: 0.01 }),
|
|
263
|
+
pairPoll: async () => ({ status: 'expired' }),
|
|
264
|
+
};
|
|
265
|
+
const pairing = createPairing({ client, openExternal: async () => {}, sleep: () => Promise.resolve() });
|
|
266
|
+
await assert.rejects(() => pairing.run(), /expired/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await ta('pairing run: cancel stops the loop', async () => {
|
|
270
|
+
const client = {
|
|
271
|
+
pairStart: async () => ({ pair_code: 'A', poll_token: 'pt', verify_url: 'https://x', expires_in: 600, interval: 0.01 }),
|
|
272
|
+
pairPoll: async () => ({ status: 'pending' }),
|
|
273
|
+
};
|
|
274
|
+
const pairing = createPairing({ client, openExternal: async () => {}, sleep: () => Promise.resolve() });
|
|
275
|
+
const p = pairing.run();
|
|
276
|
+
pairing.cancel();
|
|
277
|
+
await assert.rejects(() => p, /cancelled/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
console.log('mergeUserSshConfigs (#910 — the picker reads ~/.claude/settings.json):');
|
|
281
|
+
|
|
282
|
+
t('merges our entry into empty/missing settings', () => {
|
|
283
|
+
const { mergeUserSshConfigs } = require('../src/ssh-setup.js');
|
|
284
|
+
const out = mergeUserSshConfigs('', [{ id: 'otb-x', name: 'Box', sshHost: 'root@h' }]);
|
|
285
|
+
assert.deepEqual(out.sshConfigs, [{ id: 'otb-x', name: 'Box', sshHost: 'root@h' }]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
t('replaces our id, preserves other entries and unrelated settings', () => {
|
|
289
|
+
const { mergeUserSshConfigs } = require('../src/ssh-setup.js');
|
|
290
|
+
const existing = JSON.stringify({
|
|
291
|
+
theme: 'dark',
|
|
292
|
+
sshConfigs: [
|
|
293
|
+
{ id: 'personal', name: 'My server', sshHost: 'me@mine' },
|
|
294
|
+
{ id: 'otb-x', name: 'Old Box', sshHost: 'root@old' },
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
const out = mergeUserSshConfigs(existing, [{ id: 'otb-x', name: 'New Box', sshHost: 'root@new' }]);
|
|
298
|
+
assert.equal(out.theme, 'dark', 'unrelated settings preserved');
|
|
299
|
+
assert.deepEqual(out.sshConfigs.map((c) => c.id), ['personal', 'otb-x']);
|
|
300
|
+
assert.equal(out.sshConfigs[1].sshHost, 'root@new');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
t('corrupt settings file never throws — starts clean', () => {
|
|
304
|
+
const { mergeUserSshConfigs } = require('../src/ssh-setup.js');
|
|
305
|
+
const out = mergeUserSshConfigs('{not json', [{ id: 'a', name: 'A', sshHost: 'h' }]);
|
|
306
|
+
assert.equal(out.sshConfigs.length, 1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
console.log('resolveSshTool (#909):');
|
|
310
|
+
|
|
311
|
+
t('returns the bare name on POSIX (PATH resolution)', () => {
|
|
312
|
+
const { resolveSshTool } = require('../src/ssh-setup.js');
|
|
313
|
+
if (process.platform !== 'win32') {
|
|
314
|
+
assert.equal(resolveSshTool('ssh-keyscan'), 'ssh-keyscan');
|
|
315
|
+
} else {
|
|
316
|
+
// On Windows it must come back absolute (or fall through to PATH).
|
|
317
|
+
const out = resolveSshTool('ssh-keyscan');
|
|
318
|
+
assert.ok(out === 'ssh-keyscan' || out.endsWith('ssh-keyscan.exe'));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
console.log('hostKeys snapshot wiring (#909):');
|
|
323
|
+
|
|
324
|
+
t('setHostKeys lands in the snapshot and clears when the box leaves active', async () => {
|
|
325
|
+
const { createBoxController } = require('../src/box-controller.js');
|
|
326
|
+
let payload = { box: { state: 'active', hostname: 'h' }, open_intent: null };
|
|
327
|
+
const client = {
|
|
328
|
+
boxMe: async () => payload,
|
|
329
|
+
boxTerminal: async () => ({ url: null }),
|
|
330
|
+
};
|
|
331
|
+
const c = createBoxController({ client, signedIn: () => true, onChange: () => {}, onNotify: () => {}, envName: 'E' });
|
|
332
|
+
await c.refresh();
|
|
333
|
+
c.setHostKeys({ ok: false, reason: 'no_keys', attempt: 2 });
|
|
334
|
+
assert.deepEqual(c.snapshot().hostKeys, { ok: false, reason: 'no_keys', attempt: 2 });
|
|
335
|
+
payload = { box: { state: 'none' }, open_intent: null };
|
|
336
|
+
await c.refresh();
|
|
337
|
+
assert.equal(c.snapshot().hostKeys, null, 'cleared when not active');
|
|
338
|
+
c.stop(); // halt the (unref'd) poll timer
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
console.log('keyRegistered snapshot wiring (task 1002509):');
|
|
342
|
+
|
|
343
|
+
await ta('setKeyRegistered surfaces registration state and SURVIVES the box going away', async () => {
|
|
344
|
+
const { createBoxController } = require('../src/box-controller.js');
|
|
345
|
+
let payload = {
|
|
346
|
+
box: {
|
|
347
|
+
state: 'active', hostname: 'h',
|
|
348
|
+
host_keys: 'ssh-ed25519 K', host_keys_at: '2026-07-01T10:00:00Z', active_since: '2026-07-01T09:00:00Z',
|
|
349
|
+
},
|
|
350
|
+
open_intent: null,
|
|
351
|
+
};
|
|
352
|
+
const client = { boxMe: async () => payload, boxTerminal: async () => ({ url: null }) };
|
|
353
|
+
const c = createBoxController({ client, signedIn: () => true, onChange: () => {}, onNotify: () => {}, envName: 'E' });
|
|
354
|
+
await c.refresh();
|
|
355
|
+
assert.equal(c.snapshot().keyRegistered, null, 'nothing reported yet');
|
|
356
|
+
assert.equal(c.snapshot().box.hostKeysAt, '2026-07-01T10:00:00Z');
|
|
357
|
+
assert.equal(c.snapshot().box.activeSince, '2026-07-01T09:00:00Z');
|
|
358
|
+
c.setKeyRegistered({ ok: false, reason: 'verify_failed', fingerprint: 'SHA256:x', attempts: 3 });
|
|
359
|
+
assert.deepEqual(c.snapshot().keyRegistered, { ok: false, reason: 'verify_failed', fingerprint: 'SHA256:x', attempts: 3 });
|
|
360
|
+
payload = { box: { state: 'none' }, open_intent: null };
|
|
361
|
+
await c.refresh();
|
|
362
|
+
assert.deepEqual(
|
|
363
|
+
c.snapshot().keyRegistered,
|
|
364
|
+
{ ok: false, reason: 'verify_failed', fingerprint: 'SHA256:x', attempts: 3 },
|
|
365
|
+
'the key lives on the ACCOUNT, not the box — a failure must stay visible after the box closes'
|
|
366
|
+
);
|
|
367
|
+
c.stop();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
t('ready notification tells the user to RESTART Claude Desktop (#909 picker finding)', () => {
|
|
371
|
+
const n = transitionNotification('provisioning', 'active', { envName: 'X', settingsInstalled: true });
|
|
372
|
+
assert.ok(/restart claude desktop/i.test(n.body), n.body);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
console.log('config:');
|
|
376
|
+
|
|
377
|
+
t('apiBase: env override > session base > production default; slashes stripped', () => {
|
|
378
|
+
delete process.env.OTB_DEVBOX_API_BASE;
|
|
379
|
+
assert.equal(apiBase(null), brand.PUBLIC_ORIGIN);
|
|
380
|
+
assert.equal(apiBase('https://staging.example.com/'), 'https://staging.example.com');
|
|
381
|
+
process.env.OTB_DEVBOX_API_BASE = 'http://127.0.0.1:3000/';
|
|
382
|
+
assert.equal(apiBase('https://staging.example.com'), 'http://127.0.0.1:3000');
|
|
383
|
+
delete process.env.OTB_DEVBOX_API_BASE;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
console.log('updater (#924 — in-app update check):');
|
|
387
|
+
|
|
388
|
+
t('compareVersions orders X.Y.Z numerically (not lexically)', () => {
|
|
389
|
+
assert.equal(compareVersions('1.0.5', '1.0.6'), -1);
|
|
390
|
+
assert.equal(compareVersions('1.0.6', '1.0.5'), 1);
|
|
391
|
+
assert.equal(compareVersions('1.0.10', '1.0.9'), 1, '10 > 9 numerically, not "10" < "9" lexically');
|
|
392
|
+
assert.equal(compareVersions('1.2.0', '1.10.0'), -1);
|
|
393
|
+
assert.equal(compareVersions('2.0.0', '1.9.9'), 1);
|
|
394
|
+
assert.equal(compareVersions('1.0.0', '1.0.0'), 0);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
t('isUpdateAvailable is true ONLY for a strictly-newer published release', () => {
|
|
398
|
+
assert.equal(isUpdateAvailable('1.0.5', '1.0.6'), true);
|
|
399
|
+
// The live-state bug this fixes: app AHEAD of the published config is NOT an update.
|
|
400
|
+
assert.equal(isUpdateAvailable('1.0.6', '1.0.5'), false);
|
|
401
|
+
assert.equal(isUpdateAvailable('1.0.6', '1.0.6'), false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
t('isUpdateAvailable never false-positives on a malformed version', () => {
|
|
405
|
+
assert.equal(isUpdateAvailable('1.0.6', ''), false);
|
|
406
|
+
assert.equal(isUpdateAvailable('1.0.6', null), false);
|
|
407
|
+
assert.equal(isUpdateAvailable('1.0.6', 'v1.0.7'), false);
|
|
408
|
+
assert.equal(isUpdateAvailable('1.0.6', 'latest'), false);
|
|
409
|
+
assert.equal(isUpdateAvailable('garbage', '1.0.7'), false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
t('resolveTarget maps platform/arch to a config asset key (matching devbox-app.json)', () => {
|
|
413
|
+
assert.equal(resolveTarget('darwin', 'arm64'), 'mac-arm64');
|
|
414
|
+
assert.equal(resolveTarget('darwin', 'x64'), 'mac-x64');
|
|
415
|
+
assert.equal(resolveTarget('win32', 'arm64'), 'win-arm64');
|
|
416
|
+
assert.equal(resolveTarget('win32', 'x64'), 'win-x64');
|
|
417
|
+
assert.equal(resolveTarget('linux', 'x64'), null, 'no installer target → no in-app offer');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
t('updateBannerText is the exact copy the task specifies', () => {
|
|
421
|
+
assert.equal(updateBannerText('1.0.6'), 'Update available — v1.0.6');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
console.log('\ngds-client (backed by the vendored generated client, task 1996 / R09):');
|
|
425
|
+
|
|
426
|
+
// A fake fetch that records the last request and returns a fixed response, so we
|
|
427
|
+
// assert the generated named methods produce the exact GDS endpoints/verbs/bodies
|
|
428
|
+
// the app has always used — and that the GdsError contract (status + raw body) holds.
|
|
429
|
+
function recordingClient(response = { ok: true, status: 200, text: async () => '{"ok":true}' }) {
|
|
430
|
+
const rec = {};
|
|
431
|
+
const fetchImpl = async (url, init) => { rec.url = url; rec.init = init; return response; };
|
|
432
|
+
const client = createGdsClient({ getToken: () => 'tok', getApiBase: () => 'https://x.test', fetchImpl });
|
|
433
|
+
return { client, rec };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const ENDPOINTS = [
|
|
437
|
+
['logout', [], 'POST', '/api/gds/auth/logout', undefined],
|
|
438
|
+
['pairStart', ['Lars-Mac', 'darwin'], 'POST', '/api/gds/auth/app-pair/start', { device_name: 'Lars-Mac', platform: 'darwin' }],
|
|
439
|
+
['pairPoll', ['pt'], 'POST', '/api/gds/auth/app-pair/poll', { poll_token: 'pt' }],
|
|
440
|
+
['boxMe', [], 'GET', '/api/gds/box/me', undefined],
|
|
441
|
+
['boxEnsure', [], 'POST', '/api/gds/box/ensure', undefined],
|
|
442
|
+
['boxClose', [], 'POST', '/api/gds/box/close', undefined],
|
|
443
|
+
['boxHeartbeat', [], 'POST', '/api/gds/box/heartbeat', undefined],
|
|
444
|
+
['boxTerminal', [], 'GET', '/api/gds/box/terminal', undefined],
|
|
445
|
+
['registerSshKey', ['ssh-ed25519 AAA', 'lars'], 'POST', '/api/gds/box/ssh-key', { public_key: 'ssh-ed25519 AAA', label: 'lars' }],
|
|
446
|
+
['managedSettings', [], 'GET', '/api/gds/box/managed-settings', undefined],
|
|
447
|
+
['claimableTasks', [], 'GET', '/api/gds/tasks/claimable', undefined],
|
|
448
|
+
['claimTask', ['1002388'], 'POST', '/api/gds/claims', { task_id: '1002388' }],
|
|
449
|
+
['boxScope', [], 'GET', '/api/gds/box/scope', undefined],
|
|
450
|
+
['latestRelease', [], 'GET', '/downloads/devbox/latest.json', undefined],
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
for (const [name, args, method, urlPath, body] of ENDPOINTS) {
|
|
454
|
+
await ta(`${name} → ${method} ${urlPath} (Bearer + exact body)`, async () => {
|
|
455
|
+
const { client, rec } = recordingClient();
|
|
456
|
+
await client[name](...args);
|
|
457
|
+
assert.equal(rec.init.method, method);
|
|
458
|
+
assert.equal(rec.url, `https://x.test${urlPath}`);
|
|
459
|
+
assert.equal(rec.init.headers.Authorization, 'Bearer tok');
|
|
460
|
+
const sentBody = rec.init.body === undefined ? undefined : JSON.parse(rec.init.body);
|
|
461
|
+
assert.deepEqual(sentBody, body);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await ta('a non-2xx rejects with GdsError carrying status + the raw (flat) body', async () => {
|
|
466
|
+
const { client } = recordingClient({ ok: false, status: 403, text: async () => '{"error":"BOX_PROVISION_DENIED","reason":"quota"}' });
|
|
467
|
+
await assert.rejects(client.boxEnsure(), (e) => {
|
|
468
|
+
assert.ok(e instanceof GdsError);
|
|
469
|
+
assert.equal(e.status, 403);
|
|
470
|
+
assert.equal(e.body.error, 'BOX_PROVISION_DENIED');
|
|
471
|
+
assert.equal(e.body.reason, 'quota');
|
|
472
|
+
return true;
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
console.log('\ntask-picker (BV1.R100 — "What are you working on?" pick + confirm):');
|
|
477
|
+
|
|
478
|
+
t('shapeClaimableTasks normalizes rows, caps to the limit, tolerates junk', () => {
|
|
479
|
+
const payload = { claimable: [
|
|
480
|
+
{ id: 1002388, title: 'R100', kind: 'feature', est_minutes: 45 },
|
|
481
|
+
{ id: 7, kind: 'bug' },
|
|
482
|
+
...Array.from({ length: 10 }, (_v, i) => ({ id: 100 + i, title: `t${i}` })),
|
|
483
|
+
] };
|
|
484
|
+
const rows = shapeClaimableTasks(payload, { limit: 6 });
|
|
485
|
+
assert.equal(rows.length, 6, 'capped to limit');
|
|
486
|
+
assert.deepEqual(rows[0], { id: '1002388', title: 'R100', kind: 'feature', estMinutes: 45 });
|
|
487
|
+
assert.equal(rows[1].title, 'Task 7', 'missing title → fallback');
|
|
488
|
+
assert.equal(rows[1].kind, 'bug');
|
|
489
|
+
assert.equal(rows[1].estMinutes, null, 'missing est_minutes → null');
|
|
490
|
+
assert.deepEqual(shapeClaimableTasks(null), [], 'no payload → empty');
|
|
491
|
+
assert.deepEqual(shapeClaimableTasks({}), [], 'no claimable key → empty');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
t('boxScopeMessage names EXACTLY the modules the box will hold (task scope) + directs to /builder-sync', () => {
|
|
495
|
+
const m = boxScopeMessage({ task: { id: '1002388' }, scope: { scope: 'task', modules: ['dev-box', 'onboarding'], task_scope: true } });
|
|
496
|
+
assert.ok(m.includes('task 1002388'), m);
|
|
497
|
+
assert.ok(m.includes('dev-box, onboarding'), m);
|
|
498
|
+
assert.ok(m.includes('nothing else'), 'the minimality is the point');
|
|
499
|
+
assert.ok(/builder-sync/.test(m), 'directs to /builder-sync (R96)');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
t('boxScopeMessage: base-only (empty modules) → nothing extra to pull', () => {
|
|
503
|
+
const m = boxScopeMessage({ task: { id: '5' }, scope: { scope: 'base', modules: [], task_scope: true } });
|
|
504
|
+
assert.ok(/base instance/.test(m) && /nothing extra to pull/.test(m), m);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
t('boxScopeMessage: rank fallback (modules null) → generic scope + sync hint', () => {
|
|
508
|
+
const m = boxScopeMessage({ task: { id: '9' }, scope: { scope: 'full', modules: null, task_scope: false } });
|
|
509
|
+
assert.ok(/full scope/.test(m) && /builder-sync/.test(m), m);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
t('boxScopeMessage: scope read failed (null) → claim still confirmed, still sync', () => {
|
|
513
|
+
const m = boxScopeMessage({ task: { id: '9' }, scope: null });
|
|
514
|
+
assert.ok(/Claimed task 9/.test(m) && /builder-sync/.test(m), m);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
t('claimErrorMessage maps known refusals; falls back to server text/code', () => {
|
|
518
|
+
assert.ok(/waiting on other work/.test(claimErrorMessage({ body: { error: 'DEPS_NOT_SHIPPED' } })));
|
|
519
|
+
assert.ok(/in flight/.test(claimErrorMessage({ body: { error: 'session_already_has_active_claim' } })));
|
|
520
|
+
assert.ok(/same files/.test(claimErrorMessage({ body: { error: 'touches_conflict' } })));
|
|
521
|
+
assert.equal(claimErrorMessage({ body: { message: 'boom' } }), 'boom', 'server message wins the fallback');
|
|
522
|
+
assert.ok(/mystery/.test(claimErrorMessage({ body: { error: 'mystery' } })), 'unknown code surfaces');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
526
|
+
process.exit(failed === 0 ? 0 : 1);
|