macro-agent 0.1.0 → 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 (337) 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 +15 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +131 -35
  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 +17 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +384 -23
  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 +3 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +258 -35
  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 +2 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +69 -20
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +18 -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__/integration.test.ts +56 -31
  197. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  198. package/src/acp/macro-agent.ts +170 -36
  199. package/src/acp/types.ts +46 -1
  200. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  201. package/src/agent/agent-manager.ts +714 -261
  202. package/src/agent/types.ts +3 -1
  203. package/src/api/server.ts +41 -7
  204. package/src/auth/__tests__/token.test.ts +100 -0
  205. package/src/auth/index.ts +1 -0
  206. package/src/auth/token.ts +82 -0
  207. package/src/cli/__tests__/acp.test.ts +1 -1
  208. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  209. package/src/cli/acp.ts +130 -72
  210. package/src/cli/index.ts +120 -14
  211. package/src/cli/mcp.ts +311 -207
  212. package/src/cli/parse-args.ts +54 -0
  213. package/src/cli/stable-instance-id.ts +14 -0
  214. package/src/config/project-config.ts +190 -27
  215. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  216. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
  217. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  218. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
  219. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  220. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  221. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  222. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  223. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  224. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  225. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  226. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  227. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  228. package/src/map/adapter/acp-over-map.ts +678 -66
  229. package/src/map/adapter/connection-manager.ts +3 -0
  230. package/src/map/adapter/event-log.ts +208 -0
  231. package/src/map/adapter/event-translator.ts +6 -6
  232. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  233. package/src/map/adapter/extensions/index.ts +60 -0
  234. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  235. package/src/map/adapter/extensions/task.ts +11 -0
  236. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  237. package/src/map/adapter/index.ts +28 -0
  238. package/src/map/adapter/interface.ts +2 -0
  239. package/src/map/adapter/map-adapter.ts +312 -47
  240. package/src/map/adapter/subscription-manager.ts +5 -1
  241. package/src/map/adapter/types.ts +2 -0
  242. package/src/mcp/__tests__/map-client.test.ts +386 -0
  243. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  244. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  245. package/src/mcp/map-client.ts +177 -0
  246. package/src/mcp/mcp-server.ts +191 -100
  247. package/src/mcp/types.ts +6 -1
  248. package/src/metrics/metrics.ts +1 -1
  249. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  250. package/src/roles/__tests__/config-loader.test.ts +7 -7
  251. package/src/roles/capabilities.ts +17 -7
  252. package/src/roles/config-loader.ts +6 -6
  253. package/src/roles/registry.ts +2 -2
  254. package/src/server/__tests__/combined-server.test.ts +94 -21
  255. package/src/server/combined-server.ts +189 -33
  256. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  257. package/src/store/__tests__/event-store.test.ts +196 -1
  258. package/src/store/__tests__/instance.test.ts +3 -3
  259. package/src/store/event-store.ts +80 -21
  260. package/src/store/types/agents.ts +15 -0
  261. package/src/store/types/events.ts +1 -1
  262. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  263. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  264. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  265. package/src/task/backend/index.ts +156 -106
  266. package/src/task/backend/memory.ts +4 -0
  267. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  268. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  269. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  270. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  271. package/src/task/backend/opentasks/backend.ts +1323 -0
  272. package/src/task/backend/opentasks/client.ts +652 -0
  273. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  274. package/src/task/backend/opentasks/index.ts +69 -0
  275. package/src/task/backend/opentasks/mapping.ts +94 -0
  276. package/src/task/backend/types.ts +42 -66
  277. package/src/task/backend/unified-tool-provider.ts +779 -0
  278. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  279. package/src/teams/team-loader.ts +3 -3
  280. package/src/teams/team-runtime.ts +2 -0
  281. package/test_fixtures/README.md +2 -3
  282. package/test_fixtures/fixtures/index.ts +0 -3
  283. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  284. package/test_fixtures/fixtures/repos/index.ts +1 -3
  285. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  286. package/test_fixtures/fixtures/repos/types.ts +0 -11
  287. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  288. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  289. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  290. package/vitest.config.ts +1 -1
  291. package/vitest.e2e.config.ts +1 -1
  292. package/vitest.setup.ts +1 -30
  293. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  294. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  296. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  297. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  298. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  299. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  300. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  301. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  302. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  303. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  304. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  305. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  306. package/.macro-agent/teams/structured/team.yaml +0 -89
  307. package/docs/sudocode-integration.md +0 -383
  308. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  309. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  310. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  311. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  312. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  313. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  314. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  315. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  316. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  317. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  318. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  319. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  320. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  321. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  322. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  323. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  324. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  325. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  326. package/src/task/backend/sudocode/backend.ts +0 -1237
  327. package/src/task/backend/sudocode/client.ts +0 -515
  328. package/src/task/backend/sudocode/index.ts +0 -120
  329. package/src/task/backend/sudocode/mapping.ts +0 -93
  330. package/src/task/backend/sudocode/server-client.ts +0 -522
  331. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  332. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  333. package/src/task/backend/sudocode/tools.ts +0 -896
  334. package/src/task/backend/tool-provider.ts +0 -506
  335. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  336. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  337. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,1187 @@
1
+ /**
2
+ * Unit tests for MCP Bridge Extensions (_macro/mcp/*)
3
+ *
4
+ * Tests the 17 server-side bridge handlers with mock adapter + mock services.
5
+ * Follows the pattern from extensions.test.ts.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, vi } from "vitest";
9
+ import {
10
+ registerMCPBridgeExtensions,
11
+ unregisterMCPBridgeExtensions,
12
+ MCP_BRIDGE_METHODS,
13
+ type MCPBridgeServices,
14
+ } from "../extensions/mcp-bridge.js";
15
+ import type { MAPAdapter, ExtensionHandler, ExtensionContext } from "../interface.js";
16
+ import type { ParticipantCapabilities } from "../types.js";
17
+ import type { EventStore } from "../../../store/event-store.js";
18
+ import type { AgentManager } from "../../../agent/agent-manager.js";
19
+ import type { TaskManager } from "../../../task/task-manager.js";
20
+ import type { MessageRouter } from "../../../router/message-router.js";
21
+ import type { PeerManager } from "../../../peer/peer-manager.js";
22
+ import type { ActivityWatcher } from "../../../activity/watcher.js";
23
+ import type { TaskBackend } from "../../../task/backend/types.js";
24
+
25
+ // =============================================================================
26
+ // Mock Setup
27
+ // =============================================================================
28
+
29
+ function createMockAdapter(): MAPAdapter & {
30
+ handlers: Map<string, ExtensionHandler>;
31
+ } {
32
+ const handlers = new Map<string, ExtensionHandler>();
33
+
34
+ return {
35
+ handlers,
36
+ registerExtension: vi.fn((method: string, handler: ExtensionHandler) => {
37
+ handlers.set(method, handler);
38
+ }),
39
+ unregisterExtension: vi.fn((method: string) => {
40
+ handlers.delete(method);
41
+ }),
42
+ hasExtension: vi.fn((method: string) => handlers.has(method)),
43
+ getExtensions: vi.fn(() => Array.from(handlers.keys())),
44
+ start: vi.fn(),
45
+ stop: vi.fn(),
46
+ isRunning: vi.fn().mockReturnValue(true),
47
+ acceptConnection: vi.fn(),
48
+ disconnectParticipant: vi.fn(),
49
+ getParticipant: vi.fn(),
50
+ getParticipants: vi.fn().mockReturnValue([]),
51
+ createSubscription: vi.fn(),
52
+ removeSubscription: vi.fn(),
53
+ pauseSubscription: vi.fn(),
54
+ resumeSubscription: vi.fn(),
55
+ getSubscriptions: vi.fn().mockReturnValue([]),
56
+ emitEvent: vi.fn(),
57
+ listAgents: vi.fn().mockReturnValue([]),
58
+ getAgent: vi.fn(),
59
+ listScopes: vi.fn().mockReturnValue([]),
60
+ getScope: vi.fn(),
61
+ sendMessage: vi.fn(),
62
+ onEvent: vi.fn().mockReturnValue(() => {}),
63
+ config: { name: "test", version: "1.0.0" },
64
+ } as unknown as MAPAdapter & { handlers: Map<string, ExtensionHandler> };
65
+ }
66
+
67
+ function createMockContext(): ExtensionContext {
68
+ return {
69
+ participantId: "p-test" as any,
70
+ capabilities: {
71
+ canQuery: true,
72
+ canSubscribe: true,
73
+ canMessage: true,
74
+ canManageTasks: true,
75
+ } as ParticipantCapabilities,
76
+ sessionId: "s-test",
77
+ };
78
+ }
79
+
80
+ function createMockEventStore(): EventStore {
81
+ return {
82
+ emit: vi.fn((input) => ({
83
+ id: `evt_${Date.now()}`,
84
+ version: 1,
85
+ timestamp: Date.now(),
86
+ ...input,
87
+ })),
88
+ query: vi.fn(() => []),
89
+ getAgent: vi.fn(() => null),
90
+ listAgents: vi.fn(() => []),
91
+ getTask: vi.fn(() => null),
92
+ listTasks: vi.fn(() => []),
93
+ getMessages: vi.fn(() => []),
94
+ getFullMessage: vi.fn(() => null),
95
+ addSubscription: vi.fn(),
96
+ removeSubscription: vi.fn(),
97
+ getSubscriptions: vi.fn(() => []),
98
+ getSubscribers: vi.fn(() => []),
99
+ onAgentChange: vi.fn(() => () => {}),
100
+ onTaskChange: vi.fn(() => () => {}),
101
+ onMessageChange: vi.fn(() => () => {}),
102
+ persist: vi.fn(async () => {}),
103
+ close: vi.fn(async () => {}),
104
+ updateAgentMetadata: vi.fn(),
105
+ } as unknown as EventStore;
106
+ }
107
+
108
+ function createMockAgentManager(): AgentManager {
109
+ return {
110
+ spawn: vi.fn(async (options: any) => ({
111
+ id: "agent_spawned",
112
+ session_id: "sess_spawned",
113
+ agent: {
114
+ id: "agent_spawned",
115
+ session_id: "sess_spawned",
116
+ task: options.task,
117
+ task_id: "task_spawned",
118
+ state: "running",
119
+ parent: options.parent,
120
+ lineage: [],
121
+ config: {},
122
+ cwd: options.cwd ?? "/test",
123
+ created_at: Date.now(),
124
+ },
125
+ session: {} as any,
126
+ })),
127
+ terminate: vi.fn(async () => {}),
128
+ resume: vi.fn(async () => ({} as any)),
129
+ get: vi.fn(() => null),
130
+ list: vi.fn(() => []),
131
+ getChildren: vi.fn(() => []),
132
+ getHierarchy: vi.fn(() => null),
133
+ getOrCreateHeadManager: vi.fn(async () => ({} as any)),
134
+ listHeadManagers: vi.fn(() => []),
135
+ prompt: vi.fn(async function* () {}),
136
+ getSession: vi.fn(() => null),
137
+ hasActiveSession: vi.fn(() => false),
138
+ onLifecycleEvent: vi.fn(() => () => {}),
139
+ close: vi.fn(async () => {}),
140
+ } as unknown as AgentManager;
141
+ }
142
+
143
+ function createMockTaskManager(): TaskManager {
144
+ return {
145
+ create: vi.fn(() => ({
146
+ id: "task_created",
147
+ description: "test",
148
+ status: "pending",
149
+ created_at: Date.now(),
150
+ created_by: "agent_test",
151
+ })),
152
+ get: vi.fn(() => null),
153
+ list: vi.fn(() => []),
154
+ getSubtasks: vi.fn(() => []),
155
+ getSubtaskStatus: vi.fn(() => ({ total: 0, pending: 0, completed: 0, failed: 0 })),
156
+ updateStatus: vi.fn(),
157
+ assign: vi.fn(),
158
+ } as unknown as TaskManager;
159
+ }
160
+
161
+ function createMockMessageRouter(): MessageRouter {
162
+ return {
163
+ sendToAddress: vi.fn(async () => ({
164
+ id: "msg_123",
165
+ delivered: [{ subscriber: "agent_target" }],
166
+ })),
167
+ getMessages: vi.fn(() => []),
168
+ } as unknown as MessageRouter;
169
+ }
170
+
171
+ function createMockPeerManager(): PeerManager {
172
+ return {
173
+ hasTransport: vi.fn(() => true),
174
+ sendMessage: vi.fn(async () => {}),
175
+ sendRequest: vi.fn(async () => ({ result: "ok" })),
176
+ respondToRequest: vi.fn(),
177
+ getPeerMessages: vi.fn(() => []),
178
+ } as unknown as PeerManager;
179
+ }
180
+
181
+ function createMockActivityWatcher(): ActivityWatcher {
182
+ return {
183
+ isRunning: vi.fn(() => true),
184
+ start: vi.fn(),
185
+ stop: vi.fn(),
186
+ processActivity: vi.fn(),
187
+ subscribeAgent: vi.fn(),
188
+ unsubscribeAgent: vi.fn(),
189
+ } as unknown as ActivityWatcher;
190
+ }
191
+
192
+ function createMockTaskBackend(): TaskBackend {
193
+ return {
194
+ list: vi.fn(async () => []),
195
+ get: vi.fn(async () => null),
196
+ create: vi.fn(async () => ({})),
197
+ update: vi.fn(async () => ({})),
198
+ claim: vi.fn(async () => ({})),
199
+ unclaim: vi.fn(async () => {}),
200
+ } as unknown as TaskBackend;
201
+ }
202
+
203
+ /**
204
+ * Valid agent context included in params.context.
205
+ */
206
+ function validContext() {
207
+ return {
208
+ agent_id: "agent_caller",
209
+ session_id: "sess_caller",
210
+ task_id: "task_caller",
211
+ lineage: ["agent_root"],
212
+ cwd: "/test/cwd",
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Create params with valid context + additional args.
218
+ */
219
+ function withContext(args: Record<string, unknown> = {}) {
220
+ return { ...args, context: validContext() };
221
+ }
222
+
223
+ // =============================================================================
224
+ // Tests
225
+ // =============================================================================
226
+
227
+ describe("MCP Bridge Extensions", () => {
228
+ let adapter: MAPAdapter & { handlers: Map<string, ExtensionHandler> };
229
+ let services: MCPBridgeServices;
230
+ let ctx: ExtensionContext;
231
+
232
+ beforeEach(() => {
233
+ adapter = createMockAdapter();
234
+ services = {
235
+ eventStore: createMockEventStore(),
236
+ agentManager: createMockAgentManager(),
237
+ taskManager: createMockTaskManager(),
238
+ messageRouter: createMockMessageRouter(),
239
+ };
240
+ ctx = createMockContext();
241
+ });
242
+
243
+ // ─────────────────────────────────────────────────────────────────
244
+ // Registration
245
+ // ─────────────────────────────────────────────────────────────────
246
+
247
+ describe("registration", () => {
248
+ it("registers 11 core methods without optional services", () => {
249
+ registerMCPBridgeExtensions(adapter, services);
250
+ expect(adapter.handlers.size).toBe(11);
251
+ expect(adapter.handlers.has("_macro/mcp/spawn_agent")).toBe(true);
252
+ expect(adapter.handlers.has("_macro/mcp/emit_status")).toBe(true);
253
+ expect(adapter.handlers.has("_macro/mcp/send_message")).toBe(true);
254
+ expect(adapter.handlers.has("_macro/mcp/check_messages")).toBe(true);
255
+ expect(adapter.handlers.has("_macro/mcp/query_index")).toBe(true);
256
+ expect(adapter.handlers.has("_macro/mcp/get_hierarchy")).toBe(true);
257
+ expect(adapter.handlers.has("_macro/mcp/get_agent_summary")).toBe(true);
258
+ expect(adapter.handlers.has("_macro/mcp/stop_agent")).toBe(true);
259
+ expect(adapter.handlers.has("_macro/mcp/done")).toBe(true);
260
+ expect(adapter.handlers.has("_macro/mcp/inject_context")).toBe(true);
261
+ expect(adapter.handlers.has("_macro/mcp/task_tools_list")).toBe(true);
262
+ });
263
+
264
+ it("registers wait_for_activity when activityWatcher is provided", () => {
265
+ services.activityWatcher = createMockActivityWatcher();
266
+ registerMCPBridgeExtensions(adapter, services);
267
+ expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(true);
268
+ });
269
+
270
+ it("does not register wait_for_activity when activityWatcher is absent", () => {
271
+ registerMCPBridgeExtensions(adapter, services);
272
+ expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(false);
273
+ });
274
+
275
+ it("registers task backend methods when taskBackend is provided", () => {
276
+ services.taskBackend = createMockTaskBackend();
277
+ registerMCPBridgeExtensions(adapter, services);
278
+ expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(true);
279
+ expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(true);
280
+ expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(true);
281
+ });
282
+
283
+ it("does not register task backend methods when taskBackend is absent", () => {
284
+ registerMCPBridgeExtensions(adapter, services);
285
+ expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(false);
286
+ expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(false);
287
+ expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(false);
288
+ });
289
+
290
+ it("registers peer methods when peerManager is provided", () => {
291
+ services.peerManager = createMockPeerManager();
292
+ registerMCPBridgeExtensions(adapter, services);
293
+ expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(true);
294
+ expect(adapter.handlers.has("_macro/mcp/send_peer_request")).toBe(true);
295
+ expect(adapter.handlers.has("_macro/mcp/respond_to_peer_request")).toBe(true);
296
+ });
297
+
298
+ it("does not register peer methods when peerManager is absent", () => {
299
+ registerMCPBridgeExtensions(adapter, services);
300
+ expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(false);
301
+ });
302
+
303
+ it("registers all 18 methods with all optional services", () => {
304
+ services.activityWatcher = createMockActivityWatcher();
305
+ services.taskBackend = createMockTaskBackend();
306
+ services.peerManager = createMockPeerManager();
307
+ registerMCPBridgeExtensions(adapter, services);
308
+ expect(adapter.handlers.size).toBe(18);
309
+ });
310
+
311
+ it("unregisters all methods via unregisterMCPBridgeExtensions", () => {
312
+ services.activityWatcher = createMockActivityWatcher();
313
+ services.taskBackend = createMockTaskBackend();
314
+ services.peerManager = createMockPeerManager();
315
+ registerMCPBridgeExtensions(adapter, services);
316
+ expect(adapter.handlers.size).toBe(18);
317
+
318
+ unregisterMCPBridgeExtensions(adapter);
319
+ // Dynamic task tool methods (registered per-tool, not in MCP_BRIDGE_METHODS)
320
+ // are not cleaned up by unregisterMCPBridgeExtensions since their names are
321
+ // dynamic. Only the static MCP_BRIDGE_METHODS entries are removed.
322
+ expect(adapter.handlers.size).toBe(0);
323
+ });
324
+
325
+ it("MCP_BRIDGE_METHODS lists all 18 static methods", () => {
326
+ expect(MCP_BRIDGE_METHODS).toHaveLength(18);
327
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/spawn_agent");
328
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/done");
329
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/wait_for_activity");
330
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/claim_task");
331
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/send_peer_message");
332
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/respond_to_peer_request");
333
+ expect(MCP_BRIDGE_METHODS).toContain("_macro/mcp/task_tools_list");
334
+ });
335
+ });
336
+
337
+ // ─────────────────────────────────────────────────────────────────
338
+ // Context extraction
339
+ // ─────────────────────────────────────────────────────────────────
340
+
341
+ describe("context extraction", () => {
342
+ beforeEach(() => {
343
+ registerMCPBridgeExtensions(adapter, services);
344
+ });
345
+
346
+ it("rejects when params.context is missing", async () => {
347
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
348
+ await expect(handler(ctx, { summary: "test" })).rejects.toThrow(
349
+ "params.context.agent_id is required"
350
+ );
351
+ });
352
+
353
+ it("rejects when params.context.agent_id is missing", async () => {
354
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
355
+ await expect(
356
+ handler(ctx, { context: { session_id: "s1" }, summary: "test" })
357
+ ).rejects.toThrow("params.context.agent_id is required");
358
+ });
359
+
360
+ it("rejects when params is null", async () => {
361
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
362
+ await expect(handler(ctx, null)).rejects.toThrow(
363
+ "params.context.agent_id is required"
364
+ );
365
+ });
366
+
367
+ it("separates context from args correctly", async () => {
368
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
369
+ await handler(ctx, withContext({ status_type: "checkpoint", summary: "half done" }));
370
+
371
+ expect(services.eventStore.emit).toHaveBeenCalledWith(
372
+ expect.objectContaining({
373
+ type: "status",
374
+ source: { agent_id: "agent_caller" },
375
+ payload: expect.objectContaining({
376
+ status_type: "checkpoint",
377
+ summary: "half done",
378
+ }),
379
+ })
380
+ );
381
+ });
382
+ });
383
+
384
+ // ─────────────────────────────────────────────────────────────────
385
+ // spawn_agent
386
+ // ─────────────────────────────────────────────────────────────────
387
+
388
+ describe("_macro/mcp/spawn_agent", () => {
389
+ beforeEach(() => {
390
+ registerMCPBridgeExtensions(adapter, services);
391
+ });
392
+
393
+ it("calls agentManager.spawn with parent = context.agent_id", async () => {
394
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
395
+ await handler(ctx, withContext({ task: "child task" }));
396
+
397
+ expect(services.agentManager.spawn).toHaveBeenCalledWith(
398
+ expect.objectContaining({
399
+ task: "child task",
400
+ parent: "agent_caller",
401
+ })
402
+ );
403
+ });
404
+
405
+ it("returns agent_id, task_id, session_id", async () => {
406
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
407
+ const result = (await handler(ctx, withContext({ task: "test" }))) as any;
408
+
409
+ expect(result.agent_id).toBe("agent_spawned");
410
+ expect(result.task_id).toBe("task_spawned");
411
+ expect(result.session_id).toBe("sess_spawned");
412
+ });
413
+
414
+ it("defaults subscribe_parent to true and topics to []", async () => {
415
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
416
+ await handler(ctx, withContext({ task: "test" }));
417
+
418
+ expect(services.agentManager.spawn).toHaveBeenCalledWith(
419
+ expect.objectContaining({
420
+ subscribeParent: true,
421
+ topics: [],
422
+ })
423
+ );
424
+ });
425
+ });
426
+
427
+ // ─────────────────────────────────────────────────────────────────
428
+ // emit_status
429
+ // ─────────────────────────────────────────────────────────────────
430
+
431
+ describe("_macro/mcp/emit_status", () => {
432
+ beforeEach(() => {
433
+ registerMCPBridgeExtensions(adapter, services);
434
+ });
435
+
436
+ it("emits status event via eventStore.emit", async () => {
437
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
438
+ const result = (await handler(
439
+ ctx,
440
+ withContext({ status_type: "checkpoint", summary: "50% done" })
441
+ )) as any;
442
+
443
+ expect(services.eventStore.emit).toHaveBeenCalledWith(
444
+ expect.objectContaining({
445
+ type: "status",
446
+ source: { agent_id: "agent_caller" },
447
+ })
448
+ );
449
+ expect(result.event_id).toBeDefined();
450
+ });
451
+
452
+ it("updates task status when complete_task is true and status is completed", async () => {
453
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
454
+ const result = (await handler(
455
+ ctx,
456
+ withContext({ status_type: "completed", summary: "done", complete_task: true })
457
+ )) as any;
458
+
459
+ expect(services.taskManager.updateStatus).toHaveBeenCalledWith("task_caller", "completed");
460
+ expect(result.task_updated).toBe(true);
461
+ });
462
+
463
+ it("updates task status for failed with complete_task", async () => {
464
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
465
+ const result = (await handler(
466
+ ctx,
467
+ withContext({ status_type: "failed", summary: "error", complete_task: true })
468
+ )) as any;
469
+
470
+ expect(services.taskManager.updateStatus).toHaveBeenCalledWith("task_caller", "failed");
471
+ expect(result.task_updated).toBe(true);
472
+ });
473
+
474
+ it("does not update task when complete_task is false", async () => {
475
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
476
+ const result = (await handler(
477
+ ctx,
478
+ withContext({ status_type: "completed", summary: "done", complete_task: false })
479
+ )) as any;
480
+
481
+ expect(services.taskManager.updateStatus).not.toHaveBeenCalled();
482
+ expect(result.task_updated).toBe(false);
483
+ });
484
+
485
+ it("emits status_emitted MAP event for TUI subscribers", async () => {
486
+ const handler = adapter.handlers.get("_macro/mcp/emit_status")!;
487
+ await handler(
488
+ ctx,
489
+ withContext({ status_type: "checkpoint", summary: "50% done", details: "halfway" })
490
+ );
491
+
492
+ expect(adapter.emitEvent).toHaveBeenCalledWith(
493
+ expect.objectContaining({
494
+ type: "status_emitted",
495
+ agentId: "agent_caller",
496
+ data: expect.objectContaining({
497
+ agentId: "agent_caller",
498
+ taskId: "task_caller",
499
+ statusType: "checkpoint",
500
+ summary: "50% done",
501
+ details: "halfway",
502
+ }),
503
+ })
504
+ );
505
+ });
506
+ });
507
+
508
+ // ─────────────────────────────────────────────────────────────────
509
+ // send_message
510
+ // ─────────────────────────────────────────────────────────────────
511
+
512
+ describe("_macro/mcp/send_message", () => {
513
+ beforeEach(() => {
514
+ registerMCPBridgeExtensions(adapter, services);
515
+ });
516
+
517
+ it("sends to agent by agent_id", async () => {
518
+ const handler = adapter.handlers.get("_macro/mcp/send_message")!;
519
+ await handler(
520
+ ctx,
521
+ withContext({ to: { agent_id: "agent_target" }, content: "hello" })
522
+ );
523
+
524
+ expect(services.messageRouter.sendToAddress).toHaveBeenCalledWith(
525
+ expect.objectContaining({
526
+ from: "agent_caller",
527
+ to: { agent: "agent_target" },
528
+ content: "hello",
529
+ })
530
+ );
531
+ });
532
+
533
+ it("sends to task by task_id", async () => {
534
+ const handler = adapter.handlers.get("_macro/mcp/send_message")!;
535
+ await handler(
536
+ ctx,
537
+ withContext({ to: { task_id: "task_target" }, content: "update" })
538
+ );
539
+
540
+ expect(services.messageRouter.sendToAddress).toHaveBeenCalledWith(
541
+ expect.objectContaining({
542
+ to: { task: "task_target" },
543
+ })
544
+ );
545
+ });
546
+
547
+ it("throws when 'to' is missing", async () => {
548
+ const handler = adapter.handlers.get("_macro/mcp/send_message")!;
549
+ await expect(
550
+ handler(ctx, withContext({ content: "hello" }))
551
+ ).rejects.toThrow("params.to is required");
552
+ });
553
+
554
+ it("throws when no target specified in to", async () => {
555
+ const handler = adapter.handlers.get("_macro/mcp/send_message")!;
556
+ await expect(
557
+ handler(ctx, withContext({ to: {}, content: "hello" }))
558
+ ).rejects.toThrow("Must specify one of");
559
+ });
560
+
561
+ it("returns message_id and delivered_to count", async () => {
562
+ const handler = adapter.handlers.get("_macro/mcp/send_message")!;
563
+ const result = (await handler(
564
+ ctx,
565
+ withContext({ to: { agent_id: "agent_target" }, content: "hello" })
566
+ )) as any;
567
+
568
+ expect(result.message_id).toBe("msg_123");
569
+ expect(result.delivered_to).toBe(1);
570
+ });
571
+ });
572
+
573
+ // ─────────────────────────────────────────────────────────────────
574
+ // check_messages
575
+ // ─────────────────────────────────────────────────────────────────
576
+
577
+ describe("_macro/mcp/check_messages", () => {
578
+ beforeEach(() => {
579
+ registerMCPBridgeExtensions(adapter, services);
580
+ });
581
+
582
+ it("returns formatted messages", async () => {
583
+ (services.messageRouter.getMessages as ReturnType<typeof vi.fn>).mockReturnValue([
584
+ {
585
+ id: "msg_1",
586
+ from: { agent_id: "agent_sender" },
587
+ content: "hello",
588
+ timestamp: 1000,
589
+ truncated: false,
590
+ },
591
+ ]);
592
+
593
+ const handler = adapter.handlers.get("_macro/mcp/check_messages")!;
594
+ const result = (await handler(ctx, withContext({ limit: 10 }))) as any;
595
+
596
+ expect(result.messages).toHaveLength(1);
597
+ expect(result.messages[0].from).toBe("agent:agent_sender");
598
+ expect(result.messages[0].content).toBe("hello");
599
+ });
600
+
601
+ it("returns total_pending count", async () => {
602
+ (services.messageRouter.getMessages as ReturnType<typeof vi.fn>)
603
+ .mockReturnValueOnce([]) // limited query
604
+ .mockReturnValueOnce([{ id: "m1" }, { id: "m2" }]); // all query
605
+
606
+ const handler = adapter.handlers.get("_macro/mcp/check_messages")!;
607
+ const result = (await handler(ctx, withContext({}))) as any;
608
+
609
+ expect(result.total_pending).toBe(2);
610
+ });
611
+ });
612
+
613
+ // ─────────────────────────────────────────────────────────────────
614
+ // query_index
615
+ // ─────────────────────────────────────────────────────────────────
616
+
617
+ describe("_macro/mcp/query_index", () => {
618
+ beforeEach(() => {
619
+ registerMCPBridgeExtensions(adapter, services);
620
+ });
621
+
622
+ it("queries agents when type is 'agents'", async () => {
623
+ (services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
624
+ { id: "agent_1", task: "Task 1", state: "running" },
625
+ { id: "agent_2", task: "Task 2", state: "stopped" },
626
+ ]);
627
+
628
+ const handler = adapter.handlers.get("_macro/mcp/query_index")!;
629
+ const result = (await handler(ctx, withContext({ type: "agents" }))) as any;
630
+
631
+ expect(result.entries).toHaveLength(2);
632
+ expect(result.entries[0].type).toBe("agent");
633
+ expect(result.total).toBe(2);
634
+ });
635
+
636
+ it("queries tasks when type is 'tasks'", async () => {
637
+ (services.taskManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
638
+ { id: "task_1", description: "Do thing", status: "pending" },
639
+ ]);
640
+
641
+ const handler = adapter.handlers.get("_macro/mcp/query_index")!;
642
+ const result = (await handler(ctx, withContext({ type: "tasks" }))) as any;
643
+
644
+ expect(result.entries).toHaveLength(1);
645
+ expect(result.entries[0].type).toBe("task");
646
+ });
647
+
648
+ it("applies state filter for agents", async () => {
649
+ (services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue([
650
+ { id: "agent_1", task: "Task 1", state: "running" },
651
+ { id: "agent_2", task: "Task 2", state: "stopped" },
652
+ ]);
653
+
654
+ const handler = adapter.handlers.get("_macro/mcp/query_index")!;
655
+ const result = (await handler(
656
+ ctx,
657
+ withContext({ type: "agents", filter: { state: "running" } })
658
+ )) as any;
659
+
660
+ expect(result.entries).toHaveLength(1);
661
+ expect(result.entries[0].id).toBe("agent_1");
662
+ });
663
+
664
+ it("returns has_more flag with pagination", async () => {
665
+ const agents = Array.from({ length: 25 }, (_, i) => ({
666
+ id: `agent_${i}`,
667
+ task: `Task ${i}`,
668
+ state: "running",
669
+ }));
670
+ (services.agentManager.list as ReturnType<typeof vi.fn>).mockReturnValue(agents);
671
+
672
+ const handler = adapter.handlers.get("_macro/mcp/query_index")!;
673
+ const result = (await handler(
674
+ ctx,
675
+ withContext({ type: "agents", limit: 10 })
676
+ )) as any;
677
+
678
+ expect(result.entries).toHaveLength(10);
679
+ expect(result.total).toBe(25);
680
+ expect(result.has_more).toBe(true);
681
+ });
682
+ });
683
+
684
+ // ─────────────────────────────────────────────────────────────────
685
+ // get_hierarchy
686
+ // ─────────────────────────────────────────────────────────────────
687
+
688
+ describe("_macro/mcp/get_hierarchy", () => {
689
+ beforeEach(() => {
690
+ registerMCPBridgeExtensions(adapter, services);
691
+ });
692
+
693
+ it("returns hierarchy tree for agent", async () => {
694
+ (services.agentManager.getHierarchy as ReturnType<typeof vi.fn>).mockReturnValue({
695
+ root: {
696
+ agent: { id: "agent_root", task: "Root task", state: "running" },
697
+ children: [],
698
+ },
699
+ depth: 1,
700
+ totalAgents: 1,
701
+ });
702
+
703
+ const handler = adapter.handlers.get("_macro/mcp/get_hierarchy")!;
704
+ const result = (await handler(
705
+ ctx,
706
+ withContext({ root: "agent_root" })
707
+ )) as any;
708
+
709
+ expect(result.tree.agent_id).toBe("agent_root");
710
+ expect(result.depth).toBe(1);
711
+ expect(result.total_agents).toBe(1);
712
+ });
713
+
714
+ it("throws notFound for non-existent agent", async () => {
715
+ const handler = adapter.handlers.get("_macro/mcp/get_hierarchy")!;
716
+ await expect(
717
+ handler(ctx, withContext({ root: "nonexistent" }))
718
+ ).rejects.toThrow("not found");
719
+ });
720
+ });
721
+
722
+ // ─────────────────────────────────────────────────────────────────
723
+ // get_agent_summary
724
+ // ─────────────────────────────────────────────────────────────────
725
+
726
+ describe("_macro/mcp/get_agent_summary", () => {
727
+ beforeEach(() => {
728
+ registerMCPBridgeExtensions(adapter, services);
729
+ });
730
+
731
+ it("returns agent details", async () => {
732
+ (services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
733
+ id: "agent_target",
734
+ session_id: "sess_target",
735
+ task: "Target task",
736
+ state: "running",
737
+ parent: "agent_root",
738
+ created_at: 1000,
739
+ });
740
+ (services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([
741
+ { id: "child_1" },
742
+ ]);
743
+
744
+ const handler = adapter.handlers.get("_macro/mcp/get_agent_summary")!;
745
+ const result = (await handler(
746
+ ctx,
747
+ withContext({ agent_id: "agent_target" })
748
+ )) as any;
749
+
750
+ expect(result.id).toBe("agent_target");
751
+ expect(result.task).toBe("Target task");
752
+ expect(result.children_count).toBe(1);
753
+ });
754
+
755
+ it("throws notFound for non-existent agent", async () => {
756
+ const handler = adapter.handlers.get("_macro/mcp/get_agent_summary")!;
757
+ await expect(
758
+ handler(ctx, withContext({ agent_id: "nonexistent" }))
759
+ ).rejects.toThrow("not found");
760
+ });
761
+ });
762
+
763
+ // ─────────────────────────────────────────────────────────────────
764
+ // stop_agent
765
+ // ─────────────────────────────────────────────────────────────────
766
+
767
+ describe("_macro/mcp/stop_agent", () => {
768
+ beforeEach(() => {
769
+ registerMCPBridgeExtensions(adapter, services);
770
+ });
771
+
772
+ it("terminates agent in caller's subtree", async () => {
773
+ (services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
774
+ id: "agent_child",
775
+ state: "running",
776
+ lineage: ["agent_root", "agent_caller"],
777
+ });
778
+ (services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([]);
779
+
780
+ const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
781
+ const result = (await handler(
782
+ ctx,
783
+ withContext({ agent_id: "agent_child" })
784
+ )) as any;
785
+
786
+ expect(services.agentManager.terminate).toHaveBeenCalledWith("agent_child", "cancelled");
787
+ expect(result.success).toBe(true);
788
+ expect(result.stopped_agents).toContain("agent_child");
789
+ });
790
+
791
+ it("throws permissionDenied for agent outside subtree", async () => {
792
+ (services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
793
+ id: "agent_other",
794
+ state: "running",
795
+ lineage: ["agent_root"],
796
+ });
797
+
798
+ const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
799
+ await expect(
800
+ handler(ctx, withContext({ agent_id: "agent_other" }))
801
+ ).rejects.toThrow("Cannot stop agent outside your subtree");
802
+ });
803
+
804
+ it("throws notFound for non-existent agent", async () => {
805
+ const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
806
+ await expect(
807
+ handler(ctx, withContext({ agent_id: "nonexistent" }))
808
+ ).rejects.toThrow("not found");
809
+ });
810
+
811
+ it("allows stopping self", async () => {
812
+ (services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
813
+ id: "agent_caller",
814
+ state: "running",
815
+ lineage: ["agent_root"],
816
+ });
817
+ (services.agentManager.getChildren as ReturnType<typeof vi.fn>).mockReturnValue([]);
818
+
819
+ const handler = adapter.handlers.get("_macro/mcp/stop_agent")!;
820
+ const result = (await handler(
821
+ ctx,
822
+ withContext({ agent_id: "agent_caller" })
823
+ )) as any;
824
+
825
+ expect(result.success).toBe(true);
826
+ });
827
+ });
828
+
829
+ // ─────────────────────────────────────────────────────────────────
830
+ // done (delegates to createDoneHandler)
831
+ // ─────────────────────────────────────────────────────────────────
832
+
833
+ describe("_macro/mcp/done", () => {
834
+ beforeEach(() => {
835
+ registerMCPBridgeExtensions(adapter, services);
836
+ // done handler needs the agent to exist for role resolution
837
+ (services.agentManager.get as ReturnType<typeof vi.fn>).mockReturnValue({
838
+ id: "agent_caller",
839
+ state: "running",
840
+ role: "worker",
841
+ task_id: "task_caller",
842
+ parent: "agent_root",
843
+ lineage: ["agent_root"],
844
+ });
845
+ });
846
+
847
+ it("delegates to createDoneHandler and returns result", async () => {
848
+ const handler = adapter.handlers.get("_macro/mcp/done")!;
849
+ const result = (await handler(
850
+ ctx,
851
+ withContext({ status: "completed", summary: "all done" })
852
+ )) as any;
853
+
854
+ // The done handler should return a result with shouldTerminate
855
+ expect(result).toBeDefined();
856
+ });
857
+ });
858
+
859
+ // ─────────────────────────────────────────────────────────────────
860
+ // spawn_agent — fire-and-forget prompt session streaming
861
+ // ─────────────────────────────────────────────────────────────────
862
+
863
+ describe("spawn_agent session streaming", () => {
864
+ /**
865
+ * Helper: wait until adapter.emitEvent has been called with a specific event type.
866
+ * Polls every 10ms up to timeoutMs.
867
+ */
868
+ async function waitForEmitEvent(
869
+ emitFn: ReturnType<typeof vi.fn>,
870
+ eventType: string,
871
+ timeoutMs = 5000,
872
+ ): Promise<unknown> {
873
+ const start = Date.now();
874
+ while (Date.now() - start < timeoutMs) {
875
+ const call = emitFn.mock.calls.find(
876
+ (c: unknown[]) => (c[0] as Record<string, unknown>)?.type === eventType,
877
+ );
878
+ if (call) return call[0];
879
+ await new Promise((r) => setTimeout(r, 10));
880
+ }
881
+ throw new Error(
882
+ `Timeout waiting for emitEvent(${eventType}). ` +
883
+ `Received: ${emitFn.mock.calls.map((c: unknown[]) => (c[0] as Record<string, unknown>)?.type).join(", ")}`,
884
+ );
885
+ }
886
+
887
+ beforeEach(() => {
888
+ registerMCPBridgeExtensions(adapter, services);
889
+ });
890
+
891
+ it("emits session_user_message MAP event before prompt starts", async () => {
892
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
893
+ await handler(ctx, withContext({ task: "do something" }));
894
+
895
+ // Fire-and-forget IIFE starts immediately — wait for it
896
+ const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_user_message");
897
+ const evt = event as Record<string, unknown>;
898
+ expect(evt.type).toBe("session_user_message");
899
+ expect(evt.agentId).toBe("agent_spawned");
900
+ expect(evt.eventId).toBeDefined();
901
+ expect(evt.timestamp).toBeGreaterThan(0);
902
+
903
+ const data = evt.data as Record<string, unknown>;
904
+ expect(data.agentId).toBe("agent_spawned");
905
+ expect(data.sessionId).toBe("sess_spawned");
906
+ expect(data.content).toBe("do something");
907
+ });
908
+
909
+ it("emits session_prompt_done MAP event after prompt completes", async () => {
910
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
911
+ await handler(ctx, withContext({ task: "finish this" }));
912
+
913
+ const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
914
+ const evt = event as Record<string, unknown>;
915
+ expect(evt.type).toBe("session_prompt_done");
916
+ expect(evt.agentId).toBe("agent_spawned");
917
+
918
+ const data = evt.data as Record<string, unknown>;
919
+ expect(data.agentId).toBe("agent_spawned");
920
+ expect(data.stopReason).toBe("end_turn");
921
+ });
922
+
923
+ it("emits session_update MAP events for each prompt update", async () => {
924
+ // Configure prompt to yield controlled updates
925
+ const updates = [
926
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Hello" } },
927
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: " world" } },
928
+ { sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read file", status: "running" },
929
+ { sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read file", status: "completed", rawOutput: "contents" },
930
+ ];
931
+
932
+ (services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
933
+ async function* () {
934
+ for (const u of updates) {
935
+ yield u;
936
+ }
937
+ },
938
+ );
939
+
940
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
941
+ await handler(ctx, withContext({ task: "read a file" }));
942
+
943
+ // Wait for the final event
944
+ await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
945
+
946
+ // Collect all emitEvent calls
947
+ const calls = (adapter.emitEvent as ReturnType<typeof vi.fn>).mock.calls.map(
948
+ (c: unknown[]) => c[0] as Record<string, unknown>,
949
+ );
950
+
951
+ // Should have: 1 user_message + 4 session_updates + 1 prompt_done = 6 events
952
+ const userMessages = calls.filter((c) => c.type === "session_user_message");
953
+ const sessionUpdates = calls.filter((c) => c.type === "session_update");
954
+ const promptDones = calls.filter((c) => c.type === "session_prompt_done");
955
+
956
+ expect(userMessages).toHaveLength(1);
957
+ expect(sessionUpdates).toHaveLength(4);
958
+ expect(promptDones).toHaveLength(1);
959
+
960
+ // Verify update payloads contain the original update objects
961
+ const updatePayloads = sessionUpdates.map(
962
+ (c) => (c.data as Record<string, unknown>).update,
963
+ );
964
+ expect((updatePayloads[0] as Record<string, unknown>).sessionUpdate).toBe("agent_message_chunk");
965
+ expect((updatePayloads[2] as Record<string, unknown>).sessionUpdate).toBe("tool_call");
966
+ });
967
+
968
+ it("records user and assistant turns in eventStore after prompt completes", async () => {
969
+ const updates = [
970
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "I will help." } },
971
+ ];
972
+
973
+ (services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
974
+ async function* () {
975
+ for (const u of updates) yield u;
976
+ },
977
+ );
978
+
979
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
980
+ await handler(ctx, withContext({ task: "help me" }));
981
+
982
+ // Wait for prompt to finish
983
+ await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
984
+
985
+ // Small delay for turn recording (happens after emitMAPEvent)
986
+ await new Promise((r) => setTimeout(r, 50));
987
+
988
+ // Check eventStore.emit calls for turn recording
989
+ const turnCalls = (services.eventStore.emit as ReturnType<typeof vi.fn>).mock.calls.filter(
990
+ (c: unknown[]) => (c[0] as Record<string, unknown>)?.type === "turn",
991
+ );
992
+
993
+ expect(turnCalls.length).toBe(2);
994
+
995
+ // First turn: user prompt
996
+ const userTurn = turnCalls[0][0] as Record<string, unknown>;
997
+ expect(userTurn.type).toBe("turn");
998
+ expect((userTurn.source as Record<string, unknown>).agent_id).toBe("agent_spawned");
999
+ const userPayload = userTurn.payload as Record<string, unknown>;
1000
+ expect(userPayload.participant).toBe("user");
1001
+ expect(userPayload.content).toBe("help me");
1002
+ expect(userPayload.conversation_id).toBe("sess_spawned");
1003
+
1004
+ // Second turn: assistant response
1005
+ const assistantTurn = turnCalls[1][0] as Record<string, unknown>;
1006
+ const assistantPayload = assistantTurn.payload as Record<string, unknown>;
1007
+ expect(assistantPayload.participant).toBe("agent_spawned");
1008
+ expect(assistantPayload.content_type).toBe("assistant_response");
1009
+ const content = assistantPayload.content as { parts: Array<{ type: string; text?: string }> };
1010
+ expect(content.parts[0].type).toBe("text");
1011
+ expect(content.parts[0].text).toBe("I will help.");
1012
+ });
1013
+
1014
+ it("accumulates tool calls in assistant turn content", async () => {
1015
+ const updates = [
1016
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Reading file..." } },
1017
+ { sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read", status: "running", _meta: { claudeCode: { toolName: "Read" } }, rawInput: { path: "/foo" } },
1018
+ { sessionUpdate: "tool_call", toolCallId: "tc_1", title: "Read", status: "completed", rawOutput: "file contents", _meta: { claudeCode: { toolName: "Read" } }, rawInput: { path: "/foo" } },
1019
+ ];
1020
+
1021
+ (services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
1022
+ async function* () {
1023
+ for (const u of updates) yield u;
1024
+ },
1025
+ );
1026
+
1027
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
1028
+ await handler(ctx, withContext({ task: "read /foo" }));
1029
+
1030
+ await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
1031
+ await new Promise((r) => setTimeout(r, 50));
1032
+
1033
+ const turnCalls = (services.eventStore.emit as ReturnType<typeof vi.fn>).mock.calls.filter(
1034
+ (c: unknown[]) => (c[0] as Record<string, unknown>)?.type === "turn",
1035
+ );
1036
+
1037
+ const assistantTurn = turnCalls[1][0] as Record<string, unknown>;
1038
+ const assistantPayload = assistantTurn.payload as Record<string, unknown>;
1039
+ const content = assistantPayload.content as { parts: Array<Record<string, unknown>> };
1040
+
1041
+ // Should have text part + tool part
1042
+ expect(content.parts).toHaveLength(2);
1043
+ expect(content.parts[0].type).toBe("text");
1044
+ expect(content.parts[0].text).toBe("Reading file...");
1045
+ expect(content.parts[1].type).toBe("tool");
1046
+ expect(content.parts[1].toolCallId).toBe("tc_1");
1047
+ expect(content.parts[1].name).toBe("Read");
1048
+ expect(content.parts[1].output).toBe("file contents");
1049
+ });
1050
+
1051
+ it("emits session_prompt_done with stopReason 'error' when prompt throws", async () => {
1052
+ (services.agentManager.prompt as ReturnType<typeof vi.fn>).mockImplementation(
1053
+ async function* () {
1054
+ throw new Error("Agent process crashed");
1055
+ },
1056
+ );
1057
+
1058
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
1059
+ await handler(ctx, withContext({ task: "crash test" }));
1060
+
1061
+ const event = await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
1062
+ const evt = event as Record<string, unknown>;
1063
+ const data = evt.data as Record<string, unknown>;
1064
+ expect(data.stopReason).toBe("error");
1065
+ });
1066
+
1067
+ it("does not emit session events when task is empty", async () => {
1068
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
1069
+ await handler(ctx, withContext({ task: "" }));
1070
+
1071
+ // Wait a bit to ensure no events are emitted
1072
+ await new Promise((r) => setTimeout(r, 100));
1073
+
1074
+ expect(adapter.emitEvent).not.toHaveBeenCalled();
1075
+ });
1076
+
1077
+ it("all events include agentId at the top level for subscription routing", async () => {
1078
+ const handler = adapter.handlers.get("_macro/mcp/spawn_agent")!;
1079
+ await handler(ctx, withContext({ task: "routing test" }));
1080
+
1081
+ await waitForEmitEvent(adapter.emitEvent as ReturnType<typeof vi.fn>, "session_prompt_done");
1082
+
1083
+ const calls = (adapter.emitEvent as ReturnType<typeof vi.fn>).mock.calls.map(
1084
+ (c: unknown[]) => c[0] as Record<string, unknown>,
1085
+ );
1086
+
1087
+ for (const evt of calls) {
1088
+ expect(evt.agentId).toBe("agent_spawned");
1089
+ expect(evt.eventId).toBeDefined();
1090
+ expect(evt.timestamp).toBeGreaterThan(0);
1091
+ }
1092
+ });
1093
+ });
1094
+
1095
+ // ─────────────────────────────────────────────────────────────────
1096
+ // Optional handler: activityWatcher absent
1097
+ // ─────────────────────────────────────────────────────────────────
1098
+
1099
+ describe("optional handlers - activityWatcher absent", () => {
1100
+ it("wait_for_activity is not registered when absent", () => {
1101
+ registerMCPBridgeExtensions(adapter, services);
1102
+ expect(adapter.handlers.has("_macro/mcp/wait_for_activity")).toBe(false);
1103
+ });
1104
+ });
1105
+
1106
+ // ─────────────────────────────────────────────────────────────────
1107
+ // Optional handler: taskBackend absent
1108
+ // ─────────────────────────────────────────────────────────────────
1109
+
1110
+ describe("optional handlers - taskBackend absent", () => {
1111
+ it("claim_task is not registered when absent", () => {
1112
+ registerMCPBridgeExtensions(adapter, services);
1113
+ expect(adapter.handlers.has("_macro/mcp/claim_task")).toBe(false);
1114
+ });
1115
+
1116
+ it("unclaim_task is not registered when absent", () => {
1117
+ registerMCPBridgeExtensions(adapter, services);
1118
+ expect(adapter.handlers.has("_macro/mcp/unclaim_task")).toBe(false);
1119
+ });
1120
+
1121
+ it("list_claimable_tasks is not registered when absent", () => {
1122
+ registerMCPBridgeExtensions(adapter, services);
1123
+ expect(adapter.handlers.has("_macro/mcp/list_claimable_tasks")).toBe(false);
1124
+ });
1125
+ });
1126
+
1127
+ // ─────────────────────────────────────────────────────────────────
1128
+ // Optional handler: peerManager absent
1129
+ // ─────────────────────────────────────────────────────────────────
1130
+
1131
+ describe("optional handlers - peerManager absent", () => {
1132
+ it("send_peer_message is not registered when absent", () => {
1133
+ registerMCPBridgeExtensions(adapter, services);
1134
+ expect(adapter.handlers.has("_macro/mcp/send_peer_message")).toBe(false);
1135
+ });
1136
+ });
1137
+
1138
+ // ─────────────────────────────────────────────────────────────────
1139
+ // Peer bridges (with peerManager)
1140
+ // ─────────────────────────────────────────────────────────────────
1141
+
1142
+ describe("peer bridges", () => {
1143
+ beforeEach(() => {
1144
+ services.peerManager = createMockPeerManager();
1145
+ registerMCPBridgeExtensions(adapter, services);
1146
+ });
1147
+
1148
+ it("send_peer_message sends via peerManager", async () => {
1149
+ const handler = adapter.handlers.get("_macro/mcp/send_peer_message")!;
1150
+ await handler(
1151
+ ctx,
1152
+ withContext({ to: "peer_target", type: "status", payload: { data: 1 } })
1153
+ );
1154
+
1155
+ expect(services.peerManager!.sendMessage).toHaveBeenCalledWith(
1156
+ "agent_caller",
1157
+ "peer_target",
1158
+ expect.objectContaining({ type: "status", payload: { data: 1 } })
1159
+ );
1160
+ });
1161
+
1162
+ it("send_peer_request sends via peerManager and returns result", async () => {
1163
+ const handler = adapter.handlers.get("_macro/mcp/send_peer_request")!;
1164
+ const result = await handler(
1165
+ ctx,
1166
+ withContext({ to: "peer_target", method: "getStatus", params: {} })
1167
+ );
1168
+
1169
+ expect(services.peerManager!.sendRequest).toHaveBeenCalled();
1170
+ expect(result).toEqual({ result: "ok" });
1171
+ });
1172
+
1173
+ it("respond_to_peer_request calls peerManager.respondToRequest", async () => {
1174
+ const handler = adapter.handlers.get("_macro/mcp/respond_to_peer_request")!;
1175
+ await handler(
1176
+ ctx,
1177
+ withContext({ request_id: "req_1", result: { data: 42 } })
1178
+ );
1179
+
1180
+ expect(services.peerManager!.respondToRequest).toHaveBeenCalledWith(
1181
+ "agent_caller",
1182
+ "req_1",
1183
+ expect.objectContaining({ result: { data: 42 } })
1184
+ );
1185
+ });
1186
+ });
1187
+ });