@bongos/core 1.18.3 → 1.18.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1576 -286
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/migrate.sh +99 -14
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +124 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrate_root_resolution.mjs +124 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -76,15 +76,16 @@ const passingGrade = {
|
|
|
76
76
|
rubric_scores: {}, signals: {}, issues: [], notes_md: null,
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
await test('grade:
|
|
79
|
+
await test('grade: pay-on-land — NO per-task credit at grader-confirm; attribution is the claim holder', async () => {
|
|
80
80
|
const pool = fakePool({ claimHolderId: WORKER, task: TASK });
|
|
81
81
|
const out = await applyGrade({ taskId: TASK.id, builderId: ARCHON, gradeResult: passingGrade }, { pool });
|
|
82
82
|
assert.equal(out.taskStatus, 'confirmed');
|
|
83
|
+
// The #871 attribution guard still holds: creditedBuilderId is the claim
|
|
84
|
+
// holder, so when the credit DOES land (at ship) it lands on the worker.
|
|
83
85
|
assert.equal(out.creditedBuilderId, WORKER, 'creditedBuilderId must be the claim holder');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
assert.equal(
|
|
87
|
-
assert.notEqual(credit.params[0], ARCHON, 'credit must NOT land on the caller');
|
|
86
|
+
// ADR 0120: the per-task credit no longer lands at grader-confirm — it waits
|
|
87
|
+
// for the confirmed→shipped flip (shipTask). See tests/pay_on_land.mjs.
|
|
88
|
+
assert.equal(out.creditsAwarded, 0, 'no per-task credit at grader-confirm');
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
await test('grade: tasks.shipped_by ($2) is attributed to the claim holder, not the caller', async () => {
|
|
@@ -110,8 +111,7 @@ await test('grade: NO claim on the task → falls back to the caller (never refu
|
|
|
110
111
|
const pool = fakePool({ claimHolderId: null, task: TASK });
|
|
111
112
|
const out = await applyGrade({ taskId: TASK.id, builderId: ARCHON, gradeResult: passingGrade }, { pool });
|
|
112
113
|
assert.equal(out.creditedBuilderId, ARCHON, 'with no claim the caller is the only candidate');
|
|
113
|
-
|
|
114
|
-
assert.equal(credit.params[0], ARCHON);
|
|
114
|
+
assert.equal(out.creditsAwarded, 0, 'still no per-task credit at grader-confirm (lands at ship)');
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
summary();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// tests/hall_audit.mjs — the hall consistency audit, as a gate (task 2179).
|
|
2
|
+
//
|
|
3
|
+
// Task 2179 asked for an audit "checklist all green, recorded in ship notes".
|
|
4
|
+
// A checklist in a ship note is a claim about one moment; it cannot stop the
|
|
5
|
+
// thing it checked from coming back. These are the same assertions expressed as
|
|
6
|
+
// tests, so the audit re-runs on every change instead of once.
|
|
7
|
+
//
|
|
8
|
+
// Each assertion carries its exceptions inline, with the reason. An exception
|
|
9
|
+
// list that must be edited to break the rule is the point: the edit is where the
|
|
10
|
+
// conversation happens.
|
|
11
|
+
//
|
|
12
|
+
// Pure file-read (no DB/network) — runs in the DB-free unit gate.
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
21
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
22
|
+
const pages = () => fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
|
|
23
|
+
|
|
24
|
+
// ---- 1. page-local <style> stays small (the 2176 per-page-CSS rule) --------
|
|
25
|
+
|
|
26
|
+
// Pages whose inline block predates the program or is deliberate. Numbers are
|
|
27
|
+
// CURRENT sizes and act as a ratchet: growing one fails, shrinking one is free.
|
|
28
|
+
// Task 1002446 re-examined all four and kept every one, with the real reason
|
|
29
|
+
// rather than "predates the program" — knowing WHY is what tells the next
|
|
30
|
+
// reader whether promoting it would actually fix anything.
|
|
31
|
+
const INLINE_STYLE_BUDGET = {
|
|
32
|
+
'task.html': 27, // the task-751 detail-page block, deliberate
|
|
33
|
+
// my-projects + scouting are standalone surfaces that load neither style.css
|
|
34
|
+
// nor shell.js: their block is not page-local styling ON the design system,
|
|
35
|
+
// it IS their whole stylesheet, down to a hardcoded palette. Moving those
|
|
36
|
+
// bytes into a .css file would change nothing that matters — what they
|
|
37
|
+
// actually need is adoption into the shell + the branding contract, which is
|
|
38
|
+
// a far larger change than a CSS relocation.
|
|
39
|
+
'my-projects.html': 36,
|
|
40
|
+
'scouting.html': 26,
|
|
41
|
+
// Genuinely page-local, and already token-derived. One line over the default;
|
|
42
|
+
// a whole stylesheet for the legend strip is not worth the extra request, and
|
|
43
|
+
// diagram-viewer.css is shared with primer.html so it is the wrong home.
|
|
44
|
+
'diagrams.html': 16,
|
|
45
|
+
};
|
|
46
|
+
const DEFAULT_INLINE_BUDGET = 15;
|
|
47
|
+
|
|
48
|
+
function inlineStyleLines(html) {
|
|
49
|
+
let total = 0;
|
|
50
|
+
for (const m of html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
51
|
+
const body = m[1].trim();
|
|
52
|
+
if (body) total += body.split('\n').length;
|
|
53
|
+
}
|
|
54
|
+
return total;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('no page grows its inline <style> past its budget (promote to per-page CSS)', () => {
|
|
58
|
+
const over = [];
|
|
59
|
+
for (const f of pages()) {
|
|
60
|
+
const n = inlineStyleLines(read(f));
|
|
61
|
+
const budget = INLINE_STYLE_BUDGET[f] ?? DEFAULT_INLINE_BUDGET;
|
|
62
|
+
if (n > budget) over.push(`${f}: ${n} lines > budget ${budget}`);
|
|
63
|
+
}
|
|
64
|
+
assert.deepEqual(over, [], 'give the page its own stylesheet (the task-2176 pattern)');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the inline-style budget actually CATCHES growth', () => {
|
|
68
|
+
const html = '<style>\n' + 'a{color:red}\n'.repeat(20) + '</style>';
|
|
69
|
+
assert.ok(inlineStyleLines(html) > DEFAULT_INLINE_BUDGET, 'a 20-line block exceeds the default budget');
|
|
70
|
+
assert.equal(inlineStyleLines('<p>no styles</p>'), 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---- 2. the detail-surface rule: overlays frozen to idea + blocker --------
|
|
74
|
+
|
|
75
|
+
// `#/task/N` navigates to a page; `#/idea/N` and `#/blocker/N` overlay.
|
|
76
|
+
// `#/goal/N` is a sub-route WITHIN the goals page, not a fourth overlay
|
|
77
|
+
// namespace — goals-page.js owns it, and it never opens a modal over another page.
|
|
78
|
+
const ALLOWED_HASH_ROUTES = new Set(['task', 'idea', 'blocker', 'goal']);
|
|
79
|
+
|
|
80
|
+
test('no new #/… deep-link namespace has appeared', () => {
|
|
81
|
+
const found = new Set();
|
|
82
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.js'))) {
|
|
83
|
+
for (const m of read(f).matchAll(/#\/([a-z][a-z-]*)\//g)) found.add(m[1]);
|
|
84
|
+
}
|
|
85
|
+
assert.ok(found.size > 0, 'sanity: the scan found hash routes');
|
|
86
|
+
const unexpected = [...found].filter((n) => !ALLOWED_HASH_ROUTES.has(n));
|
|
87
|
+
assert.deepEqual(unexpected, [], 'overlays are frozen to idea + blocker (task navigates; goal is a goals-page sub-route)');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---- 3. the theming token block is the stable contract --------------------
|
|
91
|
+
|
|
92
|
+
// style.css :root holds the pack-driven vars every other rule derives from. The
|
|
93
|
+
// program was not allowed to change it; this pins the shape so a stray edit shows.
|
|
94
|
+
test('style.css :root token block is intact', () => {
|
|
95
|
+
const css = read('style.css');
|
|
96
|
+
const start = css.indexOf(':root {');
|
|
97
|
+
assert.ok(start > 0, ':root block present');
|
|
98
|
+
const end = css.indexOf('\n}', start);
|
|
99
|
+
const block = css.slice(start, end);
|
|
100
|
+
// The load-bearing vars other rules color-mix against. Losing one silently
|
|
101
|
+
// breaks derived colours across every page rather than erroring.
|
|
102
|
+
for (const v of ['--bg', '--surface', '--surface-2', '--border', '--ink', '--ink-soft', '--ink-faint', '--accent', '--accent-weak', '--radius']) {
|
|
103
|
+
assert.ok(block.includes(v + ':'), `${v} still defined in :root`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---- 4. deleted CSS stays deleted ----------------------------------------
|
|
108
|
+
|
|
109
|
+
// The hallux-t03 security/roster/admin leftovers, removed in task 2179 after a
|
|
110
|
+
// per-class word-boundary scan. Listed so a revert or a copy-paste brings them
|
|
111
|
+
// back loudly. NB `.voices__select` is NOT here: the class is live in markup but
|
|
112
|
+
// its rule was always overridden by settings.css's higher-specificity
|
|
113
|
+
// `select.voices__select`, so no rule for it belongs in style.css either.
|
|
114
|
+
const REMOVED_CLASSES = [
|
|
115
|
+
'sev-badge', 'sev-low', 'sev-medium', 'sev-high', 'sev-critical',
|
|
116
|
+
'security-audit-details', 'override__rationale',
|
|
117
|
+
'roster__stale', 'roster__inactive', 'roster__self',
|
|
118
|
+
'skill-prefs__select', 'skill-prefs__default',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
test('the dead security/roster/admin rules have not come back', () => {
|
|
122
|
+
const css = read('style.css');
|
|
123
|
+
const back = REMOVED_CLASSES.filter((c) => new RegExp('\\.' + c + '(?![\\w-])').test(css));
|
|
124
|
+
assert.deepEqual(back, [], 'these were verified unused and deleted in task 2179');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('the removed-class guard CATCHES a reintroduction, and tolerates lookalikes', () => {
|
|
128
|
+
const re = (c) => new RegExp('\\.' + c + '(?![\\w-])');
|
|
129
|
+
assert.equal(re('sev-low').test('.sev-low { color: red }'), true, 'a genuine reintroduction is caught');
|
|
130
|
+
// watch.css legitimately defines badge--sev-low; the guard must not match it.
|
|
131
|
+
assert.equal(re('sev-low').test('.badge--sev-low { color: red }'), false,
|
|
132
|
+
'a different class that merely ends with the name is not a reintroduction');
|
|
133
|
+
assert.match(read('watch.css'), /\.badge--sev-low/, 'watch.css still owns the live severity styles');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ---- 5. the page never scrolls sideways (task 1002446) --------------------
|
|
137
|
+
|
|
138
|
+
// Measured at 390w against origin/main before the fix: leaderboard 218px,
|
|
139
|
+
// drachmae 89px, index/Standing 62px (the .rank-tip), Home 8px. Each is carried
|
|
140
|
+
// by one rule, and each rule is one edit away from being lost — so the rules are
|
|
141
|
+
// pinned here rather than left to the next screenshot sweep to rediscover.
|
|
142
|
+
test('the phone-width overflow guards are all still in place', () => {
|
|
143
|
+
const css = read('style.css');
|
|
144
|
+
const scoped = (sheet, re, breakpoint) => {
|
|
145
|
+
// the block a rule sits in must be the narrow-width media query it was
|
|
146
|
+
// written for, or the rule is live on desktop too (or, worse, live nowhere).
|
|
147
|
+
const at = sheet.search(re);
|
|
148
|
+
assert.ok(at > 0, `rule missing: ${re}`);
|
|
149
|
+
const media = sheet.lastIndexOf('@media', at);
|
|
150
|
+
assert.ok(media >= 0, `${re} has no enclosing @media at all`);
|
|
151
|
+
const between = sheet.slice(media, at);
|
|
152
|
+
// A media block closes with `}` in column 0; anything after that is back at
|
|
153
|
+
// top level, so finding one means the nearest @media is not the enclosing
|
|
154
|
+
// one and the rule is really live everywhere.
|
|
155
|
+
assert.ok(!/\n\}/.test(between), `${re} sits AFTER its nearest @media closed — it is live at every width`);
|
|
156
|
+
// Pin the EXACT breakpoint, not a band: style.css guards at 640, primer.css
|
|
157
|
+
// at 760 (its only breakpoint, the one primer.js's matchMedia mirrors).
|
|
158
|
+
// Narrowing primer's to 640 "for consistency" would silently reopen the
|
|
159
|
+
// 641–760px band the rule exists to cover, so a tolerant regex would hide
|
|
160
|
+
// the very drift this test is here to catch.
|
|
161
|
+
assert.match(between, new RegExp(`max-width:\\s*${breakpoint}px`),
|
|
162
|
+
`${re} must sit in the max-width: ${breakpoint}px media query`);
|
|
163
|
+
};
|
|
164
|
+
scoped(css, /:has\(>\s*\.ledger\)\s*\{[^}]*overflow-x:\s*auto/, 640);
|
|
165
|
+
scoped(css, /\.ledger th\s*\{[^}]*position:\s*static/, 640);
|
|
166
|
+
scoped(css, /\.rank-tip\s*\{\s*display:\s*none/, 640);
|
|
167
|
+
scoped(read('primer.css'), /\.primer-article table\s*\{[^}]*overflow-x:\s*auto/, 760);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// The strip has to hold BOTH invariants, and the second one is the easy one to
|
|
171
|
+
// lose: a first cut of this fix put `display: block; overflow-x: auto` on the
|
|
172
|
+
// table itself, which scrolls a wide ledger correctly but re-wraps the rows in
|
|
173
|
+
// an anonymous shrink-to-fit table box — measured at 390w on /profile,
|
|
174
|
+
// #works-table's rows rendered 240px inside a 329px card (an 89px gap where the
|
|
175
|
+
// th rule and row rules stopped short), and that is every empty ledger, not an
|
|
176
|
+
// exotic case. Keeping the ledger a table is what keeps `width: 100%` filling.
|
|
177
|
+
test('a ledger narrower than its card still fills it', () => {
|
|
178
|
+
const css = read('style.css');
|
|
179
|
+
assert.match(css, /\.ledger\s*\{\s*\n\s*width:\s*100%/,
|
|
180
|
+
'.ledger fills its card — that is the declaration the strip must not defeat');
|
|
181
|
+
assert.doesNotMatch(css, /\.ledger\s*\{[^}]*display:\s*block/,
|
|
182
|
+
'.ledger must stay a table; display:block shrink-to-fits its rows inside the card');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('the sidebar scrolls its nav list, not the whole column', () => {
|
|
186
|
+
const css = read('style.css');
|
|
187
|
+
const nav = css.slice(css.indexOf('.app-sidebar__nav {'));
|
|
188
|
+
const block = nav.slice(0, nav.indexOf('\n}'));
|
|
189
|
+
// min-height:0 is what lets the flex child shrink below its content; without
|
|
190
|
+
// it the rule is inert and "Sign out" slides back under the fold.
|
|
191
|
+
assert.match(block, /overflow-y:\s*auto/, '.app-sidebar__nav is the scroller');
|
|
192
|
+
assert.match(block, /min-height:\s*0/, '.app-sidebar__nav needs min-height:0 to actually scroll');
|
|
193
|
+
const sidebar = css.slice(css.indexOf('.app-sidebar {'));
|
|
194
|
+
assert.doesNotMatch(sidebar.slice(0, sidebar.indexOf('\n}')), /overflow-y:\s*auto/,
|
|
195
|
+
'.app-sidebar must NOT scroll — that is what pushed sign-out off-screen');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---- 6. every hall list is paged through the kit (task 1002446) -----------
|
|
199
|
+
|
|
200
|
+
test('the peer-recognition feed is paged through OTBKit, not by hand', () => {
|
|
201
|
+
assert.match(read('hall-widgets.js'), /pagerBarHtml\('acclaim'/, 'the acclaim card stamps the kit pager bar');
|
|
202
|
+
const js = read('builders.js');
|
|
203
|
+
assert.match(js, /makePager\('acclaim'/, 'acclaim has a kit pager');
|
|
204
|
+
assert.match(js, /paginate\(acclaimState\.tasks, acclaimPager\)/, 'rows are sliced by the kit');
|
|
205
|
+
assert.match(js, /wirePager\(acclaimPager/, 'prev/next/per-page are wired');
|
|
206
|
+
// A vote mutates one row in place; the held array has to learn about it or a
|
|
207
|
+
// page flip re-renders the pre-vote tally over the vote just cast.
|
|
208
|
+
assert.match(js, /acclaimState\.tasks = acclaimState\.tasks\.map/,
|
|
209
|
+
'a cast vote is written back into the paged array');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ---- 7. the provenance note stays put (task 1002446) ----------------------
|
|
213
|
+
|
|
214
|
+
// The 2179 density pass landed in a DIFFERENT commit (97cbef0) whose own message
|
|
215
|
+
// declares this task "now redundant". Read only 1002446's diff and you conclude
|
|
216
|
+
// the density pass was skipped; read only that message and you close three live
|
|
217
|
+
// defects. The reconciliation is in the module doc, so pin that it survives the
|
|
218
|
+
// next doc trim — a paragraph nothing depends on is the first thing cut.
|
|
219
|
+
test('the module doc still records where the 2179 density pass landed', () => {
|
|
220
|
+
const doc = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/CLAUDE.md'), 'utf8');
|
|
221
|
+
assert.match(doc, /97cbef0/, 'names the commit that actually shipped part 1');
|
|
222
|
+
assert.match(doc, /--space-1/, 'and an anchor a reader can go verify it against');
|
|
223
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// tests/hall_nav.mjs — the hall sidebar's information architecture (task 2175).
|
|
2
|
+
//
|
|
3
|
+
// The sidebar is grouped by AUDIENCE of the data: the project (unlabeled), You,
|
|
4
|
+
// Community, Docs, Archon. That grouping is only worth anything if it stays
|
|
5
|
+
// true, so this executes the real shell.js in a minimal DOM stub (the
|
|
6
|
+
// hall_signout.mjs pattern), captures the sidebar markup it renders, and asserts
|
|
7
|
+
// the invariants that silently rot otherwise:
|
|
8
|
+
//
|
|
9
|
+
// - the group order + membership (the IA itself),
|
|
10
|
+
// - every nav href resolves to a page that exists ("all links land"),
|
|
11
|
+
// - a group's head is never more permissive than its most permissive item
|
|
12
|
+
// (otherwise the head renders over an empty group),
|
|
13
|
+
// - Achievements has no nav entry, but #achievements still routes to Standing
|
|
14
|
+
// (task 2174 folded it in; the alias is what keeps old bookmarks working).
|
|
15
|
+
//
|
|
16
|
+
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import vm from 'node:vm';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
26
|
+
const SRC = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
|
|
27
|
+
|
|
28
|
+
// Boot shell.js under a DOM stub and return { sidebarHtml, viewHidden }.
|
|
29
|
+
// Elements are memoised per id so the assertions can inspect what the router did
|
|
30
|
+
// to them (#view-standing etc.) after boot.
|
|
31
|
+
function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
32
|
+
const els = new Map();
|
|
33
|
+
const makeEl = () => {
|
|
34
|
+
const el = {
|
|
35
|
+
dataset: {}, style: {}, title: '', textContent: '', hidden: false, _html: '',
|
|
36
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
37
|
+
setAttribute() {}, getAttribute() { return null; }, removeAttribute() {},
|
|
38
|
+
addEventListener() {}, querySelectorAll() { return []; },
|
|
39
|
+
appendChild() {}, focus() {}, click() {}, scrollIntoView() {},
|
|
40
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
41
|
+
};
|
|
42
|
+
return el;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const location = {
|
|
46
|
+
hostname: 'builders.cloudbongos.com',
|
|
47
|
+
origin: 'https://builders.cloudbongos.com',
|
|
48
|
+
pathname, hash,
|
|
49
|
+
assign() {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const documentObj = {
|
|
53
|
+
body: { dataset: { page } },
|
|
54
|
+
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
55
|
+
getElementById(id) {
|
|
56
|
+
if (!els.has(id)) els.set(id, makeEl());
|
|
57
|
+
return els.get(id);
|
|
58
|
+
},
|
|
59
|
+
querySelectorAll() { return []; },
|
|
60
|
+
createElement() { return makeEl(); },
|
|
61
|
+
addEventListener() {},
|
|
62
|
+
dispatchEvent() {},
|
|
63
|
+
get cookie() { return ''; },
|
|
64
|
+
set cookie(_v) {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const windowObj = {
|
|
68
|
+
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
69
|
+
// Index pages never reach probeMe (builders.js owns /me there); a non-index
|
|
70
|
+
// boot would, so hand back a never-settling promise rather than fake auth —
|
|
71
|
+
// these assertions are about the rendered model, not the gated state.
|
|
72
|
+
BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
|
|
73
|
+
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
74
|
+
localStorage: { getItem() { return null; }, setItem() {} },
|
|
75
|
+
requestAnimationFrame() {},
|
|
76
|
+
scrollTo() {},
|
|
77
|
+
location,
|
|
78
|
+
addEventListener() {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sandbox = {
|
|
82
|
+
window: windowObj, document: documentObj, location, console, setTimeout, clearTimeout,
|
|
83
|
+
CustomEvent: class { constructor(type, init) { this.type = type; Object.assign(this, init); } },
|
|
84
|
+
};
|
|
85
|
+
sandbox.globalThis = sandbox;
|
|
86
|
+
vm.createContext(sandbox);
|
|
87
|
+
vm.runInContext(SRC, sandbox, { filename: 'shell.js' });
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
sidebarHtml: documentObj.getElementById('app-sidebar').innerHTML,
|
|
91
|
+
viewHidden: (v) => documentObj.getElementById('view-' + v).hidden,
|
|
92
|
+
shell: windowObj.OTBShell,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse the rendered sidebar back into the nav model it came from. Deliberately
|
|
97
|
+
// reads the MARKUP rather than importing NAV: what a builder sees is the markup,
|
|
98
|
+
// and a render bug (a dropped group head, an unescaped href) shows up here.
|
|
99
|
+
function parseNav(html) {
|
|
100
|
+
return html.split('<div class="nav-group').slice(1).map((block) => {
|
|
101
|
+
const label = (block.match(/nav-group__head-label">([^<]*)</) || [, ''])[1];
|
|
102
|
+
const headGate = (block.match(/class="nav-group__head" data-gate="([^"]+)"/) || [, null])[1];
|
|
103
|
+
const items = [...block.matchAll(/<a class="nav-item" id="nav-([^"]+)" href="([^"]*)"([^>]*)>/g)].map((m) => ({
|
|
104
|
+
id: m[1],
|
|
105
|
+
href: m[2],
|
|
106
|
+
gate: (m[3].match(/data-gate="([^"]+)"/) || [, null])[1],
|
|
107
|
+
external: / target="_blank"/.test(m[3]),
|
|
108
|
+
}));
|
|
109
|
+
return { label, headGate, items };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Permissiveness ladder, widest first. A group head gated MORE widely than every
|
|
114
|
+
// one of its items renders a label over nothing.
|
|
115
|
+
const GATE_WIDTH = { always: 0, signed: 1, work: 1, member: 2, archon: 3 };
|
|
116
|
+
const headOutOfRange = (g) =>
|
|
117
|
+
g.headGate != null && GATE_WIDTH[g.headGate] < Math.min(...g.items.map((i) => GATE_WIDTH[i.gate]));
|
|
118
|
+
|
|
119
|
+
// A nav href must land: an index view hash, an external apex link, or a real
|
|
120
|
+
// page file in the hall surface.
|
|
121
|
+
function hrefResolves(item) {
|
|
122
|
+
if (item.external) return true;
|
|
123
|
+
const p = item.href.replace(/^\/#.*/, '/').replace(/^#.*/, '/');
|
|
124
|
+
const slug = p === '/' ? 'index' : p.replace(/^\//, '');
|
|
125
|
+
return fs.existsSync(path.join(HALL, slug + '.html'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test('sidebar groups read by audience, in order', () => {
|
|
129
|
+
const nav = parseNav(boot().sidebarHtml);
|
|
130
|
+
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Archon']);
|
|
131
|
+
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work'],
|
|
132
|
+
'the unlabeled group is the project itself (task 1002425 order)');
|
|
133
|
+
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
134
|
+
assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard'], 'Community = everyone else');
|
|
135
|
+
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
136
|
+
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
137
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('Economy lives in Docs and points at the reward-schedule page', () => {
|
|
141
|
+
const nav = parseNav(boot().sidebarHtml);
|
|
142
|
+
const economy = nav.find((g) => g.label === 'Docs').items.find((i) => i.id === 'economy');
|
|
143
|
+
assert.ok(economy, 'Docs carries an Economy entry — /drachmae had no nav entry before task 2175');
|
|
144
|
+
assert.equal(economy.href, '/drachmae');
|
|
145
|
+
assert.ok(fs.existsSync(path.join(HALL, 'drachmae.html')), 'the page it points at exists');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('every nav link lands on a page that exists', () => {
|
|
149
|
+
const items = parseNav(boot().sidebarHtml).flatMap((g) => g.items);
|
|
150
|
+
assert.ok(items.length >= 14, `sanity: parsed the whole nav, got ${items.length} items`);
|
|
151
|
+
const dead = items.filter((i) => !hrefResolves(i));
|
|
152
|
+
assert.deepEqual(dead, [], 'no nav item points at a missing page');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('href check actually CATCHES a dead link', () => {
|
|
156
|
+
assert.equal(hrefResolves({ href: '/no-such-page', external: false }), false);
|
|
157
|
+
assert.equal(hrefResolves({ href: '/drachmae', external: false }), true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('no group head is gated wider than its own items', () => {
|
|
161
|
+
const bad = parseNav(boot().sidebarHtml).filter(headOutOfRange).map((g) => g.label);
|
|
162
|
+
assert.deepEqual(bad, [], 'a head gated wider than every item would render a label over nothing');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('group-gate check actually CATCHES a head gated wider than its items', () => {
|
|
166
|
+
assert.equal(headOutOfRange({ label: 'X', headGate: 'signed', items: [{ gate: 'member' }] }), true,
|
|
167
|
+
'signed head over a member-only item is the bug');
|
|
168
|
+
assert.equal(headOutOfRange({ label: 'X', headGate: 'member', items: [{ gate: 'member' }] }), false);
|
|
169
|
+
assert.equal(headOutOfRange({ label: 'X', headGate: 'archon', items: [{ gate: 'archon' }] }), false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('Achievements has no nav entry, but #achievements still routes to Standing', () => {
|
|
173
|
+
const ids = parseNav(boot().sidebarHtml).flatMap((g) => g.items.map((i) => i.id));
|
|
174
|
+
assert.ok(!ids.includes('achievements'), 'the nav entry retired with task 2175');
|
|
175
|
+
|
|
176
|
+
// The alias is the compatibility contract task 2174 left behind — old
|
|
177
|
+
// bookmarks and the Standing stat tiles still link #achievements.
|
|
178
|
+
const idx = boot({ page: 'home', pathname: '/', hash: '#achievements' });
|
|
179
|
+
assert.equal(idx.viewHidden('standing'), false, '#achievements shows the Standing view');
|
|
180
|
+
assert.equal(idx.viewHidden('home'), true, 'and hides Home');
|
|
181
|
+
assert.equal(idx.shell.currentView(), 'standing');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('collapsible groups carry a stable storage key (collapse persistence)', () => {
|
|
185
|
+
const html = boot().sidebarHtml;
|
|
186
|
+
for (const g of ['you', 'community', 'docs', 'archon']) {
|
|
187
|
+
assert.match(html, new RegExp(`data-group="${g}"`), `${g} is collapsible (hall-nav-collapsed key)`);
|
|
188
|
+
}
|
|
189
|
+
assert.ok(!/nav-group--collapsible[^>]*>(?!<button)/.test(html.split('<div class="nav-group')[1] || ''),
|
|
190
|
+
'the unlabeled project group stays always-expanded');
|
|
191
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// tests/hall_palette.mjs — the hall jump palette (task 2178).
|
|
2
|
+
//
|
|
3
|
+
// The palette indexes PAGES live from the rendered sidebar (so rank/module
|
|
4
|
+
// gating can never drift), but its SUB-destinations — Standing sections, Tasks
|
|
5
|
+
// tabs, Settings panels — cannot be read from another page's DOM, so palette.js
|
|
6
|
+
// declares them statically. A static list pointing at ids that no longer exist
|
|
7
|
+
// is a jump that silently goes nowhere, so the load-bearing test here holds that
|
|
8
|
+
// list against its real sources.
|
|
9
|
+
//
|
|
10
|
+
// Also covers: the id syntaxes, gating (a hidden nav item is not a destination),
|
|
11
|
+
// and the progressive-enhancement contract (shell.js renders the slot inert and
|
|
12
|
+
// never calls into palette.js, so a page without palette.js just has no palette).
|
|
13
|
+
//
|
|
14
|
+
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import vm from 'node:vm';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
24
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
25
|
+
const PALETTE_SRC = read('palette.js');
|
|
26
|
+
const SHELL_SRC = read('shell.js');
|
|
27
|
+
|
|
28
|
+
// Boot palette.js against a DOM stub whose sidebar carries `navItems`, and whose
|
|
29
|
+
// `visibleIds` decide which section groups are on offer. Returns window.OTBPalette.
|
|
30
|
+
function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work', 'shell-user'], modules = null } = {}) {
|
|
31
|
+
const makeNav = (it) => ({
|
|
32
|
+
hidden: !!it.hidden,
|
|
33
|
+
target: it.external ? '_blank' : '',
|
|
34
|
+
getAttribute: (n) => (n === 'href' ? it.href : null),
|
|
35
|
+
querySelector: (sel) => (sel === '.nav-item__label' ? { textContent: it.label } : null),
|
|
36
|
+
});
|
|
37
|
+
const stubEl = () => ({
|
|
38
|
+
hidden: false, dataset: {}, textContent: '', value: '', className: '',
|
|
39
|
+
classList: { add() {}, remove() {}, contains() { return false; }, toggle() { return false; } },
|
|
40
|
+
setAttribute() {}, getAttribute() { return null; },
|
|
41
|
+
addEventListener() {}, appendChild() {}, focus() {}, querySelector() { return stubEl(); },
|
|
42
|
+
querySelectorAll() { return []; },
|
|
43
|
+
set innerHTML(_v) {}, get innerHTML() { return ''; },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const documentObj = {
|
|
47
|
+
body: { dataset: { page: 'home' }, appendChild() {} },
|
|
48
|
+
documentElement: { classList: { add() {}, remove() {} } },
|
|
49
|
+
getElementById(id) {
|
|
50
|
+
if (id === 'topbar-jump' || id === 'topbar-jump-kbd') return stubEl();
|
|
51
|
+
return visibleIds.includes(id) ? { hidden: false } : null;
|
|
52
|
+
},
|
|
53
|
+
querySelectorAll(sel) {
|
|
54
|
+
if (sel === '#app-sidebar .nav-item') return navItems.map(makeNav);
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
createElement: () => stubEl(),
|
|
58
|
+
addEventListener() {},
|
|
59
|
+
activeElement: null,
|
|
60
|
+
};
|
|
61
|
+
const windowObj = { location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
|
|
62
|
+
if (modules) windowObj.__MODULES__ = { enabled: modules };
|
|
63
|
+
const sandbox = {
|
|
64
|
+
window: windowObj, document: documentObj, navigator: { platform: 'Win32', userAgent: 'test' },
|
|
65
|
+
console,
|
|
66
|
+
};
|
|
67
|
+
sandbox.globalThis = sandbox;
|
|
68
|
+
vm.createContext(sandbox);
|
|
69
|
+
vm.runInContext(PALETTE_SRC, sandbox, { filename: 'palette.js' });
|
|
70
|
+
// Copy results across the realm boundary — a vm-created array has the sandbox's
|
|
71
|
+
// Array.prototype, which deepStrictEqual rejects even when the values match.
|
|
72
|
+
const P = windowObj.OTBPalette;
|
|
73
|
+
return { compute: (q) => Array.from(P.compute(q), (e) => ({ ...e })) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const NAV_FIXTURE = [
|
|
77
|
+
{ label: 'Home', href: '/' },
|
|
78
|
+
{ label: 'Tasks', href: '/work' },
|
|
79
|
+
{ label: 'Standing', href: '/#standing' },
|
|
80
|
+
{ label: 'Economy', href: '/drachmae' },
|
|
81
|
+
{ label: 'API docs', href: '/docs', external: true },
|
|
82
|
+
{ label: 'Watch', href: '/watch', hidden: true }, // Archon-only, gated off
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// ---- the drift guard: the static map vs its real sources ------------------
|
|
86
|
+
|
|
87
|
+
// Anchors palette.js declares, by owning page.
|
|
88
|
+
function declaredHrefs(prefixTest) {
|
|
89
|
+
return [...PALETTE_SRC.matchAll(/href:\s*'([^']+)'/g)].map((m) => m[1]).filter(prefixTest);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('Standing section anchors exist in builders.js and are routable in shell.js', () => {
|
|
93
|
+
const groups = [...read('builders.js').matchAll(/\{ key: '([a-z]+)', label: '[^']+' \}/g)].map((m) => m[1]);
|
|
94
|
+
assert.ok(groups.length >= 4, `sanity: found STANDING_GROUPS (${groups})`);
|
|
95
|
+
const declared = declaredHrefs((h) => h.startsWith('/#standing-')).map((h) => h.replace('/#', ''));
|
|
96
|
+
assert.ok(declared.length, 'palette declares Standing sections');
|
|
97
|
+
for (const id of declared) {
|
|
98
|
+
const key = id.replace('standing-', '');
|
|
99
|
+
assert.ok(groups.includes(key), `${id} must be a real STANDING_GROUPS key (builders.js)`);
|
|
100
|
+
// shell.js must alias it, or route() ignores the hash and the jump goes nowhere.
|
|
101
|
+
assert.match(SHELL_SRC, new RegExp(`'${id}':\\s*\\{ view: 'standing', anchor: '${id}' \\}`),
|
|
102
|
+
`${id} must have a VIEW_ALIASES entry in shell.js`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Tasks tab anchors exist in work.js makeTabs', () => {
|
|
107
|
+
const ids = [...read('work.js').matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
|
|
108
|
+
assert.ok(ids.length >= 4, `sanity: found work tabs (${ids})`);
|
|
109
|
+
for (const h of declaredHrefs((x) => x.startsWith('/work#'))) {
|
|
110
|
+
assert.ok(ids.includes(h.split('#')[1]), `${h} must be a real work.js tab id`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
|
|
115
|
+
// RAIL_GROUPS is derived: RAIL_ITEMS.map(i => i.id).concat('extensions').
|
|
116
|
+
const src = read('settings.js');
|
|
117
|
+
const block = (src.match(/RAIL_ITEMS\s*=\s*\[([\s\S]*?)\n\s*\];/) || [, ''])[1];
|
|
118
|
+
const ids = [...block.matchAll(/\{ id: '([a-z-]+)'/g)].map((m) => m[1]);
|
|
119
|
+
const concat = [...(src.match(/RAIL_GROUPS\s*=\s*RAIL_ITEMS[\s\S]*?\.concat\(([^;]*)\);/) || [, ''])[1]
|
|
120
|
+
.matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
|
|
121
|
+
const groups = ids.concat(concat);
|
|
122
|
+
assert.ok(groups.length >= 5, `sanity: derived RAIL_GROUPS (${groups})`);
|
|
123
|
+
for (const h of declaredHrefs((x) => x.startsWith('/settings#'))) {
|
|
124
|
+
assert.ok(groups.includes(h.split('#')[1]), `${h} must be a real settings.js rail group`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('the Dev box panel jump carries settings.js own dev-box module condition', () => {
|
|
129
|
+
// settings.js builds that panel only when moduleOn('dev-box'); offering the
|
|
130
|
+
// jump regardless would land on a panel that was never rendered.
|
|
131
|
+
assert.match(read('settings.js'), /moduleOn\('dev-box'\)\s*\?\s*\[\{ id: 'devbox'/,
|
|
132
|
+
'settings.js still gates the devbox panel on the module');
|
|
133
|
+
assert.match(PALETTE_SRC, /href: '\/settings#devbox', module: 'dev-box'/,
|
|
134
|
+
'so the palette entry must declare the same module');
|
|
135
|
+
|
|
136
|
+
const on = bootPalette({ navItems: NAV_FIXTURE, modules: { 'dev-box': true } }).compute('dev box');
|
|
137
|
+
assert.ok(on.some((r) => r.href === '/settings#devbox'), 'offered when the module is on');
|
|
138
|
+
const off = bootPalette({ navItems: NAV_FIXTURE, modules: { 'dev-box': false } }).compute('dev box');
|
|
139
|
+
assert.ok(!off.some((r) => r.href === '/settings#devbox'), 'withheld when the module is off');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('the anchor guard actually CATCHES a declared id that does not exist', () => {
|
|
143
|
+
const groups = ['work', 'karma', 'progress', 'achievements'];
|
|
144
|
+
assert.equal(groups.includes('standing-nope'.replace('standing-', '')), false,
|
|
145
|
+
'an invented section id fails the membership check the tests above apply');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---- indexing + gating ----------------------------------------------------
|
|
149
|
+
|
|
150
|
+
test('pages come from the sidebar, and a gated-off nav item is not a destination', () => {
|
|
151
|
+
const P = bootPalette({ navItems: NAV_FIXTURE });
|
|
152
|
+
const labels = P.compute('').filter((r) => r.group === 'Pages').map((r) => r.label);
|
|
153
|
+
assert.deepEqual(labels, ['Home', 'Tasks', 'Standing', 'Economy', 'API docs']);
|
|
154
|
+
assert.ok(!labels.includes('Watch'), 'a hidden (rank-gated) nav item must not be jumpable');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('section groups appear only when their owning nav item is visible', () => {
|
|
158
|
+
const all = bootPalette({ navItems: NAV_FIXTURE }).compute('');
|
|
159
|
+
assert.ok(all.some((r) => r.group === 'Standing'), 'Standing sections offered when nav-standing shows');
|
|
160
|
+
assert.ok(all.some((r) => r.group === 'Settings'), 'Settings panels offered when the profile chip shows');
|
|
161
|
+
|
|
162
|
+
const gatedOut = bootPalette({ navItems: NAV_FIXTURE, visibleIds: [] }).compute('');
|
|
163
|
+
assert.ok(!gatedOut.some((r) => ['Standing', 'Tasks', 'Settings'].includes(r.group)),
|
|
164
|
+
'no sub-destinations when their owners are hidden');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('the external API-docs entry is marked so it opens in a new tab', () => {
|
|
168
|
+
const hit = bootPalette({ navItems: NAV_FIXTURE }).compute('api docs')[0];
|
|
169
|
+
assert.equal(hit.label, 'API docs');
|
|
170
|
+
assert.equal(hit.external, true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---- id syntax ------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
test('id syntaxes resolve to the right destination', () => {
|
|
176
|
+
const P = bootPalette({ navItems: NAV_FIXTURE });
|
|
177
|
+
const top = (q) => P.compute(q)[0];
|
|
178
|
+
assert.deepEqual({ href: top('1234').href, overlay: !!top('1234').overlay }, { href: '/task/1234', overlay: false });
|
|
179
|
+
assert.equal(top('g 12').href, '/goals#/goal/12');
|
|
180
|
+
assert.equal(top('g12').href, '/goals#/goal/12', 'the space is optional');
|
|
181
|
+
assert.deepEqual({ href: top('b 7').href, overlay: top('b 7').overlay }, { href: '#/blocker/7', overlay: true });
|
|
182
|
+
assert.deepEqual({ href: top('i 42').href, overlay: top('i 42').overlay }, { href: '#/idea/42', overlay: true });
|
|
183
|
+
assert.equal(top('1234').group, 'Go to');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('a non-matching query yields nothing (the empty state), and text still matches pages', () => {
|
|
187
|
+
const P = bootPalette({ navItems: NAV_FIXTURE });
|
|
188
|
+
assert.deepEqual(P.compute('zzzqqq'), []);
|
|
189
|
+
assert.equal(P.compute('econ')[0].label, 'Economy', 'substring match');
|
|
190
|
+
assert.equal(P.compute('stw')[0].label, 'Work', 'subsequence match reaches Standing > Work');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---- progressive enhancement + wiring -------------------------------------
|
|
194
|
+
|
|
195
|
+
test('shell.js renders the jump slot inert and never calls into palette.js', () => {
|
|
196
|
+
assert.match(SHELL_SRC, /id="topbar-jump"[^>]*hidden/, 'the slot ships hidden');
|
|
197
|
+
assert.ok(!/OTBPalette/.test(SHELL_SRC), 'the shell must not depend on the palette');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('every page that loads the shell also loads the palette', () => {
|
|
201
|
+
const pages = fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
|
|
202
|
+
const withShell = pages.filter((f) => /src="\/builders\/shell\.js/.test(read(f)));
|
|
203
|
+
assert.ok(withShell.length >= 15, `sanity: ${withShell.length} pages carry the shell`);
|
|
204
|
+
const missing = withShell.filter((f) => !/src="\/builders\/palette\.js/.test(read(f)));
|
|
205
|
+
assert.deepEqual(missing, [], 'Ctrl-K must work on every hall page');
|
|
206
|
+
});
|