@bongos/core 1.18.0 → 1.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
package/tests/upgrade.mjs CHANGED
@@ -16,10 +16,11 @@ import { strict as assert } from 'node:assert';
16
16
  import { createRequire } from 'node:module';
17
17
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
18
18
  import { tmpdir } from 'node:os';
19
- import { join } from 'node:path';
19
+ import { join, dirname } from 'node:path';
20
20
 
21
21
  const require = createRequire(import.meta.url);
22
22
  const u = require('../scripts/gds/upgrade.js');
23
+ const pc = require('../scripts/gds/package-core.js'); // the packager — reuse its EXACT hash fns to build reference pins
23
24
 
24
25
  let passed = 0, failed = 0;
25
26
  const pending = [];
@@ -159,7 +160,9 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
159
160
  let materialized = null;
160
161
  let regenArgs = null;
161
162
  const res = await u.runUpgrade(
162
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service' },
163
+ // healthCheck:false — this test exercises the full pipeline, not the health poll (which would need
164
+ // a fetch mock); a managed-restart bump now requires a --health-url unless health is opted out (H9).
165
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service', healthCheck: false },
163
166
  {
164
167
  log: () => {}, err: () => {},
165
168
  run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }),
@@ -398,7 +401,7 @@ t('runUpgrade --registry (full): pins the exact version + writes the env-fed .np
398
401
  const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
399
402
  const calls = [];
400
403
  const res = await u.runUpgrade(
401
- { to: '1.17.0', registry: true, instance: dir, service: 'demo.service' },
404
+ { to: '1.17.0', registry: true, instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out (no health poll under test)
402
405
  {
403
406
  log: () => {}, err: () => {},
404
407
  run: fakeRun(calls, { dirty: '', installTo: '1.17.0', dir }),
@@ -479,7 +482,7 @@ t('rollback on migrate failure: reverts pin + reinstalls previous core + rolled_
479
482
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
480
483
  const calls = [], queries = [];
481
484
  const res = await u.runUpgrade(
482
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
485
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out — this test is about migrate-failure rollback, not health
483
486
  {
484
487
  log: () => {}, err: () => {},
485
488
  run: pinAwareRun(calls, { dir, failMigrate: true }),
@@ -502,7 +505,7 @@ t('rollback on npm-install failure: reverts pin + reinstalls previous core', asy
502
505
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
503
506
  const calls = [], queries = [];
504
507
  const res = await u.runUpgrade(
505
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
508
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out — this test is about install-failure rollback, not health
506
509
  {
507
510
  log: () => {}, err: () => {},
508
511
  run: pinAwareRun(calls, { dir, failInstallForVersion: '1.15.0' }),
@@ -547,6 +550,391 @@ t('restorePin: writes a previous dep string back verbatim (file: or registry)',
547
550
  assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '^1.16.0');
548
551
  });
549
552
 
553
+ // ---- 7. integrity-pin verification (task 1002216 / audit H8, finding F1) ----
554
+ // Build a reference pin exactly as the packager does — canonicalTreeHash over the pinned
555
+ // files' sha256, so a matching installed tree recomputes byte-identically.
556
+ function pinManifestFor(files, { tarballSha = null, tarballBytes = 0 } = {}) {
557
+ const meta = files
558
+ .map((f) => ({ path: f.path, mode: f.mode || '0000644', sha256: pc.sha256hex(Buffer.from(f.content)) }))
559
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
560
+ const m = { artifact: 'bongos-core', tree_sha256: pc.canonicalTreeHash(meta), files: meta };
561
+ if (tarballSha) m.tarball = { name: 'bongos-core.tgz', sha256: tarballSha, bytes: tarballBytes };
562
+ return m;
563
+ }
564
+ function writeCoreTree(coreDir, files) {
565
+ for (const f of files) {
566
+ const abs = join(coreDir, ...f.path.split('/'));
567
+ mkdirSync(dirname(abs), { recursive: true });
568
+ writeFileSync(abs, f.content);
569
+ }
570
+ }
571
+ const PIN_FILES = [{ path: 'a.js', content: 'console.log(1)\n' }, { path: 'sub/b.json', content: '{"x":1}\n' }];
572
+
573
+ t('verifyInstalledPin: matching installed tree + tarball bytes → ok (both checks)', () => {
574
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
575
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
576
+ writeCoreTree(coreDir, PIN_FILES);
577
+ const tgzBytes = Buffer.from('deterministic-fake-tgz');
578
+ const tgz = join(dir, 'vendor', 'bongos-core-1.15.0.tgz');
579
+ mkdirSync(dirname(tgz), { recursive: true });
580
+ writeFileSync(tgz, tgzBytes);
581
+ const m = pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(tgzBytes), tarballBytes: tgzBytes.length });
582
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: tgz });
583
+ assert.equal(res.ran, true);
584
+ assert.equal(res.ok, true, res.problems.join('; '));
585
+ assert.deepEqual(res.checks.map((c) => c.name).sort(), ['tarball.sha256', 'tree_sha256']);
586
+ });
587
+
588
+ t('verifyInstalledPin: a corrupted installed file → mismatch (ran, not ok)', () => {
589
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
590
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
591
+ writeCoreTree(coreDir, PIN_FILES);
592
+ const m = pinManifestFor(PIN_FILES);
593
+ writeFileSync(join(coreDir, 'a.js'), 'console.log(999) // tampered\n'); // corrupt AFTER pinning
594
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: null });
595
+ assert.equal(res.ran, true);
596
+ assert.equal(res.ok, false);
597
+ assert.ok(res.problems.some((p) => /content mismatch: a\.js/.test(p)), res.problems.join('; '));
598
+ });
599
+
600
+ t('verifyInstalledPin: a missing installed file → mismatch', () => {
601
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
602
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
603
+ writeCoreTree(coreDir, [PIN_FILES[0]]); // omit sub/b.json
604
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: pinManifestFor(PIN_FILES), tgzPath: null });
605
+ assert.equal(res.ok, false);
606
+ assert.ok(res.problems.some((p) => /missing installed file: sub\/b\.json/.test(p)), res.problems.join('; '));
607
+ });
608
+
609
+ t('verifyInstalledPin: tarball bytes differ from the pin → mismatch', () => {
610
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
611
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
612
+ writeCoreTree(coreDir, PIN_FILES);
613
+ const tgz = join(dir, 'vendor', 'bongos-core-1.15.0.tgz');
614
+ mkdirSync(dirname(tgz), { recursive: true });
615
+ writeFileSync(tgz, Buffer.from('the-actual-bytes'));
616
+ const m = pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(Buffer.from('DIFFERENT-bytes')), tarballBytes: 9 });
617
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: tgz });
618
+ assert.equal(res.ok, false);
619
+ assert.ok(res.problems.some((p) => /tarball bytes/.test(p)), res.problems.join('; '));
620
+ });
621
+
622
+ t('verifyInstalledPin: no pin in the reference manifest → ran:false (nothing to check)', () => {
623
+ const res = u.verifyInstalledPin({ coreDir: '/nonexistent', referenceManifest: {}, tgzPath: null });
624
+ assert.equal(res.ran, false);
625
+ assert.equal(res.ok, false);
626
+ });
627
+
628
+ t('resolveReferenceManifest: prefers the committed vendored side-car (strong)', () => {
629
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
630
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
631
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
632
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
633
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'x');
634
+ const ref = u.resolveReferenceManifest({ instanceDir: dir, coreDir, targetVersion: '1.15.0' });
635
+ assert.equal(ref.source, 'vendored side-car manifest');
636
+ assert.equal(ref.strong, true);
637
+ assert.ok(ref.tgzPath && existsSync(ref.tgzPath));
638
+ });
639
+
640
+ t('resolveReferenceManifest: falls back to the installed inner manifest (not strong)', () => {
641
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
642
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
643
+ mkdirSync(coreDir, { recursive: true });
644
+ writeFileSync(join(coreDir, '.bongos-core.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
645
+ const ref = u.resolveReferenceManifest({ instanceDir: dir, coreDir, targetVersion: '1.15.0' });
646
+ assert.equal(ref.source, 'installed inner manifest');
647
+ assert.equal(ref.strong, false);
648
+ assert.equal(ref.tgzPath, null);
649
+ });
650
+
651
+ // A spawn shim whose `npm install` writes an installed core tree (+ version) so the
652
+ // real pin-verify step can run against it. `files` is what actually lands on disk.
653
+ function installWritingTree(calls, { dir, version, files }) {
654
+ return (cmd, args) => {
655
+ calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
656
+ if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: '' };
657
+ if (cmd === 'npm' && args[0] === 'install') {
658
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
659
+ writeCoreTree(coreDir, files);
660
+ writeFileSync(join(coreDir, 'package.json'), JSON.stringify({ name: '@bongos/core', version }) + '\n');
661
+ }
662
+ return { status: 0 };
663
+ };
664
+ }
665
+
666
+ t('runUpgrade: a matching pin passes verification (upgrade proceeds)', async () => {
667
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
668
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
669
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
670
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'),
671
+ JSON.stringify(pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(Buffer.from('fake-tgz')), tarballBytes: 8 })));
672
+ const logs = [];
673
+ const res = await u.runUpgrade(
674
+ { to: '1.15.0', instance: dir },
675
+ {
676
+ log: (m) => logs.push(m), err: () => {},
677
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
678
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
679
+ }
680
+ );
681
+ assert.equal(res.ok, true, res.error);
682
+ assert.ok(logs.some((m) => /integrity pin verified.*tarball\.sha256.*tree_sha256/.test(m)), logs.join('\n'));
683
+ });
684
+
685
+ t('runUpgrade: a pin MISMATCH after install fails and auto-rolls-back', async () => {
686
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
687
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
688
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
689
+ // The reference pin is for PIN_FILES, but install writes DIFFERENT content → tree mismatch.
690
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
691
+ const corrupted = [{ path: 'a.js', content: 'console.log(666)\n' }, { path: 'sub/b.json', content: '{"x":1}\n' }];
692
+ const res = await u.runUpgrade(
693
+ { to: '1.15.0', instance: dir },
694
+ {
695
+ log: () => {}, err: () => {},
696
+ run: installWritingTree([], { dir, version: '1.15.0', files: corrupted }),
697
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
698
+ }
699
+ );
700
+ assert.equal(res.ok, false);
701
+ assert.ok(/integrity pin MISMATCH/.test(res.error || ''), res.error);
702
+ assert.equal(res.rolledBack, true);
703
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'pin reverted to the previous version on rollback');
704
+ });
705
+
706
+ t('runUpgrade: --require-pin makes a MISSING reference manifest fatal', async () => {
707
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
708
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
709
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz'); // tgz present, but NO manifest
710
+ const res = await u.runUpgrade(
711
+ { to: '1.15.0', instance: dir, requirePin: true },
712
+ {
713
+ log: () => {}, err: () => {},
714
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
715
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
716
+ }
717
+ );
718
+ assert.equal(res.ok, false);
719
+ assert.ok(/no reference manifest/.test(res.error || ''), res.error);
720
+ assert.equal(res.rolledBack, true, 'unverifiable pin rolls back under --require-pin');
721
+ });
722
+
723
+ t('runUpgrade: --skip-pin-verify bypasses the check (no manifest, still succeeds)', async () => {
724
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
725
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
726
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
727
+ const res = await u.runUpgrade(
728
+ { to: '1.15.0', instance: dir, skipPinVerify: true },
729
+ {
730
+ log: () => {}, err: () => {},
731
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
732
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
733
+ }
734
+ );
735
+ assert.equal(res.ok, true, res.error);
736
+ });
737
+
738
+ // ---- 8. mandatory post-restart health check (task 1002222 / audit H9, finding F5) ----
739
+ t('runUpgrade: a managed-restart bump WITHOUT --health-url is refused up front (H9 mandatory)', async () => {
740
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
741
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
742
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
743
+ const calls = [];
744
+ const res = await u.runUpgrade(
745
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' }, // restarts, no --health-url, not opted out
746
+ { log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
747
+ );
748
+ assert.equal(res.ok, false);
749
+ assert.match(res.error, /health check is mandatory/);
750
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'refused in pre-flight → pin untouched');
751
+ assert.ok(!calls.some((c) => /npm install/.test(c)), 'refused before any install');
752
+ });
753
+
754
+ t('runUpgrade: --no-health-check lets a managed-restart bump proceed without a URL', async () => {
755
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
756
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
757
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
758
+ const logs = [];
759
+ const res = await u.runUpgrade(
760
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false },
761
+ { log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
762
+ );
763
+ assert.equal(res.ok, true, res.error);
764
+ assert.ok(logs.some((m) => /health check skipped \(--no-health-check\)/.test(m)), logs.join('\n'));
765
+ });
766
+
767
+ t('runUpgrade: a managed-restart bump WITH a healthy --health-url confirms + succeeds', async () => {
768
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
769
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
770
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
771
+ const logs = [];
772
+ const res = await u.runUpgrade(
773
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthUrl: 'http://x/healthz' },
774
+ {
775
+ log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }),
776
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
777
+ fetch: async () => ({ ok: true, status: 200 }), sleep: async () => {}, now: () => 0,
778
+ }
779
+ );
780
+ assert.equal(res.ok, true, res.error);
781
+ assert.equal(res.healthOk, true);
782
+ assert.ok(logs.some((m) => /health http:\/\/x\/healthz → 200/.test(m)), logs.join('\n'));
783
+ });
784
+
785
+ t('runUpgrade: no managed restart + no --health-url proceeds but flags the bump as unconfirmed', async () => {
786
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
787
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
788
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
789
+ const logs = [];
790
+ const res = await u.runUpgrade(
791
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir }, // no service/restart-cmd, no --health-url
792
+ { log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
793
+ );
794
+ assert.equal(res.ok, true, res.error);
795
+ assert.equal(res.healthOk, null);
796
+ assert.ok(logs.some((m) => /not confirmed serving/.test(m)), logs.join('\n'));
797
+ });
798
+
799
+ // ---- regenerateNavDocs (ADR 0147 / task 1002314) ----------------------------
800
+ t('regenerateNavDocs: default set is the whole-file nav docs (session-index only)', () => {
801
+ assert.deepEqual(u.NAV_WHOLE_FILE_GENERATORS, ['gen-session-index.js']);
802
+ });
803
+
804
+ t('regenerateNavDocs: spawns each generator from the CORE against the INSTANCE root (cwd + CLOUDBONGOS_INSTANCE_ROOT), ok when all succeed', () => {
805
+ const calls = [];
806
+ const run = (cmd, args, opts) => { calls.push({ cmd, arg0: args[0], cwd: opts && opts.cwd, env: opts && opts.env }); return { status: 0 }; };
807
+ const fsImpl = { existsSync: () => true };
808
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-session-index.js'] }, run, fsImpl);
809
+ assert.equal(res.ok, true);
810
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
811
+ assert.deepEqual(res.failed, []);
812
+ assert.equal(calls.length, 1);
813
+ assert.equal(calls[0].cwd, '/inst', 'runs with cwd = the instance dir');
814
+ assert.equal(calls[0].env.CLOUDBONGOS_INSTANCE_ROOT, '/inst', 'sets the instance-root env so the generator writes into the instance');
815
+ assert.equal(calls[0].arg0, join('/core', 'scripts', 'gds', 'gen-session-index.js'), 'spawns the generator from the CORE package');
816
+ });
817
+
818
+ t('regenerateNavDocs: a MISSING generator is recorded as failed but the others still run (best-effort)', () => {
819
+ const spawned = [];
820
+ const run = (_cmd, args) => { spawned.push(args[0]); return { status: 0 }; };
821
+ const fsImpl = { existsSync: (p) => !String(p).endsWith('gen-missing.js') };
822
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-missing.js', 'gen-session-index.js'] }, run, fsImpl);
823
+ assert.equal(res.ok, false);
824
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
825
+ assert.equal(res.failed.length, 1);
826
+ assert.equal(res.failed[0].gen, 'gen-missing.js');
827
+ assert.equal(spawned.length, 1, 'the missing generator is not spawned; the present one still is');
828
+ });
829
+
830
+ t('regenerateNavDocs: a generator that exits nonzero is LOUD-failed (ok:false) without aborting the rest', () => {
831
+ const run = (_cmd, args) => ({ status: String(args[0]).endsWith('gen-boom.js') ? 2 : 0 });
832
+ const fsImpl = { existsSync: () => true };
833
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-boom.js', 'gen-session-index.js'] }, run, fsImpl);
834
+ assert.equal(res.ok, false);
835
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
836
+ assert.equal(res.failed.length, 1);
837
+ assert.equal(res.failed[0].gen, 'gen-boom.js');
838
+ assert.match(res.failed[0].reason, /exited 2/);
839
+ });
840
+
841
+ // ---- healDocAssets (task 1002441) -------------------------------------------
842
+ // An upgrade replaces node_modules wholesale, so it WIPES the two gitignored, core-scoped
843
+ // generated doc assets (atlas.json + the diagram .svg/.png renders) and the new tarball carries
844
+ // only the .mmd sources. These pin that the upgrade puts them back.
845
+
846
+ // A fake core tree. `renders` controls whether each .mmd already has its .svg + .png beside it.
847
+ function docAssetFs({ atlas = true, sources = ['01-a.mmd', '02-b.mmd'], renders = true } = {}) {
848
+ const present = new Set();
849
+ if (atlas) present.add(join('/core', 'modules', 'hall-ui', 'public', 'atlas.json'));
850
+ present.add(join('/core', 'scripts', 'gds', 'gen-atlas.js'));
851
+ present.add(join('/core', 'scripts', 'render-diagrams.sh'));
852
+ if (renders) {
853
+ for (const s of sources) {
854
+ const base = join('/core', 'docs', 'onboarding', 'diagrams', s.replace(/\.mmd$/, ''));
855
+ present.add(`${base}.svg`); present.add(`${base}.png`);
856
+ }
857
+ }
858
+ return { existsSync: (p) => present.has(String(p)), readdirSync: () => sources, _present: present };
859
+ }
860
+
861
+ t('missingDocAssets: a freshly-upgraded core (only .mmd sources, no atlas.json) reports BOTH groups missing', () => {
862
+ const fsImpl = docAssetFs({ atlas: false, renders: false });
863
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), ['atlas', 'diagrams']);
864
+ });
865
+
866
+ t('missingDocAssets: a fully-generated core reports nothing missing (steady state — no work)', () => {
867
+ assert.deepEqual(u.missingDocAssets('/core', docAssetFs()), []);
868
+ });
869
+
870
+ t('missingDocAssets: only atlas.json gone → only the atlas group', () => {
871
+ assert.deepEqual(u.missingDocAssets('/core', docAssetFs({ atlas: false })), ['atlas']);
872
+ });
873
+
874
+ t('missingDocAssets: a half-rendered diagram set (one .mmd lacking its .png) counts as missing', () => {
875
+ const fsImpl = docAssetFs();
876
+ fsImpl._present.delete(join('/core', 'docs', 'onboarding', 'diagrams', '02-b.png'));
877
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), ['diagrams']);
878
+ });
879
+
880
+ t('missingDocAssets: a core with NO diagram dir does not report diagrams (readdir throws → nothing to render)', () => {
881
+ const fsImpl = docAssetFs();
882
+ fsImpl.readdirSync = () => { throw new Error('ENOENT'); };
883
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), []);
884
+ });
885
+
886
+ t('healDocAssets: the post-upgrade case regenerates BOTH, node for the atlas and bash for the renders, cwd = coreRoot', () => {
887
+ const calls = [];
888
+ const run = (cmd, args, opts) => { calls.push({ cmd, arg0: args[0], cwd: opts && opts.cwd }); return { status: 0 }; };
889
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ atlas: false, renders: false }));
890
+ assert.equal(res.ok, true);
891
+ assert.deepEqual(res.ran, ['atlas', 'diagrams']);
892
+ assert.equal(calls.length, 2);
893
+ assert.equal(calls[0].cmd, process.execPath, 'atlas generator runs under node');
894
+ assert.equal(calls[0].arg0, join('/core', 'scripts', 'gds', 'gen-atlas.js'));
895
+ assert.equal(calls[1].cmd, 'bash', 'the diagram renderer is a shell script');
896
+ assert.equal(calls[1].arg0, join('/core', 'scripts', 'render-diagrams.sh'));
897
+ assert.ok(calls.every((c) => c.cwd === '/core'), 'both run against the CORE root, where the assets live');
898
+ });
899
+
900
+ t('healDocAssets: nothing missing → spawns NOTHING (safe to call from a deploy loop)', () => {
901
+ let spawned = 0;
902
+ const res = u.healDocAssets({ coreRoot: '/core' }, () => { spawned++; return { status: 0 }; }, docAssetFs());
903
+ assert.equal(spawned, 0);
904
+ assert.deepEqual(res.missing, []);
905
+ assert.equal(res.ok, true);
906
+ });
907
+
908
+ t('healDocAssets: only the MISSING group is regenerated (atlas present → renderer only)', () => {
909
+ const spawned = [];
910
+ const run = (_c, args) => { spawned.push(args[0]); return { status: 0 }; };
911
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ renders: false }));
912
+ assert.deepEqual(res.ran, ['diagrams']);
913
+ assert.equal(spawned.length, 1);
914
+ assert.match(String(spawned[0]), /render-diagrams\.sh$/);
915
+ });
916
+
917
+ t('healDocAssets: a failing renderer (no Chromium) is LOUD-failed but the atlas still heals — never aborts the bump', () => {
918
+ const run = (_c, args) => ({ status: String(args[0]).endsWith('render-diagrams.sh') ? 1 : 0 });
919
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ atlas: false, renders: false }));
920
+ assert.equal(res.ok, false, 'reported as incomplete');
921
+ assert.deepEqual(res.ran, ['atlas'], 'the independent atlas still regenerated');
922
+ assert.equal(res.failed.length, 1);
923
+ assert.equal(res.failed[0].group, 'diagrams');
924
+ assert.match(res.failed[0].reason, /exited 1/);
925
+ });
926
+
927
+ t('healDocAssets: a generator absent from this core is recorded, not spawned', () => {
928
+ const fsImpl = docAssetFs({ atlas: false, renders: false });
929
+ fsImpl._present.delete(join('/core', 'scripts', 'render-diagrams.sh'));
930
+ const spawned = [];
931
+ const res = u.healDocAssets({ coreRoot: '/core' }, (_c, args) => { spawned.push(args[0]); return { status: 0 }; }, fsImpl);
932
+ assert.equal(res.ok, false);
933
+ assert.deepEqual(res.ran, ['atlas']);
934
+ assert.match(res.failed[0].reason, /not found in core/);
935
+ assert.equal(spawned.length, 1, 'the absent script is never spawned');
936
+ });
937
+
550
938
  await Promise.all(pending);
551
939
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
552
940
  process.exit(failed ? 1 : 0);
@@ -0,0 +1,162 @@
1
+ // tests/web_admission_status.mjs — auth.webAdmissionStatus (task 1002097).
2
+ //
3
+ // The stub must go into require.cache BEFORE auth.js loads: auth.js destructures
4
+ // accessStateByLogin at require time, so patching the export object afterwards
5
+ // would not rebind what it already captured.
6
+ //
7
+ // Run: node tests/web_admission_status.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ // --- stub the kernel BEFORE auth.js captures its bindings ------------------
15
+ const kernelPath = require.resolve('../src/bongos/db-kernel.js');
16
+ const realKernel = require(kernelPath);
17
+
18
+ let dbState = { member: false, invited: false, pending: false };
19
+ let dbThrows = false;
20
+
21
+ require.cache[kernelPath].exports = {
22
+ ...realKernel,
23
+ accessStateByLogin: async () => {
24
+ if (dbThrows) throw new Error('simulated DB outage');
25
+ return dbState;
26
+ },
27
+ };
28
+
29
+ const auth = require('../src/bongos/auth.js');
30
+
31
+ const FIRST_ADMIN = 'ownerlogin';
32
+ // OTB is a permanent entry in LEGACY_ENV_PREFIXES, so this resolves regardless
33
+ // of the instance's own env prefix — and regardless of whether the branding
34
+ // pack configures a firstAdmin (this repo's does not).
35
+ process.env.OTB_FIRST_ADMIN = FIRST_ADMIN;
36
+
37
+ function setDb({ member = false, invited = false, pending = false } = {}) {
38
+ dbState = { member, invited, pending };
39
+ dbThrows = false;
40
+ }
41
+
42
+ function withOpenEnrollment(val, fn) {
43
+ const prev = process.env.OTB_OPEN_XENOS_ENROLLMENT;
44
+ if (val === undefined) delete process.env.OTB_OPEN_XENOS_ENROLLMENT;
45
+ else process.env.OTB_OPEN_XENOS_ENROLLMENT = val;
46
+ return Promise.resolve()
47
+ .then(fn)
48
+ .finally(() => {
49
+ if (prev === undefined) delete process.env.OTB_OPEN_XENOS_ENROLLMENT;
50
+ else process.env.OTB_OPEN_XENOS_ENROLLMENT = prev;
51
+ });
52
+ }
53
+
54
+ let passed = 0;
55
+ let failed = 0;
56
+ async function t(name, fn) {
57
+ try { await fn(); passed++; console.log(` ok ${name}`); }
58
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
59
+ }
60
+
61
+ await t('a blank or missing login is never admitted', async () => {
62
+ setDb({ member: true });
63
+ for (const bad of ['', ' ', null, undefined]) {
64
+ assert.deepEqual(await auth.webAdmissionStatus(bad), { admitted: false, expectedSoon: false });
65
+ }
66
+ });
67
+
68
+ await t('an existing builder is admitted', async () => {
69
+ setDb({ member: true });
70
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: true, expectedSoon: true });
71
+ });
72
+
73
+ await t('an invited login is admitted', async () => {
74
+ setDb({ invited: true });
75
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: true, expectedSoon: true });
76
+ });
77
+
78
+ await t('PENDING is the keep-polling case: not admitted, but expected soon', async () => {
79
+ // The whole point of the waiting page — an Archon is about to flip this to
80
+ // invited, so the visitor should wait rather than see a rejection.
81
+ setDb({ pending: true });
82
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: false, expectedSoon: true });
83
+ });
84
+
85
+ await t('a genuine stranger is neither admitted nor expected', async () => {
86
+ // Must be false, or the stranger gets a spinner that never resolves instead
87
+ // of the request-access page.
88
+ setDb({});
89
+ assert.deepEqual(await auth.webAdmissionStatus('nobody'), { admitted: false, expectedSoon: false });
90
+ });
91
+
92
+ await t('the configured firstAdmin is admitted with nothing in the DB', async () => {
93
+ // The bootstrap window this task was filed for: a fresh instance where the
94
+ // owner has no builder row and no access_request yet.
95
+ setDb({});
96
+ assert.deepEqual(await auth.webAdmissionStatus(FIRST_ADMIN), { admitted: true, expectedSoon: true });
97
+ });
98
+
99
+ await t('the firstAdmin match is case-insensitive and trims', async () => {
100
+ setDb({});
101
+ assert.equal((await auth.webAdmissionStatus('OwNerLogin')).admitted, true);
102
+ assert.equal((await auth.webAdmissionStatus(` ${FIRST_ADMIN.toUpperCase()} `)).admitted, true);
103
+ });
104
+
105
+ await t('open enrollment admits anyone', async () => {
106
+ setDb({});
107
+ await withOpenEnrollment('1', async () => {
108
+ assert.deepEqual(await auth.webAdmissionStatus('anyone'), { admitted: true, expectedSoon: true });
109
+ });
110
+ });
111
+
112
+ await t('a DB outage fails CLOSED on admitted', async () => {
113
+ // Never let an error read as "you're in".
114
+ dbThrows = true;
115
+ // The helper logs the outage by design; mute it so the unit gate's output
116
+ // stays readable (the assertions below are what prove the behaviour).
117
+ const realError = console.error;
118
+ console.error = () => {};
119
+ try {
120
+ const stranger = await auth.webAdmissionStatus('nobody');
121
+ assert.equal(stranger.admitted, false);
122
+ assert.equal(stranger.expectedSoon, false);
123
+ // ...but the owner may still keep polling through a transient outage, since
124
+ // the bootstrap window is exactly when the DB may be settling.
125
+ const owner = await auth.webAdmissionStatus(FIRST_ADMIN);
126
+ assert.equal(owner.admitted, false);
127
+ assert.equal(owner.expectedSoon, true);
128
+ } finally {
129
+ console.error = realError;
130
+ dbThrows = false;
131
+ }
132
+ });
133
+
134
+ await t('ANTI-DRIFT: admitted always equals the real gate\'s verdict', async () => {
135
+ // The probe must never disagree with firstTimeAdmissionBlocked — that is the
136
+ // one gate both sign-in flows call. Sweep the full input space.
137
+ for (const member of [false, true]) {
138
+ for (const invited of [false, true]) {
139
+ for (const openEnroll of [undefined, '1']) {
140
+ for (const login of ['nobody', FIRST_ADMIN]) {
141
+ setDb({ member, invited });
142
+ await withOpenEnrollment(openEnroll, async () => {
143
+ const probe = await auth.webAdmissionStatus(login);
144
+ const blocked = auth.firstTimeAdmissionBlocked({
145
+ existing: member ? { id: 1 } : null,
146
+ approved: invited,
147
+ login,
148
+ });
149
+ assert.equal(
150
+ probe.admitted, !blocked,
151
+ `drift: member=${member} invited=${invited} open=${openEnroll} login=${login} `
152
+ + `→ probe.admitted=${probe.admitted} but gate blocked=${blocked}`,
153
+ );
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ });
160
+
161
+ console.log(`\nweb admission status (task 1002097): ${passed} passed, ${failed} failed`);
162
+ process.exit(failed > 0 ? 1 : 0);