claude-code-session-manager 0.87.0 → 0.88.0

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 (290) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-BZ6IY2g1.js} +1 -1
  3. package/dist/assets/{DataModel--mISIJ6h.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-C2ahUXTg.js → History-CF157HyX.js} +1 -1
  5. package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CCPPipBk.js} +1 -1
  6. package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-Dc8Qst1R.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-DTg3JlOT.js} +1 -1
  10. package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-DdjY1sA3.js} +1 -1
  11. package/dist/assets/{Memory-CW62MXlh.js → Memory-BJ3Jlw0C.js} +1 -1
  12. package/dist/assets/{Panel-Bw1FhRuF.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-m0KAnccF.js} +1 -1
  14. package/dist/assets/{Plugins-BnKx9flD.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-D2ppxSfy.js} +7 -7
  18. package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/{Settings-DluB-vN1.js → Settings-BHSGGFEc.js} +1 -1
  20. package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-gNdo_HNK.js → Skills-Dl3q75QV.js} +1 -1
  22. package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-fc_JjdxL.js → index-B3emy2oI.js} +356 -356
  27. package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-CdzxaqwM.js} +1 -1
  29. package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
  30. package/dist/index.html +2 -2
  31. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
  32. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  33. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
  34. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  35. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
  36. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  37. package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
  38. package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
  39. package/package.json +10 -13
  40. package/scripts/README.md +59 -0
  41. package/scripts/audit-ops-hygiene.cjs +350 -0
  42. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  43. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  44. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  45. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  46. package/scripts/ops-sweep.cjs +355 -0
  47. package/scripts/scheduler-mcp-server.cjs +28 -71
  48. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  49. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  50. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  51. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  52. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  53. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  54. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  55. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  56. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  57. package/src/main/__tests__/dod-report.test.cjs +2 -2
  58. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  59. package/src/main/__tests__/epicMint.test.cjs +2 -2
  60. package/src/main/__tests__/exchanges.test.cjs +2 -2
  61. package/src/main/__tests__/extractJson.test.cjs +2 -2
  62. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  63. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  64. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  65. package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
  66. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  67. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  68. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  69. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  70. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  71. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  72. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  73. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  74. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
  75. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
  76. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  77. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  78. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  79. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  80. package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
  81. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  82. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  83. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  84. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  85. package/src/main/__tests__/projectPages.test.cjs +63 -149
  86. package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
  87. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  88. package/src/main/__tests__/queueHistory.test.cjs +2 -2
  89. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  90. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  91. package/src/main/__tests__/runVerify.test.cjs +5 -5
  92. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  93. package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
  94. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  95. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  96. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  97. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  98. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
  99. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  100. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  101. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  102. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  103. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  104. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
  105. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  106. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  107. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  108. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  109. package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
  110. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  111. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  112. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  113. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  114. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  115. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  116. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
  118. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  119. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  120. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  121. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  122. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
  123. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
  124. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  125. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  126. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  127. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  128. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  129. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
  130. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  131. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  132. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  133. package/src/main/bilkoHost.cjs +16 -7
  134. package/src/main/build-info.json +8 -0
  135. package/src/main/chatRunner.cjs +4 -2
  136. package/src/main/config.cjs +2 -3
  137. package/src/main/docEdit.cjs +4 -2
  138. package/src/main/health.cjs +267 -51
  139. package/src/main/heapSnapshot.cjs +2 -2
  140. package/src/main/historyAggregator.cjs +1 -1
  141. package/src/main/index.cjs +61 -10
  142. package/src/main/ipcSchemas.cjs +6 -17
  143. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  144. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  145. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  146. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  147. package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
  148. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  149. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  150. package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
  151. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  152. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  153. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  154. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  155. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  156. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  157. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  158. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  159. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  160. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  161. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  162. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  163. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  164. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  165. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  166. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  167. package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
  168. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
  169. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
  170. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  171. package/src/main/lib/activeSessions.cjs +20 -88
  172. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  173. package/src/main/lib/agentModelResolve.cjs +33 -1
  174. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  175. package/src/main/lib/auditLog.cjs +30 -5
  176. package/src/main/lib/buildIdentity.cjs +113 -0
  177. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  178. package/src/main/lib/claudeBin.cjs +37 -2
  179. package/src/main/lib/cleanEnv.cjs +27 -1
  180. package/src/main/lib/credentials.cjs +4 -2
  181. package/src/main/lib/cwdClassify.cjs +185 -0
  182. package/src/main/lib/definitionOfDone.cjs +296 -52
  183. package/src/main/lib/dispatchLoop.cjs +40 -0
  184. package/src/main/lib/effectiveModelInfo.cjs +9 -10
  185. package/src/main/lib/ephemeralCwd.cjs +7 -28
  186. package/src/main/lib/gitWorktree.cjs +95 -8
  187. package/src/main/lib/guardShims.cjs +3 -3
  188. package/src/main/lib/historyRollup.cjs +6 -7
  189. package/src/main/lib/instanceLock.cjs +32 -5
  190. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  191. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  192. package/src/main/lib/localAdminHttp.cjs +9 -27
  193. package/src/main/lib/mcpToolCatalog.cjs +29 -77
  194. package/src/main/lib/opsOwnership.cjs +27 -19
  195. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  196. package/src/main/lib/prdLocations.cjs +48 -1
  197. package/src/main/lib/procIdentity.cjs +126 -0
  198. package/src/main/lib/procName.cjs +98 -0
  199. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  200. package/src/main/lib/queueHistory.cjs +8 -7
  201. package/src/main/lib/queueStore.cjs +57 -37
  202. package/src/main/lib/quietMachineLease.cjs +19 -2
  203. package/src/main/lib/reservationExpiry.cjs +30 -0
  204. package/src/main/lib/runClaudeP.cjs +4 -2
  205. package/src/main/lib/runLogRetention.cjs +3 -2
  206. package/src/main/lib/scheduleJobSchema.cjs +2 -2
  207. package/src/main/lib/scheduleJobTransitions.cjs +29 -2
  208. package/src/main/lib/schedulerBatch.cjs +27 -2
  209. package/src/main/lib/schedulerPaths.cjs +175 -0
  210. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  211. package/src/main/lib/sessionSlots.cjs +47 -9
  212. package/src/main/lib/smProcNames.cjs +51 -0
  213. package/src/main/lib/upgradeDrain.cjs +188 -0
  214. package/src/main/lib/usageCircuit.cjs +53 -6
  215. package/src/main/lib/watchdogHelpers.cjs +70 -37
  216. package/src/main/lib/withTimeout.cjs +33 -0
  217. package/src/main/mcpStatus.cjs +4 -2
  218. package/src/main/pluginInstall.cjs +6 -2
  219. package/src/main/projectPages.cjs +41 -145
  220. package/src/main/pty.cjs +8 -1
  221. package/src/main/queueOps.cjs +10 -10
  222. package/src/main/runVerify.cjs +69 -3
  223. package/src/main/scheduler.cjs +1598 -381
  224. package/src/main/seedAgentPersonas.cjs +1 -1
  225. package/src/main/seedDevPlugin.cjs +1 -1
  226. package/src/main/seedSchedulerMcp.cjs +7 -6
  227. package/src/main/seedStatus.cjs +1 -1
  228. package/src/main/supervisor.cjs +5 -3
  229. package/src/main/usage.cjs +126 -62
  230. package/src/preload/api.d.ts +18 -64
  231. package/src/preload/index.cjs +2 -4
  232. package/src/seed/agents/project-home-builder.md +31 -48
  233. package/screenshots/.gitkeep +0 -0
  234. package/screenshots/README-screenshots.md +0 -13
  235. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  236. package/src/main/teams.cjs +0 -95
  237. package/src/main/templates/project-pages-catalog.json +0 -741
  238. package/src/main/templates/project-pages-default-home.html +0 -123
  239. package/src/main/templates/project-pages-pipeline.md +0 -417
  240. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  241. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  242. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  243. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  244. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  245. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  246. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  247. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  248. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  249. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  250. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  251. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  252. package/src/seed/prompts/documentation/module-readme.md +0 -8
  253. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  254. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  255. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  256. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  257. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  258. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  259. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  260. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  261. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  262. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  263. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  264. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  265. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  266. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  267. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  268. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  269. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  270. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  271. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  272. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  273. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  274. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  275. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  276. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  277. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  278. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  279. package/src/seed/prompts/security/authz-audit.md +0 -8
  280. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  281. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  282. package/src/seed/prompts/security/dependency-audit.md +0 -8
  283. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  284. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  285. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
  286. package/web/README.md +0 -41
  287. package/web/project-pages/logic/dist/logic.cjs +0 -4709
  288. package/web/project-pages/render.cjs +0 -70
  289. package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
  290. package/web/project-pages/validate-summary.cjs +0 -62
@@ -1,21 +1,20 @@
1
1
  /**
2
- * projectHomeAdminRoutes.test.cjs — unit tests for
3
- * lib/projectHomeAdminRoutes.cjs, the app-side half of "the generating
4
- * session needs zero repo knowledge to build a Project Home page" (PRD:
5
- * project-home-admin-routes).
2
+ * projectHomeAdminRoutes.test.cjs — lib/projectHomeAdminRoutes.cjs's single
3
+ * route, POST /admin/project-home/write {cwd, html}, and its self-contained-
4
+ * HTML validator.
6
5
  *
7
6
  * Run: timeout 120 npx vitest run src/main/__tests__/projectHomeAdminRoutes.test.cjs
8
7
  */
9
8
 
10
9
  'use strict';
11
10
 
12
- import { test, expect, afterEach, vi } from 'vitest';
11
+ import { test, expect, afterEach } from 'vitest';
13
12
  const fs = require('node:fs');
14
13
  const fsp = require('node:fs/promises');
15
14
  const os = require('node:os');
16
15
  const path = require('node:path');
17
16
  const config = require('../config.cjs');
18
- const { registerAdminRoute } = require('../lib/projectHomeAdminRoutes.cjs');
17
+ const { registerAdminRoute, validateHomeHtml, MAX_HTML_BYTES } = require('../lib/projectHomeAdminRoutes.cjs');
19
18
 
20
19
  const tmpDirs = [];
21
20
  afterEach(async () => {
@@ -33,19 +32,18 @@ async function mkProjectCwd() {
33
32
  return dir;
34
33
  }
35
34
 
36
- /** Fake adminHttp stub — same pattern prdAdminRouteParity.test.cjs uses:
37
- * captures registered routes and lets a test invoke one directly with a
38
- * fake req/res/query, no real HTTP server or Electron boot needed. */
35
+ /** Fake adminHttp stub: captures registered routes and invokes one directly. */
39
36
  function makeFakeAdminHttp() {
40
37
  const routes = new Map();
41
38
  return {
42
39
  registerRoute(method, url, handler) {
43
40
  routes.set(`${method} ${url}`, handler);
44
41
  },
45
- async call(method, url, { query = new URLSearchParams(), body } = {}) {
42
+ async call(method, url, { rawBody, body } = {}) {
46
43
  const handler = routes.get(`${method} ${url}`);
47
44
  if (!handler) throw new Error(`no route registered for ${method} ${url}`);
48
- const chunks = body !== undefined ? [Buffer.from(JSON.stringify(body))] : [];
45
+ const text = rawBody !== undefined ? rawBody : body !== undefined ? JSON.stringify(body) : '';
46
+ const chunks = text ? [Buffer.from(text)] : [];
49
47
  const req = {
50
48
  on(event, cb) {
51
49
  if (event === 'data') chunks.forEach((c) => cb(c));
@@ -59,427 +57,109 @@ function makeFakeAdminHttp() {
59
57
  writeHead(s) { status = s; },
60
58
  end(b) { payload = b ? JSON.parse(b) : null; },
61
59
  };
62
- await handler(req, res, query);
60
+ await handler(req, res, new URLSearchParams());
63
61
  return { status, body: payload };
64
62
  },
65
63
  routes,
66
64
  };
67
65
  }
68
66
 
69
- function validSummary(overrides = {}) {
70
- return {
71
- identity: {
72
- name: 'Demo', tag: 'demo', version: '1.0.0', oneLine: 'A demo project.',
73
- claim: 'Ship faster.', sub: 'Subtitle text.', audience: 'Developers', install: 'npm i demo',
74
- },
75
- stats: [{ v: '5', k: 'years', n: 'since launch' }],
76
- pillars: [{ t: 'Fast', d: 'It is fast.', k: 'speed' }],
77
- quotes: [],
78
- feature: {
79
- name: 'Feature', kicker: 'New', status: 'shipped', owner: 'team', oneLine: 'One line.',
80
- problem: 'Problem text.', solution: 'Solution text.',
81
- steps: [{ t: 'Step', d: 'desc' }], rules: [{ t: 'Rule', d: 'desc' }],
82
- specs: [['label', 'value', 'note']], faq: [{ q: 'Q', a: 'A' }],
83
- timeline: [{ w: 'W1', t: 'Thing', s: 'done' }],
84
- },
85
- architecture: {
86
- summary: 'Arch summary.',
87
- principles: [{ t: 'P', d: 'd' }],
88
- layers: [{ n: 'Layer', d: 'd', f: '5', tone: 'accent' }],
89
- modules: [{ n: 'Mod', d: 'd', f: 3, dep: [], heat: 1 }],
90
- flow: [{ a: 'A', b: 'B', t: 't', n: 'n' }],
91
- decisions: [{ id: 'd1', t: 't', w: 'w', s: 'accepted' }],
92
- risks: [],
93
- },
94
- ...overrides,
95
- };
96
- }
67
+ const GOOD_HTML = '<!DOCTYPE html><html><head><style>body{background:url(data:image/png;base64,AAAA)}</style></head><body><script>1+1</script></body></html>';
97
68
 
98
- function picksFromCatalog(catalog) {
99
- const picks = {};
100
- for (const lens of catalog.lenses) {
101
- picks[lens.id] = {};
102
- for (const slot of lens.slots) {
103
- picks[lens.id][slot.id] = slot.variants[0].id;
104
- }
105
- }
106
- return picks;
107
- }
108
-
109
- // ─── Route registration ────────────────────────────────────────────────
110
- test('registers all four project-home admin routes', () => {
111
- const fake = makeFakeAdminHttp();
112
- registerAdminRoute(fake);
113
- expect(fake.routes.has('GET /admin/project-home/contract')).toBe(true);
114
- expect(fake.routes.has('POST /admin/project-home/validate-summary')).toBe(true);
115
- expect(fake.routes.has('POST /admin/project-home/render')).toBe(true);
116
- expect(fake.routes.has('GET /admin/project-home/status')).toBe(true);
69
+ test('registers exactly one route: POST /admin/project-home/write', () => {
70
+ const adminHttp = makeFakeAdminHttp();
71
+ registerAdminRoute(adminHttp);
72
+ expect([...adminHttp.routes.keys()]).toEqual(['POST /admin/project-home/write']);
117
73
  });
118
74
 
119
- // ─── contract ──────────────────────────────────────────────────────────
120
- test('contract: self-sufficient — 5 lenses, non-empty slots, absolute paths, no repo-relative path strings', async () => {
75
+ test('valid html is written atomically to project-pages/home.html', async () => {
121
76
  const cwd = await mkProjectCwd();
122
- const fake = makeFakeAdminHttp();
123
- registerAdminRoute(fake);
124
-
125
- const { status, body } = await fake.call('GET', '/admin/project-home/contract', {
126
- query: new URLSearchParams({ cwd }),
127
- });
77
+ const adminHttp = makeFakeAdminHttp();
78
+ registerAdminRoute(adminHttp);
79
+ const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: GOOD_HTML } });
128
80
  expect(status).toBe(200);
129
81
  expect(body.ok).toBe(true);
130
- expect(Array.isArray(body.protocol)).toBe(true);
131
- expect(body.protocol.length).toBeGreaterThan(0);
132
-
133
- expect(body.catalog.lenses.map((l) => l.id).sort()).toEqual(
134
- ['architecture', 'brief', 'feature', 'home', 'marketing'].sort(),
135
- );
136
- for (const lens of body.catalog.lenses) {
137
- expect(Array.isArray(lens.slots)).toBe(true);
138
- expect(lens.slots.length).toBeGreaterThan(0);
139
- }
140
-
141
- expect(path.isAbsolute(body.paths.summaryPath)).toBe(true);
142
- expect(path.isAbsolute(body.paths.picksPath)).toBe(true);
143
- expect(path.isAbsolute(body.paths.outputDir)).toBe(true);
144
- expect(body.paths.summaryPath).toContain(cwd);
145
-
146
- expect(body.summarySchema).toBeTruthy();
147
- expect(body.summarySchema.properties.identity).toBeTruthy();
148
-
149
- // The operational fields (catalog/paths/protocol) must never point the
150
- // caller at a repo-relative path — the whole point of this route. The
151
- // free-form "spec.text" field is the architecture doc's own prose, which
152
- // legitimately documents source file locations for background reading;
153
- // it is explicitly excluded from this check (see route file's loadCatalog
154
- // comment for why the catalog's own $comment is stripped for the same
155
- // reason).
156
- const operationalFields = JSON.stringify({ protocol: body.protocol, catalog: body.catalog, paths: body.paths });
157
- expect(operationalFields).not.toMatch(/src\/renderer|src\/main|scripts\//);
158
- // PRD 1093's exact banned-substring list (the class of path that broke
159
- // generation on a foreign machine before this route existed).
160
- for (const banned of [
161
- 'session-manager-operations/architecture/',
162
- 'session-manager-operations/design-mocks/',
163
- '.claude/agents/',
164
- 'src/renderer/',
165
- 'npm run build:project-pages',
166
- ]) {
167
- expect(operationalFields, `operational fields must not mention "${banned}"`).not.toContain(banned);
168
- }
169
- });
170
-
171
- test('contract: rejects a missing/non-absolute cwd with a structured error, not a throw', async () => {
172
- const fake = makeFakeAdminHttp();
173
- registerAdminRoute(fake);
174
-
175
- const missing = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams() });
176
- expect(missing.status).toBe(400);
177
- expect(missing.body.ok).toBe(false);
178
-
179
- const relative = await fake.call('GET', '/admin/project-home/contract', {
180
- query: new URLSearchParams({ cwd: 'relative/path' }),
181
- });
182
- expect(relative.status).toBe(400);
183
- expect(relative.body.ok).toBe(false);
184
- });
185
-
186
- test('contract: rejects a cwd with no session-manager-operations/ directory', async () => {
187
- const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-not-a-project-'));
188
- config.addAllowedRoot(dir);
189
- tmpDirs.push(dir);
190
- const fake = makeFakeAdminHttp();
191
- registerAdminRoute(fake);
192
-
193
- const { status, body } = await fake.call('GET', '/admin/project-home/contract', {
194
- query: new URLSearchParams({ cwd: dir }),
195
- });
196
- expect(status).toBe(400);
197
- expect(body.ok).toBe(false);
198
- expect(body.error).toMatch(/not a Session Manager project/);
82
+ const target = path.join(fs.realpathSync(cwd), 'session-manager-operations', 'project-pages', 'home.html');
83
+ expect(body.path).toBe(target);
84
+ expect(fs.readFileSync(target, 'utf8')).toBe(GOOD_HTML);
85
+ // no per-lens / summary / manifest side files
86
+ expect(fs.readdirSync(path.dirname(target))).toEqual(['home.html']);
199
87
  });
200
88
 
201
- // ─── validate-summary ──────────────────────────────────────────────────
202
- test('validate-summary: accepts a well-formed summary', async () => {
89
+ test('a second write replaces the first', async () => {
203
90
  const cwd = await mkProjectCwd();
204
- const fake = makeFakeAdminHttp();
205
- registerAdminRoute(fake);
206
-
207
- const { status, body } = await fake.call('POST', '/admin/project-home/validate-summary', {
208
- body: { cwd, summary: validSummary() },
209
- });
210
- expect(status).toBe(200);
211
- expect(body.valid).toBe(true);
212
- expect(body.errors).toEqual([]);
91
+ const adminHttp = makeFakeAdminHttp();
92
+ registerAdminRoute(adminHttp);
93
+ await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>one</p>' } });
94
+ await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>two</p>' } });
95
+ const target = path.join(fs.realpathSync(cwd), 'session-manager-operations', 'project-pages', 'home.html');
96
+ expect(fs.readFileSync(target, 'utf8')).toBe('<p>two</p>');
213
97
  });
214
98
 
215
- test('validate-summary: rejects a malformed summary with per-field errors', async () => {
99
+ test.each([
100
+ ['empty', ''],
101
+ ['whitespace only', ' \n '],
102
+ ['script src', '<html><script src="app.js"></script></html>'],
103
+ ['script src (remote, single quotes, attrs first)', "<script defer type='module' src='https://cdn.example.com/x.js'></script>"],
104
+ ['link href http', '<link rel="stylesheet" href="https://fonts.googleapis.com/css">'],
105
+ ['link href protocol-relative', '<link rel="stylesheet" href="//cdn.example.com/a.css">'],
106
+ ['@import', '<style>@import url("https://x.com/a.css");</style>'],
107
+ ['@import bare string', '<style>@import "a.css";</style>'],
108
+ ['css url(http)', '<style>a{background:url(https://x.com/a.png)}</style>'],
109
+ ['css url(quoted http)', "<style>a{background:url( 'http://x.com/a.png')}</style>"],
110
+ ])('rejects %s with 400 and writes nothing', async (_label, html) => {
216
111
  const cwd = await mkProjectCwd();
217
- const fake = makeFakeAdminHttp();
218
- registerAdminRoute(fake);
219
-
220
- const bad = validSummary({ identity: { ...validSummary().identity, name: '' } });
221
- const { status, body } = await fake.call('POST', '/admin/project-home/validate-summary', {
222
- body: { cwd, summary: bad },
223
- });
224
- expect(status).toBe(200);
225
- expect(body.valid).toBe(false);
226
- expect(body.errors.length).toBeGreaterThan(0);
227
- expect(body.errors[0]).toHaveProperty('field');
228
- expect(body.errors[0]).toHaveProperty('message');
229
- expect(body.errors.some((e) => e.field === 'identity.name')).toBe(true);
230
- });
231
-
232
- // ─── render ────────────────────────────────────────────────────────────
233
- test('render: happy path writes summary.json, picks.json, all 5 lens htmls, and manifest.json', async () => {
234
- const cwd = await mkProjectCwd();
235
- const fake = makeFakeAdminHttp();
236
- registerAdminRoute(fake);
237
-
238
- const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
239
- const picks = picksFromCatalog(contract.body.catalog);
240
- const summary = validSummary();
241
-
242
- const { status, body } = await fake.call('POST', '/admin/project-home/render', {
243
- body: { cwd, summary, picks },
244
- });
245
- expect(status).toBe(200);
246
- expect(body.ok).toBe(true);
247
- expect(typeof body.generatedAt).toBe('string');
248
- expect(Number.isNaN(Date.parse(body.generatedAt))).toBe(false);
249
- expect(Array.isArray(body.filesWritten)).toBe(true);
250
-
251
- const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
252
- expect(fs.existsSync(path.join(dir, 'summary.json'))).toBe(true);
253
- expect(fs.existsSync(path.join(dir, 'picks.json'))).toBe(true);
254
- for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
255
- expect(fs.existsSync(path.join(dir, 'output', `${lens}.html`))).toBe(true);
256
- }
257
- const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'output', 'manifest.json'), 'utf8'));
258
- expect(manifest.generatedAt).toBe(body.generatedAt);
259
-
260
- expect(JSON.parse(fs.readFileSync(path.join(dir, 'summary.json'), 'utf8'))).toEqual(summary);
261
- });
262
-
263
- test('render: a malformed summary is rejected and writes NOTHING', async () => {
264
- const cwd = await mkProjectCwd();
265
- const fake = makeFakeAdminHttp();
266
- registerAdminRoute(fake);
267
-
268
- const bad = validSummary({ architecture: { ...validSummary().architecture, summary: '' } });
269
- const { status, body } = await fake.call('POST', '/admin/project-home/render', {
270
- body: { cwd, summary: bad, picks: {} },
271
- });
112
+ const adminHttp = makeFakeAdminHttp();
113
+ registerAdminRoute(adminHttp);
114
+ const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
272
115
  expect(status).toBe(400);
273
116
  expect(body.ok).toBe(false);
274
- expect(body.valid).toBe(false);
275
- expect(body.errors.length).toBeGreaterThan(0);
276
-
277
- const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
278
- expect(fs.existsSync(dir)).toBe(false);
117
+ expect(fs.existsSync(path.join(cwd, 'session-manager-operations', 'project-pages'))).toBe(false);
279
118
  });
280
119
 
281
- test('render: a malformed picks shape is rejected with a field error and writes NOTHING', async () => {
120
+ test('rejects html over 1MB', async () => {
282
121
  const cwd = await mkProjectCwd();
283
- const fake = makeFakeAdminHttp();
284
- registerAdminRoute(fake);
285
-
286
- const { status, body } = await fake.call('POST', '/admin/project-home/render', {
287
- body: { cwd, summary: validSummary(), picks: { home: { hero: 42 } } },
288
- });
122
+ const adminHttp = makeFakeAdminHttp();
123
+ registerAdminRoute(adminHttp);
124
+ const html = 'a'.repeat(MAX_HTML_BYTES + 1);
125
+ const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
289
126
  expect(status).toBe(400);
290
- expect(body.ok).toBe(false);
291
- expect(body.errors.some((e) => e.field === 'picks.home.hero')).toBe(true);
292
-
293
- const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
294
- expect(fs.existsSync(dir)).toBe(false);
127
+ expect(body.error).toContain('limit');
295
128
  });
296
129
 
297
- test('GET routes (contract, status) never widen the write boundary — only render does', async () => {
298
- // mkProjectCwd() (used by every other test here) calls config.addAllowedRoot
299
- // itself so validatePath succeeds for an arbitrary os.tmpdir() location —
300
- // which would defeat this specific assertion. Use a cwd that's readable
301
- // via validatePath's own default allowed root (os.homedir()) WITHOUT any
302
- // addAllowedRoot call, so a write only succeeds if a route handler itself
303
- // called it — matching prdCreate.test.cjs's / prdMigration.test.cjs's own
304
- // "homedir-rooted tmp cwd" pattern for exactly this kind of check.
305
- const cwd = fs.realpathSync(await fsp.mkdtemp(path.join(os.homedir(), '.sm-project-home-admin-')));
306
- tmpDirs.push(cwd);
307
- fs.mkdirSync(path.join(cwd, 'session-manager-operations'), { recursive: true });
308
-
309
- const fake = makeFakeAdminHttp();
310
- registerAdminRoute(fake);
311
-
312
- await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
313
- await fake.call('GET', '/admin/project-home/status', { query: new URLSearchParams({ cwd }) });
314
-
315
- // A write through config.cjs for this cwd must still be refused — the
316
- // GET routes above must not have called config.addAllowedRoot for it.
317
- const probePath = path.join(cwd, 'session-manager-operations', 'project-pages', 'probe.json');
318
- await expect(config.writeJson(probePath, { a: 1 }, { writer: 'project-home' })).rejects.toThrow(
319
- /Write outside allowed write boundaries/,
320
- );
321
- });
322
-
323
- test('render: two concurrent renders for the same cwd are serialized, not interleaved', async () => {
130
+ test('a request body far beyond the cap is a clean 400, not a throw', async () => {
324
131
  const cwd = await mkProjectCwd();
325
- const fake = makeFakeAdminHttp();
326
- registerAdminRoute(fake);
327
-
328
- const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
329
- const picks = picksFromCatalog(contract.body.catalog);
330
-
331
- // Distinct summaries so writes for render A vs render B can be told apart
332
- // by content — identity.oneLine is echoed into every lens's html output
333
- // (see renderer.cjs's htmlDocument/proj.oneLine usage).
334
- const summaryA = validSummary({ identity: { ...validSummary().identity, oneLine: 'MARKER_RENDER_A' } });
335
- const summaryB = validSummary({ identity: { ...validSummary().identity, oneLine: 'MARKER_RENDER_B' } });
336
-
337
- const events = [];
338
- let releaseGate;
339
- const gate = new Promise((resolve) => { releaseGate = resolve; });
340
- let gateArmed = false;
341
-
342
- function labelForArgs(args) {
343
- const text = typeof args[1] === 'string' ? args[1] : JSON.stringify(args[1]);
344
- if (text.includes('MARKER_RENDER_A')) return 'A';
345
- if (text.includes('MARKER_RENDER_B')) return 'B';
346
- return 'unknown';
347
- }
348
-
349
- const realWriteJson = config.writeJson.bind(config);
350
- const realWriteTextAtomic = config.writeTextAtomic.bind(config);
351
-
352
- async function instrumented(real, args) {
353
- const label = labelForArgs(args);
354
- events.push({ label, phase: 'start' });
355
- // Only the very first write of the whole test is gated — this is render
356
- // A's first write, deep inside its own Promise.all of writes. Holding it
357
- // open gives render B a real window to start writing if serialization
358
- // were absent; every other write (A's remaining writes, all of B's
359
- // writes) proceeds unblocked.
360
- if (!gateArmed) {
361
- gateArmed = true;
362
- await gate;
363
- }
364
- const result = await real(...args);
365
- events.push({ label, phase: 'end' });
366
- return result;
367
- }
368
-
369
- const writeJsonSpy = vi.spyOn(config, 'writeJson').mockImplementation((...args) => instrumented(realWriteJson, args));
370
- const writeTextAtomicSpy = vi
371
- .spyOn(config, 'writeTextAtomic')
372
- .mockImplementation((...args) => instrumented(realWriteTextAtomic, args));
373
-
374
- try {
375
- const renderA = fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: summaryA, picks } });
376
- const renderB = fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: summaryB, picks } });
377
-
378
- // Yield microtasks (no wall-clock wait) until render A's first write has
379
- // registered, then release it.
380
- for (let i = 0; i < 1000 && events.length === 0; i += 1) {
381
- await Promise.resolve();
382
- }
383
- expect(events.length).toBeGreaterThan(0);
384
- releaseGate();
385
-
386
- const [resA, resB] = await Promise.all([renderA, renderB]);
387
- expect(resA.status).toBe(200);
388
- expect(resB.status).toBe(200);
389
-
390
- const labels = events.map((e) => e.label);
391
- const firstA = labels.indexOf('A');
392
- const lastA = labels.lastIndexOf('A');
393
- const firstB = labels.indexOf('B');
394
- const lastB = labels.lastIndexOf('B');
395
- expect(firstA).toBeGreaterThanOrEqual(0);
396
- expect(firstB).toBeGreaterThanOrEqual(0);
397
- // Serialized means one render's entire write span is fully outside the
398
- // other's — never interleaved.
399
- expect(lastA < firstB || lastB < firstA).toBe(true);
400
-
401
- const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
402
- const outputFiles = fs.readdirSync(path.join(dir, 'output')).sort();
403
- expect(outputFiles).toEqual([
404
- 'architecture.html',
405
- 'brief.html',
406
- 'feature.html',
407
- 'home.html',
408
- 'manifest.json',
409
- 'marketing.html',
410
- ]);
411
- } finally {
412
- writeJsonSpy.mockRestore();
413
- writeTextAtomicSpy.mockRestore();
414
- }
132
+ const adminHttp = makeFakeAdminHttp();
133
+ registerAdminRoute(adminHttp);
134
+ const html = 'a'.repeat(MAX_HTML_BYTES * 3);
135
+ const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
136
+ expect(status).toBe(400);
137
+ expect(body.error).toContain('limit');
415
138
  });
416
139
 
417
- // ─── status ────────────────────────────────────────────────────────────
418
- test('status: a project with no output dir yet returns a well-formed all-absent response, not an error', async () => {
419
- const cwd = await mkProjectCwd();
420
- const fake = makeFakeAdminHttp();
421
- registerAdminRoute(fake);
422
-
423
- const { status, body } = await fake.call('GET', '/admin/project-home/status', {
424
- query: new URLSearchParams({ cwd }),
425
- });
426
- expect(status).toBe(200);
427
- expect(body.ok).toBe(true);
428
- expect(body.summary.exists).toBe(false);
429
- expect(body.picks.exists).toBe(false);
430
- expect(body.manifest.exists).toBe(false);
431
- expect(body.manifest.generatedAt).toBe(null);
432
- for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
433
- expect(body.output[lens].exists).toBe(false);
434
- }
140
+ test('accepts an inline <link> to a local/data href and plain http text content', () => {
141
+ expect(validateHomeHtml('<p>see https://example.com in prose</p>')).toBeNull();
142
+ expect(validateHomeHtml('<link rel="icon" href="data:image/png;base64,AAAA">')).toBeNull();
435
143
  });
436
144
 
437
- test('status: reflects what render() just wrote', async () => {
438
- const cwd = await mkProjectCwd();
439
- const fake = makeFakeAdminHttp();
440
- registerAdminRoute(fake);
441
-
442
- const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
443
- const picks = picksFromCatalog(contract.body.catalog);
444
- await fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: validSummary(), picks } });
445
-
446
- const { body } = await fake.call('GET', '/admin/project-home/status', { query: new URLSearchParams({ cwd }) });
447
- expect(body.summary.exists).toBe(true);
448
- expect(body.picks.exists).toBe(true);
449
- expect(body.manifest.exists).toBe(true);
450
- expect(typeof body.manifest.generatedAt).toBe('string');
451
- for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
452
- expect(body.output[lens].exists).toBe(true);
453
- expect(typeof body.output[lens].mtimeMs).toBe('number');
454
- }
145
+ test('validateHomeHtml exact 1MB is accepted, non-string rejected', () => {
146
+ expect(validateHomeHtml('a'.repeat(MAX_HTML_BYTES))).toBeNull();
147
+ expect(validateHomeHtml(undefined)).toMatch(/non-empty/);
455
148
  });
456
149
 
457
- // ─── bilkoHost interaction (PRD 1093) ─────────────────────────────────
458
- // bilkoHost.cjs reads opsPath(cwd, 'project-pages', 'output', 'marketing.html')
459
- // (around line 107) and opsPath(cwd, 'project-pages', 'output', `${lens}.html`)
460
- // for project-page-lens documents (around line 219). Prove render()'s real
461
- // output satisfies both read paths, so the publish half of the pipeline
462
- // doesn't silently break the next time this file's output layout changes.
463
- test('render output lands exactly where bilkoHost.cjs expects it', async () => {
464
- const cwd = await mkProjectCwd();
465
- fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify({ name: 'demo-app', version: '1.0.0' }));
466
- const fake = makeFakeAdminHttp();
467
- registerAdminRoute(fake);
468
-
469
- const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
470
- const picks = picksFromCatalog(contract.body.catalog);
471
- await fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: validSummary(), picks } });
472
-
473
- const bilkoHost = require('../bilkoHost.cjs');
474
- const hostState = await bilkoHost.get({ cwd });
475
- expect(hostState.hasMarketingPage).toBe(true);
476
-
477
- await bilkoHost.addDocument({
478
- cwd,
479
- subpath: 'feature',
480
- title: 'Feature',
481
- source: { kind: 'project-page-lens', lens: 'feature' },
482
- });
483
- const result = await bilkoHost.prepareBundle({ cwd, slug: 'demo-app' });
484
- expect(fs.existsSync(path.join(result.distPath, 'feature', 'index.html'))).toBe(true);
150
+ test('bad JSON, missing/relative cwd, non-project cwd, and unknown keys are 400s', async () => {
151
+ const adminHttp = makeFakeAdminHttp();
152
+ registerAdminRoute(adminHttp);
153
+ const route = '/admin/project-home/write';
154
+ expect((await adminHttp.call('POST', route, { rawBody: '{nope' })).status).toBe(400);
155
+ expect((await adminHttp.call('POST', route, { body: { html: 'x' } })).status).toBe(400);
156
+ expect((await adminHttp.call('POST', route, { body: { cwd: 'relative/dir', html: 'x' } })).status).toBe(400);
157
+ expect((await adminHttp.call('POST', route, { body: { cwd: '/x', html: 'x', extra: 1 } })).status).toBe(400);
158
+
159
+ const bare = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-project-home-bare-'));
160
+ tmpDirs.push(bare);
161
+ config.addAllowedRoot(bare);
162
+ const res = await adminHttp.call('POST', route, { body: { cwd: bare, html: '<p>x</p>' } });
163
+ expect(res.status).toBe(400);
164
+ expect(res.body.error).toContain('not a Session Manager project');
485
165
  });