@bongos/core 1.18.3 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1575 -285
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// tests/smoke_target.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1002418 — smoke-gds.js must verify THIS instance, never a hardcoded one.
|
|
4
|
+
//
|
|
5
|
+
// The bug: `BASE = GDS_API_BASE || PMS_API_BASE || 'https://amazonprimea.com'`.
|
|
6
|
+
// A builder signed into any other instance who shipped without an env override
|
|
7
|
+
// smoked the founder's box, which is healthy, so the ship reported
|
|
8
|
+
// "smoke=clean" about a system it never touched. resolveSmokeTarget replaces the
|
|
9
|
+
// literal with the ship.js precedence chain and fails closed.
|
|
10
|
+
//
|
|
11
|
+
// These tests pin the three things that make the fix real: the precedence order,
|
|
12
|
+
// the mismatch warning, and the nothing-resolves case. Pure over injected inputs
|
|
13
|
+
// — no fs, no env mutation, no network.
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/smoke_target.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { resolveSmokeTarget, formatTargetReport, buildChecks } = require('../scripts/gds/smoke-gds.js');
|
|
26
|
+
const ic = require('../src/instance-config.js');
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
function test(name, fn) {
|
|
31
|
+
try {
|
|
32
|
+
fn();
|
|
33
|
+
passed++;
|
|
34
|
+
console.log(` ok ${name}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
failed++;
|
|
37
|
+
console.log(` FAIL ${name}`);
|
|
38
|
+
console.log(` ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SESSION = 'https://cloudbongos.com';
|
|
43
|
+
const BRAND = { domains: { publicOrigin: 'https://brand.example' } };
|
|
44
|
+
// Explicit empty env: the real process.env must never leak into a precedence test.
|
|
45
|
+
const NO_ENV = {};
|
|
46
|
+
|
|
47
|
+
// ---- precedence: env → session → branding → null ----
|
|
48
|
+
|
|
49
|
+
test('env override wins over session and branding', () => {
|
|
50
|
+
const t = resolveSmokeTarget({
|
|
51
|
+
env: { GDS_API_BASE: 'http://localhost:3000' },
|
|
52
|
+
sessionApiBase: SESSION,
|
|
53
|
+
branding: BRAND,
|
|
54
|
+
});
|
|
55
|
+
assert.equal(t.base, 'http://localhost:3000');
|
|
56
|
+
assert.equal(t.source, 'env');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('GDS_API_BASE specifically still works (local dev relies on it)', () => {
|
|
60
|
+
const t = resolveSmokeTarget({ env: { GDS_API_BASE: 'http://localhost:3000' }, sessionApiBase: null, branding: {} });
|
|
61
|
+
assert.equal(t.base, 'http://localhost:3000');
|
|
62
|
+
assert.equal(t.envVar, 'GDS_API_BASE');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('legacy PMS_API_BASE and OTB_API_BASE still work', () => {
|
|
66
|
+
assert.equal(resolveSmokeTarget({ env: { PMS_API_BASE: 'http://localhost:4000' }, sessionApiBase: null, branding: {} }).base, 'http://localhost:4000');
|
|
67
|
+
assert.equal(resolveSmokeTarget({ env: { OTB_API_BASE: 'http://localhost:4001' }, sessionApiBase: null, branding: {} }).base, 'http://localhost:4001');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// The CONFIGURED prefix is the leg the old code never had: it read GDS_/PMS_ only.
|
|
71
|
+
// Without this, reverting resolveEnv() to `env.GDS_API_BASE || env.PMS_API_BASE`
|
|
72
|
+
// would leave the whole suite green. Derived from instance-config rather than
|
|
73
|
+
// hardcoded 'CLOUDBONGOS_' so the test follows a rebranded instance.
|
|
74
|
+
test('the configured <PREFIX>_API_BASE is honored, not just the legacy names', () => {
|
|
75
|
+
const key = `${ic.envPrefix()}_API_BASE`;
|
|
76
|
+
const t = resolveSmokeTarget({ env: { [key]: 'http://localhost:5000' }, sessionApiBase: SESSION, branding: BRAND });
|
|
77
|
+
assert.equal(t.base, 'http://localhost:5000', `${key} should win`);
|
|
78
|
+
assert.equal(t.envVar, key);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('the configured prefix outranks the legacy ones', () => {
|
|
82
|
+
const key = `${ic.envPrefix()}_API_BASE`;
|
|
83
|
+
if (key === 'GDS_API_BASE') return; // same var; nothing to order
|
|
84
|
+
const t = resolveSmokeTarget({
|
|
85
|
+
env: { [key]: 'http://configured:1', GDS_API_BASE: 'http://legacy:2' },
|
|
86
|
+
sessionApiBase: null,
|
|
87
|
+
branding: {},
|
|
88
|
+
});
|
|
89
|
+
assert.equal(t.base, 'http://configured:1');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('session api_base wins over branding when no env override', () => {
|
|
93
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: SESSION, branding: BRAND });
|
|
94
|
+
assert.equal(t.base, SESSION);
|
|
95
|
+
assert.equal(t.source, 'session');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('branding publicOrigin is the last resort, not a foreign instance', () => {
|
|
99
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: BRAND });
|
|
100
|
+
assert.equal(t.base, 'https://brand.example');
|
|
101
|
+
assert.equal(t.source, 'branding');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// The regression that started this: signed into cloudbongos.com, no env override.
|
|
105
|
+
// Pre-fix this resolved to the hardcoded amazonprimea.com and passed.
|
|
106
|
+
test('signed into another instance with no env override → that instance, never amazonprimea', () => {
|
|
107
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: SESSION, branding: BRAND });
|
|
108
|
+
assert.equal(t.base, SESSION);
|
|
109
|
+
assert.doesNotMatch(t.base, /amazonprimea/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('no hardcoded host survives anywhere in the resolution', () => {
|
|
113
|
+
for (const t of [
|
|
114
|
+
resolveSmokeTarget({ env: NO_ENV, sessionApiBase: SESSION, branding: BRAND }),
|
|
115
|
+
resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: BRAND }),
|
|
116
|
+
resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: {} }),
|
|
117
|
+
]) {
|
|
118
|
+
assert.doesNotMatch(String(t.base), /amazonprimea/);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('trailing slashes are normalized away', () => {
|
|
123
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: 'https://cloudbongos.com//', branding: {} });
|
|
124
|
+
assert.equal(t.base, SESSION);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('blank/whitespace values are skipped, not treated as a target', () => {
|
|
128
|
+
const t = resolveSmokeTarget({ env: { GDS_API_BASE: ' ' }, sessionApiBase: SESSION, branding: BRAND });
|
|
129
|
+
assert.equal(t.base, SESSION);
|
|
130
|
+
assert.equal(t.source, 'session');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---- nothing resolves → fail closed ----
|
|
134
|
+
|
|
135
|
+
test('nothing resolves → null base + null source (caller must fail, not guess)', () => {
|
|
136
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: {} });
|
|
137
|
+
assert.equal(t.base, null);
|
|
138
|
+
assert.equal(t.source, null);
|
|
139
|
+
assert.equal(t.mismatch, false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('unreadable branding (no domains key) still resolves to null, never a default host', () => {
|
|
143
|
+
const t = resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: { domains: {} } });
|
|
144
|
+
assert.equal(t.base, null);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---- mismatch detection + the loud warning ----
|
|
148
|
+
|
|
149
|
+
test('mismatch is true when the env override disagrees with the session', () => {
|
|
150
|
+
const t = resolveSmokeTarget({ env: { GDS_API_BASE: 'http://localhost:3000' }, sessionApiBase: SESSION, branding: BRAND });
|
|
151
|
+
assert.equal(t.mismatch, true);
|
|
152
|
+
assert.equal(t.sessionApiBase, SESSION);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('mismatch is false when the target IS the session', () => {
|
|
156
|
+
assert.equal(resolveSmokeTarget({ env: NO_ENV, sessionApiBase: SESSION, branding: BRAND }).mismatch, false);
|
|
157
|
+
assert.equal(resolveSmokeTarget({ env: { GDS_API_BASE: SESSION }, sessionApiBase: SESSION, branding: BRAND }).mismatch, false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('mismatch ignores trailing-slash and case differences (no false alarms)', () => {
|
|
161
|
+
const t = resolveSmokeTarget({ env: { GDS_API_BASE: 'https://CloudBongos.com/' }, sessionApiBase: SESSION, branding: BRAND });
|
|
162
|
+
assert.equal(t.mismatch, false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('mismatch is false when not signed in (nothing to disagree with)', () => {
|
|
166
|
+
assert.equal(resolveSmokeTarget({ env: NO_ENV, sessionApiBase: null, branding: BRAND }).mismatch, false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('report always states the resolved target and its source', () => {
|
|
170
|
+
const lines = formatTargetReport(resolveSmokeTarget({ env: NO_ENV, sessionApiBase: SESSION, branding: BRAND }));
|
|
171
|
+
assert.equal(lines.length, 1, 'no warning when target matches the session');
|
|
172
|
+
assert.match(lines[0], /https:\/\/cloudbongos\.com/);
|
|
173
|
+
assert.match(lines[0], /session api_base/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('report shouts BOTH origins when they disagree — the silence that made this dangerous', () => {
|
|
177
|
+
const t = resolveSmokeTarget({ env: { GDS_API_BASE: 'http://localhost:3000' }, sessionApiBase: SESSION, branding: BRAND });
|
|
178
|
+
const text = formatTargetReport(t).join('\n');
|
|
179
|
+
assert.match(text, /WARNING/);
|
|
180
|
+
assert.match(text, /http:\/\/localhost:3000/, 'names the target actually being verified');
|
|
181
|
+
assert.match(text, /https:\/\/cloudbongos\.com/, 'names the session it disagrees with');
|
|
182
|
+
assert.ok(text.split('\n').length > 5, 'a one-liner is not loud enough to be noticed');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---- main() actually fails closed (the pure function alone can't prove this) ----
|
|
186
|
+
//
|
|
187
|
+
// Everything above tests resolveSmokeTarget. A hardcoded fallback reintroduced
|
|
188
|
+
// INSIDE main() — `const BASE = target.base || 'https://amazonprimea.com'` — would
|
|
189
|
+
// leave all of it green. So run the real script with nothing resolvable: no
|
|
190
|
+
// session (HOME/USERPROFILE point at an empty dir, covering both platforms'
|
|
191
|
+
// os.homedir()), no *_API_BASE, and a branding pack with an empty publicOrigin.
|
|
192
|
+
|
|
193
|
+
function runScript(env) {
|
|
194
|
+
const script = path.resolve(process.cwd(), 'scripts', 'gds', 'smoke-gds.js');
|
|
195
|
+
try {
|
|
196
|
+
const stdout = execFileSync(process.execPath, [script], { env, encoding: 'utf8', timeout: 30_000 });
|
|
197
|
+
return { code: 0, stdout };
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return { code: err.status, stdout: (err.stdout || '') + (err.stderr || '') };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
test('main() exits 1 and names no host when nothing resolves', () => {
|
|
204
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'smoke-target-'));
|
|
205
|
+
const brandPack = path.join(tmp, 'branding.json');
|
|
206
|
+
fs.writeFileSync(brandPack, JSON.stringify({ domains: { publicOrigin: '' } }));
|
|
207
|
+
|
|
208
|
+
const env = { ...process.env, HOME: tmp, USERPROFILE: tmp, GDS_BRANDING_FILE: brandPack };
|
|
209
|
+
for (const k of Object.keys(env)) if (k.endsWith('_API_BASE')) delete env[k];
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const { code, stdout } = runScript(env);
|
|
213
|
+
assert.equal(code, 1, `expected exit 1, got ${code}\n${stdout}`);
|
|
214
|
+
assert.match(stdout, /no target/i);
|
|
215
|
+
// The whole point: it must not have picked a host to verify.
|
|
216
|
+
assert.doesNotMatch(stdout, /smoke target: http/i, 'resolved a target it should not have');
|
|
217
|
+
assert.doesNotMatch(stdout, /amazonprimea/, 'the hardcoded fallback is back');
|
|
218
|
+
assert.doesNotMatch(stdout, /passed, \d+ failed/, 'it ran checks against something');
|
|
219
|
+
} finally {
|
|
220
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---- the checks are built from the resolved base, not a baked-in one ----
|
|
225
|
+
|
|
226
|
+
test('every check URL is built from the resolved base', () => {
|
|
227
|
+
const urls = buildChecks(SESSION).map(([, url]) => url);
|
|
228
|
+
assert.ok(urls.length >= 17, `expected the full check set, got ${urls.length}`);
|
|
229
|
+
for (const u of urls) assert.ok(u.startsWith(`${SESSION}/`), `${u} is not on the resolved base`);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(`\nsmoke_target: ${passed} passed, ${failed} failed`);
|
|
233
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// tests/start_env_guard.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002403 (goal 1000052, first-run onboarding) — /builder-start is often the
|
|
4
|
+
// FIRST command run in a freshly-cloned instance, before the local server is up.
|
|
5
|
+
// This exercises the pure helpers start.js exports for that guard (isConnectionError
|
|
6
|
+
// / instanceNotRunningCard) DB-free — no live server, mirroring start_rebase_warning.
|
|
7
|
+
// Invariants under test:
|
|
8
|
+
// - isConnectionError recognizes the network-failure shapes fetch throws
|
|
9
|
+
// (code, cause.code, and message forms) and ignores non-network errors
|
|
10
|
+
// - the guided card names `bongos dev` + the re-run, and only mentions
|
|
11
|
+
// `npm install` when deps are actually missing
|
|
12
|
+
// - the card is MARKDOWN (never leads with `<`) so the builder-start skill
|
|
13
|
+
// relays it verbatim even in --widget mode, and carries no bare "#N"
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/start_env_guard.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { isConnectionError, instanceNotRunningCard } = require('../scripts/gds/start.js');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
function t(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
27
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- isConnectionError: the network-failure classifier --------------------
|
|
31
|
+
t('true for a direct connection-refused code', () => {
|
|
32
|
+
assert.equal(isConnectionError({ code: 'ECONNREFUSED' }), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
t('true for the node fetch shape (TypeError with a cause carrying the code)', () => {
|
|
36
|
+
// `await fetch()` to a dead port throws exactly this: TypeError('fetch failed')
|
|
37
|
+
// with an AggregateError cause whose code is ECONNREFUSED.
|
|
38
|
+
const err = new TypeError('fetch failed');
|
|
39
|
+
err.cause = Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' });
|
|
40
|
+
assert.equal(isConnectionError(err), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
t('true for DNS failure (ENOTFOUND) and message-only signals', () => {
|
|
44
|
+
assert.equal(isConnectionError({ code: 'ENOTFOUND' }), true);
|
|
45
|
+
assert.equal(isConnectionError(new Error('fetch failed')), true);
|
|
46
|
+
assert.equal(isConnectionError(new Error('socket hang up')), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('false for null and for a non-network error', () => {
|
|
50
|
+
assert.equal(isConnectionError(null), false);
|
|
51
|
+
assert.equal(isConnectionError(undefined), false);
|
|
52
|
+
assert.equal(isConnectionError(new Error('something unrelated blew up')), false);
|
|
53
|
+
// An HTTP-ish error object with no network signal must NOT be misread as a
|
|
54
|
+
// connection failure (those are handled as normal { ok:false } results).
|
|
55
|
+
assert.equal(isConnectionError({ status: 500, message: 'internal error' }), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- instanceNotRunningCard: the guided bring-up card ---------------------
|
|
59
|
+
t('always points at bongos dev and the re-run', () => {
|
|
60
|
+
const card = instanceNotRunningCard({ origin: 'http://localhost:3007', depsMissing: false });
|
|
61
|
+
assert.ok(card.includes('bongos dev'), 'names the one-command bring-up');
|
|
62
|
+
assert.ok(/re-run/i.test(card) && card.includes('/builder-start'), 'tells them to re-run /builder-start');
|
|
63
|
+
assert.ok(card.includes('localhost:3007'), 'names the unreachable origin when known');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
t('mentions npm install ONLY when deps are missing', () => {
|
|
67
|
+
const missing = instanceNotRunningCard({ origin: 'http://x', depsMissing: true });
|
|
68
|
+
const present = instanceNotRunningCard({ origin: 'http://x', depsMissing: false });
|
|
69
|
+
assert.ok(missing.includes('npm install'), 'fresh clone (no deps) → npm install step');
|
|
70
|
+
assert.ok(!present.includes('npm install'), 'deps present → skip the npm install step');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('is markdown (never leads with `<`) so the skill relays it verbatim', () => {
|
|
74
|
+
for (const deps of [true, false]) {
|
|
75
|
+
const card = instanceNotRunningCard({ origin: '', depsMissing: deps });
|
|
76
|
+
assert.notEqual(card.trimStart()[0], '<', 'must not start with `<` (that would trigger show_widget)');
|
|
77
|
+
assert.ok(!/#\d/.test(card), 'no bare #N (the linkify rule)');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
t('offers the wrong-instance escape hatch (bongos login)', () => {
|
|
82
|
+
const card = instanceNotRunningCard({ origin: 'http://x', depsMissing: false });
|
|
83
|
+
assert.ok(/bongos login/.test(card), 'points at bongos login for a different running instance');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(`\nstart_env_guard: ${passed} passed, ${failed} failed`);
|
|
87
|
+
if (failed > 0) process.exitCode = 1;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// tests/start_rebase_warning.mjs
|
|
2
|
+
//
|
|
3
|
+
// ADR 0120 part 5 — /builder-start surfaces a builder's OWED REBASES (the
|
|
4
|
+
// un-landed confirmed tasks that GATE new claims). This exercises the pure
|
|
5
|
+
// render helpers start.js exports (rebaseWarningModel / rebaseWarningMarkdown /
|
|
6
|
+
// rebaseWarningWidget) DB-free — no live server, mirroring claim_error_surface.
|
|
7
|
+
// The invariants under test:
|
|
8
|
+
// - nothing owed → no warning (null / '')
|
|
9
|
+
// - the count + singular/plural agreement
|
|
10
|
+
// - refs use the `task N` form, NEVER a bare "#N" (the CLAUDE.md linkify rule:
|
|
11
|
+
// a bare #N in relayed chat autolinks to GitHub issues and 404s)
|
|
12
|
+
// - the widget block is HTML-safe and carries the danger-tinted alert markup
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/start_rebase_warning.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { rebaseWarningModel, rebaseWarningMarkdown, rebaseWarningWidget } =
|
|
21
|
+
require('../scripts/gds/start.js');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
function t(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
27
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- rebaseWarningModel: the normalized render model ----------------------
|
|
31
|
+
t('model is null when nothing is owed (empty / null / undefined / junk)', () => {
|
|
32
|
+
for (const input of [[], null, undefined, 'nope', 42, {}]) {
|
|
33
|
+
assert.equal(rebaseWarningModel(input), null, `expected null for ${JSON.stringify(input)}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
t('model drops entries with no id, and is null if none remain', () => {
|
|
38
|
+
assert.equal(rebaseWarningModel([{ title: 'x' }, null, { id: null }]), null);
|
|
39
|
+
const m = rebaseWarningModel([{ id: 7 }, { title: 'noid' }]);
|
|
40
|
+
assert.equal(m.count, 1, 'only the id-bearing row counts');
|
|
41
|
+
assert.equal(m.refs, 'task 7');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t('model: singular vs plural agreement', () => {
|
|
45
|
+
const one = rebaseWarningModel([{ id: 123, title: 'a' }]);
|
|
46
|
+
assert.equal(one.count, 1);
|
|
47
|
+
assert.equal(one.noun, 'task');
|
|
48
|
+
assert.equal(one.pronoun, 'it');
|
|
49
|
+
assert.equal(one.refs, 'task 123');
|
|
50
|
+
|
|
51
|
+
const two = rebaseWarningModel([{ id: 123 }, { id: 456 }]);
|
|
52
|
+
assert.equal(two.count, 2);
|
|
53
|
+
assert.equal(two.noun, 'tasks');
|
|
54
|
+
assert.equal(two.pronoun, 'them');
|
|
55
|
+
assert.equal(two.refs, 'task 123, task 456');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- rebaseWarningMarkdown: the card line ---------------------------------
|
|
59
|
+
t('markdown is null when nothing is owed', () => {
|
|
60
|
+
assert.equal(rebaseWarningMarkdown([]), null);
|
|
61
|
+
assert.equal(rebaseWarningMarkdown(null), null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('markdown names the gate, the fix, and every owed task', () => {
|
|
65
|
+
const md = rebaseWarningMarkdown([{ id: 123, title: 'a' }, { id: 456, title: 'b' }]);
|
|
66
|
+
assert.ok(md.includes('GATED'), 'signals the queue is gated');
|
|
67
|
+
assert.ok(/2 tasks/.test(md), 'shows the count with plural agreement');
|
|
68
|
+
assert.ok(md.includes('/builder-release'), 'points at the release escape hatch');
|
|
69
|
+
assert.ok(md.includes('task 123') && md.includes('task 456'), 'names each owed task');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('markdown uses the `task N` form, never a bare #N (linkify rule)', () => {
|
|
73
|
+
for (const input of [[{ id: 1 }], [{ id: 1 }, { id: 2 }, { id: 3 }]]) {
|
|
74
|
+
const md = rebaseWarningMarkdown(input);
|
|
75
|
+
assert.ok(!/#\d/.test(md), `must not emit a bare #N: ${md}`);
|
|
76
|
+
assert.ok(/task \d/.test(md), 'uses the task N form');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// --- rebaseWarningWidget: the HTML block ----------------------------------
|
|
81
|
+
t('widget is empty string when nothing is owed', () => {
|
|
82
|
+
assert.equal(rebaseWarningWidget([]), '');
|
|
83
|
+
assert.equal(rebaseWarningWidget(null), '');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
t('widget renders the danger-tinted alert block with the owed tasks', () => {
|
|
87
|
+
const html = rebaseWarningWidget([{ id: 123 }, { id: 456 }]);
|
|
88
|
+
assert.ok(html.includes('class="bs-warn"'), 'uses the bs-warn danger block');
|
|
89
|
+
assert.ok(html.includes('ti-alert-triangle'), 'carries the alert icon');
|
|
90
|
+
assert.ok(html.includes('task 123') && html.includes('task 456'), 'names each owed task');
|
|
91
|
+
assert.ok(!/#\d/.test(html), 'no bare #N in the widget either');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
t('widget HTML-escapes the refs (no raw angle brackets from ids)', () => {
|
|
95
|
+
// ids are numeric in practice, but the renderer must not trust that — a
|
|
96
|
+
// crafted id string must be escaped, never injected raw.
|
|
97
|
+
const html = rebaseWarningWidget([{ id: '<b>x</b>' }]);
|
|
98
|
+
assert.ok(!html.includes('<b>x</b>'), 'raw markup from an id must be escaped');
|
|
99
|
+
assert.ok(html.includes('<b>'), 'the id is HTML-escaped');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(`\nstart_rebase_warning: ${passed} passed, ${failed} failed`);
|
|
103
|
+
if (failed > 0) process.exitCode = 1;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// tests/status_mirror_sync.mjs — the off-box status-mirror publisher (task 1002510,
|
|
2
|
+
// slice of parent 1000594). Covers the PURE index transform only; the CLI half
|
|
3
|
+
// (disk copies, `git add/commit/push` into a real external checkout) is never
|
|
4
|
+
// invoked from here.
|
|
5
|
+
//
|
|
6
|
+
// This file importing at all is the load-bearing regression: before task 1002510 the
|
|
7
|
+
// CLI body ran at require-time, so `import`ing the module printed the usage banner and
|
|
8
|
+
// exited 2. The `require.main === module` guard is what makes the transform testable.
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import mirror from '../scripts/status-mirror-sync.js';
|
|
13
|
+
|
|
14
|
+
const { buildMirrorIndex, injectOnce, brandTokens } = mirror;
|
|
15
|
+
|
|
16
|
+
// Obviously-fake values: a regression back to a baked-in host literal fails here.
|
|
17
|
+
const API_BASE = 'https://api.example.test';
|
|
18
|
+
const STATUS_DATA_URL = 'https://data.example.test/status-data/status.json';
|
|
19
|
+
const BRAND = {
|
|
20
|
+
identity: { productName: 'Acme Rockets', worldName: 'Acme' },
|
|
21
|
+
currency: { label: 'tokens' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const STATUS_SCRIPT = '<script src="/status.js?v=test"></script>';
|
|
25
|
+
|
|
26
|
+
// An in-test fixture — no dependency on the real modules/status-ui/public/index.html,
|
|
27
|
+
// so an edit to the dashboard can never make this file spuriously red. Any of the
|
|
28
|
+
// three anchors can be dropped to prove the abort path.
|
|
29
|
+
function fixture({ headClose = '</head>', bodyOpen = '<body>', statusScript = STATUS_SCRIPT } = {}) {
|
|
30
|
+
return [
|
|
31
|
+
'<!doctype html>',
|
|
32
|
+
'<html>',
|
|
33
|
+
'<head>',
|
|
34
|
+
'<title>{{worldName}} status</title>',
|
|
35
|
+
headClose,
|
|
36
|
+
bodyOpen,
|
|
37
|
+
'<h1>{{productName}} — {{Currency}} ({{currency}}) at {{publicOrigin}}</h1>',
|
|
38
|
+
statusScript,
|
|
39
|
+
'</body>',
|
|
40
|
+
'</html>',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const build = (over = {}) => buildMirrorIndex({
|
|
46
|
+
srcHtml: fixture(),
|
|
47
|
+
apiBase: API_BASE,
|
|
48
|
+
statusDataUrl: STATUS_DATA_URL,
|
|
49
|
+
brand: BRAND,
|
|
50
|
+
...over,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the head script emits the apiBase that was passed in (no baked-in host)', () => {
|
|
54
|
+
const out = build();
|
|
55
|
+
assert.ok(out.includes(`window.STATUS_API_BASE = ${JSON.stringify(API_BASE)};`), 'STATUS_API_BASE carries the injected origin');
|
|
56
|
+
assert.ok(out.indexOf('window.STATUS_API_BASE') < out.indexOf('</head>'), 'lands in <head>');
|
|
57
|
+
// and the client-safe brand pack rides along, as serve-internal does on the droplet
|
|
58
|
+
assert.ok(out.includes(`window.__BRANDING__ = ${JSON.stringify(BRAND)};`));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('the banner driver emits the statusDataUrl that was passed in, and names the configured site', () => {
|
|
62
|
+
const out = build();
|
|
63
|
+
assert.ok(out.includes(`var OFFBOX_URL = ${JSON.stringify(STATUS_DATA_URL)};`), 'the prober URL is the injected one');
|
|
64
|
+
// the up/down copy names the host derived from apiBase, not a hardcoded domain
|
|
65
|
+
assert.ok(out.includes("'\\uD83D\\uDFE2 api.example.test is operational"));
|
|
66
|
+
assert.ok(out.includes("'\\uD83D\\uDD34 api.example.test is DOWN"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the banner div lands immediately after <body>, and the driver before status.js', () => {
|
|
70
|
+
const out = build();
|
|
71
|
+
const afterBody = out.slice(out.indexOf('<body>') + '<body>'.length);
|
|
72
|
+
assert.ok(afterBody.startsWith('\n<!-- Off-box availability banner (ADR 0029)'), 'banner block opens right after <body>');
|
|
73
|
+
assert.ok(afterBody.includes('<div id="offbox-status" role="status" aria-live="polite"></div>'));
|
|
74
|
+
assert.ok(afterBody.indexOf('<div id="offbox-status"') < afterBody.indexOf('<h1>'), 'banner precedes the page content');
|
|
75
|
+
assert.ok(out.indexOf('var OFFBOX_URL') < out.indexOf('<script src="/status.js'), 'driver loads before the dashboard script');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('a fixture missing any one anchor ABORTS — a silent mis-publish is worse than a failed run', () => {
|
|
79
|
+
assert.throws(() => build({ srcHtml: fixture({ headClose: '' }) }), /anchor not found.*<\/head>/s);
|
|
80
|
+
assert.throws(() => build({ srcHtml: fixture({ bodyOpen: '' }) }), /anchor not found.*<body>/s);
|
|
81
|
+
assert.throws(() => build({ srcHtml: fixture({ statusScript: '' }) }), /anchor not found.*status\.js/s);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('injectOnce places before/after, and throws rather than skipping a missing anchor', () => {
|
|
85
|
+
assert.equal(injectOnce('ab', 'a', 'X', 'after'), 'aXb');
|
|
86
|
+
assert.equal(injectOnce('ab', 'b', 'X', 'before'), 'aXb');
|
|
87
|
+
assert.throws(() => injectOnce('ab', 'zz', 'X', 'before'), /refusing a silent mis-publish/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a source that already carries the offbox markup is refused (it must be the droplet copy)', () => {
|
|
91
|
+
const alreadyMirrored = build();
|
|
92
|
+
assert.throws(() => build({ srcHtml: alreadyMirrored }), /already contains offbox markup/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('an empty statusDataUrl throws and names the config key, instead of publishing a dead banner', () => {
|
|
96
|
+
assert.throws(() => build({ statusDataUrl: '' }), /domains\.statusDataUrl/);
|
|
97
|
+
assert.throws(() => build({ statusDataUrl: undefined }), /domains\.statusDataUrl/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('brand tokens resolve from the INJECTED brand, not ambient config', () => {
|
|
101
|
+
const out = build();
|
|
102
|
+
assert.ok(out.includes('<title>Acme status</title>'));
|
|
103
|
+
assert.ok(out.includes('<h1>Acme Rockets — Tokens (tokens) at https://api.example.test</h1>'));
|
|
104
|
+
assert.doesNotMatch(out, /\{\{(worldName|productName|Currency|currency|publicOrigin)\}\}/);
|
|
105
|
+
// pure: an empty brand blanks the tokens rather than reaching for branding()
|
|
106
|
+
assert.equal(brandTokens('[{{worldName}}][{{currency}}][{{publicOrigin}}]', {}, ''), '[][credits][]');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the publisher source hardcodes no host identity (the ratchet this slice installs)', () => {
|
|
110
|
+
// Mirrors scripts/gds/fitness.js stripCommentsForIdentity: block comments, then
|
|
111
|
+
// `://`-safe line comments split on /\r?\n/ — NOT '\n'. On this CRLF checkout a
|
|
112
|
+
// '\n' split leaves a trailing \r that `.*$` can never cross, so no line comment
|
|
113
|
+
// would strip and rationale prose would scan as code (the task 1002428 bug).
|
|
114
|
+
const src = readFileSync(new URL('../scripts/status-mirror-sync.js', import.meta.url), 'utf8');
|
|
115
|
+
const stripped = src
|
|
116
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.map((l) => l.replace(/(^|[^:])\/\/.*$/, '$1'))
|
|
119
|
+
.join('\n');
|
|
120
|
+
const hit = stripped.match(/amazonprimea|lars589|off the boats/i);
|
|
121
|
+
assert.equal(hit, null, `scripts/status-mirror-sync.js hardcodes the host identity "${hit && hit[0]}" — resolve it from branding() instead`);
|
|
122
|
+
});
|
package/tests/stop_refcheck.mjs
CHANGED
|
@@ -16,7 +16,25 @@ import os from 'node:os';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
|
|
18
18
|
const require = createRequire(import.meta.url);
|
|
19
|
-
|
|
19
|
+
// The Stop hook lives in .claude/hooks/, which the neutral core does NOT carry:
|
|
20
|
+
// it's instance-materialized and package-excluded (PROTECTED_TOPOLOGY_EXCLUDE in
|
|
21
|
+
// scripts/gds/publish-manifest.js). A clean core checkout therefore has no hook to
|
|
22
|
+
// require — an unconditional require() threw MODULE_NOT_FOUND and CRASHED this file
|
|
23
|
+
// at load (uncaught, printing a requireStack), red-Xing the unit gate (task 1002234).
|
|
24
|
+
// Skip cleanly when the hook is absent; the suite still runs in full on an instance
|
|
25
|
+
// whose .claude/hooks/stop-refcheck.js is materialized.
|
|
26
|
+
let hook;
|
|
27
|
+
try {
|
|
28
|
+
hook = require('../.claude/hooks/stop-refcheck.js');
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err && err.code === 'MODULE_NOT_FOUND') {
|
|
31
|
+
console.log('stop_refcheck: SKIP — .claude/hooks/stop-refcheck.js is not present in this checkout');
|
|
32
|
+
console.log(' (instance-materialized + package-excluded; the neutral core carries no copy to test).');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
const { evaluate, buildReason, lastAssistantText, RESOLVER_BASE } = hook;
|
|
20
38
|
|
|
21
39
|
let passed = 0;
|
|
22
40
|
let failed = 0;
|
package/tests/task_goal_link.mjs
CHANGED
|
@@ -62,10 +62,14 @@ await test('a version with a general goal → returns its id via one SELECT on t
|
|
|
62
62
|
assert.equal(out, 3, 'returns the general goal id');
|
|
63
63
|
assert.equal(pool.queries.length, 1, 'exactly one SELECT');
|
|
64
64
|
const q = pool.queries[0];
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// task 1002539: the title is no longer inlined in SQL — it is BOUND, built by the
|
|
66
|
+
// shared goal-advisory.generalGoalTitle so this lookup and the orphan advisory that
|
|
67
|
+
// names the same goal cannot drift apart.
|
|
68
|
+
assert.match(q.sql, /SELECT id FROM goals WHERE version_id = \$1 AND title = \$2/,
|
|
69
|
+
'keyed on version_id + a BOUND title (one definition, in goal-advisory.js)');
|
|
67
70
|
assert.match(q.sql, /LIMIT 1/, 'single row');
|
|
68
|
-
assert.deepEqual(q.params, ['BONGOS-V1'
|
|
71
|
+
assert.deepEqual(q.params, ['BONGOS-V1', 'BONGOS-V1 — general'],
|
|
72
|
+
'binds [versionId, "<version> — general"] — the spaced em-dash from migration 160');
|
|
69
73
|
});
|
|
70
74
|
|
|
71
75
|
await test('a version with no general goal → null (caller leaves goal_id NULL, never fails the create)', async () => {
|
|
@@ -73,14 +77,19 @@ await test('a version with no general goal → null (caller leaves goal_id NULL,
|
|
|
73
77
|
const out = await generalGoalIdForVersion('SOME-VERSION', { pool });
|
|
74
78
|
assert.equal(out, null, 'no general goal → null fallback');
|
|
75
79
|
assert.equal(pool.queries.length, 1, 'it did query — just found nothing');
|
|
76
|
-
assert.deepEqual(pool.queries[0].params, ['SOME-VERSION']);
|
|
80
|
+
assert.deepEqual(pool.queries[0].params, ['SOME-VERSION', 'SOME-VERSION — general']);
|
|
77
81
|
});
|
|
78
82
|
|
|
79
83
|
await test('the em-dash in the title match is U+2014 (not a hyphen/en-dash) — or the backfill silently matches nothing', async () => {
|
|
80
84
|
const pool = fakePool([{ id: 9 }]);
|
|
81
85
|
await generalGoalIdForVersion('V2', { pool });
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// The em-dash moved from the SQL text into the BOUND param (task 1002539); assert
|
|
87
|
+
// it there, which is stricter — the exact string Postgres compares.
|
|
88
|
+
const title = pool.queries[0].params[1];
|
|
89
|
+
assert.ok(title.includes('—'), 'the bound title contains an em-dash (U+2014)');
|
|
90
|
+
assert.equal(title.charCodeAt(title.indexOf('—')), 0x2014, 'U+2014 exactly');
|
|
91
|
+
assert.ok(!title.includes(' - general'), 'not a hyphen');
|
|
92
|
+
assert.ok(!title.includes(' – general'), 'not an en dash');
|
|
84
93
|
});
|
|
85
94
|
|
|
86
95
|
// -------------------------------------------------------------------------
|