@bongos/core 1.18.0 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// tests/criterion_authoring_gate.mjs — who may author a goal's done-when criteria
|
|
2
|
+
// (task 1002538 / ADR 0154).
|
|
3
|
+
//
|
|
4
|
+
// POST /versions/:id/done-when was Archon-only. It is now Metic+ WALLED BY GOAL
|
|
5
|
+
// OWNERSHIP: a non-Archon must name a goal they own or manage, on an open
|
|
6
|
+
// (planning|building) version. The wall is load-bearing, not decoration — creating an unsatisfied
|
|
7
|
+
// criterion is a backward ratchet with no undo:
|
|
8
|
+
// - version-close.js counts satisfied === total per VERSION (regardless of goal),
|
|
9
|
+
// so one unsatisfied row parks the version's close suggestion indefinitely;
|
|
10
|
+
// - db.achieveGoalIfComplete refuses while unsatisfied > 0, so it also parks the
|
|
11
|
+
// goal and the dependents its achievement would release;
|
|
12
|
+
// - there is NO delete route — descoping is direct SQL only.
|
|
13
|
+
// That is why this is not a flat re-rank, and why the pins below matter.
|
|
14
|
+
//
|
|
15
|
+
// Runs the PURE decision core (no DB, no network): the ordered gate
|
|
16
|
+
// authorizeCriterionCreate, the route-source contract, and the route-rank PIN.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/criterion_authoring_gate.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { makeRunner } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
process.env.NODE_ENV = 'test';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const buildDoneWhenRouter = require('../modules/lifecycle/routes/done-when.js');
|
|
29
|
+
const { authorizeCriterionCreate } = buildDoneWhenRouter;
|
|
30
|
+
const { authorizeGoalManagement, isGoalOwnerOrManager } = require('../modules/lifecycle/goal-authz.js');
|
|
31
|
+
const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
|
|
32
|
+
|
|
33
|
+
const doneWhenSrc = readFileSync(new URL('../modules/lifecycle/routes/done-when.js', import.meta.url), 'utf8');
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
// ---- fixtures ----
|
|
37
|
+
const OWNER = 1001;
|
|
38
|
+
const MANAGER = 1002;
|
|
39
|
+
const MEMBER = 1003;
|
|
40
|
+
const STRANGER = 1004;
|
|
41
|
+
|
|
42
|
+
const V_BUILDING = { id: 'BONGOS-V1', status: 'building' };
|
|
43
|
+
const V_SHIPPED = { id: 'GDS-V4', status: 'shipped' };
|
|
44
|
+
const V_PLANNING = { id: 'CB-V2', status: 'planning' };
|
|
45
|
+
const V_FROZEN = { id: 'CB-V3', status: 'frozen' };
|
|
46
|
+
|
|
47
|
+
const G_OPEN = { id: 42, version_id: 'BONGOS-V1', status: 'open', created_by: OWNER };
|
|
48
|
+
const G_ACHIEVED = { id: 43, version_id: 'BONGOS-V1', status: 'achieved', created_by: OWNER };
|
|
49
|
+
const G_ARCHIVED = { id: 44, version_id: 'BONGOS-V1', status: 'archived', created_by: OWNER };
|
|
50
|
+
const G_SYSTEM = { id: 45, version_id: 'BONGOS-V1', status: 'open', created_by: null };
|
|
51
|
+
|
|
52
|
+
const LEAD = { role: 'lead' };
|
|
53
|
+
const PLAIN = { role: 'member' };
|
|
54
|
+
|
|
55
|
+
const ask = (o) => authorizeCriterionCreate({ version: V_BUILDING, actorRank: 'metic', ...o });
|
|
56
|
+
|
|
57
|
+
// ---- the allow cases ----
|
|
58
|
+
|
|
59
|
+
await test('the goal OWNER may author their goal\'s criterion', () => {
|
|
60
|
+
const d = ask({ goal: G_OPEN, actorId: OWNER, actorMembership: LEAD });
|
|
61
|
+
assert.equal(d.ok, true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await test('owner authority is created_by, not the role row — an owner with no membership still passes', () => {
|
|
65
|
+
const d = ask({ goal: G_OPEN, actorId: OWNER, actorMembership: null });
|
|
66
|
+
assert.equal(d.ok, true, 'created_by is the fixed authority (goal-authz isGoalOwner)');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await test('a MANAGER (goal_members.role=lead) may author', () => {
|
|
70
|
+
const d = ask({ goal: G_OPEN, actorId: MANAGER, actorMembership: LEAD });
|
|
71
|
+
assert.equal(d.ok, true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---- the refusals, in gate order ----
|
|
75
|
+
|
|
76
|
+
await test('a non-Archon with NO goal_id is refused — version-level criteria stay Archon-only', () => {
|
|
77
|
+
const d = ask({ goal: null, actorId: OWNER, actorMembership: null });
|
|
78
|
+
assert.equal(d.ok, false);
|
|
79
|
+
assert.equal(d.status, 403);
|
|
80
|
+
assert.equal(d.body.error, 'goal_id_required');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('a plain MEMBER may not author — membership is not management', () => {
|
|
84
|
+
const d = ask({ goal: G_OPEN, actorId: MEMBER, actorMembership: PLAIN });
|
|
85
|
+
assert.equal(d.ok, false);
|
|
86
|
+
assert.equal(d.maskAsBadGoalId, true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('a STRANGER (no membership at all) may not author', () => {
|
|
90
|
+
const d = ask({ goal: G_OPEN, actorId: STRANGER, actorMembership: null });
|
|
91
|
+
assert.equal(d.ok, false);
|
|
92
|
+
assert.equal(d.maskAsBadGoalId, true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await test('an ACHIEVED goal is closed to new criteria (409) — no reopening settled scope', () => {
|
|
96
|
+
const d = ask({ goal: G_ACHIEVED, actorId: OWNER, actorMembership: LEAD });
|
|
97
|
+
assert.equal(d.ok, false);
|
|
98
|
+
assert.equal(d.status, 409);
|
|
99
|
+
assert.equal(d.body.error, 'goal_not_open');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await test('an ARCHIVED goal is closed to new criteria (409)', () => {
|
|
103
|
+
const d = ask({ goal: G_ARCHIVED, actorId: OWNER, actorMembership: LEAD });
|
|
104
|
+
assert.equal(d.ok, false);
|
|
105
|
+
assert.equal(d.status, 409);
|
|
106
|
+
assert.equal(d.body.error, 'goal_not_open');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---- NO-LEAK: the route must not become a goal-existence oracle (ADR 0112) ----
|
|
110
|
+
// Before ADR 0154 this route was Archon-only and Archons read every goal, so no
|
|
111
|
+
// probe existed. Now a Metic may call it — so an unauthorized caller must not be
|
|
112
|
+
// able to tell "this goal id exists but isn't mine" from "no such goal".
|
|
113
|
+
|
|
114
|
+
await test('an unauthorized caller is MASKED — no status/body of its own to compare', () => {
|
|
115
|
+
const d = ask({ goal: G_OPEN, actorId: STRANGER, actorMembership: null });
|
|
116
|
+
assert.equal(d.maskAsBadGoalId, true);
|
|
117
|
+
assert.equal(d.status, undefined, 'the gate must not pick a distinguishable status');
|
|
118
|
+
assert.equal(d.body, undefined, 'the route renders the identical bad_goal_id 400');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await test('the mask is IDENTICAL across every unauthorized shape — open, achieved, archived, private-ish', () => {
|
|
122
|
+
const probes = [G_OPEN, G_ACHIEVED, G_ARCHIVED, G_SYSTEM].map((goal) =>
|
|
123
|
+
ask({ goal, actorId: STRANGER, actorMembership: null }));
|
|
124
|
+
for (const d of probes) assert.deepEqual(d, { ok: false, maskAsBadGoalId: true });
|
|
125
|
+
// A goal's STATE must not leak either: an outsider cannot learn that a goal id
|
|
126
|
+
// exists-but-is-archived, which `goal_not_open` would have told them.
|
|
127
|
+
assert.equal(new Set(probes.map((d) => JSON.stringify(d))).size, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await test('authority is checked BEFORE goal state — the ordering that makes the mask total', () => {
|
|
131
|
+
// An owner on an archived goal DOES get goal_not_open (useful, and they already
|
|
132
|
+
// know the goal exists). A stranger on the same goal gets the mask instead.
|
|
133
|
+
assert.equal(ask({ goal: G_ARCHIVED, actorId: OWNER, actorMembership: LEAD }).body.error, 'goal_not_open');
|
|
134
|
+
assert.equal(ask({ goal: G_ARCHIVED, actorId: STRANGER, actorMembership: null }).maskAsBadGoalId, true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await test('the route renders the mask through the SAME bad_goal_id helper as a truly absent goal', () => {
|
|
138
|
+
assert.match(doneWhenSrc, /const badGoalId = \(\) => res\.fail\('bad_goal_id'/, 'one helper builds the refusal');
|
|
139
|
+
assert.match(doneWhenSrc, /if \(!goal \|\| String\(goal\.version_id\) !== String\(versionId\)\) return badGoalId\(\);/, 'absent/foreign goal path');
|
|
140
|
+
assert.match(doneWhenSrc, /if \(decision\.maskAsBadGoalId\) return badGoalId\(\);/, 'masked path — same helper, so same code+message+envelope');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await test('a SHIPPED version is refused (409) — closed scope', () => {
|
|
144
|
+
const d = authorizeCriterionCreate({
|
|
145
|
+
goal: G_OPEN, version: V_SHIPPED, actorId: OWNER, actorRank: 'metic', actorMembership: LEAD,
|
|
146
|
+
});
|
|
147
|
+
assert.equal(d.ok, false);
|
|
148
|
+
assert.equal(d.status, 409);
|
|
149
|
+
assert.equal(d.body.error, 'version_closed');
|
|
150
|
+
assert.equal(d.body.status, 'shipped', 'the refusal names the version status');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await test('a FROZEN version is refused (409) — freezing exists to stop scope writes', () => {
|
|
154
|
+
const d = authorizeCriterionCreate({
|
|
155
|
+
goal: G_OPEN, version: V_FROZEN, actorId: OWNER, actorRank: 'metic', actorMembership: LEAD,
|
|
156
|
+
});
|
|
157
|
+
assert.equal(d.ok, false);
|
|
158
|
+
assert.equal(d.body.error, 'version_closed');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// The bug this test exists to prevent: gating on `status === 'building'` blocks a
|
|
162
|
+
// PLANNING version, which is exactly when a planning session writes its criteria.
|
|
163
|
+
await test('a PLANNING version is ALLOWED — that is when criteria get written', () => {
|
|
164
|
+
const d = authorizeCriterionCreate({
|
|
165
|
+
goal: G_OPEN, version: V_PLANNING, actorId: OWNER, actorRank: 'metic', actorMembership: LEAD,
|
|
166
|
+
});
|
|
167
|
+
assert.equal(d.ok, true, 'planning is an OPEN version state, not a closed one');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await test('a system goal (created_by NULL) has no owner — a Metic cannot claim it', () => {
|
|
171
|
+
const d = ask({ goal: G_SYSTEM, actorId: OWNER, actorMembership: null });
|
|
172
|
+
assert.equal(d.ok, false);
|
|
173
|
+
assert.equal(d.maskAsBadGoalId, true);
|
|
174
|
+
// ...but a manager row still admits, and an Archon always does (below).
|
|
175
|
+
assert.equal(ask({ goal: G_SYSTEM, actorId: OWNER, actorMembership: LEAD }).ok, true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---- fail-closed on ambiguity ----
|
|
179
|
+
|
|
180
|
+
await test('FAIL-CLOSED: a missing version denies rather than allows', () => {
|
|
181
|
+
const d = authorizeCriterionCreate({
|
|
182
|
+
goal: G_OPEN, version: null, actorId: OWNER, actorRank: 'metic', actorMembership: LEAD,
|
|
183
|
+
});
|
|
184
|
+
assert.equal(d.ok, false);
|
|
185
|
+
assert.equal(d.body.error, 'version_closed');
|
|
186
|
+
assert.equal(d.body.status, null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await test('FAIL-CLOSED: an unknown rank is treated as a non-Archon', () => {
|
|
190
|
+
const d = authorizeCriterionCreate({
|
|
191
|
+
goal: null, version: V_BUILDING, actorId: STRANGER, actorRank: 'xenos', actorMembership: null,
|
|
192
|
+
});
|
|
193
|
+
assert.equal(d.ok, false, 'xenos never reaches here (requireRank stops it) but the gate must not allow');
|
|
194
|
+
assert.equal(d.body.error, 'goal_id_required');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ---- the Archon arm ----
|
|
198
|
+
|
|
199
|
+
await test('an ARCHON short-circuits every branch — goal-less, achieved goal, shipped version', () => {
|
|
200
|
+
const archon = { actorId: STRANGER, actorRank: 'archon', actorMembership: null };
|
|
201
|
+
assert.equal(authorizeCriterionCreate({ goal: null, version: V_BUILDING, ...archon }).ok, true);
|
|
202
|
+
assert.equal(authorizeCriterionCreate({ goal: G_ACHIEVED, version: V_BUILDING, ...archon }).ok, true);
|
|
203
|
+
assert.equal(authorizeCriterionCreate({ goal: G_OPEN, version: V_SHIPPED, ...archon }).ok, true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---- ordering (no information leaks, cheapest check first) ----
|
|
207
|
+
|
|
208
|
+
await test('ordering: goal_id_required precedes the version check', () => {
|
|
209
|
+
const d = authorizeCriterionCreate({
|
|
210
|
+
goal: null, version: V_SHIPPED, actorId: STRANGER, actorRank: 'metic', actorMembership: null,
|
|
211
|
+
});
|
|
212
|
+
assert.equal(d.body.error, 'goal_id_required', 'a caller who named no goal never learns the version state');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await test('ordering: an unauthorized caller never reaches the goal-state branch', () => {
|
|
216
|
+
const d = ask({ goal: G_ARCHIVED, actorId: STRANGER, actorMembership: null });
|
|
217
|
+
assert.equal(d.maskAsBadGoalId, true, 'goal_not_open would confirm the goal exists');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---- the wall is the SAME one the sibling goal routes use ----
|
|
221
|
+
|
|
222
|
+
await test('the gate reuses the SAME owner|manager predicate as routes/goals.js — one definition', () => {
|
|
223
|
+
// It cannot delegate to authorizeGoalManagement wholesale (that checks goal state
|
|
224
|
+
// FIRST, which is exactly the leak); it shares the authority predicate instead.
|
|
225
|
+
for (const [membership, actorId, expected] of [
|
|
226
|
+
[LEAD, MANAGER, true], [PLAIN, MEMBER, false], [null, OWNER, true], [null, STRANGER, false],
|
|
227
|
+
]) {
|
|
228
|
+
assert.equal(
|
|
229
|
+
isGoalOwnerOrManager({ goal: G_OPEN, actorId, actorRank: 'metic', actorMembership: membership }),
|
|
230
|
+
expected
|
|
231
|
+
);
|
|
232
|
+
assert.equal(ask({ goal: G_OPEN, actorId, actorMembership: membership }).ok === true, expected,
|
|
233
|
+
'the gate agrees with the shared predicate');
|
|
234
|
+
}
|
|
235
|
+
// ...and authorizeGoalManagement is built on that same predicate.
|
|
236
|
+
assert.equal(authorizeGoalManagement({ goal: G_OPEN, actorId: MEMBER, actorRank: 'metic', actorMembership: PLAIN }).ok, false);
|
|
237
|
+
assert.equal(authorizeGoalManagement({ goal: G_OPEN, actorId: OWNER, actorRank: 'metic', actorMembership: null }).ok, true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ---- route-source + pin contracts (drift catchers) ----
|
|
241
|
+
|
|
242
|
+
await test('the route is gated metic+archon and calls the pure gate', () => {
|
|
243
|
+
assert.match(
|
|
244
|
+
doneWhenSrc,
|
|
245
|
+
/router\.post\('\/versions\/:id\/done-when',\s*auth\.requireBuilder,\s*auth\.requireRank\('metic',\s*'archon'\)/,
|
|
246
|
+
'the middleware must be requireRank(metic, archon)'
|
|
247
|
+
);
|
|
248
|
+
assert.match(doneWhenSrc, /authorizeCriterionCreate\(\{/, 'the handler must call the pure gate, not inline the decision');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await test('the `// rank:` annotation matches the middleware (gen-api-docs reads the annotation)', () => {
|
|
252
|
+
assert.match(
|
|
253
|
+
doneWhenSrc,
|
|
254
|
+
/\/\/ rank: metic\+archon — criterion create/,
|
|
255
|
+
'a stale annotation publishes the wrong rank to openapi.json + the typed client'
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await test('unsatisfy stays ARCHON — the backward ratchet is not part of this grant', () => {
|
|
260
|
+
assert.match(
|
|
261
|
+
doneWhenSrc,
|
|
262
|
+
/router\.post\('\/done-when\/:criterionId\/unsatisfy',\s*auth\.requireBuilder,\s*auth\.requireRank\('archon'\)/,
|
|
263
|
+
'un-flipping a confirmed criterion rewrites shipped scope-truth'
|
|
264
|
+
);
|
|
265
|
+
assert.equal(MODULE_EXPECTED_RANKS.lifecycle['POST /done-when/:criterionId/unsatisfy'], 'archon');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await test('the route-rank PIN records metic+archon (fitness reds the build on drift)', () => {
|
|
269
|
+
assert.equal(MODULE_EXPECTED_RANKS.lifecycle['POST /versions/:id/done-when'], 'metic+archon');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
summary();
|
|
@@ -17,7 +17,7 @@ import { strict as assert } from 'node:assert';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
|
-
const { buildCriterionProgress, buildGoals } = require('../modules/lifecycle/done-when.js');
|
|
20
|
+
const { buildCriterionProgress, buildGoals, isPendingReview } = require('../modules/lifecycle/done-when.js');
|
|
21
21
|
|
|
22
22
|
let passed = 0;
|
|
23
23
|
let failed = 0;
|
|
@@ -215,6 +215,38 @@ test('buildGoals: pure grouping, empty goalRows → []', () => {
|
|
|
215
215
|
assert.deepEqual(buildGoals([], []), []);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
// isPendingReview — the ONE derivation of the "met — pending review" flag
|
|
219
|
+
// (task 1002164). Extracted so the rollup here and the hall's goal-detail read
|
|
220
|
+
// (db.listCriteriaForGoal) cannot drift apart; the SQL HAVING clause in
|
|
221
|
+
// listPendingReviewCriteria is the third expression of the same rule.
|
|
222
|
+
test('isPendingReview: every linked task shipped + not yet confirmed → true', () => {
|
|
223
|
+
assert.equal(isPendingReview({ satisfied: false, linked: 2, unshipped: 0 }), true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('isPendingReview: an already-confirmed criterion is never pending', () => {
|
|
227
|
+
assert.equal(isPendingReview({ satisfied: true, linked: 2, unshipped: 0 }), false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('isPendingReview: zero linked tasks is NOT pending (no vacuous auto-flag)', () => {
|
|
231
|
+
assert.equal(isPendingReview({ satisfied: false, linked: 0, unshipped: 0 }), false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('isPendingReview: any unshipped linked task blocks the flag', () => {
|
|
235
|
+
assert.equal(isPendingReview({ satisfied: false, linked: 3, unshipped: 1 }), false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('isPendingReview: agrees with the rollup it was extracted from', () => {
|
|
239
|
+
// Same fixture through buildCriterionProgress — the two must never disagree.
|
|
240
|
+
const shipped = [row(1, 10, 'c', false, 100, 'a', 'shipped'), row(1, 10, 'c', false, 101, 'b', 'shipped')];
|
|
241
|
+
const mixed = [row(1, 11, 'd', false, 200, 'c', 'shipped'), row(1, 11, 'd', false, 201, 'd', 'ready')];
|
|
242
|
+
const a = buildCriterionProgress('V', shipped, 0).criteria[0];
|
|
243
|
+
const b = buildCriterionProgress('V', mixed, 0).criteria[0];
|
|
244
|
+
assert.equal(a.pending_review, isPendingReview({ satisfied: a.satisfied, linked: a.counts.total, unshipped: a.counts.remaining }));
|
|
245
|
+
assert.equal(b.pending_review, isPendingReview({ satisfied: b.satisfied, linked: b.counts.total, unshipped: b.counts.remaining }));
|
|
246
|
+
assert.equal(a.pending_review, true);
|
|
247
|
+
assert.equal(b.pending_review, false);
|
|
248
|
+
});
|
|
249
|
+
|
|
218
250
|
console.log('');
|
|
219
251
|
console.log(`criterionProgress aggregation tests: ${passed} passed, ${failed} failed`);
|
|
220
252
|
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// tests/dev_lib.mjs — hermetic unit smoke for scripts/gds/dev-lib.js (the `bongos
|
|
2
|
+
// dev` local launcher core, ADR 0149). NO real spawn, DB, network, or fs writes:
|
|
3
|
+
// fs / spawn / kill / fetchImpl / env / userInfo are all injected.
|
|
4
|
+
//
|
|
5
|
+
// Run: node tests/dev_lib.mjs
|
|
6
|
+
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const lib = require('../scripts/gds/dev-lib.js');
|
|
12
|
+
|
|
13
|
+
let passed = 0, failed = 0;
|
|
14
|
+
function t(name, fn) {
|
|
15
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
16
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
17
|
+
}
|
|
18
|
+
async function ta(name, fn) {
|
|
19
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
20
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A fake fs.readFileSync that maps by path suffix; unknown paths throw ENOENT.
|
|
24
|
+
function fakeFs(map) {
|
|
25
|
+
return {
|
|
26
|
+
readFileSync(p) {
|
|
27
|
+
for (const [suffix, body] of Object.entries(map)) {
|
|
28
|
+
if (String(p).endsWith(suffix)) return body;
|
|
29
|
+
}
|
|
30
|
+
const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const NEUTRAL = JSON.stringify({
|
|
36
|
+
domains: { publicOrigin: 'http://localhost:3000' },
|
|
37
|
+
db: { database: null }, firstAdmin: '', envPrefix: 'CLOUDBONGOS', configDir: 'cloudbongos',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---------- port derivation ----------
|
|
41
|
+
t('parsePort: reads the branding port, falls back when none/invalid', () => {
|
|
42
|
+
assert.equal(lib.parsePort('http://localhost:3006'), 3006);
|
|
43
|
+
assert.equal(lib.parsePort('https://cloudbongos.com'), 3000); // no explicit port
|
|
44
|
+
assert.equal(lib.parsePort('not-a-url'), 3000);
|
|
45
|
+
assert.equal(lib.parsePort('http://localhost:3006', 9999), 3006);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ---------- db name coercion + precedence ----------
|
|
49
|
+
t('sanitizeDbName: legal pg identifier, leading digit fixed, junk -> null', () => {
|
|
50
|
+
assert.equal(lib.sanitizeDbName('Test4'), 'test4');
|
|
51
|
+
assert.equal(lib.sanitizeDbName('my cool app!'), 'my_cool_app');
|
|
52
|
+
assert.equal(lib.sanitizeDbName('9lives'), 'db_9lives');
|
|
53
|
+
assert.equal(lib.sanitizeDbName('---'), null);
|
|
54
|
+
assert.equal(lib.sanitizeDbName(''), null);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('deriveDbName: override > branding > repo dir > default', () => {
|
|
58
|
+
assert.equal(lib.deriveDbName({ override: 'Chosen', brandingDbName: 'b', instanceRoot: '/x/r' }), 'chosen');
|
|
59
|
+
assert.equal(lib.deriveDbName({ brandingDbName: 'brandDb', instanceRoot: '/x/r' }), 'branddb');
|
|
60
|
+
assert.equal(lib.deriveDbName({ brandingDbName: null, instanceRoot: '/Users/x/test4' }), 'test4');
|
|
61
|
+
assert.equal(lib.deriveDbName({ brandingDbName: null, instanceRoot: null }), 'cloudbongos_dev');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------- branding facts (instance over neutral) ----------
|
|
65
|
+
t('loadBrandingFacts: instance pack wins, neutral fills gaps', () => {
|
|
66
|
+
const f = fakeFs({
|
|
67
|
+
'config/branding.json': JSON.stringify({ domains: { publicOrigin: 'http://localhost:3006' }, firstAdmin: 'lars589', envPrefix: 'CLOUDBONGOS' }),
|
|
68
|
+
'config/branding.neutral.json': NEUTRAL,
|
|
69
|
+
});
|
|
70
|
+
const facts = lib.loadBrandingFacts({ instanceRoot: '/x/test4', coreRoot: '/core', f });
|
|
71
|
+
assert.equal(facts.publicOrigin, 'http://localhost:3006');
|
|
72
|
+
assert.equal(facts.firstAdmin, 'lars589');
|
|
73
|
+
assert.equal(facts.configDir, 'cloudbongos');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------- full run params ----------
|
|
77
|
+
t('resolveRunParams: port from branding, db from repo dir, TCP localhost, gdsOnly', () => {
|
|
78
|
+
const f = fakeFs({
|
|
79
|
+
'config/branding.json': JSON.stringify({ domains: { publicOrigin: 'http://localhost:3006' }, firstAdmin: 'lars589' }),
|
|
80
|
+
'config/branding.neutral.json': NEUTRAL,
|
|
81
|
+
'config/modules.json': JSON.stringify({ modules: { game: false } }),
|
|
82
|
+
});
|
|
83
|
+
const p = lib.resolveRunParams({ instanceRoot: '/Users/x/test4', coreRoot: '/core', f, env: {}, userInfo: () => ({ username: 'lars' }), argv: [] });
|
|
84
|
+
assert.equal(p.port, 3006);
|
|
85
|
+
assert.equal(p.dbName, 'test4');
|
|
86
|
+
assert.equal(p.pgHost, 'localhost'); // NOT the /var/run/postgresql linux socket
|
|
87
|
+
assert.equal(p.pgUser, 'lars');
|
|
88
|
+
assert.equal(p.gdsOnly, true); // game off -> skip OTB seeds
|
|
89
|
+
assert.equal(p.publicOrigin, 'http://localhost:3006');
|
|
90
|
+
assert.equal(p.firstAdmin, 'lars589');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
t('resolveRunParams: game module ON -> not GDS-only; env PGHOST/PGUSER honored', () => {
|
|
94
|
+
const f = fakeFs({
|
|
95
|
+
'config/branding.json': JSON.stringify({ domains: { publicOrigin: 'http://localhost:3006' } }),
|
|
96
|
+
'config/branding.neutral.json': NEUTRAL,
|
|
97
|
+
'config/modules.json': JSON.stringify({ modules: { game: true } }),
|
|
98
|
+
});
|
|
99
|
+
const p = lib.resolveRunParams({ instanceRoot: '/x/g', coreRoot: '/core', f, env: { PGHOST: '/tmp', PGUSER: 'bob' }, userInfo: () => ({ username: 'ignored' }), argv: [] });
|
|
100
|
+
assert.equal(p.gdsOnly, false);
|
|
101
|
+
assert.equal(p.pgHost, '/tmp');
|
|
102
|
+
assert.equal(p.pgUser, 'bob');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
t('resolveRunParams: --port and --db overrides win', () => {
|
|
106
|
+
const f = fakeFs({
|
|
107
|
+
'config/branding.json': JSON.stringify({ domains: { publicOrigin: 'http://localhost:3006' } }),
|
|
108
|
+
'config/branding.neutral.json': NEUTRAL,
|
|
109
|
+
'config/modules.json': JSON.stringify({ modules: {} }),
|
|
110
|
+
});
|
|
111
|
+
const p = lib.resolveRunParams({ instanceRoot: '/x/test4', coreRoot: '/core', f, env: {}, userInfo: () => ({ username: 'lars' }), argv: ['--port', '4010', '--db', 'Scratch'] });
|
|
112
|
+
assert.equal(p.port, 4010);
|
|
113
|
+
assert.equal(p.dbName, 'scratch');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ---------- child env assembly ----------
|
|
117
|
+
t('buildEnv: server role sets PORT/HOST + PG trio + seed policy + instance-root', () => {
|
|
118
|
+
const params = { pgHost: 'localhost', pgUser: 'lars', dbName: 'test4', gdsOnly: true, port: 3006, host: '127.0.0.1', envPrefix: 'CLOUDBONGOS', instanceRoot: '/x/test4' };
|
|
119
|
+
const env = lib.buildEnv(params, { baseEnv: { PATH: '/bin' }, role: 'server' });
|
|
120
|
+
assert.equal(env.PORT, '3006');
|
|
121
|
+
assert.equal(env.HOST, '127.0.0.1');
|
|
122
|
+
assert.equal(env.PGDATABASE, 'test4');
|
|
123
|
+
assert.equal(env.PGHOST, 'localhost');
|
|
124
|
+
assert.equal(env.PGUSER, 'lars');
|
|
125
|
+
assert.equal(env.MEDUSA_GDS_ONLY, '1');
|
|
126
|
+
assert.equal(env.CLOUDBONGOS_INSTANCE_ROOT, '/x/test4');
|
|
127
|
+
assert.equal(env.PATH, '/bin'); // baseEnv preserved
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
t('buildEnv: migrate role sets PGDATABASE but NOT PORT', () => {
|
|
131
|
+
const params = { pgHost: 'localhost', pgUser: 'lars', dbName: 'test4', gdsOnly: false, port: 3006, host: '127.0.0.1', envPrefix: 'CLOUDBONGOS', instanceRoot: '/x/test4' };
|
|
132
|
+
const env = lib.buildEnv(params, { baseEnv: {}, role: 'migrate' });
|
|
133
|
+
assert.equal(env.PGDATABASE, 'test4');
|
|
134
|
+
assert.equal(env.PORT, undefined);
|
|
135
|
+
assert.equal(env.MEDUSA_GDS_ONLY, undefined); // gdsOnly false -> not set
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------- auth prediction (mirrors auth.js loadGithubCreds) ----------
|
|
139
|
+
t('authWillWork: true from env creds', () => {
|
|
140
|
+
assert.equal(lib.authWillWork({ resolveEnv: (s) => (s === 'GITHUB_CLIENT_ID' ? 'id' : s === 'GITHUB_CLIENT_SECRET' ? 'sec' : undefined), readConfigFileSync: () => null }), true);
|
|
141
|
+
});
|
|
142
|
+
t('authWillWork: true from config file', () => {
|
|
143
|
+
assert.equal(lib.authWillWork({ resolveEnv: () => undefined, readConfigFileSync: (n) => (n === 'gds-github.json' ? JSON.stringify({ client_id: 'i', client_secret: 's' }) : null) }), true);
|
|
144
|
+
});
|
|
145
|
+
t('authWillWork: false when neither', () => {
|
|
146
|
+
assert.equal(lib.authWillWork({ resolveEnv: () => undefined, readConfigFileSync: () => null }), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---------- managed process (pid/log per instance; probes) ----------
|
|
150
|
+
t('pid/log paths are keyed per (dbName, port)', () => {
|
|
151
|
+
const a = lib.pidPath({ dbName: 'test4', port: 3006 });
|
|
152
|
+
const b = lib.pidPath({ dbName: 'test3', port: 3005 });
|
|
153
|
+
assert.notEqual(a, b);
|
|
154
|
+
assert.match(a, /bongos-dev-test4-3006\.pid$/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
t('readPid / pidAlive / isRunning with injected fs + kill', () => {
|
|
158
|
+
const params = { dbName: 'test4', port: 3006 };
|
|
159
|
+
const deps = { fs: { readFileSync: () => '4242\n' }, kill: (pid, sig) => { if (pid !== 4242) { const e = new Error('ESRCH'); e.code = 'ESRCH'; throw e; } } };
|
|
160
|
+
assert.equal(lib.readPid(params, deps), 4242);
|
|
161
|
+
assert.equal(lib.pidAlive(4242, deps), true);
|
|
162
|
+
assert.equal(lib.isRunning(params, deps), true);
|
|
163
|
+
assert.equal(lib.pidAlive(0, deps), false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
t('startDetached: no-op when already running', () => {
|
|
167
|
+
const params = { dbName: 'test4', port: 3006, publicOrigin: 'http://localhost:3006' };
|
|
168
|
+
const deps = { fs: { readFileSync: () => '99\n' }, kill: () => true };
|
|
169
|
+
const r = lib.startDetached(params, deps);
|
|
170
|
+
assert.equal(r.already, true);
|
|
171
|
+
assert.equal(r.pid, 99);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
t('startDetached: spawns the server entry with the server env, detached', () => {
|
|
175
|
+
let spawnCall = null;
|
|
176
|
+
const params = { dbName: 'test4', port: 3006, host: '127.0.0.1', pgHost: 'localhost', pgUser: 'lars', gdsOnly: true, envPrefix: 'CLOUDBONGOS', instanceRoot: '/x/test4', coreRoot: '/core', publicOrigin: 'http://localhost:3006' };
|
|
177
|
+
const writes = {};
|
|
178
|
+
const deps = {
|
|
179
|
+
fs: { readFileSync: () => { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }, openSync: () => 7, writeFileSync: (p, v) => { writes[p] = v; } },
|
|
180
|
+
kill: () => { const e = new Error('ESRCH'); e.code = 'ESRCH'; throw e; },
|
|
181
|
+
spawn: (bin, args, opts) => { spawnCall = { bin, args, opts }; return { pid: 555, unref() {} }; },
|
|
182
|
+
env: { PATH: '/bin' }, nodePath: '/usr/bin/node',
|
|
183
|
+
};
|
|
184
|
+
const r = lib.startDetached(params, deps);
|
|
185
|
+
assert.equal(r.already, false);
|
|
186
|
+
assert.equal(r.pid, 555);
|
|
187
|
+
assert.equal(spawnCall.args[0], '/core/src/platform-server.js');
|
|
188
|
+
assert.equal(spawnCall.opts.cwd, '/x/test4');
|
|
189
|
+
assert.equal(spawnCall.opts.detached, true);
|
|
190
|
+
assert.equal(spawnCall.opts.env.PORT, '3006');
|
|
191
|
+
assert.equal(spawnCall.opts.env.PGDATABASE, 'test4');
|
|
192
|
+
assert.match(writes[lib.pidPath(params, deps)], /555/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
t('stop: kills a live pid, reports not-running otherwise', () => {
|
|
196
|
+
const params = { dbName: 'test4', port: 3006 };
|
|
197
|
+
let killed = null;
|
|
198
|
+
const live = { fs: { readFileSync: () => '77\n', rmSync() {} }, kill: (pid, sig) => { if (sig === 0) return true; killed = pid; } };
|
|
199
|
+
assert.equal(lib.stop(params, live).wasRunning, true);
|
|
200
|
+
assert.equal(killed, 77);
|
|
201
|
+
const dead = { fs: { readFileSync: () => { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }, rmSync() {} }, kill: () => {} };
|
|
202
|
+
assert.equal(lib.stop(params, dead).wasRunning, false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await ta('health: up + authConfigured parsed from the two healthz routes', async () => {
|
|
206
|
+
const params = { port: 3006 };
|
|
207
|
+
const deps = { fetchImpl: async (url) => {
|
|
208
|
+
if (url.endsWith('/api/gds/healthz')) return { status: 200, json: async () => ({ ok: true, auth_configured: true }) };
|
|
209
|
+
if (url.endsWith('/healthz')) return { status: 200 };
|
|
210
|
+
return { status: 404 };
|
|
211
|
+
} };
|
|
212
|
+
const h = await lib.health(params, deps);
|
|
213
|
+
assert.equal(h.up, true);
|
|
214
|
+
assert.equal(h.authConfigured, true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await ta('health: down when /healthz unreachable', async () => {
|
|
218
|
+
const params = { port: 3006 };
|
|
219
|
+
const deps = { fetchImpl: async () => { throw new Error('ECONNREFUSED'); } };
|
|
220
|
+
const h = await lib.health(params, deps);
|
|
221
|
+
assert.equal(h.up, false);
|
|
222
|
+
assert.equal(h.authConfigured, false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------- depsInstallPlan (task 1002405) ----------
|
|
226
|
+
// A fake fs.existsSync: a path "exists" iff it ends with one of the given suffixes.
|
|
227
|
+
function fakeExists(present = []) {
|
|
228
|
+
return { existsSync: (p) => present.some((suf) => String(p).endsWith(suf)) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
t('depsInstallPlan: node_modules present → not needed', () => {
|
|
232
|
+
const plan = lib.depsInstallPlan({ instanceRoot: '/x/inst', f: fakeExists(['node_modules']) });
|
|
233
|
+
assert.equal(plan.needed, false);
|
|
234
|
+
assert.equal(plan.cmd, null);
|
|
235
|
+
assert.deepEqual(plan.args, []);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
t('depsInstallPlan: no node_modules + lockfile → npm ci (reproducible)', () => {
|
|
239
|
+
const plan = lib.depsInstallPlan({ instanceRoot: '/x/inst', f: fakeExists(['package-lock.json']) });
|
|
240
|
+
assert.equal(plan.needed, true);
|
|
241
|
+
assert.equal(plan.cmd, 'ci');
|
|
242
|
+
assert.deepEqual(plan.args, ['ci']);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
t('depsInstallPlan: no node_modules + no lockfile → npm install', () => {
|
|
246
|
+
const plan = lib.depsInstallPlan({ instanceRoot: '/x/inst', f: fakeExists([]) });
|
|
247
|
+
assert.equal(plan.needed, true);
|
|
248
|
+
assert.equal(plan.cmd, 'install');
|
|
249
|
+
assert.deepEqual(plan.args, ['install']);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
253
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// tests/devbox_artifact_gate.mjs — task 1001463 (from idea 308).
|
|
2
|
+
//
|
|
3
|
+
// Guards the Actions-storage quota invariant in .github/workflows/build-devbox-app.yml:
|
|
4
|
+
// an ungated upload of ~500MB of installers on every `claude/**` push filled the shared
|
|
5
|
+
// storage cap and blocked ALL artifact uploads repo-wide. So every upload-artifact step
|
|
6
|
+
// must be ref-gated to `main` AND retention-bounded — while the BUILD jobs stay ungated
|
|
7
|
+
// (feature-branch CI feedback is the point of building there at all).
|
|
8
|
+
//
|
|
9
|
+
// Regex/line based like scripts/gds/lint-workflow-exec.js — the repo carries no YAML
|
|
10
|
+
// parser in its declared deps, and a test in tests/ must stay publishable (no transitive
|
|
11
|
+
// imports). DB-free, no network. Run: node tests/devbox_artifact_gate.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const WF = path.resolve(__dirname, '..', '.github', 'workflows', 'build-devbox-app.yml');
|
|
20
|
+
const MAX_RETENTION_DAYS = 7; // installers are republished to the releases repo; CI is not the archive
|
|
21
|
+
|
|
22
|
+
let passed = 0, failed = 0;
|
|
23
|
+
function test(name, fn) {
|
|
24
|
+
try { fn(); console.log(` ok ${name}`); passed++; }
|
|
25
|
+
catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lines = fs.readFileSync(WF, 'utf8').split(/\r?\n/);
|
|
29
|
+
// A step starts at a `- key:` list item; its block runs to the next list item at any indent.
|
|
30
|
+
const stepStarts = lines.reduce((acc, l, i) => (/^\s+-\s+\S/.test(l) ? [...acc, i] : acc), []);
|
|
31
|
+
function stepBlockAt(lineIdx) {
|
|
32
|
+
const start = stepStarts.filter((i) => i <= lineIdx).pop();
|
|
33
|
+
const end = stepStarts.find((i) => i > lineIdx) ?? lines.length;
|
|
34
|
+
return lines.slice(start, end);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const uploadLines = lines.reduce(
|
|
38
|
+
(acc, l, i) => (l.includes('uses: actions/upload-artifact@') ? [...acc, i] : acc), []);
|
|
39
|
+
|
|
40
|
+
test('both installer uploads are still present (mac dmg + win exe)', () => {
|
|
41
|
+
assert.equal(uploadLines.length, 2, `expected 2 upload-artifact steps, found ${uploadLines.length}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('every upload is ref-gated to main (quota) — ref-based so workflow_dispatch on main uploads', () => {
|
|
45
|
+
for (const i of uploadLines) {
|
|
46
|
+
const block = stepBlockAt(i).join('\n');
|
|
47
|
+
const m = /^\s+if:\s*(.+?)\s*$/m.exec(block);
|
|
48
|
+
assert.ok(m, `upload step at line ${i + 1} has no if: gate — an ungated upload refills the quota`);
|
|
49
|
+
assert.equal(m[1], "github.ref == 'refs/heads/main'",
|
|
50
|
+
`upload step at line ${i + 1} gate must be the trusted-ref test, not an event_name test (got: ${m[1]})`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('every upload is retention-bounded', () => {
|
|
55
|
+
for (const i of uploadLines) {
|
|
56
|
+
const block = stepBlockAt(i).join('\n');
|
|
57
|
+
const m = /^\s+retention-days:\s*(\d+)\s*$/m.exec(block);
|
|
58
|
+
assert.ok(m, `upload step at line ${i + 1} has no retention-days — GitHub defaults to 90`);
|
|
59
|
+
const days = Number(m[1]);
|
|
60
|
+
assert.ok(days >= 1 && days <= MAX_RETENTION_DAYS,
|
|
61
|
+
`upload step at line ${i + 1} retention-days=${days}, want 1..${MAX_RETENTION_DAYS}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the BUILD jobs stay ungated — feature branches must still compile (CI feedback)', () => {
|
|
66
|
+
const jobLevelIf = lines.filter((l) => /^ {4}if:/.test(l));
|
|
67
|
+
assert.deepEqual(jobLevelIf, [], `a job-level if: would stop feature branches from building: ${jobLevelIf}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('claude/** push trigger intact — the gate is on upload, not on running the workflow', () => {
|
|
71
|
+
assert.ok(lines.some((l) => /^\s+-\s+'claude\/\*\*'\s*$/.test(l)), "push trigger lost 'claude/**'");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`\ndevbox_artifact_gate: ${passed} passed, ${failed} failed`);
|
|
75
|
+
process.exit(failed ? 1 : 0);
|