macro-agent 0.0.17 → 0.1.1

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 (338) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +16 -14
  4. package/README.md +11 -29
  5. package/dist/acp/macro-agent.d.ts +17 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +183 -55
  8. package/dist/acp/macro-agent.js.map +1 -1
  9. package/dist/acp/types.d.ts +32 -1
  10. package/dist/acp/types.d.ts.map +1 -1
  11. package/dist/acp/types.js.map +1 -1
  12. package/dist/agent/agent-manager.d.ts +65 -1
  13. package/dist/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/agent/agent-manager.js +464 -183
  15. package/dist/agent/agent-manager.js.map +1 -1
  16. package/dist/agent/types.d.ts +1 -1
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/api/server.d.ts +3 -0
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +37 -6
  21. package/dist/api/server.js.map +1 -1
  22. package/dist/auth/index.d.ts +2 -0
  23. package/dist/auth/index.d.ts.map +1 -0
  24. package/dist/auth/index.js +2 -0
  25. package/dist/auth/index.js.map +1 -0
  26. package/dist/auth/token.d.ts +41 -0
  27. package/dist/auth/token.d.ts.map +1 -0
  28. package/dist/auth/token.js +73 -0
  29. package/dist/auth/token.js.map +1 -0
  30. package/dist/cli/acp.d.ts +2 -23
  31. package/dist/cli/acp.d.ts.map +1 -1
  32. package/dist/cli/acp.js +127 -61
  33. package/dist/cli/acp.js.map +1 -1
  34. package/dist/cli/index.js +147 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -0
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +268 -181
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/parse-args.d.ts +20 -0
  41. package/dist/cli/parse-args.d.ts.map +1 -0
  42. package/dist/cli/parse-args.js +43 -0
  43. package/dist/cli/parse-args.js.map +1 -0
  44. package/dist/cli/stable-instance-id.d.ts +8 -0
  45. package/dist/cli/stable-instance-id.d.ts.map +1 -0
  46. package/dist/cli/stable-instance-id.js +14 -0
  47. package/dist/cli/stable-instance-id.js.map +1 -0
  48. package/dist/config/project-config.d.ts +74 -7
  49. package/dist/config/project-config.d.ts.map +1 -1
  50. package/dist/config/project-config.js +123 -20
  51. package/dist/config/project-config.js.map +1 -1
  52. package/dist/map/adapter/acp-over-map.d.ts +23 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +482 -55
  55. package/dist/map/adapter/acp-over-map.js.map +1 -1
  56. package/dist/map/adapter/connection-manager.d.ts.map +1 -1
  57. package/dist/map/adapter/connection-manager.js +3 -0
  58. package/dist/map/adapter/connection-manager.js.map +1 -1
  59. package/dist/map/adapter/event-log.d.ts +87 -0
  60. package/dist/map/adapter/event-log.d.ts.map +1 -0
  61. package/dist/map/adapter/event-log.js +122 -0
  62. package/dist/map/adapter/event-log.js.map +1 -0
  63. package/dist/map/adapter/event-translator.js +6 -6
  64. package/dist/map/adapter/event-translator.js.map +1 -1
  65. package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
  66. package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
  67. package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
  68. package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
  69. package/dist/map/adapter/extensions/index.d.ts +10 -1
  70. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  71. package/dist/map/adapter/extensions/index.js +34 -0
  72. package/dist/map/adapter/extensions/index.js.map +1 -1
  73. package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
  74. package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
  75. package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
  76. package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
  77. package/dist/map/adapter/extensions/rename.d.ts +29 -0
  78. package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
  79. package/dist/map/adapter/extensions/rename.js +49 -0
  80. package/dist/map/adapter/extensions/rename.js.map +1 -0
  81. package/dist/map/adapter/extensions/task.d.ts.map +1 -1
  82. package/dist/map/adapter/extensions/task.js +10 -0
  83. package/dist/map/adapter/extensions/task.js.map +1 -1
  84. package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
  85. package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
  86. package/dist/map/adapter/extensions/update-metadata.js +67 -0
  87. package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
  88. package/dist/map/adapter/index.d.ts +2 -1
  89. package/dist/map/adapter/index.d.ts.map +1 -1
  90. package/dist/map/adapter/index.js +8 -2
  91. package/dist/map/adapter/index.js.map +1 -1
  92. package/dist/map/adapter/interface.d.ts +2 -0
  93. package/dist/map/adapter/interface.d.ts.map +1 -1
  94. package/dist/map/adapter/map-adapter.d.ts +4 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +302 -30
  97. package/dist/map/adapter/map-adapter.js.map +1 -1
  98. package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
  99. package/dist/map/adapter/subscription-manager.js +5 -1
  100. package/dist/map/adapter/subscription-manager.js.map +1 -1
  101. package/dist/map/adapter/types.d.ts +2 -0
  102. package/dist/map/adapter/types.d.ts.map +1 -1
  103. package/dist/mcp/map-client.d.ts +39 -0
  104. package/dist/mcp/map-client.d.ts.map +1 -0
  105. package/dist/mcp/map-client.js +129 -0
  106. package/dist/mcp/map-client.js.map +1 -0
  107. package/dist/mcp/mcp-server.d.ts +14 -0
  108. package/dist/mcp/mcp-server.d.ts.map +1 -1
  109. package/dist/mcp/mcp-server.js +113 -85
  110. package/dist/mcp/mcp-server.js.map +1 -1
  111. package/dist/mcp/types.d.ts +9 -1
  112. package/dist/mcp/types.d.ts.map +1 -1
  113. package/dist/mcp/types.js.map +1 -1
  114. package/dist/metrics/metrics.js +1 -1
  115. package/dist/metrics/metrics.js.map +1 -1
  116. package/dist/roles/capabilities.d.ts +3 -1
  117. package/dist/roles/capabilities.d.ts.map +1 -1
  118. package/dist/roles/capabilities.js +17 -7
  119. package/dist/roles/capabilities.js.map +1 -1
  120. package/dist/roles/config-loader.d.ts +6 -6
  121. package/dist/roles/config-loader.d.ts.map +1 -1
  122. package/dist/roles/config-loader.js +6 -6
  123. package/dist/roles/config-loader.js.map +1 -1
  124. package/dist/roles/registry.d.ts +2 -2
  125. package/dist/roles/registry.js +2 -2
  126. package/dist/server/combined-server.d.ts +20 -0
  127. package/dist/server/combined-server.d.ts.map +1 -1
  128. package/dist/server/combined-server.js +107 -8
  129. package/dist/server/combined-server.js.map +1 -1
  130. package/dist/store/event-store.d.ts +7 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +91 -8
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +23 -0
  135. package/dist/store/types/agents.d.ts.map +1 -1
  136. package/dist/store/types/events.d.ts +1 -1
  137. package/dist/store/types/events.d.ts.map +1 -1
  138. package/dist/task/backend/index.d.ts +47 -29
  139. package/dist/task/backend/index.d.ts.map +1 -1
  140. package/dist/task/backend/index.js +109 -71
  141. package/dist/task/backend/index.js.map +1 -1
  142. package/dist/task/backend/memory.d.ts +1 -0
  143. package/dist/task/backend/memory.d.ts.map +1 -1
  144. package/dist/task/backend/memory.js +3 -0
  145. package/dist/task/backend/memory.js.map +1 -1
  146. package/dist/task/backend/opentasks/backend.d.ts +140 -0
  147. package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
  148. package/dist/task/backend/opentasks/backend.js +1023 -0
  149. package/dist/task/backend/opentasks/backend.js.map +1 -0
  150. package/dist/task/backend/opentasks/client.d.ts +337 -0
  151. package/dist/task/backend/opentasks/client.d.ts.map +1 -0
  152. package/dist/task/backend/opentasks/client.js +225 -0
  153. package/dist/task/backend/opentasks/client.js.map +1 -0
  154. package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
  155. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
  156. package/dist/task/backend/opentasks/daemon-manager.js +195 -0
  157. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
  158. package/dist/task/backend/opentasks/index.d.ts +21 -0
  159. package/dist/task/backend/opentasks/index.d.ts.map +1 -0
  160. package/dist/task/backend/opentasks/index.js +21 -0
  161. package/dist/task/backend/opentasks/index.js.map +1 -0
  162. package/dist/task/backend/opentasks/mapping.d.ts +48 -0
  163. package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
  164. package/dist/task/backend/opentasks/mapping.js +77 -0
  165. package/dist/task/backend/opentasks/mapping.js.map +1 -0
  166. package/dist/task/backend/types.d.ts +33 -53
  167. package/dist/task/backend/types.d.ts.map +1 -1
  168. package/dist/task/backend/types.js +7 -11
  169. package/dist/task/backend/types.js.map +1 -1
  170. package/dist/task/backend/unified-tool-provider.d.ts +57 -0
  171. package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
  172. package/dist/task/backend/unified-tool-provider.js +623 -0
  173. package/dist/task/backend/unified-tool-provider.js.map +1 -0
  174. package/dist/teams/team-loader.d.ts +2 -2
  175. package/dist/teams/team-loader.js +3 -3
  176. package/dist/teams/team-loader.js.map +1 -1
  177. package/dist/teams/team-runtime.d.ts.map +1 -1
  178. package/dist/teams/team-runtime.js +2 -0
  179. package/dist/teams/team-runtime.js.map +1 -1
  180. package/docs/architecture.md +7 -6
  181. package/docs/configuration.md +26 -62
  182. package/docs/implementation-details.md +5 -5
  183. package/docs/implementation-summary.md +17 -17
  184. package/docs/plan-self-driving-support.md +4 -4
  185. package/docs/spec-self-driving-support.md +10 -10
  186. package/docs/team-templates.md +2 -2
  187. package/docs/teams.md +3 -3
  188. package/docs/troubleshooting.md +10 -11
  189. package/package.json +6 -4
  190. package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
  191. package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
  192. package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
  193. package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
  194. package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
  195. package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
  196. package/src/acp/__tests__/history.test.ts +8 -4
  197. package/src/acp/__tests__/integration.test.ts +56 -31
  198. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  199. package/src/acp/macro-agent.ts +230 -62
  200. package/src/acp/types.ts +46 -1
  201. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  202. package/src/agent/agent-manager.ts +714 -261
  203. package/src/agent/types.ts +3 -1
  204. package/src/api/server.ts +41 -7
  205. package/src/auth/__tests__/token.test.ts +100 -0
  206. package/src/auth/index.ts +1 -0
  207. package/src/auth/token.ts +82 -0
  208. package/src/cli/__tests__/acp.test.ts +1 -1
  209. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  210. package/src/cli/acp.ts +130 -72
  211. package/src/cli/index.ts +120 -14
  212. package/src/cli/mcp.ts +311 -207
  213. package/src/cli/parse-args.ts +54 -0
  214. package/src/cli/stable-instance-id.ts +14 -0
  215. package/src/config/project-config.ts +190 -27
  216. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  217. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +820 -0
  218. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  219. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +724 -2
  220. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  221. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  222. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  223. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  224. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  225. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  226. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  227. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  228. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  229. package/src/map/adapter/acp-over-map.ts +777 -92
  230. package/src/map/adapter/connection-manager.ts +3 -0
  231. package/src/map/adapter/event-log.ts +208 -0
  232. package/src/map/adapter/event-translator.ts +6 -6
  233. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  234. package/src/map/adapter/extensions/index.ts +60 -0
  235. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  236. package/src/map/adapter/extensions/task.ts +11 -0
  237. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  238. package/src/map/adapter/index.ts +28 -0
  239. package/src/map/adapter/interface.ts +2 -0
  240. package/src/map/adapter/map-adapter.ts +373 -38
  241. package/src/map/adapter/subscription-manager.ts +5 -1
  242. package/src/map/adapter/types.ts +2 -0
  243. package/src/mcp/__tests__/map-client.test.ts +386 -0
  244. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  245. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  246. package/src/mcp/map-client.ts +177 -0
  247. package/src/mcp/mcp-server.ts +191 -100
  248. package/src/mcp/types.ts +6 -1
  249. package/src/metrics/metrics.ts +1 -1
  250. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  251. package/src/roles/__tests__/config-loader.test.ts +7 -7
  252. package/src/roles/capabilities.ts +17 -7
  253. package/src/roles/config-loader.ts +6 -6
  254. package/src/roles/registry.ts +2 -2
  255. package/src/server/__tests__/combined-server.test.ts +94 -21
  256. package/src/server/combined-server.ts +189 -33
  257. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  258. package/src/store/__tests__/event-store.test.ts +236 -1
  259. package/src/store/__tests__/instance.test.ts +3 -3
  260. package/src/store/event-store.ts +109 -8
  261. package/src/store/types/agents.ts +16 -0
  262. package/src/store/types/events.ts +1 -1
  263. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  264. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  265. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  266. package/src/task/backend/index.ts +156 -106
  267. package/src/task/backend/memory.ts +4 -0
  268. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  269. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  270. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  271. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  272. package/src/task/backend/opentasks/backend.ts +1323 -0
  273. package/src/task/backend/opentasks/client.ts +652 -0
  274. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  275. package/src/task/backend/opentasks/index.ts +69 -0
  276. package/src/task/backend/opentasks/mapping.ts +94 -0
  277. package/src/task/backend/types.ts +42 -66
  278. package/src/task/backend/unified-tool-provider.ts +779 -0
  279. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  280. package/src/teams/team-loader.ts +3 -3
  281. package/src/teams/team-runtime.ts +2 -0
  282. package/test_fixtures/README.md +2 -3
  283. package/test_fixtures/fixtures/index.ts +0 -3
  284. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  285. package/test_fixtures/fixtures/repos/index.ts +1 -3
  286. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  287. package/test_fixtures/fixtures/repos/types.ts +0 -11
  288. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  289. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  290. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  291. package/vitest.config.ts +1 -1
  292. package/vitest.e2e.config.ts +1 -1
  293. package/vitest.setup.ts +1 -30
  294. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  296. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  297. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  298. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  299. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  300. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  301. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  302. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  303. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  304. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  305. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  306. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  307. package/.macro-agent/teams/structured/team.yaml +0 -89
  308. package/docs/sudocode-integration.md +0 -383
  309. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  310. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  311. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  312. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  313. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  314. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  315. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  316. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  317. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  318. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  319. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  320. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  321. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  322. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  323. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  324. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  325. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  326. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  327. package/src/task/backend/sudocode/backend.ts +0 -1237
  328. package/src/task/backend/sudocode/client.ts +0 -515
  329. package/src/task/backend/sudocode/index.ts +0 -120
  330. package/src/task/backend/sudocode/mapping.ts +0 -93
  331. package/src/task/backend/sudocode/server-client.ts +0 -522
  332. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  333. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  334. package/src/task/backend/sudocode/tools.ts +0 -896
  335. package/src/task/backend/tool-provider.ts +0 -506
  336. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  337. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  338. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Tests for DaemonManager
3
+ *
4
+ * Covers:
5
+ * - connectProject() IPC location.register flow
6
+ * - connectProject() edge cases (missing dir, missing config, no hash, already connected)
7
+ * - getConnectedProjects() tracking
8
+ * - shutdown() cleanup
9
+ *
10
+ * Uses mocked filesystem and IPC client — no real daemon needed.
11
+ *
12
+ * @module task/backend/opentasks/__tests__/daemon-manager.test
13
+ */
14
+
15
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { DaemonManager } from "../daemon-manager.js";
19
+ import { IPCOpenTasksClient } from "../client.js";
20
+
21
+ // Mock opentasks to avoid real daemon operations
22
+ vi.mock("opentasks", () => ({
23
+ checkExistingDaemon: vi.fn(),
24
+ createDaemonWithStore: vi.fn(),
25
+ }));
26
+
27
+ // Mock fs for connectProject tests
28
+ vi.mock("node:fs", async () => {
29
+ const actual = await vi.importActual<typeof fs>("node:fs");
30
+ return {
31
+ ...actual,
32
+ existsSync: vi.fn(),
33
+ readFileSync: vi.fn(),
34
+ mkdirSync: vi.fn(),
35
+ };
36
+ });
37
+
38
+ // =============================================================================
39
+ // Tests
40
+ // =============================================================================
41
+
42
+ describe("DaemonManager", () => {
43
+ let manager: DaemonManager;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ afterEach(async () => {
50
+ if (manager) {
51
+ await manager.shutdown();
52
+ }
53
+ });
54
+
55
+ // ─────────────────────────────────────────────────────────────────
56
+ // connectProject()
57
+ // ─────────────────────────────────────────────────────────────────
58
+
59
+ describe("connectProject()", () => {
60
+ let mockClient: IPCOpenTasksClient;
61
+
62
+ beforeEach(() => {
63
+ manager = new DaemonManager({ connectOnSpawn: true });
64
+
65
+ // Create a mock IPCOpenTasksClient with a call method
66
+ mockClient = {
67
+ call: vi.fn().mockResolvedValue({ success: true }),
68
+ isConnected: vi.fn().mockReturnValue(true),
69
+ connect: vi.fn().mockResolvedValue(undefined),
70
+ disconnect: vi.fn(),
71
+ } as unknown as IPCOpenTasksClient;
72
+
73
+ // Inject the mock client via private field access
74
+ (manager as any).client = mockClient;
75
+ });
76
+
77
+ it("should register project location with daemon via IPC", async () => {
78
+ const projectPath = "/projects/my-app";
79
+ const opentasksDir = path.join(projectPath, ".opentasks");
80
+
81
+ vi.mocked(fs.existsSync).mockReturnValue(true);
82
+ vi.mocked(fs.readFileSync).mockReturnValue(
83
+ JSON.stringify({
84
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
85
+ })
86
+ );
87
+
88
+ await manager.connectProject(projectPath);
89
+
90
+ expect(mockClient.call).toHaveBeenCalledWith("location.register", {
91
+ hash: "abc123",
92
+ opentasksPath: opentasksDir,
93
+ });
94
+ expect(manager.getConnectedProjects()).toContain(opentasksDir);
95
+ });
96
+
97
+ it("should skip if connectOnSpawn is false", async () => {
98
+ manager = new DaemonManager({ connectOnSpawn: false });
99
+ (manager as any).client = mockClient;
100
+
101
+ await manager.connectProject("/projects/my-app");
102
+
103
+ expect(mockClient.call).not.toHaveBeenCalled();
104
+ expect(manager.getConnectedProjects()).toHaveLength(0);
105
+ });
106
+
107
+ it("should skip if client is not set", async () => {
108
+ (manager as any).client = null;
109
+
110
+ await manager.connectProject("/projects/my-app");
111
+
112
+ expect(manager.getConnectedProjects()).toHaveLength(0);
113
+ });
114
+
115
+ it("should skip if .opentasks/ directory does not exist", async () => {
116
+ vi.mocked(fs.existsSync).mockReturnValue(false);
117
+
118
+ await manager.connectProject("/projects/my-app");
119
+
120
+ expect(mockClient.call).not.toHaveBeenCalled();
121
+ expect(manager.getConnectedProjects()).toHaveLength(0);
122
+ });
123
+
124
+ it("should skip if config.json has no location hash", async () => {
125
+ vi.mocked(fs.existsSync).mockReturnValue(true);
126
+ vi.mocked(fs.readFileSync).mockReturnValue(
127
+ JSON.stringify({ location: { uuid: "uuid-1" } }) // no hash
128
+ );
129
+
130
+ await manager.connectProject("/projects/my-app");
131
+
132
+ expect(mockClient.call).not.toHaveBeenCalled();
133
+ expect(manager.getConnectedProjects()).toHaveLength(0);
134
+ });
135
+
136
+ it("should skip if project is already connected", async () => {
137
+ const projectPath = "/projects/my-app";
138
+ const opentasksDir = path.join(projectPath, ".opentasks");
139
+
140
+ vi.mocked(fs.existsSync).mockReturnValue(true);
141
+ vi.mocked(fs.readFileSync).mockReturnValue(
142
+ JSON.stringify({
143
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
144
+ })
145
+ );
146
+
147
+ // Connect once
148
+ await manager.connectProject(projectPath);
149
+ expect(mockClient.call).toHaveBeenCalledTimes(1);
150
+
151
+ // Try to connect again — should be idempotent
152
+ await manager.connectProject(projectPath);
153
+ expect(mockClient.call).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ it("should handle IPC errors gracefully (non-fatal)", async () => {
157
+ vi.mocked(fs.existsSync).mockReturnValue(true);
158
+ vi.mocked(fs.readFileSync).mockReturnValue(
159
+ JSON.stringify({
160
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
161
+ })
162
+ );
163
+ (mockClient as any).call.mockRejectedValue(new Error("IPC failed"));
164
+
165
+ // Should not throw
166
+ await manager.connectProject("/projects/my-app");
167
+
168
+ // Should not be tracked as connected since the IPC call failed
169
+ expect(manager.getConnectedProjects()).toHaveLength(0);
170
+ });
171
+
172
+ it("should handle malformed config.json gracefully", async () => {
173
+ vi.mocked(fs.existsSync).mockReturnValue(true);
174
+ vi.mocked(fs.readFileSync).mockReturnValue("not valid json");
175
+
176
+ // Should not throw
177
+ await manager.connectProject("/projects/my-app");
178
+
179
+ expect(mockClient.call).not.toHaveBeenCalled();
180
+ expect(manager.getConnectedProjects()).toHaveLength(0);
181
+ });
182
+
183
+ it("should not call IPC if client is not an IPCOpenTasksClient", async () => {
184
+ // Replace with a plain mock that isn't an instance of IPCOpenTasksClient
185
+ const plainClient = {
186
+ isConnected: vi.fn().mockReturnValue(true),
187
+ connect: vi.fn().mockResolvedValue(undefined),
188
+ disconnect: vi.fn(),
189
+ } as any;
190
+ (manager as any).client = plainClient;
191
+
192
+ vi.mocked(fs.existsSync).mockReturnValue(true);
193
+ vi.mocked(fs.readFileSync).mockReturnValue(
194
+ JSON.stringify({
195
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
196
+ })
197
+ );
198
+
199
+ await manager.connectProject("/projects/my-app");
200
+
201
+ // Should still track as connected (IPC call is skipped, not failed)
202
+ expect(manager.getConnectedProjects()).toContain(
203
+ path.join("/projects/my-app", ".opentasks")
204
+ );
205
+ });
206
+ });
207
+
208
+ // ─────────────────────────────────────────────────────────────────
209
+ // isProjectConnected()
210
+ // ─────────────────────────────────────────────────────────────────
211
+
212
+ describe("isProjectConnected()", () => {
213
+ it("should return false for unconnected project", () => {
214
+ manager = new DaemonManager();
215
+ expect(manager.isProjectConnected("/projects/my-app")).toBe(false);
216
+ });
217
+
218
+ it("should return true after connecting", async () => {
219
+ manager = new DaemonManager({ connectOnSpawn: true });
220
+ const mockClient = {
221
+ call: vi.fn().mockResolvedValue({ success: true }),
222
+ isConnected: vi.fn().mockReturnValue(true),
223
+ connect: vi.fn(),
224
+ disconnect: vi.fn(),
225
+ } as unknown as IPCOpenTasksClient;
226
+ (manager as any).client = mockClient;
227
+
228
+ vi.mocked(fs.existsSync).mockReturnValue(true);
229
+ vi.mocked(fs.readFileSync).mockReturnValue(
230
+ JSON.stringify({
231
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
232
+ })
233
+ );
234
+
235
+ await manager.connectProject("/projects/my-app");
236
+ expect(manager.isProjectConnected("/projects/my-app")).toBe(true);
237
+ });
238
+ });
239
+
240
+ // ─────────────────────────────────────────────────────────────────
241
+ // shutdown()
242
+ // ─────────────────────────────────────────────────────────────────
243
+
244
+ describe("shutdown()", () => {
245
+ it("should clear connected projects on shutdown", async () => {
246
+ manager = new DaemonManager({ connectOnSpawn: true });
247
+ const mockClient = {
248
+ call: vi.fn().mockResolvedValue({ success: true }),
249
+ isConnected: vi.fn().mockReturnValue(true),
250
+ connect: vi.fn(),
251
+ disconnect: vi.fn(),
252
+ } as unknown as IPCOpenTasksClient;
253
+ (manager as any).client = mockClient;
254
+
255
+ vi.mocked(fs.existsSync).mockReturnValue(true);
256
+ vi.mocked(fs.readFileSync).mockReturnValue(
257
+ JSON.stringify({
258
+ location: { hash: "abc123", uuid: "uuid-1", name: "my-app" },
259
+ })
260
+ );
261
+
262
+ await manager.connectProject("/projects/my-app");
263
+ expect(manager.getConnectedProjects()).toHaveLength(1);
264
+
265
+ await manager.shutdown();
266
+ expect(manager.getConnectedProjects()).toHaveLength(0);
267
+ });
268
+
269
+ it("should disconnect client on shutdown", async () => {
270
+ manager = new DaemonManager();
271
+ const mockClient = {
272
+ disconnect: vi.fn(),
273
+ isConnected: vi.fn().mockReturnValue(true),
274
+ } as any;
275
+ (manager as any).client = mockClient;
276
+
277
+ await manager.shutdown();
278
+
279
+ expect(mockClient.disconnect).toHaveBeenCalled();
280
+ });
281
+
282
+ it("should stop daemon only if ownsDaemon is true", async () => {
283
+ manager = new DaemonManager();
284
+ const mockDaemon = { stop: vi.fn().mockResolvedValue(undefined) };
285
+ const mockClient = { disconnect: vi.fn() } as any;
286
+
287
+ (manager as any).client = mockClient;
288
+ (manager as any).daemon = mockDaemon;
289
+ (manager as any).ownsDaemon = true;
290
+
291
+ await manager.shutdown();
292
+
293
+ expect(mockDaemon.stop).toHaveBeenCalled();
294
+ });
295
+
296
+ it("should NOT stop daemon if ownsDaemon is false", async () => {
297
+ manager = new DaemonManager();
298
+ const mockDaemon = { stop: vi.fn().mockResolvedValue(undefined) };
299
+ const mockClient = { disconnect: vi.fn() } as any;
300
+
301
+ (manager as any).client = mockClient;
302
+ (manager as any).daemon = mockDaemon;
303
+ (manager as any).ownsDaemon = false;
304
+
305
+ await manager.shutdown();
306
+
307
+ expect(mockDaemon.stop).not.toHaveBeenCalled();
308
+ });
309
+
310
+ it("should delay daemon.stop() by drain grace period when ownsDaemon", async () => {
311
+ manager = new DaemonManager();
312
+ const stopTimes: number[] = [];
313
+ const mockDaemon = {
314
+ stop: vi.fn().mockImplementation(async () => {
315
+ stopTimes.push(Date.now());
316
+ }),
317
+ };
318
+ const mockClient = { disconnect: vi.fn() } as any;
319
+
320
+ (manager as any).client = mockClient;
321
+ (manager as any).daemon = mockDaemon;
322
+ (manager as any).ownsDaemon = true;
323
+
324
+ const startTime = Date.now();
325
+ await manager.shutdown();
326
+
327
+ expect(mockDaemon.stop).toHaveBeenCalled();
328
+ // Drain delay should be at least ~450ms (500ms target with timing tolerance)
329
+ const elapsed = stopTimes[0] - startTime;
330
+ expect(elapsed).toBeGreaterThanOrEqual(450);
331
+ });
332
+ });
333
+
334
+ // ─────────────────────────────────────────────────────────────────
335
+ // ensureDaemon()
336
+ // ─────────────────────────────────────────────────────────────────
337
+
338
+ describe("ensureDaemon()", () => {
339
+ it("should return socketPath when connecting to existing daemon", async () => {
340
+ const { checkExistingDaemon } = await import("opentasks");
341
+
342
+ vi.mocked(checkExistingDaemon).mockResolvedValue({
343
+ running: true,
344
+ socketPath: "/tmp/existing.sock",
345
+ pid: 1234,
346
+ });
347
+
348
+ // Mock IPCOpenTasksClient constructor and connect
349
+ const connectSpy = vi.fn().mockResolvedValue(undefined);
350
+ vi.spyOn(
351
+ await import("../client.js"),
352
+ "IPCOpenTasksClient"
353
+ ).mockImplementation(
354
+ () =>
355
+ ({
356
+ connect: connectSpy,
357
+ disconnect: vi.fn(),
358
+ isConnected: vi.fn().mockReturnValue(true),
359
+ }) as any
360
+ );
361
+
362
+ manager = new DaemonManager({ centralPath: "/tmp/central" });
363
+ const result = await manager.ensureDaemon();
364
+
365
+ expect(result.socketPath).toBe("/tmp/existing.sock");
366
+ expect(result.ownsDaemon).toBe(false);
367
+ expect(connectSpy).toHaveBeenCalled();
368
+ });
369
+
370
+ it("should return socketPath when starting new daemon", async () => {
371
+ const { checkExistingDaemon, createDaemonWithStore } =
372
+ await import("opentasks");
373
+
374
+ vi.mocked(checkExistingDaemon).mockResolvedValue({
375
+ running: false,
376
+ });
377
+
378
+ const mockDaemon = {
379
+ start: vi.fn().mockResolvedValue(undefined),
380
+ stop: vi.fn().mockResolvedValue(undefined),
381
+ socketPath: "/tmp/new-daemon.sock",
382
+ };
383
+ vi.mocked(createDaemonWithStore).mockResolvedValue(mockDaemon as any);
384
+
385
+ const connectSpy = vi.fn().mockResolvedValue(undefined);
386
+ vi.spyOn(
387
+ await import("../client.js"),
388
+ "IPCOpenTasksClient"
389
+ ).mockImplementation(
390
+ () =>
391
+ ({
392
+ connect: connectSpy,
393
+ disconnect: vi.fn(),
394
+ isConnected: vi.fn().mockReturnValue(true),
395
+ }) as any
396
+ );
397
+
398
+ manager = new DaemonManager({ centralPath: "/tmp/central" });
399
+ const result = await manager.ensureDaemon();
400
+
401
+ expect(result.socketPath).toBe("/tmp/new-daemon.sock");
402
+ expect(result.ownsDaemon).toBe(true);
403
+ expect(mockDaemon.start).toHaveBeenCalled();
404
+ });
405
+ });
406
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Tests for OpenTasks status mapping utilities
3
+ *
4
+ * @module task/backend/opentasks/__tests__/mapping.test
5
+ */
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import {
9
+ mapOpenTasksStatus,
10
+ mapTaskStatus,
11
+ isIssueComplete,
12
+ isIssueBlocked,
13
+ } from "../mapping.js";
14
+
15
+ describe("OpenTasks Mapping", () => {
16
+ describe("mapOpenTasksStatus", () => {
17
+ it("should map 'open' to 'pending'", () => {
18
+ expect(mapOpenTasksStatus("open")).toBe("pending");
19
+ });
20
+
21
+ it("should map 'in_progress' to 'in_progress'", () => {
22
+ expect(mapOpenTasksStatus("in_progress")).toBe("in_progress");
23
+ });
24
+
25
+ it("should map 'blocked' to 'pending'", () => {
26
+ expect(mapOpenTasksStatus("blocked")).toBe("pending");
27
+ });
28
+
29
+ it("should map 'closed' to 'completed'", () => {
30
+ expect(mapOpenTasksStatus("closed")).toBe("completed");
31
+ });
32
+
33
+ it("should map unknown statuses to 'pending'", () => {
34
+ expect(mapOpenTasksStatus("unknown")).toBe("pending");
35
+ expect(mapOpenTasksStatus("")).toBe("pending");
36
+ });
37
+ });
38
+
39
+ describe("mapTaskStatus", () => {
40
+ it("should map 'pending' to 'open'", () => {
41
+ expect(mapTaskStatus("pending")).toBe("open");
42
+ });
43
+
44
+ it("should map 'assigned' to 'open'", () => {
45
+ expect(mapTaskStatus("assigned")).toBe("open");
46
+ });
47
+
48
+ it("should map 'in_progress' to 'in_progress'", () => {
49
+ expect(mapTaskStatus("in_progress")).toBe("in_progress");
50
+ });
51
+
52
+ it("should map 'completed' to 'closed'", () => {
53
+ expect(mapTaskStatus("completed")).toBe("closed");
54
+ });
55
+
56
+ it("should map 'failed' to 'closed'", () => {
57
+ expect(mapTaskStatus("failed")).toBe("closed");
58
+ });
59
+ });
60
+
61
+ describe("isIssueComplete", () => {
62
+ it("should return true for 'closed'", () => {
63
+ expect(isIssueComplete("closed")).toBe(true);
64
+ });
65
+
66
+ it("should return false for other statuses", () => {
67
+ expect(isIssueComplete("open")).toBe(false);
68
+ expect(isIssueComplete("in_progress")).toBe(false);
69
+ expect(isIssueComplete("blocked")).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("isIssueBlocked", () => {
74
+ it("should return true for 'blocked'", () => {
75
+ expect(isIssueBlocked("blocked")).toBe(true);
76
+ });
77
+
78
+ it("should return false for other statuses", () => {
79
+ expect(isIssueBlocked("open")).toBe(false);
80
+ expect(isIssueBlocked("in_progress")).toBe(false);
81
+ expect(isIssueBlocked("closed")).toBe(false);
82
+ });
83
+ });
84
+ });