discoclaw 0.1.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 (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { autoTagBead } from './auto-tag.js';
3
+ import { autoTagTask } from '../tasks/auto-tag.js';
4
+ function makeMockRuntime(output) {
5
+ return {
6
+ id: 'other',
7
+ capabilities: new Set(),
8
+ async *invoke() {
9
+ yield { type: 'text_final', text: output };
10
+ },
11
+ };
12
+ }
13
+ function makeMockErrorRuntime() {
14
+ return {
15
+ id: 'other',
16
+ capabilities: new Set(),
17
+ async *invoke() {
18
+ yield { type: 'error', message: 'fail' };
19
+ },
20
+ };
21
+ }
22
+ const TAGS = ['feature', 'bug', 'personal', 'work', 'urgent'];
23
+ describe('autoTagBead', () => {
24
+ it('returns valid tags from AI output', async () => {
25
+ const runtime = makeMockRuntime('feature, personal');
26
+ const result = await autoTagBead(runtime, 'Add login', 'New feature', TAGS);
27
+ expect(result).toEqual(['feature', 'personal']);
28
+ });
29
+ it('drops unknown tags silently', async () => {
30
+ const runtime = makeMockRuntime('feature, nonexistent, bug');
31
+ const result = await autoTagBead(runtime, 'Fix crash', '', TAGS);
32
+ expect(result).toEqual(['feature', 'bug']);
33
+ });
34
+ it('limits to 3 tags', async () => {
35
+ const runtime = makeMockRuntime('feature, bug, personal, work, urgent');
36
+ const result = await autoTagBead(runtime, 'Big task', '', TAGS);
37
+ expect(result).toHaveLength(3);
38
+ });
39
+ it('handles case-insensitive matching', async () => {
40
+ const runtime = makeMockRuntime('Feature, BUG');
41
+ const result = await autoTagBead(runtime, 'Test', '', TAGS);
42
+ expect(result).toEqual(['feature', 'bug']);
43
+ });
44
+ it('returns empty on runtime error', async () => {
45
+ const runtime = makeMockErrorRuntime();
46
+ const result = await autoTagBead(runtime, 'Test', '', TAGS);
47
+ expect(result).toEqual([]);
48
+ });
49
+ it('returns empty when no available tags', async () => {
50
+ const runtime = makeMockRuntime('feature');
51
+ const result = await autoTagBead(runtime, 'Test', '', []);
52
+ expect(result).toEqual([]);
53
+ });
54
+ it('handles empty AI output', async () => {
55
+ const runtime = makeMockRuntime('');
56
+ const result = await autoTagBead(runtime, 'Test', '', TAGS);
57
+ expect(result).toEqual([]);
58
+ });
59
+ it('keeps bead compatibility export aligned to canonical task helper', () => {
60
+ expect(autoTagBead).toBe(autoTagTask);
61
+ });
62
+ });
@@ -0,0 +1,9 @@
1
+ import { buildTaskContextSummary, } from '../tasks/context-summary.js';
2
+ import { normalizeTaskData, } from '../tasks/bd-cli.js';
3
+ /**
4
+ * Legacy compatibility shim. Canonical implementation now lives at
5
+ * `src/tasks/bd-cli.ts`.
6
+ */
7
+ export * from '../tasks/bd-cli.js';
8
+ export const buildBeadContextSummary = buildTaskContextSummary;
9
+ export const normalizeBeadData = normalizeTaskData;
@@ -0,0 +1,495 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'node:path';
3
+ import { parseBdJson, normalizeBeadData, bdShow, bdList, bdFindByTitle, ensureBdDatabaseReady, buildBeadContextSummary } from './bd-cli.js';
4
+ import { parseBdJson as parseTaskBdJson, bdList as taskBdList } from '../tasks/bd-cli.js';
5
+ import { buildTaskContextSummary } from '../tasks/context-summary.js';
6
+ import { TaskStore } from '../tasks/store.js';
7
+ vi.mock('execa', () => ({
8
+ execa: vi.fn(),
9
+ }));
10
+ vi.mock('node:fs/promises', () => ({
11
+ default: {
12
+ realpath: vi.fn(async (p) => p),
13
+ },
14
+ }));
15
+ // ---------------------------------------------------------------------------
16
+ // parseBdJson
17
+ // ---------------------------------------------------------------------------
18
+ describe('parseBdJson', () => {
19
+ it('keeps compatibility exports aligned to canonical task bd-cli', () => {
20
+ expect(parseBdJson).toBe(parseTaskBdJson);
21
+ expect(bdList).toBe(taskBdList);
22
+ });
23
+ it('parses array output', () => {
24
+ const input = JSON.stringify([
25
+ { id: 'ws-001', title: 'Test', status: 'open' },
26
+ { id: 'ws-002', title: 'Test 2', status: 'closed' },
27
+ ]);
28
+ const result = parseBdJson(input);
29
+ expect(result).toHaveLength(2);
30
+ expect(result[0].id).toBe('ws-001');
31
+ expect(result[1].id).toBe('ws-002');
32
+ });
33
+ it('parses single-object output', () => {
34
+ const input = JSON.stringify({ id: 'ws-001', title: 'Test', status: 'open' });
35
+ const result = parseBdJson(input);
36
+ expect(result).toHaveLength(1);
37
+ expect(result[0].id).toBe('ws-001');
38
+ });
39
+ it('strips markdown fences', () => {
40
+ const input = '```json\n[{"id":"ws-001","title":"Test"}]\n```';
41
+ const result = parseBdJson(input);
42
+ expect(result).toHaveLength(1);
43
+ expect(result[0].id).toBe('ws-001');
44
+ });
45
+ it('strips bare markdown fences (no language tag)', () => {
46
+ const input = '```\n{"id":"ws-001","title":"Test"}\n```';
47
+ const result = parseBdJson(input);
48
+ expect(result).toHaveLength(1);
49
+ });
50
+ it('returns empty array for empty input', () => {
51
+ expect(parseBdJson('')).toEqual([]);
52
+ expect(parseBdJson(' \n ')).toEqual([]);
53
+ });
54
+ it('throws on error-only object', () => {
55
+ const input = JSON.stringify({ error: 'not found' });
56
+ expect(() => parseBdJson(input)).toThrow('not found');
57
+ });
58
+ it('throws on malformed JSON', () => {
59
+ expect(() => parseBdJson('{bad json}')).toThrow();
60
+ });
61
+ it('returns empty array for non-object JSON', () => {
62
+ expect(parseBdJson('"just a string"')).toEqual([]);
63
+ expect(parseBdJson('42')).toEqual([]);
64
+ expect(parseBdJson('null')).toEqual([]);
65
+ });
66
+ });
67
+ // ---------------------------------------------------------------------------
68
+ // normalizeBeadData
69
+ // ---------------------------------------------------------------------------
70
+ describe('normalizeBeadData', () => {
71
+ const baseBead = {
72
+ id: 'ws-001',
73
+ title: 'Test bead',
74
+ status: 'open',
75
+ };
76
+ it('maps "done" → "closed"', () => {
77
+ const bead = { ...baseBead, status: 'done' };
78
+ expect(normalizeBeadData(bead).status).toBe('closed');
79
+ });
80
+ it('maps "tombstone" → "closed"', () => {
81
+ const bead = { ...baseBead, status: 'tombstone' };
82
+ expect(normalizeBeadData(bead).status).toBe('closed');
83
+ });
84
+ it('does not mutate the original bead when mapping', () => {
85
+ const bead = { ...baseBead, status: 'done' };
86
+ normalizeBeadData(bead);
87
+ expect(bead.status).toBe('done');
88
+ });
89
+ it.each(['open', 'in_progress', 'blocked', 'closed'])('passes through valid status "%s" unchanged', (status) => {
90
+ const bead = { ...baseBead, status };
91
+ const result = normalizeBeadData(bead);
92
+ expect(result.status).toBe(status);
93
+ expect(result).toBe(bead); // same reference — no copy
94
+ });
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // bdShow — "not found" error handling
98
+ // ---------------------------------------------------------------------------
99
+ describe('bdShow', () => {
100
+ it('returns null for "not found" errors', async () => {
101
+ const { execa } = await import('execa');
102
+ execa.mockResolvedValueOnce({
103
+ exitCode: 1,
104
+ stdout: '',
105
+ stderr: 'Error: not found',
106
+ });
107
+ const result = await bdShow('ws-999', '/tmp');
108
+ expect(result).toBeNull();
109
+ });
110
+ it('returns null for "no issue found matching" errors (bd resolve failure)', async () => {
111
+ const { execa } = await import('execa');
112
+ execa.mockResolvedValueOnce({
113
+ exitCode: 1,
114
+ stdout: '',
115
+ stderr: 'Error: resolving ID ws-007: operation failed: failed to resolve ID: no issue found matching "ws-007"',
116
+ });
117
+ const result = await bdShow('ws-007', '/tmp');
118
+ expect(result).toBeNull();
119
+ });
120
+ it('returns bead data on success', async () => {
121
+ const { execa } = await import('execa');
122
+ execa.mockResolvedValueOnce({
123
+ exitCode: 0,
124
+ stdout: JSON.stringify([{ id: 'ws-001', title: 'Test', status: 'open' }]),
125
+ stderr: '',
126
+ });
127
+ const result = await bdShow('ws-001', '/tmp');
128
+ expect(result).toEqual({ id: 'ws-001', title: 'Test', status: 'open' });
129
+ });
130
+ it('throws on unexpected errors', async () => {
131
+ const { execa } = await import('execa');
132
+ execa.mockResolvedValueOnce({
133
+ exitCode: 1,
134
+ stdout: '',
135
+ stderr: 'Error: database corruption detected',
136
+ });
137
+ await expect(bdShow('ws-001', '/tmp')).rejects.toThrow('database corruption');
138
+ });
139
+ });
140
+ // ---------------------------------------------------------------------------
141
+ // runBd — argument construction (--db, --no-daemon pinning)
142
+ // ---------------------------------------------------------------------------
143
+ describe('runBd argument construction', () => {
144
+ let mockExeca;
145
+ beforeEach(async () => {
146
+ const mod = await import('execa');
147
+ mockExeca = mod.execa;
148
+ mockExeca.mockReset();
149
+ });
150
+ it('prepends --db and --no-daemon to execa args', async () => {
151
+ mockExeca.mockResolvedValueOnce({
152
+ exitCode: 0,
153
+ stdout: '[]',
154
+ stderr: '',
155
+ });
156
+ await bdList({}, '/home/user/workspace');
157
+ expect(mockExeca).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([
158
+ '--db', '/home/user/workspace/.beads/beads.db',
159
+ '--no-daemon',
160
+ ]), expect.objectContaining({ cwd: '/home/user/workspace' }));
161
+ });
162
+ it('resolves relative cwd to absolute dbPath', async () => {
163
+ mockExeca.mockResolvedValueOnce({
164
+ exitCode: 0,
165
+ stdout: '[]',
166
+ stderr: '',
167
+ });
168
+ await bdList({}, 'workspace');
169
+ const calledArgs = mockExeca.mock.calls[0][1];
170
+ const dbArg = calledArgs[calledArgs.indexOf('--db') + 1];
171
+ // path.resolve('workspace', ...) produces an absolute path
172
+ expect(path.isAbsolute(dbArg)).toBe(true);
173
+ expect(dbArg).toBe(path.resolve('workspace', '.beads', 'beads.db'));
174
+ });
175
+ it('passes --limit 0 when status is all and no explicit limit', async () => {
176
+ mockExeca.mockResolvedValueOnce({
177
+ exitCode: 0,
178
+ stdout: '[]',
179
+ stderr: '',
180
+ });
181
+ await bdList({ status: 'all' }, '/tmp');
182
+ const calledArgs = mockExeca.mock.calls[0][1];
183
+ expect(calledArgs).toContain('--all');
184
+ expect(calledArgs).toContain('--limit');
185
+ const limitIdx = calledArgs.indexOf('--limit');
186
+ expect(calledArgs[limitIdx + 1]).toBe('0');
187
+ });
188
+ it('respects explicit limit when status is all', async () => {
189
+ mockExeca.mockResolvedValueOnce({
190
+ exitCode: 0,
191
+ stdout: '[]',
192
+ stderr: '',
193
+ });
194
+ await bdList({ status: 'all', limit: 10 }, '/tmp');
195
+ const calledArgs = mockExeca.mock.calls[0][1];
196
+ expect(calledArgs).toContain('--all');
197
+ expect(calledArgs).toContain('--limit');
198
+ const limitIdx = calledArgs.indexOf('--limit');
199
+ expect(calledArgs[limitIdx + 1]).toBe('10');
200
+ });
201
+ it('places --db and --no-daemon before subcommand args', async () => {
202
+ mockExeca.mockResolvedValueOnce({
203
+ exitCode: 0,
204
+ stdout: '[]',
205
+ stderr: '',
206
+ });
207
+ await bdList({ status: 'open' }, '/tmp');
208
+ const calledArgs = mockExeca.mock.calls[0][1];
209
+ const dbIdx = calledArgs.indexOf('--db');
210
+ const noDaemonIdx = calledArgs.indexOf('--no-daemon');
211
+ const listIdx = calledArgs.indexOf('list');
212
+ expect(dbIdx).toBeLessThan(listIdx);
213
+ expect(noDaemonIdx).toBeLessThan(listIdx);
214
+ });
215
+ });
216
+ // ---------------------------------------------------------------------------
217
+ // bdFindByTitle — title-match dedup
218
+ // ---------------------------------------------------------------------------
219
+ describe('bdFindByTitle', () => {
220
+ let mockExeca;
221
+ beforeEach(async () => {
222
+ const mod = await import('execa');
223
+ mockExeca = mod.execa;
224
+ mockExeca.mockReset();
225
+ });
226
+ it('returns matching open bead (case-insensitive, trimmed)', async () => {
227
+ mockExeca.mockResolvedValueOnce({
228
+ exitCode: 0,
229
+ stdout: JSON.stringify([
230
+ { id: 'ws-001', title: ' Fix The Bug ', status: 'open' },
231
+ ]),
232
+ stderr: '',
233
+ });
234
+ const result = await bdFindByTitle('fix the bug', '/tmp');
235
+ expect(result).toEqual({ id: 'ws-001', title: ' Fix The Bug ', status: 'open' });
236
+ });
237
+ it('returns null when no title matches', async () => {
238
+ mockExeca.mockResolvedValueOnce({
239
+ exitCode: 0,
240
+ stdout: JSON.stringify([
241
+ { id: 'ws-001', title: 'Something else', status: 'open' },
242
+ ]),
243
+ stderr: '',
244
+ });
245
+ const result = await bdFindByTitle('Fix the bug', '/tmp');
246
+ expect(result).toBeNull();
247
+ });
248
+ it('skips closed beads with matching title', async () => {
249
+ mockExeca.mockResolvedValueOnce({
250
+ exitCode: 0,
251
+ stdout: JSON.stringify([
252
+ { id: 'ws-001', title: 'Fix the bug', status: 'closed' },
253
+ ]),
254
+ stderr: '',
255
+ });
256
+ const result = await bdFindByTitle('Fix the bug', '/tmp');
257
+ expect(result).toBeNull();
258
+ });
259
+ it('matches in_progress beads', async () => {
260
+ mockExeca.mockResolvedValueOnce({
261
+ exitCode: 0,
262
+ stdout: JSON.stringify([
263
+ { id: 'ws-002', title: 'Add auth', status: 'in_progress' },
264
+ ]),
265
+ stderr: '',
266
+ });
267
+ const result = await bdFindByTitle('Add auth', '/tmp');
268
+ expect(result).toEqual({ id: 'ws-002', title: 'Add auth', status: 'in_progress' });
269
+ });
270
+ it('returns null for empty/whitespace title without calling bd', async () => {
271
+ const result = await bdFindByTitle(' ', '/tmp');
272
+ expect(result).toBeNull();
273
+ expect(mockExeca).not.toHaveBeenCalled();
274
+ });
275
+ it('passes label filter to bdList when provided', async () => {
276
+ mockExeca.mockResolvedValueOnce({
277
+ exitCode: 0,
278
+ stdout: JSON.stringify([]),
279
+ stderr: '',
280
+ });
281
+ await bdFindByTitle('Some title', '/tmp', { label: 'plan' });
282
+ const calledArgs = mockExeca.mock.calls[0][1];
283
+ expect(calledArgs).toContain('--label');
284
+ expect(calledArgs).toContain('plan');
285
+ });
286
+ it('returns first match when multiple beads match', async () => {
287
+ mockExeca.mockResolvedValueOnce({
288
+ exitCode: 0,
289
+ stdout: JSON.stringify([
290
+ { id: 'ws-001', title: 'Fix the bug', status: 'open' },
291
+ { id: 'ws-002', title: 'Fix the bug', status: 'in_progress' },
292
+ ]),
293
+ stderr: '',
294
+ });
295
+ const result = await bdFindByTitle('Fix the bug', '/tmp');
296
+ expect(result?.id).toBe('ws-001');
297
+ });
298
+ });
299
+ // ---------------------------------------------------------------------------
300
+ // ensureBdDatabaseReady
301
+ // ---------------------------------------------------------------------------
302
+ describe('ensureBdDatabaseReady', () => {
303
+ let mockExeca;
304
+ let mockRealpath;
305
+ beforeEach(async () => {
306
+ const mod = await import('execa');
307
+ mockExeca = mod.execa;
308
+ mockExeca.mockReset();
309
+ const fsMod = await import('node:fs/promises');
310
+ mockRealpath = fsMod.default.realpath;
311
+ // Default: identity (no symlink). Tests that need symlink behavior override this.
312
+ mockRealpath.mockReset();
313
+ mockRealpath.mockImplementation(async (p) => p);
314
+ });
315
+ it('returns ready with prefix when prefix already set', async () => {
316
+ mockExeca.mockResolvedValueOnce({
317
+ exitCode: 0,
318
+ stdout: 'dev',
319
+ stderr: '',
320
+ });
321
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
322
+ expect(result).toEqual({ ready: true, prefix: 'dev' });
323
+ // Should only call config get, never config set
324
+ expect(mockExeca).toHaveBeenCalledTimes(1);
325
+ expect(mockExeca).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['config', 'get', 'issue_prefix']), expect.objectContaining({ cwd: '/home/user/discoclaw-data/workspace', reject: false }));
326
+ });
327
+ it('auto-initializes prefix when "(not set)" and set succeeds', async () => {
328
+ // First call: config get returns "(not set)"
329
+ mockExeca.mockResolvedValueOnce({
330
+ exitCode: 0,
331
+ stdout: 'issue_prefix (not set)',
332
+ stderr: '',
333
+ });
334
+ // Second call: config set succeeds
335
+ mockExeca.mockResolvedValueOnce({
336
+ exitCode: 0,
337
+ stdout: '',
338
+ stderr: '',
339
+ });
340
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
341
+ expect(result).toEqual({ ready: true, prefix: 'data' });
342
+ expect(mockExeca).toHaveBeenCalledTimes(2);
343
+ // Verify the set call used the derived prefix
344
+ const setArgs = mockExeca.mock.calls[1][1];
345
+ expect(setArgs).toEqual(expect.arrayContaining(['config', 'set', 'issue_prefix', 'data']));
346
+ });
347
+ it('auto-initializes prefix when config get returns empty output', async () => {
348
+ // exitCode 0 but empty stdout → falls through to auto-init
349
+ mockExeca.mockResolvedValueOnce({
350
+ exitCode: 0,
351
+ stdout: '',
352
+ stderr: '',
353
+ });
354
+ mockExeca.mockResolvedValueOnce({
355
+ exitCode: 0,
356
+ stdout: '',
357
+ stderr: '',
358
+ });
359
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
360
+ expect(result).toEqual({ ready: true, prefix: 'personal' });
361
+ });
362
+ it('auto-initializes prefix when config get returns non-zero exit code', async () => {
363
+ mockExeca.mockResolvedValueOnce({
364
+ exitCode: 1,
365
+ stdout: '',
366
+ stderr: 'config key not found',
367
+ });
368
+ mockExeca.mockResolvedValueOnce({
369
+ exitCode: 0,
370
+ stdout: '',
371
+ stderr: '',
372
+ });
373
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
374
+ expect(result).toEqual({ ready: true, prefix: 'data' });
375
+ });
376
+ it('returns not ready when auto-init set fails', async () => {
377
+ mockExeca.mockResolvedValueOnce({
378
+ exitCode: 0,
379
+ stdout: 'issue_prefix (not set)',
380
+ stderr: '',
381
+ });
382
+ mockExeca.mockResolvedValueOnce({
383
+ exitCode: 1,
384
+ stdout: '',
385
+ stderr: 'permission denied',
386
+ });
387
+ const result = await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
388
+ expect(result).toEqual({ ready: false });
389
+ });
390
+ it('returns not ready when execa throws', async () => {
391
+ mockExeca.mockRejectedValueOnce(new Error('ENOENT: bd not found'));
392
+ const result = await ensureBdDatabaseReady('/tmp/workspace');
393
+ expect(result).toEqual({ ready: false });
394
+ });
395
+ // ---- Prefix derivation logic ----
396
+ it('derives prefix from "discoclaw-personal" → "personal"', async () => {
397
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
398
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
399
+ await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
400
+ const setArgs = mockExeca.mock.calls[1][1];
401
+ expect(setArgs).toContain('personal');
402
+ });
403
+ it('derives prefix from "discoclaw-data" → "data"', async () => {
404
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
405
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
406
+ await ensureBdDatabaseReady('/home/user/discoclaw-data/workspace');
407
+ const setArgs = mockExeca.mock.calls[1][1];
408
+ expect(setArgs).toContain('data');
409
+ });
410
+ it('derives prefix from bare "discoclaw" → "dc" (fallback)', async () => {
411
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
412
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
413
+ await ensureBdDatabaseReady('/home/user/discoclaw/workspace');
414
+ const setArgs = mockExeca.mock.calls[1][1];
415
+ expect(setArgs).toContain('dc');
416
+ });
417
+ it('strips non-alphanumeric chars from derived prefix', async () => {
418
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
419
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
420
+ // Parent dir has special chars: "my-project_v2" → "myprojectv2"
421
+ await ensureBdDatabaseReady('/home/user/my-project_v2/workspace');
422
+ const setArgs = mockExeca.mock.calls[1][1];
423
+ expect(setArgs).toContain('myprojectv2');
424
+ });
425
+ it('uses correct --db path derived from cwd', async () => {
426
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ws', stderr: '' });
427
+ await ensureBdDatabaseReady('/home/user/discoclaw-personal/workspace');
428
+ const getArgs = mockExeca.mock.calls[0][1];
429
+ const dbIdx = getArgs.indexOf('--db');
430
+ expect(getArgs[dbIdx + 1]).toBe(path.resolve('/home/user/discoclaw-personal/workspace', '.beads', 'beads.db'));
431
+ });
432
+ it('resolves symlinks before deriving prefix', async () => {
433
+ // Symlink: code/discoclaw/workspace → discoclaw-data/workspace
434
+ // Without realpath, parent would be "discoclaw" → "dc" (wrong)
435
+ // With realpath, parent is "discoclaw-data" → "data" (correct)
436
+ mockRealpath.mockResolvedValueOnce('/home/user/discoclaw-data/workspace');
437
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '(not set)', stderr: '' });
438
+ mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' });
439
+ const result = await ensureBdDatabaseReady('/home/user/code/discoclaw/workspace');
440
+ expect(result).toEqual({ ready: true, prefix: 'data' });
441
+ expect(mockRealpath).toHaveBeenCalledWith('/home/user/code/discoclaw/workspace');
442
+ const setArgs = mockExeca.mock.calls[1][1];
443
+ expect(setArgs).toContain('data');
444
+ });
445
+ });
446
+ // ---------------------------------------------------------------------------
447
+ // buildBeadContextSummary — in-process TaskStore path
448
+ // ---------------------------------------------------------------------------
449
+ describe('buildBeadContextSummary', () => {
450
+ it('keeps compatibility export aligned to canonical task helper', () => {
451
+ expect(buildBeadContextSummary).toBe(buildTaskContextSummary);
452
+ });
453
+ it('returns undefined when beadId is undefined', () => {
454
+ const store = new TaskStore();
455
+ expect(buildBeadContextSummary(undefined, store)).toBeUndefined();
456
+ });
457
+ it('returns undefined when store is undefined', () => {
458
+ expect(buildBeadContextSummary('t-001', undefined)).toBeUndefined();
459
+ });
460
+ it('returns undefined when bead is not found in store', () => {
461
+ const store = new TaskStore();
462
+ expect(buildBeadContextSummary('t-001', store)).toBeUndefined();
463
+ });
464
+ it('returns summary with title only when no description', () => {
465
+ const store = new TaskStore();
466
+ const bead = store.create({ title: 'Fix the bug' });
467
+ const result = buildBeadContextSummary(bead.id, store);
468
+ expect(result?.summary).toBe('Task context for this thread:\nTitle: Fix the bug');
469
+ expect(result?.description).toBeUndefined();
470
+ });
471
+ it('returns summary with title and truncated description', () => {
472
+ const store = new TaskStore();
473
+ const bead = store.create({ title: 'My task', description: 'a'.repeat(500) });
474
+ const result = buildBeadContextSummary(bead.id, store);
475
+ expect(result?.summary).toContain('Title: My task');
476
+ expect(result?.description).toHaveLength(400);
477
+ expect(result?.description?.endsWith('\u2026')).toBe(true);
478
+ });
479
+ it('collapses whitespace in description', () => {
480
+ const store = new TaskStore();
481
+ const bead = store.create({ title: 'T', description: 'hello\n world' });
482
+ const result = buildBeadContextSummary(bead.id, store);
483
+ expect(result?.description).toBe('hello world');
484
+ });
485
+ it('does not call execa — store.get is synchronous', () => {
486
+ // This test verifies no subprocess is spawned. Since buildBeadContextSummary
487
+ // is now a synchronous function, it cannot be an async subprocess call.
488
+ const store = new TaskStore();
489
+ const bead = store.create({ title: 'Sync task' });
490
+ const returnValue = buildBeadContextSummary(bead.id, store);
491
+ // Must return a plain object, not a Promise.
492
+ expect(returnValue).not.toBeInstanceOf(Promise);
493
+ expect(returnValue?.summary).toContain('Sync task');
494
+ });
495
+ });