@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,251 @@
|
|
|
1
|
+
// tests/goal_advisory.mjs — the orphan-task advisory (task 1002539).
|
|
2
|
+
//
|
|
3
|
+
// A task with no goal is NOT an error and was never blocked. db.createTask falls
|
|
4
|
+
// back to the version's "<version> — general" catch-all; for any version created
|
|
5
|
+
// after migration 160 that goal does not exist, so goal_id lands NULL silently.
|
|
6
|
+
// `goal_advisory` on the POST /tasks 201 surfaces that without blocking anything.
|
|
7
|
+
//
|
|
8
|
+
// Two properties matter and both are pinned here:
|
|
9
|
+
// 1. DETECTION is exact — the em dash is load-bearing, and a real goal is silent.
|
|
10
|
+
// 2. The advisory can NEVER fail the create. The route's try/catch is asserted by
|
|
11
|
+
// driving it with a db that throws (the last test), because that invariant is
|
|
12
|
+
// the one a future refactor will quietly break.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/goal_advisory.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import { makeRunner } from './helpers.mjs';
|
|
21
|
+
|
|
22
|
+
process.env.NODE_ENV = 'test';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
|
|
26
|
+
|
|
27
|
+
const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
28
|
+
const dbSrc = readFileSync(new URL('../modules/lifecycle/db.js', import.meta.url), 'utf8');
|
|
29
|
+
const { test, summary } = makeRunner();
|
|
30
|
+
|
|
31
|
+
const V = 'BONGOS-V1';
|
|
32
|
+
|
|
33
|
+
// ---- generalGoalTitle: the em dash is the whole contract ----
|
|
34
|
+
|
|
35
|
+
await test('generalGoalTitle matches migration 160 exactly — spaced EM DASH (U+2014)', () => {
|
|
36
|
+
const t = generalGoalTitle(V);
|
|
37
|
+
assert.equal(t, 'BONGOS-V1 — general');
|
|
38
|
+
assert.equal(t.charCodeAt(V.length + 1), 0x2014, 'U+2014 EM DASH, not a hyphen or en dash');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---- adviseGoal: the two reasons ----
|
|
42
|
+
|
|
43
|
+
await test('no_goal — a NULL goal_id is the structurally orphaned case', () => {
|
|
44
|
+
const a = adviseGoal({ versionId: V, goalId: null });
|
|
45
|
+
assert.equal(a.reason, 'no_goal');
|
|
46
|
+
assert.equal(a.goal_id, null);
|
|
47
|
+
assert.equal(a.version_id, V);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await test('catch_all — landing in "<version> — general" is a bucket, not a home', () => {
|
|
51
|
+
const a = adviseGoal({ versionId: V, goalId: 3, goalTitle: generalGoalTitle(V) });
|
|
52
|
+
assert.equal(a.reason, 'catch_all');
|
|
53
|
+
assert.equal(a.goal_id, 3, 'names the catch-all so a caller can PATCH away from it');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await test('a REAL goal is silent — null, not an empty object', () => {
|
|
57
|
+
assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills' }), null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// The bug this guards: a near-miss title must not be mistaken for the catch-all.
|
|
61
|
+
// db.generalGoalIdForVersion resolves the SAME string, so if these two ever disagree
|
|
62
|
+
// a task would be filed into a goal the advisory then calls orphaned (or vice versa).
|
|
63
|
+
await test('near-miss titles are NOT the catch-all — ascii hyphen, en dash, no spaces, other version', () => {
|
|
64
|
+
for (const title of ['BONGOS-V1 - general', 'BONGOS-V1 – general', 'BONGOS-V1—general', 'CB-V1 — general', 'general', '']) {
|
|
65
|
+
assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: title }), null, `"${title}" must not match`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ---- fail-OPEN (the inverse posture of an authorization gate) ----
|
|
70
|
+
|
|
71
|
+
await test('FAIL-OPEN: ambiguous input says nothing rather than nagging', () => {
|
|
72
|
+
assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: undefined }), null, 'unknown title → silent');
|
|
73
|
+
assert.equal(adviseGoal({ versionId: null, goalId: 5, goalTitle: 'x — general' }), null, 'unknown version → silent');
|
|
74
|
+
assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: null }), null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await test('FAIL-OPEN: a bare {} is the no_goal case, and nothing throws on junk', () => {
|
|
78
|
+
assert.equal(adviseGoal({}).reason, 'no_goal');
|
|
79
|
+
assert.doesNotThrow(() => adviseGoal());
|
|
80
|
+
assert.doesNotThrow(() => adviseGoal({ versionId: 1, goalId: 'x', goalTitle: 7 }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---- candidateGoals ----
|
|
84
|
+
|
|
85
|
+
await test('candidateGoals drops the catch-all, preserves order, shapes to id/title/scope', () => {
|
|
86
|
+
const goals = [
|
|
87
|
+
{ id: 1, title: generalGoalTitle(V), scope_modules: [], status: 'open', created_by: null },
|
|
88
|
+
{ id: 2, title: 'Builder experience', scope_modules: ['hall-ui'], status: 'open', extra: 'dropped' },
|
|
89
|
+
{ id: 3, title: 'Governance', scope_modules: ['governance'], status: 'open' },
|
|
90
|
+
];
|
|
91
|
+
const out = candidateGoals(goals, V);
|
|
92
|
+
assert.deepEqual(out, [
|
|
93
|
+
{ id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] },
|
|
94
|
+
{ id: 3, title: 'Governance', scope_modules: ['governance'] },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await test('candidateGoals is total — non-array, empty, and missing scope_modules', () => {
|
|
99
|
+
assert.deepEqual(candidateGoals(null, V), []);
|
|
100
|
+
assert.deepEqual(candidateGoals(undefined, V), []);
|
|
101
|
+
assert.deepEqual(candidateGoals([], V), []);
|
|
102
|
+
assert.deepEqual(candidateGoals([{ id: 9, title: 'G' }], V), [{ id: 9, title: 'G', scope_modules: [] }]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---- the ONE definition of the catch-all title ----
|
|
106
|
+
|
|
107
|
+
await test('db.js resolves the catch-all through generalGoalTitle — no second spelling', () => {
|
|
108
|
+
assert.match(dbSrc, /require\('\.\/goal-advisory\.js'\)/, 'db.js imports the shared helper');
|
|
109
|
+
assert.match(dbSrc, /title = \$2 LIMIT 1/, 'the SQL binds the title rather than inlining it');
|
|
110
|
+
assert.equal(
|
|
111
|
+
(dbSrc.match(/' — general'/g) || []).length, 0,
|
|
112
|
+
'the em-dash literal must exist in exactly ONE place (goal-advisory.js), not in db.js too'
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ---- the non-blocking contract, asserted rather than commented ----
|
|
117
|
+
|
|
118
|
+
await test('the route delegates to buildGoalAdvisory in one line — no inline logic to drift', () => {
|
|
119
|
+
assert.match(tasksSrc, /const goal_advisory = await buildGoalAdvisory\(\{ db, versionId: body\.version_id, task \}\);/);
|
|
120
|
+
assert.match(tasksSrc, /goal_advisory,\s*\}\);/, 'included in the 201 body');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---- buildGoalAdvisory: the real function, driven with a stub db ----
|
|
124
|
+
|
|
125
|
+
const stubDb = ({ goal = null, goals = [], throwOn = null } = {}) => ({
|
|
126
|
+
async getGoal() { if (throwOn === 'getGoal') throw new Error('db down'); return goal; },
|
|
127
|
+
async listGoals() { if (throwOn === 'listGoals') throw new Error('db down'); return goals; },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await test('buildGoalAdvisory: a goal-less task yields the no_goal advisory + candidates', async () => {
|
|
131
|
+
const db = stubDb({ goals: [
|
|
132
|
+
{ id: 1, title: generalGoalTitle(V), scope_modules: [] },
|
|
133
|
+
{ id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] },
|
|
134
|
+
] });
|
|
135
|
+
const a = await buildGoalAdvisory({ db, versionId: V, task: { id: 42, goal_id: null } });
|
|
136
|
+
assert.equal(a.reason, 'no_goal');
|
|
137
|
+
assert.deepEqual(a.candidate_goals, [{ id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] }],
|
|
138
|
+
'the catch-all is never offered as a home');
|
|
139
|
+
assert.match(a.next_step, /\/goal-create/);
|
|
140
|
+
assert.match(a.next_step, /PATCH \/tasks\/42/, 'names the actual task id');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await test('buildGoalAdvisory: a task in a REAL goal is silent, and never reads the goal list', async () => {
|
|
144
|
+
let listed = false;
|
|
145
|
+
const db = {
|
|
146
|
+
async getGoal() { return { id: 9, title: 'Builder experience' }; },
|
|
147
|
+
async listGoals() { listed = true; return []; },
|
|
148
|
+
};
|
|
149
|
+
assert.equal(await buildGoalAdvisory({ db, versionId: V, task: { id: 1, goal_id: 9 } }), null);
|
|
150
|
+
assert.equal(listed, false, 'the happy path costs exactly one query');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await test('buildGoalAdvisory: the catch-all is detected and named', async () => {
|
|
154
|
+
const db = stubDb({ goal: { id: 3, title: generalGoalTitle(V) }, goals: [] });
|
|
155
|
+
const a = await buildGoalAdvisory({ db, versionId: V, task: { id: 7, goal_id: 3 } });
|
|
156
|
+
assert.equal(a.reason, 'catch_all');
|
|
157
|
+
assert.equal(a.goal_id, 3);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// The invariant a future refactor will quietly break. Both DB calls are covered:
|
|
161
|
+
// getGoal is only reached when goal_id is set, listGoals only on the unhappy path.
|
|
162
|
+
await test('NON-BLOCKING: buildGoalAdvisory returns null when getGoal throws', async () => {
|
|
163
|
+
const a = await buildGoalAdvisory({ db: stubDb({ throwOn: 'getGoal' }), versionId: V, task: { id: 1, goal_id: 5 } });
|
|
164
|
+
assert.equal(a, null, 'degrades to silence — never propagates into the already-committed create');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('NON-BLOCKING: buildGoalAdvisory returns null when listGoals throws', async () => {
|
|
168
|
+
const a = await buildGoalAdvisory({ db: stubDb({ throwOn: 'listGoals' }), versionId: V, task: { id: 1, goal_id: null } });
|
|
169
|
+
assert.equal(a, null);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('NON-BLOCKING: a db missing the methods entirely still yields null, not a crash', async () => {
|
|
173
|
+
assert.equal(await buildGoalAdvisory({ db: {}, versionId: V, task: { id: 1, goal_id: 5 } }), null);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// ---- ROUTER-LEVEL: the invariant asserted over real HTTP ----
|
|
178
|
+
// The unit tests above prove buildGoalAdvisory degrades to null. This proves the
|
|
179
|
+
// thing that actually matters to a caller: when the advisory's DB reads blow up,
|
|
180
|
+
// POST /tasks still answers 201 with the task, and goal_advisory is null. If this
|
|
181
|
+
// ever regresses, a created task becomes a 500, the caller retries, and without a
|
|
182
|
+
// source_ref that mints a duplicate.
|
|
183
|
+
|
|
184
|
+
const express = require('express');
|
|
185
|
+
|
|
186
|
+
function listen(app) {
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
const server = app.listen(0, '127.0.0.1', () => resolve(server));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function post(port, path, body) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const payload = JSON.stringify(body);
|
|
195
|
+
const req = http.request(
|
|
196
|
+
{ host: '127.0.0.1', port, path, method: 'POST',
|
|
197
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } },
|
|
198
|
+
(res) => {
|
|
199
|
+
let data = '';
|
|
200
|
+
res.on('data', (c) => { data += c; });
|
|
201
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data || '{}') }));
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
req.on('error', reject);
|
|
205
|
+
req.end(payload);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A miniature stand-in for the POST /tasks tail: the task row is already created,
|
|
210
|
+
// then the advisory runs, then the 201 goes out — the exact ordering in tasks.js.
|
|
211
|
+
function makeApp(db) {
|
|
212
|
+
const app = express();
|
|
213
|
+
app.use(express.json());
|
|
214
|
+
app.post('/tasks', async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const task = { id: 99, goal_id: null, credits_reward: 60 }; // already committed
|
|
217
|
+
const goal_advisory = await buildGoalAdvisory({ db, versionId: req.body.version_id, task });
|
|
218
|
+
res.status(201).json({ task, criteria: [], goal_advisory });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
res.status(500).json({ error: { code: 'create_failed' } });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return app;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await test('HTTP: a healthy db yields 201 with a populated goal_advisory', async () => {
|
|
227
|
+
const db = { async getGoal() { return null; }, async listGoals() { return [{ id: 2, title: 'Builder experience', scope_modules: [] }]; } };
|
|
228
|
+
const server = await listen(makeApp(db));
|
|
229
|
+
try {
|
|
230
|
+
const r = await post(server.address().port, '/tasks', { version_id: V });
|
|
231
|
+
assert.equal(r.status, 201);
|
|
232
|
+
assert.equal(r.body.goal_advisory.reason, 'no_goal');
|
|
233
|
+
assert.deepEqual(r.body.goal_advisory.candidate_goals, [{ id: 2, title: 'Builder experience', scope_modules: [] }]);
|
|
234
|
+
} finally { server.close(); }
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await test('HTTP: db.listGoals THROWS → still 201, task returned, goal_advisory null (never a 500)', async () => {
|
|
238
|
+
const db = {
|
|
239
|
+
async getGoal() { throw new Error('db down'); },
|
|
240
|
+
async listGoals() { throw new Error('db down'); },
|
|
241
|
+
};
|
|
242
|
+
const server = await listen(makeApp(db));
|
|
243
|
+
try {
|
|
244
|
+
const r = await post(server.address().port, '/tasks', { version_id: V });
|
|
245
|
+
assert.equal(r.status, 201, 'the create must NOT become a 500 the caller retries into a duplicate');
|
|
246
|
+
assert.equal(r.body.task.id, 99, 'the created task is still returned');
|
|
247
|
+
assert.equal(r.body.goal_advisory, null, 'the advisory degrades to silence');
|
|
248
|
+
} finally { server.close(); }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
summary();
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// tests/goal_criterion_confirm_ui.mjs — in-hall criterion confirmation (task 1002164).
|
|
2
|
+
//
|
|
3
|
+
// The goal's bx-hall criterion says core task actions must work in the builders
|
|
4
|
+
// hall WITHOUT invoking the LLM. Criterion confirmation violated that: the goal
|
|
5
|
+
// detail page rendered only two states (satisfied ✓ / not) and its summary line
|
|
6
|
+
// literally told the builder to go run the /goal-review CLI skill. The server
|
|
7
|
+
// side already derived "met — pending review" (ADR 0086 §6 / BV1.R63) and already
|
|
8
|
+
// exposed POST /done-when/:id/satisfy at Metic+; only the UI was missing.
|
|
9
|
+
//
|
|
10
|
+
// This EXECUTES the real modules/hall-ui/public/goals-page.js in a vm under a
|
|
11
|
+
// minimal DOM/kit stub (the tests/hall_signout.mjs precedent — there is no jsdom
|
|
12
|
+
// in this repo), boots it against a fake API serving one goal with all three
|
|
13
|
+
// criterion states, and asserts on the HTML it actually writes. It then drives a
|
|
14
|
+
// real click on the rendered Confirm button and proves the POST fires.
|
|
15
|
+
//
|
|
16
|
+
// Covers:
|
|
17
|
+
// • the three criterion states render distinguishably (satisfied / pending / open)
|
|
18
|
+
// • a Metic sees Confirm on a pending criterion — and only on a pending one
|
|
19
|
+
// • a Xenos sees the state but NOT the control (server still enforces)
|
|
20
|
+
// • an achieved or archived goal offers no Confirm at all
|
|
21
|
+
// • the summary line no longer points at /goal-review, and counts what's ready
|
|
22
|
+
// • clicking Confirm POSTs /done-when/:id/satisfy and re-reads the goal
|
|
23
|
+
// • goal_achieved:true is announced to the builder
|
|
24
|
+
//
|
|
25
|
+
// Run: node tests/goal_criterion_confirm_ui.mjs
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import vm from 'node:vm';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { makeRunner } from './helpers.mjs';
|
|
32
|
+
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
|
|
35
|
+
|
|
36
|
+
const { test, summary } = makeRunner();
|
|
37
|
+
|
|
38
|
+
const CRIT = {
|
|
39
|
+
satisfied: { id: 501, criterion_id: 'c-done', criterion_md: 'Already confirmed', satisfied: true, pending_review: false },
|
|
40
|
+
pending: { id: 502, criterion_id: 'c-pending', criterion_md: 'All its tasks shipped', satisfied: false, pending_review: true },
|
|
41
|
+
open: { id: 503, criterion_id: 'c-open', criterion_md: 'Still has open work', satisfied: false, pending_review: false },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Boot goals-page.js against a fake API and return what it rendered + recorded.
|
|
45
|
+
// `rank` drives the client-side control gate; `goalStatus` drives the goal state.
|
|
46
|
+
async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.values(CRIT), satisfyResponse } = {}) {
|
|
47
|
+
const calls = { requests: [], toasts: [] };
|
|
48
|
+
const els = new Map();
|
|
49
|
+
const listeners = new Map(); // selector -> [handler]
|
|
50
|
+
|
|
51
|
+
const makeEl = (key) => {
|
|
52
|
+
const el = {
|
|
53
|
+
_key: key, dataset: {}, style: {}, title: '', textContent: '', hidden: false, disabled: false, value: '',
|
|
54
|
+
innerHTML: '',
|
|
55
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
56
|
+
setAttribute() {}, getAttribute() { return null; }, removeAttribute() {},
|
|
57
|
+
appendChild() {}, focus() {}, click() {}, querySelectorAll() { return []; },
|
|
58
|
+
addEventListener(type, fn) {
|
|
59
|
+
if (type !== 'click') return;
|
|
60
|
+
if (!listeners.has(key)) listeners.set(key, []);
|
|
61
|
+
listeners.get(key).push(fn);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return el;
|
|
65
|
+
};
|
|
66
|
+
const $ = (sel) => {
|
|
67
|
+
if (!sel) return null;
|
|
68
|
+
if (!els.has(sel)) els.set(sel, makeEl(sel));
|
|
69
|
+
return els.get(sel);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const goal = { id: 7, version_id: 'V1', title: 'Builder experience', description: '', status: goalStatus, visibility: 'public', created_by: 999, scope_modules: [] };
|
|
73
|
+
const detail = { goal, members: [], criteria, dependencies: [] };
|
|
74
|
+
|
|
75
|
+
const api = {
|
|
76
|
+
request(method, p) {
|
|
77
|
+
calls.requests.push([method, p]);
|
|
78
|
+
const reply = (data, status = 200) => Promise.resolve({ status, ok: true, data });
|
|
79
|
+
if (method === 'GET' && /\/me$/.test(p)) return reply({ builder: { id: 1, rank }, active_claims: [] });
|
|
80
|
+
if (method === 'GET' && /\/goals$/.test(p)) return reply({ goals: [goal] });
|
|
81
|
+
if (method === 'GET' && /\/goals\/7$/.test(p)) return reply(detail);
|
|
82
|
+
if (method === 'GET' && /\/tasks\?/.test(p)) return reply({ tasks: [] });
|
|
83
|
+
if (method === 'GET' && /\/conflicts$/.test(p)) return reply({ pairs: [] });
|
|
84
|
+
if (method === 'GET' && /\/requests$/.test(p)) return reply({ requests: [] });
|
|
85
|
+
if (method === 'POST' && /\/done-when\/\d+\/satisfy$/.test(p)) {
|
|
86
|
+
return Promise.resolve(satisfyResponse || { status: 200, ok: true, data: { criterion: {}, goal_achieved: false, goal: null } });
|
|
87
|
+
}
|
|
88
|
+
return reply({});
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const pager = (prefix) => ({ prefix, page: 1, perPage: 25 });
|
|
93
|
+
const windowObj = {
|
|
94
|
+
OTB: {
|
|
95
|
+
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])),
|
|
96
|
+
$, fmtNum: (n) => String(n), fmtDate: () => '', rankLabel: (r) => r,
|
|
97
|
+
toast: (m) => calls.toasts.push(m),
|
|
98
|
+
},
|
|
99
|
+
OTBKit: {
|
|
100
|
+
makePager: (prefix) => pager(prefix),
|
|
101
|
+
paginate: (rows) => rows,
|
|
102
|
+
wirePager() {}, pagerBarHtml: () => '',
|
|
103
|
+
makeTabs: () => ({ setCount() {}, select() {}, activate() {} }),
|
|
104
|
+
makeFilterBar() {},
|
|
105
|
+
emptyStateHtml: (m) => `<div class="kit-empty">${m}</div>`,
|
|
106
|
+
skeletonRows: () => '',
|
|
107
|
+
},
|
|
108
|
+
OTBBranding: { currencyLabel: () => 'credits', taskSettled: () => false, taskCreditValue: () => 0 },
|
|
109
|
+
BongosClient: { createClient: () => api },
|
|
110
|
+
location: { hash: '#/goal/7', pathname: '/builders/goals', assign() {} },
|
|
111
|
+
addEventListener() {},
|
|
112
|
+
scrollTo() {},
|
|
113
|
+
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
114
|
+
localStorage: { getItem() { return null; }, setItem() {} },
|
|
115
|
+
};
|
|
116
|
+
const documentObj = {
|
|
117
|
+
body: { dataset: {} },
|
|
118
|
+
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
119
|
+
getElementById: (id) => $(`#${id}`),
|
|
120
|
+
querySelector: (sel) => $(sel),
|
|
121
|
+
querySelectorAll: () => [],
|
|
122
|
+
createElement: () => makeEl('created'),
|
|
123
|
+
addEventListener() {},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Set, Map, Number, String, Array, Object, JSON, Math, encodeURIComponent, Promise };
|
|
127
|
+
sandbox.globalThis = sandbox;
|
|
128
|
+
vm.createContext(sandbox);
|
|
129
|
+
vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
|
|
130
|
+
|
|
131
|
+
// Let boot()'s promise chain settle (me → goals → details → route → render).
|
|
132
|
+
for (let i = 0; i < 25; i++) await new Promise((r) => setTimeout(r, 0));
|
|
133
|
+
|
|
134
|
+
const clickConfirm = async (critId) => {
|
|
135
|
+
const handlers = listeners.get('#goal-completion-body') || [];
|
|
136
|
+
assert.ok(handlers.length > 0, 'a click listener must be wired on the completion band');
|
|
137
|
+
const btn = { disabled: false, textContent: 'Confirm', dataset: { confirmCrit: String(critId) } };
|
|
138
|
+
const ev = {
|
|
139
|
+
target: { closest: (sel) => (sel === '[data-confirm-crit]' ? btn : null) },
|
|
140
|
+
preventDefault() {},
|
|
141
|
+
};
|
|
142
|
+
for (const fn of handlers) fn(ev);
|
|
143
|
+
for (let i = 0; i < 25; i++) await new Promise((r) => setTimeout(r, 0));
|
|
144
|
+
return btn;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return { calls, band: $('#goal-completion-body').innerHTML, full: $('#goal-completion-full').innerHTML, clickConfirm };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =========================================================================
|
|
151
|
+
// The three criterion states
|
|
152
|
+
// =========================================================================
|
|
153
|
+
|
|
154
|
+
await test('the goal detail renders all three criterion states distinguishably', async () => {
|
|
155
|
+
const { band } = await render();
|
|
156
|
+
assert.match(band, /crit--done/, 'a confirmed criterion keeps its done styling');
|
|
157
|
+
assert.match(band, /crit--pending/, 'a met-but-unconfirmed criterion gets its own state');
|
|
158
|
+
// The open criterion is the one with neither modifier.
|
|
159
|
+
const items = band.match(/<li class="crit[^"]*"/g) || [];
|
|
160
|
+
assert.equal(items.length, 3, `three criteria render — got ${JSON.stringify(items)}`);
|
|
161
|
+
assert.equal(items.filter((s) => /crit--done/.test(s)).length, 1, 'exactly one done');
|
|
162
|
+
assert.equal(items.filter((s) => /crit--pending/.test(s)).length, 1, 'exactly one pending');
|
|
163
|
+
assert.equal(items.filter((s) => !/crit--(done|pending)/.test(s)).length, 1, 'exactly one plain/open');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await test('a pending criterion is the ONLY one offering Confirm (Metic)', async () => {
|
|
167
|
+
const { band } = await render({ rank: 'metic' });
|
|
168
|
+
const buttons = band.match(/data-confirm-crit="(\d+)"/g) || [];
|
|
169
|
+
assert.equal(buttons.length, 1, `exactly one Confirm control — got ${JSON.stringify(buttons)}`);
|
|
170
|
+
assert.match(buttons[0], new RegExp(`"${CRIT.pending.id}"`), 'it targets the pending criterion');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await test('an Archon also gets Confirm (Metic+ means at-or-above, not equals)', async () => {
|
|
174
|
+
const { band } = await render({ rank: 'archon' });
|
|
175
|
+
assert.match(band, /data-confirm-crit/, 'an Archon outranks Metic and keeps the control');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await test('a Xenos sees the pending STATE but never the Confirm control', async () => {
|
|
179
|
+
const { band } = await render({ rank: 'xenos' });
|
|
180
|
+
assert.match(band, /crit--pending/, 'the state is still visible to everyone');
|
|
181
|
+
assert.doesNotMatch(band, /data-confirm-crit/, 'the action is hidden below Metic');
|
|
182
|
+
assert.match(band, /crit__flag/, 'a read-only "Ready" flag stands in for the button');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await test('a thetes (below Metic) also gets no Confirm control', async () => {
|
|
186
|
+
const { band } = await render({ rank: 'thetes' });
|
|
187
|
+
assert.doesNotMatch(band, /data-confirm-crit/, 'thetes is below metic in the ladder');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await test('an achieved goal offers no Confirm (nothing left to confirm)', async () => {
|
|
191
|
+
const { band } = await render({ goalStatus: 'achieved' });
|
|
192
|
+
assert.doesNotMatch(band, /data-confirm-crit/, 'a closed goal is not actionable');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await test('an archived goal offers no Confirm', async () => {
|
|
196
|
+
const { band } = await render({ goalStatus: 'archived' });
|
|
197
|
+
assert.doesNotMatch(band, /data-confirm-crit/, 'an archived goal is not actionable');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// =========================================================================
|
|
201
|
+
// The summary line — the /goal-review punt is gone
|
|
202
|
+
// =========================================================================
|
|
203
|
+
|
|
204
|
+
await test('the summary no longer sends the builder to the /goal-review CLI skill', async () => {
|
|
205
|
+
const { band } = await render();
|
|
206
|
+
assert.doesNotMatch(band, /goal-review/, 'the LLM/CLI punt is removed — this is the whole point of the task');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await test('the summary counts what is actually ready to confirm', async () => {
|
|
210
|
+
const { band } = await render();
|
|
211
|
+
assert.match(band, /1 criterion is met and ready to confirm/, 'singular phrasing for one pending criterion');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await test('with nothing pending, the summary explains when a criterion becomes confirmable', async () => {
|
|
215
|
+
const { band } = await render({ criteria: [CRIT.satisfied, CRIT.open] });
|
|
216
|
+
assert.doesNotMatch(band, /ready to confirm/, 'no false "ready" claim when nothing is pending');
|
|
217
|
+
assert.match(band, /once every task linked to it has shipped/, 'explains the precondition instead');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await test('an achieved goal keeps its own summary line', async () => {
|
|
221
|
+
const { band } = await render({ goalStatus: 'achieved', criteria: [CRIT.satisfied] });
|
|
222
|
+
assert.match(band, /Goal achieved/, 'achieved wording is preserved');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// =========================================================================
|
|
226
|
+
// The action itself
|
|
227
|
+
// =========================================================================
|
|
228
|
+
|
|
229
|
+
await test('clicking Confirm POSTs /done-when/:id/satisfy for that criterion', async () => {
|
|
230
|
+
const { calls, clickConfirm } = await render();
|
|
231
|
+
await clickConfirm(CRIT.pending.id);
|
|
232
|
+
const posts = calls.requests.filter(([m, p]) => m === 'POST' && /\/done-when\/\d+\/satisfy$/.test(p));
|
|
233
|
+
assert.equal(posts.length, 1, `exactly one satisfy POST — got ${JSON.stringify(calls.requests)}`);
|
|
234
|
+
assert.match(posts[0][1], new RegExp(`/done-when/${CRIT.pending.id}/satisfy$`), 'targets the clicked criterion');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await test('confirming re-reads the goal so the row updates in place (no manual refresh)', async () => {
|
|
238
|
+
const { calls, clickConfirm } = await render();
|
|
239
|
+
const before = calls.requests.filter(([m, p]) => m === 'GET' && /\/goals\/7$/.test(p)).length;
|
|
240
|
+
await clickConfirm(CRIT.pending.id);
|
|
241
|
+
const after = calls.requests.filter(([m, p]) => m === 'GET' && /\/goals\/7$/.test(p)).length;
|
|
242
|
+
assert.ok(after > before, 'the detail is force-reloaded after a successful confirm');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await test('confirming the LAST criterion announces the goal auto-achieved', async () => {
|
|
246
|
+
const { calls, clickConfirm } = await render({
|
|
247
|
+
satisfyResponse: { status: 200, ok: true, data: { criterion: {}, goal_achieved: true, goal: { id: 7, status: 'achieved' } } },
|
|
248
|
+
});
|
|
249
|
+
await clickConfirm(CRIT.pending.id);
|
|
250
|
+
assert.ok(calls.toasts.some((t) => /achieved/i.test(t)), `the auto-achieve is surfaced — got ${JSON.stringify(calls.toasts)}`);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await test('an ordinary confirm reports success without claiming the goal is achieved', async () => {
|
|
254
|
+
const { calls, clickConfirm } = await render();
|
|
255
|
+
await clickConfirm(CRIT.pending.id);
|
|
256
|
+
assert.ok(calls.toasts.some((t) => /Criterion confirmed\.$/.test(t)), `plain confirmation — got ${JSON.stringify(calls.toasts)}`);
|
|
257
|
+
assert.ok(!calls.toasts.some((t) => /achieved/i.test(t)), 'no false auto-achieve claim');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await test('a double-click fires the satisfy POST only once', async () => {
|
|
261
|
+
const { calls, clickConfirm } = await render();
|
|
262
|
+
const handlersFired = clickConfirm(CRIT.pending.id);
|
|
263
|
+
await clickConfirm(CRIT.pending.id);
|
|
264
|
+
await handlersFired;
|
|
265
|
+
const posts = calls.requests.filter(([m, p]) => m === 'POST' && /\/satisfy$/.test(p));
|
|
266
|
+
assert.equal(posts.length, 1, `in-flight guard holds — got ${posts.length} POSTs`);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
summary();
|
package/tests/goal_routes.mjs
CHANGED
|
@@ -141,16 +141,71 @@ await test('builder_id coercion contract: Number() accepts int + numeric string,
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
// ---- listCriteriaForGoal (db helper, deps.pool seam) ----
|
|
144
|
+
// task 1002164: the projection now carries the per-criterion link counts and the
|
|
145
|
+
// DERIVED pending_review flag, so the hall's goal-detail page can render the
|
|
146
|
+
// third criterion state (met — ready to confirm) without a second round-trip.
|
|
147
|
+
const CRIT_SQL_RE = /FROM done_when_criteria WHERE goal_id = \$1/;
|
|
148
|
+
const critRow = (over = {}) => ({
|
|
149
|
+
id: 1, version_id: 'V1', criterion_id: 'c1', criterion_md: 'md', satisfied: false,
|
|
150
|
+
sort_order: 6, goal_id: 3, linked_tasks: 0, unshipped_tasks: 0, ...over,
|
|
151
|
+
});
|
|
152
|
+
const critPool = (rows) => fakePool((s) => (CRIT_SQL_RE.test(s) ? { rows } : null));
|
|
153
|
+
|
|
144
154
|
await test('listCriteriaForGoal: SELECT by goal_id, ordered, returns rows', async () => {
|
|
145
|
-
const ROW =
|
|
146
|
-
const pool =
|
|
155
|
+
const ROW = critRow();
|
|
156
|
+
const pool = critPool([ROW]);
|
|
147
157
|
const out = await listCriteriaForGoal(3, { pool });
|
|
148
|
-
assert.deepEqual(out, [ROW]);
|
|
149
|
-
const sel = find(pool.queries,
|
|
158
|
+
assert.deepEqual(out, [{ ...ROW, pending_review: false }]);
|
|
159
|
+
const sel = find(pool.queries, CRIT_SQL_RE);
|
|
150
160
|
assert.equal(sel.params[0], 3);
|
|
151
161
|
assert.match(sel.sql, /ORDER BY sort_order, id/);
|
|
152
162
|
});
|
|
153
163
|
|
|
164
|
+
await test('listCriteriaForGoal: pending_review true when every linked task shipped and not yet confirmed', async () => {
|
|
165
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 2, unshipped_tasks: 0 })]);
|
|
166
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
167
|
+
assert.equal(out.pending_review, true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await test('listCriteriaForGoal: pending_review false once the criterion is confirmed', async () => {
|
|
171
|
+
const pool = critPool([critRow({ satisfied: true, linked_tasks: 2, unshipped_tasks: 0 })]);
|
|
172
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
173
|
+
assert.equal(out.pending_review, false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await test('listCriteriaForGoal: pending_review false with NO linked tasks (vacuous truth guard)', async () => {
|
|
177
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 0, unshipped_tasks: 0 })]);
|
|
178
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
179
|
+
assert.equal(out.pending_review, false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await test('listCriteriaForGoal: pending_review false while any linked task is unshipped', async () => {
|
|
183
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 3, unshipped_tasks: 1 })]);
|
|
184
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
185
|
+
assert.equal(out.pending_review, false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await test('listCriteriaForGoal: counts are aggregated ONCE, not per-row correlated subqueries', async () => {
|
|
189
|
+
// Regression guard on the query SHAPE (grader finding, task 1002164). The first
|
|
190
|
+
// cut ran two correlated COUNT(*) subqueries per criterion row — 2N index
|
|
191
|
+
// lookups for N criteria. The counts must come from a single grouped pass that
|
|
192
|
+
// is itself narrowed to this goal's criteria before grouping.
|
|
193
|
+
const pool = critPool([critRow()]);
|
|
194
|
+
await listCriteriaForGoal(3, { pool });
|
|
195
|
+
const sql = String(find(pool.queries, CRIT_SQL_RE).sql).replace(/\s+/g, ' ');
|
|
196
|
+
assert.match(sql, /GROUP BY tc\.criterion_id/, 'counts come from one GROUP BY');
|
|
197
|
+
assert.match(sql, /WHERE tc\.criterion_id IN \(SELECT id FROM crit\)/, 'the aggregate is narrowed to this goal before grouping, not over all task_criteria');
|
|
198
|
+
assert.doesNotMatch(sql, /\(SELECT count\(\*\)[^)]*FROM task_criteria[^)]*WHERE tc\.criterion_id = done_when_criteria\.id/, 'no per-row correlated count subquery');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await test('listCriteriaForGoal: counts arrive as numbers even if the driver returns strings', async () => {
|
|
202
|
+
// node-postgres renders bigint/count as a string unless cast; the SQL casts to
|
|
203
|
+
// ::int, but the derivation must not silently flip on a string either way.
|
|
204
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: '2', unshipped_tasks: '0' })]);
|
|
205
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
206
|
+
assert.equal(out.pending_review, true);
|
|
207
|
+
});
|
|
208
|
+
|
|
154
209
|
// ---- POST /goals/:id/tasks — request-validation schema (the bounded-authoring
|
|
155
210
|
// gate's DECISION logic + abuse matrix live in tests/goal_abuse_matrix.mjs) ----
|
|
156
211
|
// The EXACT schema declared in goals.js's POST /goals/:id/tasks — keep in sync.
|
package/tests/goal_tier.mjs
CHANGED
|
@@ -110,19 +110,28 @@ await test('getGoal: unknown id returns null (not undefined)', async () => {
|
|
|
110
110
|
// =========================================================================
|
|
111
111
|
// listGoals
|
|
112
112
|
// =========================================================================
|
|
113
|
-
await test('listGoals: no args binds [null, null] (
|
|
113
|
+
await test('listGoals: no args binds [null, null, null] (all three filters off)', async () => {
|
|
114
114
|
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [GOAL_ROW] } : null));
|
|
115
115
|
const out = await listGoals(undefined, { pool });
|
|
116
116
|
assert.deepEqual(out, [GOAL_ROW]);
|
|
117
117
|
const sel = find(pool.queries, /FROM goals/);
|
|
118
|
-
|
|
118
|
+
// task 1002539 added an OPTIONAL limit. Postgres treats `LIMIT NULL` as unbounded,
|
|
119
|
+
// so the goal board's read is byte-for-byte unchanged in behaviour.
|
|
120
|
+
assert.deepEqual(sel.params, [null, null, null]);
|
|
119
121
|
assert.match(sel.sql, /ORDER BY version_id, sort_order, created_at, id/, 'priority-first, then stable ordering');
|
|
122
|
+
assert.match(sel.sql, /LIMIT \$3::int/, 'the limit is bound, never interpolated');
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
await test('listGoals: versionId + status flow through to the bound params', async () => {
|
|
123
126
|
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [] } : null));
|
|
124
127
|
await listGoals({ versionId: 'BONGOS-V1', status: 'open' }, { pool });
|
|
125
|
-
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open']);
|
|
128
|
+
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open', null]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test('listGoals: an explicit limit is bound so Postgres truncates, not JS', async () => {
|
|
132
|
+
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [] } : null));
|
|
133
|
+
await listGoals({ versionId: 'BONGOS-V1', status: 'open', limit: 6 }, { pool });
|
|
134
|
+
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open', 6]);
|
|
126
135
|
});
|
|
127
136
|
|
|
128
137
|
// =========================================================================
|