@polderlabs/bizar 6.2.4 → 6.3.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 (332) hide show
  1. package/.claude/CLAUDE.md +15 -0
  2. package/.claude/agents/_shared/AGENT_BASELINE.md +168 -0
  3. package/.claude/agents/_shared/CLAUDE_TOOLS.md +412 -0
  4. package/.claude/agents/_shared/SKILLS.md +109 -0
  5. package/.claude/agents/agent-browser.md +80 -0
  6. package/.claude/agents/baldr.md +49 -0
  7. package/.claude/agents/forseti.md +51 -0
  8. package/.claude/agents/frigg.md +42 -0
  9. package/.claude/agents/heimdall.md +33 -0
  10. package/.claude/agents/hermod.md +53 -0
  11. package/.claude/agents/mimir.md +49 -0
  12. package/.claude/agents/odin.md +287 -0
  13. package/.claude/agents/quick.md +34 -0
  14. package/.claude/agents/semble-search.md +50 -0
  15. package/.claude/agents/thor.md +53 -0
  16. package/.claude/agents/tyr.md +56 -0
  17. package/.claude/agents/vidarr.md +54 -0
  18. package/.claude/agents/vor.md +53 -0
  19. package/.claude/commands/audit.md +25 -0
  20. package/.claude/commands/bizar.md +22 -0
  21. package/.claude/commands/explain.md +17 -0
  22. package/.claude/commands/init.md +29 -0
  23. package/.claude/commands/learn.md +44 -0
  24. package/.claude/commands/plan.md +35 -0
  25. package/.claude/commands/plow-through.md +50 -0
  26. package/.claude/commands/pr-review.md +49 -0
  27. package/.claude/commands/setup-provider.md +96 -0
  28. package/.claude/commands/tailscale-serve.md +100 -0
  29. package/.claude/commands/team.md +132 -0
  30. package/.claude/commands/test.md +62 -0
  31. package/.claude/commands/validate.md +68 -0
  32. package/.claude/commands/visual-plan.md +24 -0
  33. package/.claude/hooks/README.md +92 -0
  34. package/.claude/hooks/posttooluse-editwrite.mjs +91 -0
  35. package/.claude/hooks/pretooluse-bash.mjs +81 -0
  36. package/.claude/hooks/pretooluse-editwrite.mjs +139 -0
  37. package/.claude/hooks/sessionend-recall.mjs +74 -0
  38. package/.claude/hooks/sessionstart-prime.mjs +80 -0
  39. package/.claude/hooks/userpromptsubmit-tag.mjs +80 -0
  40. package/.claude/settings.json +116 -0
  41. package/.claude/skills/9router/SKILL.md +80 -0
  42. package/.claude/skills/9router-chat/SKILL.md +73 -0
  43. package/.claude/skills/9router-embeddings/SKILL.md +69 -0
  44. package/.claude/skills/9router-image/SKILL.md +86 -0
  45. package/.claude/skills/9router-stt/SKILL.md +79 -0
  46. package/.claude/skills/9router-tts/SKILL.md +80 -0
  47. package/.claude/skills/9router-web-fetch/SKILL.md +99 -0
  48. package/.claude/skills/9router-web-search/SKILL.md +91 -0
  49. package/.claude/skills/bizar/README.md +9 -0
  50. package/.claude/skills/bizar/SKILL.md +450 -0
  51. package/.claude/skills/cpp-coding-standards/README.md +28 -0
  52. package/.claude/skills/cpp-coding-standards/SKILL.md +634 -0
  53. package/.claude/skills/cpp-coding-standards/references/concurrency.md +320 -0
  54. package/.claude/skills/cpp-coding-standards/references/error-handling.md +229 -0
  55. package/.claude/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  56. package/.claude/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  57. package/.claude/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  58. package/.claude/skills/cpp-testing/README.md +28 -0
  59. package/.claude/skills/cpp-testing/SKILL.md +304 -0
  60. package/.claude/skills/cpp-testing/references/coverage.md +370 -0
  61. package/.claude/skills/cpp-testing/references/framework-compare.md +175 -0
  62. package/.claude/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  63. package/.claude/skills/cpp-testing/references/mocking.md +364 -0
  64. package/.claude/skills/cpp-testing/references/tdd-workflow.md +308 -0
  65. package/.claude/skills/cubesandbox/SKILL.md +148 -0
  66. package/.claude/skills/embedded-esp-idf/README.md +41 -0
  67. package/.claude/skills/embedded-esp-idf/SKILL.md +439 -0
  68. package/.claude/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  69. package/.claude/skills/embedded-esp-idf/references/host-tests.md +164 -0
  70. package/.claude/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  71. package/.claude/skills/embedded-esp-idf/references/kconfig.md +159 -0
  72. package/.claude/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  73. package/.claude/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  74. package/.claude/skills/embedded-esp-idf/references/nvs.md +121 -0
  75. package/.claude/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  76. package/.claude/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  77. package/.claude/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  78. package/.claude/skills/glyph/SKILL.md +163 -0
  79. package/.claude/skills/harness-engineering/SKILL.md +143 -0
  80. package/.claude/skills/lightrag/SKILL.md +81 -0
  81. package/.claude/skills/memory-protocol/SKILL.md +105 -0
  82. package/.claude/skills/obsidian/SKILL.md +306 -0
  83. package/.claude/skills/read-the-damn-docs/SKILL.md +113 -0
  84. package/.claude/skills/self-improvement/SKILL.md +64 -0
  85. package/README.md +87 -59
  86. package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js → EnvVarsSection-B58aiJiE.js} +1 -1
  87. package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js.map → EnvVarsSection-B58aiJiE.js.map} +1 -1
  88. package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js → MobileChat-BJrqwVDd.js} +1 -1
  89. package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js.map → MobileChat-BJrqwVDd.js.map} +1 -1
  90. package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js → MobileSettings-CEQNJNLJ.js} +1 -1
  91. package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js.map → MobileSettings-CEQNJNLJ.js.map} +1 -1
  92. package/bizar-dash/dist/assets/{main-DYiZqMrn.js → main-IvfQAOfy.js} +1 -1
  93. package/bizar-dash/dist/assets/{main-DYiZqMrn.js.map → main-IvfQAOfy.js.map} +1 -1
  94. package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js → markdown-tOLaD6nm.js} +1 -1
  95. package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js.map → markdown-tOLaD6nm.js.map} +1 -1
  96. package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js → mobile-DYCHcUpq.js} +1 -1
  97. package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js.map → mobile-DYCHcUpq.js.map} +1 -1
  98. package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js → mobile-layout-CBHjpwsb.js} +2 -2
  99. package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js.map → mobile-layout-CBHjpwsb.js.map} +1 -1
  100. package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js → useSlashCommands-Bd7_FA6U.js} +2 -2
  101. package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js.map → useSlashCommands-Bd7_FA6U.js.map} +1 -1
  102. package/bizar-dash/dist/assets/{vendor-CeHGtduv.js → vendor-C843201K.js} +12 -12
  103. package/bizar-dash/dist/assets/vendor-C843201K.js.map +1 -0
  104. package/bizar-dash/dist/index.html +6 -6
  105. package/bizar-dash/dist/mobile.html +2 -2
  106. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  107. package/bizar-dash/src/server/api.mjs +4 -4
  108. package/bizar-dash/src/server/background-store.mjs +15 -16
  109. package/bizar-dash/src/server/bg-poller.mjs +25 -35
  110. package/bizar-dash/src/server/bg-retry.mjs +66 -194
  111. package/bizar-dash/src/server/claude-bg-spawner.mjs +390 -0
  112. package/bizar-dash/src/server/claude-info.mjs +412 -0
  113. package/bizar-dash/src/server/{cline-runner.mjs → claude-runner.mjs} +53 -48
  114. package/bizar-dash/src/server/claude-sdk.mjs +191 -0
  115. package/bizar-dash/src/server/providers-store.mjs +22 -49
  116. package/bizar-dash/src/server/routes/background.mjs +24 -20
  117. package/bizar-dash/src/server/routes/chat.mjs +223 -326
  118. package/bizar-dash/src/server/routes/claude-session-detail.mjs +329 -0
  119. package/bizar-dash/src/server/routes/claude-sessions.mjs +259 -0
  120. package/bizar-dash/src/server/routes/tasks.mjs +20 -28
  121. package/bizar-dash/src/server/task-delegator.mjs +76 -77
  122. package/bizar-dash/src/web/components/chat/useChat.ts +23 -27
  123. package/bizar-dash/src/web/views/Chat.tsx +8 -8
  124. package/cli/bin.mjs +31 -18
  125. package/cli/commands/claude-cmd.mjs +348 -0
  126. package/cli/commands/install.mjs +12 -11
  127. package/cli/commands/sandbox.mjs +220 -0
  128. package/cli/commands/validate.mjs +252 -288
  129. package/cli/dev-link.test.mjs +11 -11
  130. package/cli/doctor.mjs +131 -213
  131. package/cli/install.mjs +33 -37
  132. package/cli/install.test.mjs +9 -9
  133. package/cli/provision-claude.mjs +893 -0
  134. package/cli/provision.mjs +330 -226
  135. package/cli/provision.test.mjs +102 -0
  136. package/cli/utils.mjs +72 -58
  137. package/config/AGENTS.md +28 -28
  138. package/config/agents/_shared/AGENT_BASELINE.md +104 -684
  139. package/config/skills/bizar/SKILL.md +197 -0
  140. package/config/skills/cubesandbox/SKILL.md +148 -0
  141. package/config/skills/harness-engineering/SKILL.md +142 -0
  142. package/install.sh +66 -44
  143. package/package.json +19 -11
  144. package/packages/sdk/ARCHITECTURE.md +40 -36
  145. package/packages/sdk/package-lock.json +32 -0
  146. package/packages/sdk/package.json +15 -5
  147. package/{plugins/bizar → packages/sdk}/src/dangerous-patterns.ts +6 -28
  148. package/packages/sdk/src/fingerprint.ts +0 -0
  149. package/packages/sdk/src/index.ts +38 -73
  150. package/packages/sdk/src/mcp/bin.ts +51 -0
  151. package/packages/sdk/src/mcp/server.ts +498 -0
  152. package/{plugins/bizar/src/memory-vault.ts → packages/sdk/src/memory/index.ts} +18 -21
  153. package/packages/sdk/tests/sdk.test.mjs +148 -0
  154. package/packages/sdk/vitest.config.ts +1 -1
  155. package/scripts/bh-full-e2e.mjs +166 -363
  156. package/scripts/check-deps.mjs +28 -46
  157. package/scripts/mirror-claude-md.sh +78 -0
  158. package/scripts/test-in-container.sh +157 -0
  159. package/templates/clean-state-checklist.md +3 -3
  160. package/templates/sprint-contract.md +2 -2
  161. package/bizar-dash/.bizar/activity.log +0 -3
  162. package/bizar-dash/.omx/logs/omx-2026-07-07.jsonl +0 -1
  163. package/bizar-dash/.omx/state/session.json +0 -10
  164. package/bizar-dash/bizar-design/EXPLANATION.md +0 -307
  165. package/bizar-dash/bizar-design/INSTRUCTIONS.md +0 -149
  166. package/bizar-dash/bizar-design/canvas.html +0 -281
  167. package/bizar-dash/bizar-design/canvas.html.artifact.json +0 -19
  168. package/bizar-dash/bizar-design/components.css +0 -1665
  169. package/bizar-dash/bizar-design/components.html +0 -557
  170. package/bizar-dash/bizar-design/components.html.artifact.json +0 -19
  171. package/bizar-dash/bizar-design/desktop/agents.html +0 -74
  172. package/bizar-dash/bizar-design/desktop/agents.html.artifact.json +0 -19
  173. package/bizar-dash/bizar-design/desktop/memory.html +0 -75
  174. package/bizar-dash/bizar-design/desktop/memory.html.artifact.json +0 -19
  175. package/bizar-dash/bizar-design/desktop/metrics.html +0 -63
  176. package/bizar-dash/bizar-design/desktop/metrics.html.artifact.json +0 -19
  177. package/bizar-dash/bizar-design/desktop/overview.html +0 -95
  178. package/bizar-dash/bizar-design/desktop/overview.html.artifact.json +0 -19
  179. package/bizar-dash/bizar-design/desktop/settings.html +0 -70
  180. package/bizar-dash/bizar-design/desktop/settings.html.artifact.json +0 -19
  181. package/bizar-dash/bizar-design/icons.svg +0 -44
  182. package/bizar-dash/bizar-design/index.html +0 -192
  183. package/bizar-dash/bizar-design/index.html.artifact.json +0 -19
  184. package/bizar-dash/bizar-design/mobile/agents.html +0 -29
  185. package/bizar-dash/bizar-design/mobile/agents.html.artifact.json +0 -19
  186. package/bizar-dash/bizar-design/mobile/memory.html +0 -28
  187. package/bizar-dash/bizar-design/mobile/memory.html.artifact.json +0 -19
  188. package/bizar-dash/bizar-design/mobile/metrics.html +0 -29
  189. package/bizar-dash/bizar-design/mobile/metrics.html.artifact.json +0 -19
  190. package/bizar-dash/bizar-design/mobile/overview.html +0 -33
  191. package/bizar-dash/bizar-design/mobile/overview.html.artifact.json +0 -19
  192. package/bizar-dash/bizar-design/mobile/settings.html +0 -28
  193. package/bizar-dash/bizar-design/mobile/settings.html.artifact.json +0 -19
  194. package/bizar-dash/bizar-design/prototype.js +0 -114
  195. package/bizar-dash/bizar-design/tokens.css +0 -106
  196. package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +0 -1
  197. package/bizar-dash/node_modules/.package-lock.json +0 -6
  198. package/bizar-dash/package-lock.json +0 -6
  199. package/bizar-dash/src/server/cline-sdk.mjs +0 -132
  200. package/bizar-dash/src/server/routes/cline-session-detail.mjs +0 -559
  201. package/bizar-dash/src/server/routes/cline-sessions.mjs +0 -291
  202. package/cli/commands/cline-cmd.mjs +0 -289
  203. package/cli/commands/validate.test.mjs +0 -344
  204. package/cli/doctor.test.mjs +0 -350
  205. package/config/cline.json.template +0 -342
  206. package/packages/sdk/src/client.ts +0 -188
  207. package/packages/sdk/src/cline-events.ts +0 -134
  208. package/packages/sdk/src/cline-types.ts +0 -66
  209. package/packages/sdk/src/cline.ts +0 -339
  210. package/packages/sdk/src/errors.ts +0 -129
  211. package/packages/sdk/src/events.ts +0 -153
  212. package/packages/sdk/src/types.ts +0 -176
  213. package/packages/sdk/tests/client.test.ts +0 -217
  214. package/packages/sdk/tests/errors.test.ts +0 -108
  215. package/packages/sdk/tests/events.test.ts +0 -139
  216. package/packages/sdk/tests/fixtures/fetch-mock.ts +0 -152
  217. package/packages/sdk/tests/fixtures/sse-mock.ts +0 -30
  218. package/plugins/bizar/ARCHITECTURE.md +0 -142
  219. package/plugins/bizar/CONSTRAINTS.md +0 -67
  220. package/plugins/bizar/LICENSE +0 -21
  221. package/plugins/bizar/README.md +0 -448
  222. package/plugins/bizar/index.ts +0 -872
  223. package/plugins/bizar/package.json +0 -39
  224. package/plugins/bizar/scripts/check-forbidden-imports.sh +0 -33
  225. package/plugins/bizar/src/background-state.ts +0 -641
  226. package/plugins/bizar/src/background.ts +0 -1806
  227. package/plugins/bizar/src/cline-runner.ts +0 -203
  228. package/plugins/bizar/src/clineruntime.ts +0 -227
  229. package/plugins/bizar/src/commands-impl.ts +0 -151
  230. package/plugins/bizar/src/commands.ts +0 -1799
  231. package/plugins/bizar/src/compaction.d.mts +0 -48
  232. package/plugins/bizar/src/compaction.mjs +0 -192
  233. package/plugins/bizar/src/dashboard-client.ts +0 -233
  234. package/plugins/bizar/src/event-stream.ts +0 -606
  235. package/plugins/bizar/src/fingerprint.ts +0 -120
  236. package/plugins/bizar/src/handoff.ts +0 -79
  237. package/plugins/bizar/src/hooks/memory-flush-on-compact.ts +0 -123
  238. package/plugins/bizar/src/hooks/memory-inject.ts +0 -247
  239. package/plugins/bizar/src/hooks/memory-write-on-end.ts +0 -188
  240. package/plugins/bizar/src/hooks/skill-curator.ts +0 -180
  241. package/plugins/bizar/src/http-client.ts +0 -467
  242. package/plugins/bizar/src/key-rotation.ts +0 -218
  243. package/plugins/bizar/src/logger.ts +0 -144
  244. package/plugins/bizar/src/loop-engineering.ts +0 -241
  245. package/plugins/bizar/src/loop.ts +0 -176
  246. package/plugins/bizar/src/mistake-recovery.ts +0 -98
  247. package/plugins/bizar/src/odin.ts +0 -227
  248. package/plugins/bizar/src/options.ts +0 -470
  249. package/plugins/bizar/src/plan-fs.ts +0 -323
  250. package/plugins/bizar/src/reasoning-clean.ts +0 -454
  251. package/plugins/bizar/src/report.ts +0 -178
  252. package/plugins/bizar/src/research-prompt.ts +0 -35
  253. package/plugins/bizar/src/serve-info.ts +0 -228
  254. package/plugins/bizar/src/serve.ts +0 -496
  255. package/plugins/bizar/src/settings.ts +0 -349
  256. package/plugins/bizar/src/state.ts +0 -298
  257. package/plugins/bizar/src/tool-discipline.ts +0 -105
  258. package/plugins/bizar/src/tools/agent-browser.ts +0 -315
  259. package/plugins/bizar/src/tools/bg-collect.ts +0 -131
  260. package/plugins/bizar/src/tools/bg-get-comments.ts +0 -266
  261. package/plugins/bizar/src/tools/bg-kill.ts +0 -116
  262. package/plugins/bizar/src/tools/bg-pause.ts +0 -99
  263. package/plugins/bizar/src/tools/bg-report-progress.ts +0 -115
  264. package/plugins/bizar/src/tools/bg-resume.ts +0 -94
  265. package/plugins/bizar/src/tools/bg-send-message.ts +0 -223
  266. package/plugins/bizar/src/tools/bg-spawn.ts +0 -502
  267. package/plugins/bizar/src/tools/bg-status.ts +0 -130
  268. package/plugins/bizar/src/tools/graph-query.ts +0 -278
  269. package/plugins/bizar/src/tools/loop-engineering.ts +0 -193
  270. package/plugins/bizar/src/tools/memory-list.ts +0 -43
  271. package/plugins/bizar/src/tools/memory-read.ts +0 -69
  272. package/plugins/bizar/src/tools/memory-search.ts +0 -47
  273. package/plugins/bizar/src/tools/memory-write.ts +0 -54
  274. package/plugins/bizar/src/tools/open-kb.ts +0 -198
  275. package/plugins/bizar/src/tools/plan-action.ts +0 -785
  276. package/plugins/bizar/src/tools/read-glyph-feedback.ts +0 -191
  277. package/plugins/bizar/src/tools/team-spawn.ts +0 -73
  278. package/plugins/bizar/src/tools/team-status.ts +0 -76
  279. package/plugins/bizar/src/tools/wait-for-feedback.ts +0 -415
  280. package/plugins/bizar/src/trajectory.ts +0 -104
  281. package/plugins/bizar/tests/README.md +0 -99
  282. package/plugins/bizar/tests/attach-handler-bug.test.ts +0 -169
  283. package/plugins/bizar/tests/background-state.test.ts +0 -277
  284. package/plugins/bizar/tests/background.test.ts +0 -402
  285. package/plugins/bizar/tests/block.test.ts +0 -195
  286. package/plugins/bizar/tests/canonical-key-order.test.ts +0 -75
  287. package/plugins/bizar/tests/clineruntime-config.test.ts +0 -283
  288. package/plugins/bizar/tests/commands-impl.test.ts +0 -316
  289. package/plugins/bizar/tests/commands.test.ts +0 -584
  290. package/plugins/bizar/tests/compaction.test.ts +0 -264
  291. package/plugins/bizar/tests/config.test.ts +0 -128
  292. package/plugins/bizar/tests/dashboard-client.test.ts +0 -159
  293. package/plugins/bizar/tests/dispose.test.ts +0 -336
  294. package/plugins/bizar/tests/event-stream.test.ts +0 -409
  295. package/plugins/bizar/tests/event.test.ts +0 -262
  296. package/plugins/bizar/tests/fingerprint.test.ts +0 -162
  297. package/plugins/bizar/tests/http-client.test.ts +0 -404
  298. package/plugins/bizar/tests/init-helpers.test.ts +0 -203
  299. package/plugins/bizar/tests/integration/slash-command.test.ts +0 -349
  300. package/plugins/bizar/tests/integration/tool-routing.test.ts +0 -98
  301. package/plugins/bizar/tests/key-rotation.test.ts +0 -396
  302. package/plugins/bizar/tests/loop-engineering.test.ts +0 -168
  303. package/plugins/bizar/tests/loop.test.ts +0 -397
  304. package/plugins/bizar/tests/memory-write-on-end.test.ts +0 -92
  305. package/plugins/bizar/tests/mistake-recovery.test.ts +0 -116
  306. package/plugins/bizar/tests/odin.test.ts +0 -125
  307. package/plugins/bizar/tests/options.test.ts +0 -329
  308. package/plugins/bizar/tests/reasoning-clean.test.ts +0 -422
  309. package/plugins/bizar/tests/safety.test.ts +0 -256
  310. package/plugins/bizar/tests/serve.test.ts +0 -339
  311. package/plugins/bizar/tests/settings.test.ts +0 -351
  312. package/plugins/bizar/tests/stall-think.test.ts +0 -750
  313. package/plugins/bizar/tests/state.test.ts +0 -276
  314. package/plugins/bizar/tests/tool-discipline.test.ts +0 -77
  315. package/plugins/bizar/tests/tools/agent-browser.test.ts +0 -98
  316. package/plugins/bizar/tests/tools/bg-collect.test.ts +0 -337
  317. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +0 -485
  318. package/plugins/bizar/tests/tools/bg-kill.test.ts +0 -235
  319. package/plugins/bizar/tests/tools/bg-pause.test.ts +0 -61
  320. package/plugins/bizar/tests/tools/bg-report-progress.test.ts +0 -79
  321. package/plugins/bizar/tests/tools/bg-resume.test.ts +0 -40
  322. package/plugins/bizar/tests/tools/bg-send-message.test.ts +0 -116
  323. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +0 -147
  324. package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +0 -233
  325. package/plugins/bizar/tests/tools/bg-spawn.test.ts +0 -311
  326. package/plugins/bizar/tests/tools/bg-status.test.ts +0 -217
  327. package/plugins/bizar/tests/tools/cline-runner.test.ts +0 -115
  328. package/plugins/bizar/tests/tools/plan-action.test.ts +0 -599
  329. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +0 -253
  330. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +0 -390
  331. package/plugins/bizar/tests/update-deadlock.test.ts +0 -151
  332. package/plugins/bizar/tsconfig.json +0 -29
@@ -1,339 +0,0 @@
1
- /**
2
- * ServeLifecycle tests.
3
- *
4
- * Tests: Bun.spawn args (MEDIUM-23), ENOENT/EACCES fallback (MEDIUM-23),
5
- * unexpected exit marks all instances failed (HIGH-9), health check timeout
6
- * (MEDIUM-42), BIZAR_SERVE_DISABLE=1 (LOW-43), restart with backoff (HIGH-9).
7
- *
8
- * We mock Bun.spawn to return a fake subprocess so tests are deterministic
9
- * and do not actually spawn cline serve.
10
- */
11
-
12
- import { describe, it, expect, beforeEach, vi } from "bun:test";
13
- import type { ServeLifecycle } from "../src/serve.ts";
14
-
15
- // ---------------------------------------------------------------------------
16
- // Fake subprocess helpers
17
- // ---------------------------------------------------------------------------
18
-
19
- /** Makes a fake Bun.spawn ExitedPromise that resolves immediately */
20
- function makeFakeProc(exitCode: number, signal?: string) {
21
- return {
22
- exited: Promise.resolve(exitCode),
23
- kill: vi.fn(),
24
- stdout: { readable: true } as unknown as ReadableStream & { pipe: () => void },
25
- stderr: { readable: true } as unknown as ReadableStream & { pipe: () => void },
26
- };
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // ServeLifecycle test doubles — mirrors the expected API from §5.1 / §5.2
31
- // ---------------------------------------------------------------------------
32
-
33
- interface FakeServeHooks {
34
- start: () => Promise<void>;
35
- stop: () => Promise<void>;
36
- healthCheck: () => Promise<boolean>;
37
- pid: number | null;
38
- port: number;
39
- password: string;
40
- baseUrl: string;
41
- }
42
-
43
- /** In-memory fake ServeLifecycle for testing */
44
- class FakeServeLifecycle implements FakeServeHooks {
45
- pid: number | null = null;
46
- port = 0;
47
- password = "";
48
- baseUrl = "";
49
- private _exitedPromise: Promise<number> = Promise.resolve(0);
50
- private _intentionalShutdown = false;
51
- private _onUnexpectedExit: ((exitCode: number) => void) | null = null;
52
- private _spawnCalls: Array<{ args: string[]; env: Record<string, string> }> = [];
53
- private _serveDisabled = false;
54
-
55
- constructor(opts: { serveDisabled?: boolean; spawnCalls?: Array<{ args: string[]; env: Record<string, string> }> } = {}) {
56
- this._serveDisabled = opts.serveDisabled ?? false;
57
- this._spawnCalls = opts.spawnCalls ?? [];
58
- }
59
-
60
- get spawnCalls() {
61
- return this._spawnCalls;
62
- }
63
-
64
- async start(): Promise<void> {
65
- if (this._serveDisabled) return;
66
- // Simulate finding the port from stdout
67
- this.port = 4096;
68
- this.baseUrl = `http://127.0.0.1:${this.port}`;
69
- this.pid = 12345;
70
- this.password = "fake-password";
71
- }
72
-
73
- async stop(): Promise<void> {
74
- this.pid = null;
75
- }
76
-
77
- async healthCheck(): Promise<boolean> {
78
- return this.port > 0;
79
- }
80
- }
81
-
82
- // ---------------------------------------------------------------------------
83
- // Bun.spawn args verification tests (MEDIUM-23)
84
- // ---------------------------------------------------------------------------
85
-
86
- describe("ServeLifecycle spawn args", () => {
87
- it("uses --port 0 by default (random OS-assigned port)", async () => {
88
- const fake = new FakeServeLifecycle();
89
- await fake.start();
90
- // The real implementation would call Bun.spawn with ["cline", "serve", "--port", "0", "--hostname", "127.0.0.1"]
91
- expect(fake.port).toBe(4096); // fake sets this; real impl reads from stdout
92
- });
93
-
94
- it("uses --hostname 127.0.0.1 (MEDIUM-28)", async () => {
95
- const fake = new FakeServeLifecycle();
96
- await fake.start();
97
- expect(fake.baseUrl).toContain("127.0.0.1");
98
- });
99
-
100
- it("passes CLINE_SERVER_PASSWORD in env (HIGH-24)", async () => {
101
- const fake = new FakeServeLifecycle();
102
- await fake.start();
103
- expect(fake.password).toBeTruthy();
104
- });
105
-
106
- it("does NOT include --dangerously-skip-permissions by default (MEDIUM-29)", async () => {
107
- const fake = new FakeServeLifecycle();
108
- await fake.start();
109
- // Real impl checks env BIZAR_BACKGROUND_SKIP_PERMISSIONS=1 before adding flag
110
- // This test verifies the default (no flag) is honoured
111
- expect(fake.pid).not.toBeNull();
112
- });
113
-
114
- it("adds --dangerously-skip-permissions when BIZAR_BACKGROUND_SKIP_PERMISSIONS=1 (MEDIUM-29)", async () => {
115
- // This would be verified by checking spawnCalls in a full mock
116
- // The env var handling is in options.ts — tested there
117
- expect(true).toBe(true);
118
- });
119
- });
120
-
121
- // ---------------------------------------------------------------------------
122
- // ENOENT / EACCES fallback (MEDIUM-23)
123
- // ---------------------------------------------------------------------------
124
-
125
- describe("ENOENT and EACCES handling", () => {
126
- it("ENOENT sets servePID to null and does not crash", async () => {
127
- const fake = new FakeServeLifecycle();
128
- // In real impl, ENOENT is caught and logged; servePID = null
129
- fake.pid = null;
130
- expect(fake.pid).toBeNull();
131
- });
132
-
133
- it("EACCES sets servePID to null and does not crash", async () => {
134
- const fake = new FakeServeLifecycle();
135
- fake.pid = null;
136
- expect(fake.pid).toBeNull();
137
- });
138
-
139
- it("bizar_spawn_background returns error when servePID is null", async () => {
140
- const fake = new FakeServeLifecycle();
141
- await fake.start();
142
- fake.pid = null;
143
- // Real impl checks: if (servePID === null) return { error: "..." }
144
- const result = fake.pid === null
145
- ? { error: "Background agent serve is not available. See plugin logs." }
146
- : { instanceId: "bgr_test" };
147
- expect(result).toHaveProperty("error");
148
- });
149
- });
150
-
151
- // ---------------------------------------------------------------------------
152
- // Unexpected exit handling (HIGH-9)
153
- // ---------------------------------------------------------------------------
154
-
155
- describe("unexpected exit", () => {
156
- it("marks all running instances failed when serve child exits unexpectedly", async () => {
157
- // Simulate: proc.exited resolves with non-zero code (not intentional shutdown)
158
- const exitCode: number = 1;
159
- const runningInstances = [
160
- { instanceId: "bgr_01", status: "running", error: undefined as string | undefined },
161
- { instanceId: "bgr_02", status: "pending", error: undefined as string | undefined },
162
- ];
163
-
164
- // Simulate the HIGH-9 handler
165
- if (exitCode !== 0) {
166
- for (const inst of runningInstances) {
167
- if (inst.status === "running" || inst.status === "pending") {
168
- inst.status = "failed";
169
- inst.error = "serve child exited unexpectedly";
170
- }
171
- }
172
- }
173
-
174
- const first = runningInstances[0];
175
- const second = runningInstances[1];
176
- expect(first).toBeDefined();
177
- expect(second).toBeDefined();
178
- expect(first?.status).toBe("failed");
179
- expect(first?.error).toBe("serve child exited unexpectedly");
180
- expect(second?.status).toBe("failed");
181
- expect(second?.error).toBe("serve child exited unexpectedly");
182
- });
183
-
184
- it("clean exit (code 0) does not mark instances failed", async () => {
185
- const exitCode: number = 0;
186
- const runningInstances = [
187
- { instanceId: "bgr_01", status: "running" },
188
- ];
189
-
190
- if (exitCode !== 0) {
191
- for (const inst of runningInstances) {
192
- inst.status = "failed";
193
- }
194
- }
195
-
196
- expect(runningInstances[0]?.status).toBe("running"); // unchanged
197
- });
198
-
199
- it("intentional shutdown does not trigger unexpected-exit path", async () => {
200
- const exitCode: number = 1;
201
- const intentionalShutdown = true;
202
-
203
- const runningInstances = [{ instanceId: "bgr_01", status: "running" }];
204
-
205
- if (exitCode === 0 || intentionalShutdown) {
206
- // clean path — no marking
207
- } else {
208
- for (const inst of runningInstances) inst.status = "failed";
209
- }
210
-
211
- expect(runningInstances[0]?.status).toBe("running");
212
- });
213
-
214
- it("servePID is cleared after unexpected exit", async () => {
215
- const fake = new FakeServeLifecycle();
216
- await fake.start();
217
- expect(fake.pid).not.toBeNull();
218
-
219
- // Simulate unexpected exit
220
- fake.pid = null;
221
- expect(fake.pid).toBeNull();
222
- });
223
- });
224
-
225
- // ---------------------------------------------------------------------------
226
- // Restart with backoff (HIGH-9)
227
- // ---------------------------------------------------------------------------
228
-
229
- describe("restart with backoff", () => {
230
- it("uses exponential backoff: 250ms, 500ms, 1s", async () => {
231
- const delays: number[] = [];
232
- let attempt = 0;
233
-
234
- // Simulate retry loop with backoff
235
- while (attempt < 3) {
236
- const delay = 250 * Math.pow(2, attempt);
237
- delays.push(delay);
238
- attempt++;
239
- }
240
-
241
- expect(delays).toEqual([250, 500, 1000]);
242
- });
243
-
244
- it("gives up after 3 retries and returns error", async () => {
245
- let success = false;
246
- let attempts = 0;
247
- const maxAttempts = 3;
248
-
249
- while (attempts < maxAttempts) {
250
- attempts++;
251
- // Simulate failure
252
- success = false;
253
- }
254
-
255
- expect(success).toBe(false);
256
- expect(attempts).toBe(3);
257
- });
258
-
259
- it("succeeds on first retry", async () => {
260
- let attempts = 0;
261
-
262
- while (true) {
263
- attempts++;
264
- if (attempts >= 1) break; // simulate success on first retry
265
- }
266
-
267
- expect(attempts).toBe(1);
268
- });
269
- });
270
-
271
- // ---------------------------------------------------------------------------
272
- // Health check timeout (MEDIUM-42)
273
- // ---------------------------------------------------------------------------
274
-
275
- describe("health check timeout", () => {
276
- it("init logs timeout error and sets servePID to null when port never appears", async () => {
277
- // Simulate: process stdout never emits the listening line within 5s
278
- const fake = new FakeServeLifecycle();
279
- await fake.start();
280
-
281
- // If health check times out, servePID should be null
282
- fake.pid = null;
283
- expect(fake.pid).toBeNull();
284
- });
285
-
286
- it("health check polls GET /health with 100ms interval", async () => {
287
- // The interval is a timing parameter; verified by integration test
288
- // Unit test documents the expected interval
289
- const interval = 100;
290
- expect(interval).toBe(100);
291
- });
292
-
293
- it("health check times out after 5s", async () => {
294
- const timeout = 5000;
295
- expect(timeout).toBe(5000);
296
- });
297
- });
298
-
299
- // ---------------------------------------------------------------------------
300
- // BIZAR_SERVE_DISABLE=1 (LOW-43)
301
- // ---------------------------------------------------------------------------
302
-
303
- describe("BIZAR_SERVE_DISABLE=1", () => {
304
- it("when set, servePID is never set", async () => {
305
- const fake = new FakeServeLifecycle({ serveDisabled: true });
306
- await fake.start();
307
- expect(fake.pid).toBeNull();
308
- });
309
-
310
- it("bizar_spawn_background returns 'background agents disabled' when serve is disabled", async () => {
311
- const fake = new FakeServeLifecycle({ serveDisabled: true });
312
- await fake.start();
313
-
314
- const result = fake.pid === null
315
- ? { error: "Background agents are disabled (BIZAR_SERVE_DISABLE=1)." }
316
- : { instanceId: "bgr_test" };
317
-
318
- expect(result).toHaveProperty("error");
319
- expect((result as { error: string }).error).toContain("disabled");
320
- });
321
- });
322
-
323
- // ---------------------------------------------------------------------------
324
- // ServeLifecycle interface contract verification
325
- // ---------------------------------------------------------------------------
326
-
327
- describe("ServeLifecycle interface contract", () => {
328
- it("has start(), stop(), healthCheck(), pid, port, password, baseUrl", () => {
329
- const fake = new FakeServeLifecycle();
330
- expect(typeof fake.start).toBe("function");
331
- expect(typeof fake.stop).toBe("function");
332
- expect(typeof fake.healthCheck).toBe("function");
333
- // pid is number | null; use the pattern that matches both
334
- expect(fake.pid === null || typeof fake.pid === "number").toBe(true);
335
- expect(typeof fake.port).toBe("number");
336
- expect(typeof fake.password).toBe("string");
337
- expect(typeof fake.baseUrl).toBe("string");
338
- });
339
- });
@@ -1,351 +0,0 @@
1
- /**
2
- * settings.test.ts
3
- *
4
- * Tests for SettingsStore — read/write round-trip, atomic writes,
5
- * corrupt-file fallback, validation, and concurrency.
6
- *
7
- * Groups:
8
- * 1. defaults-on-missing-file
9
- * 2. defaults-on-corrupt-file
10
- * 3. round-trip update()
11
- * 4. typed set() for each key
12
- * 5. invalid-value rejection (clamped to safe value)
13
- * 6. file path expansion (~ → home)
14
- * 7. concurrent update() serialization
15
- * 8. sequential update() round-trip
16
- */
17
-
18
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
19
- import {
20
- rmSync,
21
- mkdirSync,
22
- writeFileSync,
23
- existsSync,
24
- readFileSync,
25
- } from "node:fs";
26
- import path from "node:path";
27
-
28
- import {
29
- SettingsStore,
30
- DEFAULT_PLAN_SETTINGS,
31
- type PlanSettings,
32
- } from "../src/settings";
33
- import os from "node:os";
34
-
35
- // Minimal mock logger
36
- class MockLogger {
37
- messages: Array<{ level: string; message: string }> = [];
38
- log(opts: { level: string; message: string }) {
39
- this.messages.push(opts);
40
- }
41
- debug(m: string) { this.messages.push({ level: "debug", message: m }); }
42
- info(m: string) { this.messages.push({ level: "info", message: m }); }
43
- warn(m: string) { this.messages.push({ level: "warn", message: m }); }
44
- error(m: string) { this.messages.push({ level: "error", message: m }); }
45
- }
46
-
47
- const TEST_DIR = path.join(os.tmpdir(), "bizar-settings-test");
48
-
49
- beforeEach(() => {
50
- try { rmSync(TEST_DIR, { recursive: true, force: true }); } catch { /* ok */ }
51
- mkdirSync(TEST_DIR, { recursive: true });
52
- });
53
-
54
- afterEach(() => {
55
- try { rmSync(TEST_DIR, { recursive: true, force: true }); } catch { /* ok */ }
56
- });
57
-
58
- function makeLogger(): MockLogger {
59
- return new MockLogger();
60
- }
61
-
62
- // ===========================================================================
63
- // Group 1 — defaults on missing file
64
- // ===========================================================================
65
-
66
- describe("SettingsStore — missing file", () => {
67
- test("returns DEFAULT_PLAN_SETTINGS when file does not exist", async () => {
68
- const logger = makeLogger();
69
- const store = new SettingsStore(TEST_DIR, logger);
70
- const s = await store.get();
71
- expect(s).toEqual(DEFAULT_PLAN_SETTINGS);
72
- });
73
-
74
- test("does not log a warning when file is simply missing", async () => {
75
- const logger = makeLogger();
76
- const store = new SettingsStore(TEST_DIR, logger);
77
- await store.get();
78
- // ENOENT is silent — only SyntaxError / schema errors log.
79
- const warns = logger.messages.filter((m) => m.level === "warn");
80
- expect(warns).toHaveLength(0);
81
- });
82
- });
83
-
84
- // ===========================================================================
85
- // Group 2 — corrupt file fallback
86
- // ===========================================================================
87
-
88
- describe("SettingsStore — corrupt file", () => {
89
- test("returns defaults + warning when JSON is invalid", async () => {
90
- const logger = makeLogger();
91
- const filePath = path.join(TEST_DIR, "plan-settings.json");
92
- writeFileSync(filePath, "{ not valid json :: !!", "utf8");
93
- const store = new SettingsStore(TEST_DIR, logger);
94
- const s = await store.get();
95
- expect(s).toEqual(DEFAULT_PLAN_SETTINGS);
96
- expect(logger.messages.some((m) => m.level === "warn" && m.message.includes("corrupt"))).toBe(true);
97
- });
98
-
99
- test("returns defaults + warning when JSON is null", async () => {
100
- const logger = makeLogger();
101
- const filePath = path.join(TEST_DIR, "plan-settings.json");
102
- writeFileSync(filePath, "null", "utf8");
103
- const store = new SettingsStore(TEST_DIR, logger);
104
- const s = await store.get();
105
- expect(s).toEqual(DEFAULT_PLAN_SETTINGS);
106
- });
107
-
108
- test("returns defaults + warning when top-level is an array", async () => {
109
- const logger = makeLogger();
110
- const filePath = path.join(TEST_DIR, "plan-settings.json");
111
- writeFileSync(filePath, "[]", "utf8");
112
- const store = new SettingsStore(TEST_DIR, logger);
113
- const s = await store.get();
114
- expect(s).toEqual(DEFAULT_PLAN_SETTINGS);
115
- });
116
-
117
- test("clamps invalid values in valid JSON without losing the file", async () => {
118
- const logger = makeLogger();
119
- const filePath = path.join(TEST_DIR, "plan-settings.json");
120
- writeFileSync(
121
- filePath,
122
- JSON.stringify({
123
- visualPlanEnabled: "yes", // wrong type
124
- defaultTemplate: "nonsense", // unknown
125
- lastUsedSlug: 42, // wrong type
126
- }),
127
- "utf8",
128
- );
129
- const store = new SettingsStore(TEST_DIR, logger);
130
- const s = await store.get();
131
- // Invalid values clamped to defaults, but the file is preserved.
132
- expect(s.visualPlanEnabled).toBe(false);
133
- expect(s.defaultTemplate).toBe("blank");
134
- expect(s.lastUsedSlug).toBeNull();
135
- expect(existsSync(filePath)).toBe(true);
136
- });
137
- });
138
-
139
- // ===========================================================================
140
- // Group 3 — round-trip update()
141
- // ===========================================================================
142
-
143
- describe("SettingsStore — update() round-trip", () => {
144
- test("update() persists a single field and returns the merged state", async () => {
145
- const logger = makeLogger();
146
- const store = new SettingsStore(TEST_DIR, logger);
147
- const after = await store.update({ visualPlanEnabled: true });
148
- expect(after.visualPlanEnabled).toBe(true);
149
- expect(after.defaultTemplate).toBe("blank"); // unchanged
150
- // File written
151
- const raw = JSON.parse(readFileSync(path.join(TEST_DIR, "plan-settings.json"), "utf8"));
152
- expect(raw.visualPlanEnabled).toBe(true);
153
- });
154
-
155
- test("update() merges multiple fields in a single call", async () => {
156
- const logger = makeLogger();
157
- const store = new SettingsStore(TEST_DIR, logger);
158
- await store.update({
159
- visualPlanEnabled: true,
160
- defaultTemplate: "feature-design",
161
- lastUsedSlug: "my-feature",
162
- });
163
- const loaded = await store.get();
164
- expect(loaded).toEqual({
165
- visualPlanEnabled: true,
166
- defaultTemplate: "feature-design",
167
- lastUsedSlug: "my-feature",
168
- });
169
- });
170
-
171
- test("update() preserves existing fields not in the patch", async () => {
172
- const logger = makeLogger();
173
- const store = new SettingsStore(TEST_DIR, logger);
174
- await store.update({ visualPlanEnabled: true, lastUsedSlug: "alpha" });
175
- await store.update({ defaultTemplate: "decision-record" });
176
- const loaded = await store.get();
177
- expect(loaded.visualPlanEnabled).toBe(true); // preserved
178
- expect(loaded.lastUsedSlug).toBe("alpha"); // preserved
179
- expect(loaded.defaultTemplate).toBe("decision-record"); // updated
180
- });
181
- });
182
-
183
- // ===========================================================================
184
- // Group 4 — typed set() for each key
185
- // ===========================================================================
186
-
187
- describe("SettingsStore — set() with typed keys", () => {
188
- test("set('visualPlanEnabled', true) persists correctly", async () => {
189
- const logger = makeLogger();
190
- const store = new SettingsStore(TEST_DIR, logger);
191
- const after = await store.set("visualPlanEnabled", true);
192
- expect(after.visualPlanEnabled).toBe(true);
193
- });
194
-
195
- test("set('defaultTemplate', 'feature-design') persists correctly", async () => {
196
- const logger = makeLogger();
197
- const store = new SettingsStore(TEST_DIR, logger);
198
- const after = await store.set("defaultTemplate", "feature-design");
199
- expect(after.defaultTemplate).toBe("feature-design");
200
- });
201
-
202
- test("set('lastUsedSlug', 'foo') persists correctly", async () => {
203
- const logger = makeLogger();
204
- const store = new SettingsStore(TEST_DIR, logger);
205
- const after = await store.set("lastUsedSlug", "foo");
206
- expect(after.lastUsedSlug).toBe("foo");
207
- });
208
-
209
- test("set('lastUsedSlug', null) clears the slug", async () => {
210
- const logger = makeLogger();
211
- const store = new SettingsStore(TEST_DIR, logger);
212
- await store.set("lastUsedSlug", "foo");
213
- const after = await store.set("lastUsedSlug", null);
214
- expect(after.lastUsedSlug).toBeNull();
215
- });
216
- });
217
-
218
- // ===========================================================================
219
- // Group 5 — invalid-value rejection
220
- // ===========================================================================
221
-
222
- describe("SettingsStore — invalid-value rejection", () => {
223
- test("set('defaultTemplate', 'nonsense') is rejected, default preserved", async () => {
224
- const logger = makeLogger();
225
- const store = new SettingsStore(TEST_DIR, logger);
226
- // Force a type-coercion via cast
227
- const after = await store.set("defaultTemplate", "nonsense" as never);
228
- expect(after.defaultTemplate).toBe("blank"); // unchanged
229
- expect(logger.messages.some((m) => m.level === "warn")).toBe(true);
230
- });
231
-
232
- test("update() with invalid template is silently ignored", async () => {
233
- const logger = makeLogger();
234
- const store = new SettingsStore(TEST_DIR, logger);
235
- await store.set("defaultTemplate", "feature-design");
236
- await store.update({ defaultTemplate: "nonsense" as never });
237
- const after = await store.get();
238
- expect(after.defaultTemplate).toBe("feature-design"); // unchanged
239
- });
240
- });
241
-
242
- // ===========================================================================
243
- // Group 6 — file path expansion
244
- // ===========================================================================
245
-
246
- describe("SettingsStore — file path", () => {
247
- test("constructor expands ~ in settingsDir", () => {
248
- const logger = makeLogger();
249
- const store = new SettingsStore("~/.cache/test-bizar", logger);
250
- const expected = path.join(os.homedir(), ".cache/test-bizar/plan-settings.json");
251
- expect(store.getFilePath()).toBe(expected);
252
- });
253
-
254
- test("settings written with ~ path land in the expanded location", async () => {
255
- const tmp = path.join(os.tmpdir(), "bizar-settings-expansion-test");
256
- rmSync(tmp, { recursive: true, force: true });
257
- try {
258
- const logger = makeLogger();
259
- const store = new SettingsStore(tmp, logger);
260
- await store.update({ visualPlanEnabled: true });
261
- expect(existsSync(path.join(tmp, "plan-settings.json"))).toBe(true);
262
- } finally {
263
- rmSync(tmp, { recursive: true, force: true });
264
- }
265
- });
266
- });
267
-
268
- // ===========================================================================
269
- // Group 7 — concurrent update() serialization
270
- // ===========================================================================
271
-
272
- describe("SettingsStore — concurrent updates", () => {
273
- test("concurrent update() calls do not corrupt the file", async () => {
274
- const logger = makeLogger();
275
- const store = new SettingsStore(TEST_DIR, logger);
276
-
277
- // Fire many updates in parallel that each flip the visualPlanEnabled
278
- // flag. The mutex guarantees no write is lost or interleaved.
279
- const updates = Array.from({ length: 30 }, (_, i) =>
280
- store.update({ visualPlanEnabled: i % 2 === 0 }),
281
- );
282
- const results = await Promise.all(updates);
283
-
284
- // All returned states are valid (either true or false).
285
- for (const r of results) {
286
- expect(typeof r.visualPlanEnabled).toBe("boolean");
287
- }
288
-
289
- // The on-disk file is valid JSON (parseable).
290
- const filePath = path.join(TEST_DIR, "plan-settings.json");
291
- expect(existsSync(filePath)).toBe(true);
292
- const raw = readFileSync(filePath, "utf8");
293
- const parsed = JSON.parse(raw) as PlanSettings;
294
- expect(typeof parsed.visualPlanEnabled).toBe("boolean");
295
- });
296
-
297
- test("concurrent set() calls do not corrupt the file", async () => {
298
- const logger = makeLogger();
299
- const store = new SettingsStore(TEST_DIR, logger);
300
- const sets = Array.from({ length: 20 }, (_, i) =>
301
- store.set("lastUsedSlug", `slug-${i}`),
302
- );
303
- const results = await Promise.all(sets);
304
- for (const r of results) {
305
- expect(typeof r.lastUsedSlug).toBe("string");
306
- expect(r.lastUsedSlug).toMatch(/^slug-\d+$/);
307
- }
308
- // File is valid JSON
309
- const filePath = path.join(TEST_DIR, "plan-settings.json");
310
- const parsed = JSON.parse(readFileSync(filePath, "utf8"));
311
- expect(parsed.lastUsedSlug).toMatch(/^slug-\d+$/);
312
- });
313
- });
314
-
315
- // ===========================================================================
316
- // Group 8 — atomic write (no .tmp file left behind)
317
- // ===========================================================================
318
-
319
- describe("SettingsStore — atomic write", () => {
320
- test("update() uses atomic rename (no .tmp file on disk)", async () => {
321
- const logger = makeLogger();
322
- const store = new SettingsStore(TEST_DIR, logger);
323
- await store.update({ visualPlanEnabled: true });
324
- const filePath = path.join(TEST_DIR, "plan-settings.json");
325
- expect(existsSync(filePath)).toBe(true);
326
- expect(existsSync(`${filePath}.tmp`)).toBe(false);
327
- });
328
- });
329
-
330
- // ===========================================================================
331
- // Group 9 — round-trip persistence
332
- // ===========================================================================
333
-
334
- describe("SettingsStore — full round-trip", () => {
335
- test("write, then create a fresh store, reads back the same value", async () => {
336
- const logger1 = makeLogger();
337
- const store1 = new SettingsStore(TEST_DIR, logger1);
338
- const written = await store1.update({
339
- visualPlanEnabled: true,
340
- defaultTemplate: "bug-investigation",
341
- lastUsedSlug: "round-trip",
342
- });
343
-
344
- // Fresh store from the same dir
345
- const logger2 = makeLogger();
346
- const store2 = new SettingsStore(TEST_DIR, logger2);
347
- const read = await store2.get();
348
-
349
- expect(read).toEqual(written);
350
- });
351
- });