airaknit 1.1.2-rc.9

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 (440) hide show
  1. package/LICENSE +84 -0
  2. package/README.md +202 -0
  3. package/bin/airaknit +9 -0
  4. package/bin/airaknit-project +14 -0
  5. package/bin/kanna +9 -0
  6. package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
  7. package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
  8. package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
  9. package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
  10. package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
  11. package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
  12. package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
  13. package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
  14. package/dist/client/assets/arc-B07zg7ol.js +1 -0
  15. package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
  16. package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
  17. package/dist/client/assets/array-BGFCBI0e.js +1 -0
  18. package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
  19. package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
  20. package/dist/client/assets/button-DO50qOGv.js +1 -0
  21. package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
  22. package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
  23. package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
  24. package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
  25. package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
  26. package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
  27. package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
  28. package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
  29. package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
  30. package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
  31. package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
  32. package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
  33. package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
  34. package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
  35. package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
  36. package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
  37. package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
  38. package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
  39. package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
  40. package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
  41. package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
  42. package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
  43. package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
  44. package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
  45. package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
  46. package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
  47. package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
  48. package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
  49. package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
  50. package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
  51. package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
  52. package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
  53. package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
  54. package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
  55. package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
  56. package/dist/client/assets/client-BrKWI4CM.js +1 -0
  57. package/dist/client/assets/client-CGgNRU9w.js +1 -0
  58. package/dist/client/assets/client-DMSLRzg9.js +6 -0
  59. package/dist/client/assets/clone-DWcL7whJ.js +1 -0
  60. package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
  61. package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
  62. package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
  63. package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
  64. package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
  65. package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
  66. package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
  67. package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
  68. package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
  69. package/dist/client/assets/dist-CuB4kiSK.js +1 -0
  70. package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
  71. package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
  72. package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
  73. package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
  74. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
  75. package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
  76. package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
  77. package/dist/client/assets/index-CetCiuqP.js +105 -0
  78. package/dist/client/assets/index-N29Mip7A.css +1 -0
  79. package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
  80. package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
  81. package/dist/client/assets/init-B8gtcn7T.js +1 -0
  82. package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
  83. package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
  84. package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
  85. package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
  86. package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
  87. package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
  88. package/dist/client/assets/katex-B94qP8b6.js +265 -0
  89. package/dist/client/assets/lib--QVjyxmL.js +29 -0
  90. package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
  91. package/dist/client/assets/line-DCrYfLBn.js +1 -0
  92. package/dist/client/assets/linear-_4upLmeo.js +1 -0
  93. package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
  94. package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
  95. package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
  96. package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
  97. package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
  98. package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
  99. package/dist/client/assets/path-DZF-JdEe.js +1 -0
  100. package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
  101. package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
  102. package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
  103. package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
  104. package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
  105. package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
  106. package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
  107. package/dist/client/assets/react-CClhXMB2.js +1 -0
  108. package/dist/client/assets/react-Dd6D81m0.js +1 -0
  109. package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
  110. package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
  111. package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
  112. package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
  113. package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
  114. package/dist/client/assets/src-DeTlMJAU.js +1 -0
  115. package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
  116. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
  117. package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
  118. package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
  119. package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
  120. package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
  121. package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
  122. package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
  123. package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
  124. package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
  125. package/dist/client/favicon.png +0 -0
  126. package/dist/client/favicon.svg +15 -0
  127. package/dist/client/fonts/body-medium.woff2 +0 -0
  128. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  129. package/dist/client/fonts/body-regular.woff2 +0 -0
  130. package/dist/client/fonts/body-semibold.woff2 +0 -0
  131. package/dist/client/index.html +31 -0
  132. package/dist/client/manifest.webmanifest +12 -0
  133. package/dist/client/sw.js +32 -0
  134. package/package.json +122 -0
  135. package/src/nats/auth-callout/callout-config.test.ts +93 -0
  136. package/src/nats/auth-callout/callout-config.ts +109 -0
  137. package/src/nats/auth-callout/callout.integration.test.ts +332 -0
  138. package/src/nats/auth-callout/keys.ts +103 -0
  139. package/src/nats/auth-callout/responder.ts +241 -0
  140. package/src/nats/auth-callout/scope-policy.test.ts +159 -0
  141. package/src/nats/auth-callout/scope-policy.ts +210 -0
  142. package/src/nats/auth-callout/token.test.ts +163 -0
  143. package/src/nats/auth-callout/token.ts +157 -0
  144. package/src/nats/nats-daemon-callout.ts +194 -0
  145. package/src/nats/nats-daemon.test.ts +77 -0
  146. package/src/nats/nats-daemon.ts +50 -0
  147. package/src/nats/nats-token.test.ts +61 -0
  148. package/src/nats/nats-token.ts +59 -0
  149. package/src/runner/coordination-mcp-integration.test.ts +134 -0
  150. package/src/runner/nats-coordination-client.test.ts +49 -0
  151. package/src/runner/nats-coordination-client.ts +94 -0
  152. package/src/runner/runner-agent.test.ts +469 -0
  153. package/src/runner/runner-agent.ts +453 -0
  154. package/src/runner/runner-credential.test.ts +93 -0
  155. package/src/runner/runner-credential.ts +82 -0
  156. package/src/runner/runner-nats.test.ts +495 -0
  157. package/src/runner/runner-nats.ts +323 -0
  158. package/src/runner/runner-pair.test.ts +107 -0
  159. package/src/runner/runner-pair.ts +81 -0
  160. package/src/runner/runner.test.ts +135 -0
  161. package/src/runner/runner.ts +212 -0
  162. package/src/runner/turn-factories.test.ts +97 -0
  163. package/src/runner/turn-factories.ts +475 -0
  164. package/src/server/agent-config-journey.test.ts +106 -0
  165. package/src/server/agent.ts +8 -0
  166. package/src/server/auto-continue/auth-error-detector.ts +66 -0
  167. package/src/server/auto-continue/limit-detector.ts +194 -0
  168. package/src/server/bm25.test.ts +92 -0
  169. package/src/server/bm25.ts +101 -0
  170. package/src/server/chat-events-jetstream.test.ts +135 -0
  171. package/src/server/claude-harness.ts +360 -0
  172. package/src/server/claude-pty/agent-normalizers.ts +309 -0
  173. package/src/server/claude-pty/auth.test.ts +38 -0
  174. package/src/server/claude-pty/auth.ts +32 -0
  175. package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
  176. package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
  177. package/src/server/claude-pty/driver.test.ts +902 -0
  178. package/src/server/claude-pty/driver.ts +807 -0
  179. package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
  180. package/src/server/claude-pty/jsonl-path.test.ts +114 -0
  181. package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
  182. package/src/server/claude-pty/jsonl-to-event.ts +174 -0
  183. package/src/server/claude-pty/output-ring.test.ts +35 -0
  184. package/src/server/claude-pty/output-ring.ts +25 -0
  185. package/src/server/claude-pty/parity-matrix.test.ts +227 -0
  186. package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
  187. package/src/server/claude-pty/pid-registry.test.ts +122 -0
  188. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
  189. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
  190. package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
  191. package/src/server/claude-pty/pty-instance-registry.ts +166 -0
  192. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
  193. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
  194. package/src/server/claude-pty/pty-process.adapter.ts +66 -0
  195. package/src/server/claude-pty/pty-process.test.ts +49 -0
  196. package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
  197. package/src/server/claude-pty/resolve-binary.test.ts +118 -0
  198. package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
  199. package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
  200. package/src/server/claude-pty/settings-writer.test.ts +22 -0
  201. package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
  202. package/src/server/claude-pty/smoke-test.test.ts +191 -0
  203. package/src/server/claude-pty/smoke-test.ts +185 -0
  204. package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
  205. package/src/server/claude-pty/tool-callback.ts +274 -0
  206. package/src/server/claude-pty/tui-control.test.ts +272 -0
  207. package/src/server/claude-pty/tui-control.ts +182 -0
  208. package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
  209. package/src/server/claude-pty/tui-source.adapter.ts +343 -0
  210. package/src/server/claude-pty/tunnel-gateway.ts +12 -0
  211. package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
  212. package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
  213. package/src/server/claude-pty-mcp/history-primer.ts +90 -0
  214. package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
  215. package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
  216. package/src/server/claude-pty-mcp/mcp.ts +412 -0
  217. package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
  218. package/src/server/claude-pty-mcp/paths.ts +24 -0
  219. package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
  220. package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
  221. package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
  222. package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
  223. package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
  224. package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
  225. package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
  226. package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
  227. package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
  228. package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
  229. package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
  230. package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
  231. package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
  232. package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
  233. package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
  234. package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
  235. package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
  236. package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
  237. package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
  238. package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
  239. package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
  240. package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
  241. package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
  242. package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
  243. package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
  244. package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
  245. package/src/server/claude-pty-mcp/uploads.ts +38 -0
  246. package/src/server/claude-turn.test.ts +176 -0
  247. package/src/server/cli-runtime.test.ts +456 -0
  248. package/src/server/cli-runtime.ts +374 -0
  249. package/src/server/cli-supervisor.ts +81 -0
  250. package/src/server/cli.ts +78 -0
  251. package/src/server/client-log-forwarder.test.ts +74 -0
  252. package/src/server/client-log-forwarder.ts +75 -0
  253. package/src/server/codex-app-server-protocol.ts +449 -0
  254. package/src/server/codex-app-server.test.ts +2990 -0
  255. package/src/server/codex-app-server.ts +1713 -0
  256. package/src/server/coordination-integration.test.ts +63 -0
  257. package/src/server/coordination-mcp.test.ts +149 -0
  258. package/src/server/coordination-mcp.ts +197 -0
  259. package/src/server/delegation-coordinator.test.ts +675 -0
  260. package/src/server/delegation-coordinator.ts +454 -0
  261. package/src/server/discovery.test.ts +211 -0
  262. package/src/server/discovery.ts +301 -0
  263. package/src/server/event-store-agent-config.test.ts +124 -0
  264. package/src/server/event-store-coordination.test.ts +149 -0
  265. package/src/server/event-store-profile.test.ts +132 -0
  266. package/src/server/event-store-repo.test.ts +154 -0
  267. package/src/server/event-store-runner-team.test.ts +104 -0
  268. package/src/server/event-store.test.ts +342 -0
  269. package/src/server/event-store.ts +2208 -0
  270. package/src/server/events.ts +379 -0
  271. package/src/server/extension-router.test.ts +183 -0
  272. package/src/server/extension-router.ts +114 -0
  273. package/src/server/extensions/agents/server.test.ts +191 -0
  274. package/src/server/extensions/agents/server.ts +108 -0
  275. package/src/server/extensions/c3/server.test.ts +284 -0
  276. package/src/server/extensions/c3/server.ts +212 -0
  277. package/src/server/extensions/code/server.test.ts +200 -0
  278. package/src/server/extensions/code/server.ts +150 -0
  279. package/src/server/extensions.config.ts +10 -0
  280. package/src/server/external-open.ts +69 -0
  281. package/src/server/generate-fork-context.ts +58 -0
  282. package/src/server/generate-merge-context.test.ts +290 -0
  283. package/src/server/generate-merge-context.ts +141 -0
  284. package/src/server/generate-title.ts +36 -0
  285. package/src/server/git-clone-policy.test.ts +138 -0
  286. package/src/server/git-clone-policy.ts +27 -0
  287. package/src/server/harness-types.ts +1 -0
  288. package/src/server/journey-verification.test.ts +640 -0
  289. package/src/server/journey-verification.ts +195 -0
  290. package/src/server/machine-name.ts +22 -0
  291. package/src/server/nats-auth.test.ts +92 -0
  292. package/src/server/nats-auth.ts +6 -0
  293. package/src/server/nats-bind-guard.test.ts +71 -0
  294. package/src/server/nats-bind-guard.ts +42 -0
  295. package/src/server/nats-bridge.test.ts +141 -0
  296. package/src/server/nats-bridge.ts +111 -0
  297. package/src/server/nats-connector.test.ts +56 -0
  298. package/src/server/nats-connector.ts +71 -0
  299. package/src/server/nats-daemon-manager.test.ts +76 -0
  300. package/src/server/nats-daemon-manager.ts +174 -0
  301. package/src/server/nats-publisher.test.ts +356 -0
  302. package/src/server/nats-publisher.ts +271 -0
  303. package/src/server/nats-responders.test.ts +1018 -0
  304. package/src/server/nats-responders.ts +925 -0
  305. package/src/server/nats-streams.test.ts +112 -0
  306. package/src/server/nats-streams.ts +129 -0
  307. package/src/server/oauth-pool/oauth-responders.ts +185 -0
  308. package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
  309. package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
  310. package/src/server/orchestration.test.ts +1063 -0
  311. package/src/server/orchestration.ts +716 -0
  312. package/src/server/pairing-endpoints.test.ts +171 -0
  313. package/src/server/pairing-store.test.ts +154 -0
  314. package/src/server/pairing-store.ts +124 -0
  315. package/src/server/paths.ts +27 -0
  316. package/src/server/pr3-liveness.test.ts +252 -0
  317. package/src/server/process-utils.ts +10 -0
  318. package/src/server/project-cli.ts +180 -0
  319. package/src/server/provider-catalog.test.ts +177 -0
  320. package/src/server/provider-catalog.ts +146 -0
  321. package/src/server/pty-responders.ts +345 -0
  322. package/src/server/push-notifications.test.ts +234 -0
  323. package/src/server/push-notifications.ts +188 -0
  324. package/src/server/quick-response.test.ts +196 -0
  325. package/src/server/quick-response.ts +154 -0
  326. package/src/server/read-models-agent-config.test.ts +59 -0
  327. package/src/server/read-models-coordination.test.ts +69 -0
  328. package/src/server/read-models.test.ts +332 -0
  329. package/src/server/read-models.ts +258 -0
  330. package/src/server/repo-journey.test.ts +96 -0
  331. package/src/server/repo-manager.test.ts +118 -0
  332. package/src/server/repo-manager.ts +97 -0
  333. package/src/server/repo-status.test.ts +54 -0
  334. package/src/server/repo-status.ts +82 -0
  335. package/src/server/restart.test.ts +27 -0
  336. package/src/server/restart.ts +30 -0
  337. package/src/server/runner-incompatible-gate.test.ts +383 -0
  338. package/src/server/runner-manager.test.ts +72 -0
  339. package/src/server/runner-manager.ts +312 -0
  340. package/src/server/runner-pairing-urls.test.ts +59 -0
  341. package/src/server/runner-pairing-urls.ts +67 -0
  342. package/src/server/runner-proxy.test.ts +456 -0
  343. package/src/server/runner-proxy.ts +494 -0
  344. package/src/server/runner-router.test.ts +429 -0
  345. package/src/server/runner-router.ts +212 -0
  346. package/src/server/runner-routing.test.ts +584 -0
  347. package/src/server/runtime-registry.test.ts +436 -0
  348. package/src/server/runtime-registry.ts +307 -0
  349. package/src/server/sandbox-health.test.ts +127 -0
  350. package/src/server/sandbox-health.ts +94 -0
  351. package/src/server/sandbox-journey.test.ts +232 -0
  352. package/src/server/sandbox-manager.test.ts +146 -0
  353. package/src/server/sandbox-manager.ts +159 -0
  354. package/src/server/server.test.ts +287 -0
  355. package/src/server/server.ts +1108 -0
  356. package/src/server/session-discovery.test.ts +129 -0
  357. package/src/server/session-discovery.ts +475 -0
  358. package/src/server/session-index.test.ts +362 -0
  359. package/src/server/session-index.ts +119 -0
  360. package/src/server/session-seed.ts +288 -0
  361. package/src/server/share.test.ts +108 -0
  362. package/src/server/share.ts +113 -0
  363. package/src/server/skill-discovery.test.ts +201 -0
  364. package/src/server/skill-discovery.ts +77 -0
  365. package/src/server/storage/test-helpers.ts +67 -0
  366. package/src/server/terminal-manager.test.ts +309 -0
  367. package/src/server/terminal-manager.ts +354 -0
  368. package/src/server/transcript-consumer.test.ts +339 -0
  369. package/src/server/transcript-consumer.ts +162 -0
  370. package/src/server/transcript-search.test.ts +193 -0
  371. package/src/server/transcript-search.ts +83 -0
  372. package/src/server/transcript-utils.ts +52 -0
  373. package/src/server/update-manager.test.ts +107 -0
  374. package/src/server/update-manager.ts +230 -0
  375. package/src/server/workflow-engine.test.ts +251 -0
  376. package/src/server/workflow-engine.ts +169 -0
  377. package/src/server/workflow-mcp.ts +49 -0
  378. package/src/server/workflow-store.test.ts +155 -0
  379. package/src/server/workflow-store.ts +139 -0
  380. package/src/server/workspace-agent-integration.test.ts +167 -0
  381. package/src/server/workspace-agent-routes.test.ts +127 -0
  382. package/src/server/workspace-agent-routes.ts +89 -0
  383. package/src/server/workspace-agent.test.ts +103 -0
  384. package/src/server/workspace-agent.ts +102 -0
  385. package/src/server/workspace-cli.test.ts +79 -0
  386. package/src/server/workspace-config-manager.test.ts +109 -0
  387. package/src/server/workspace-config-manager.ts +83 -0
  388. package/src/server/workspace-directory-policy.test.ts +109 -0
  389. package/src/server/workspace-directory-policy.ts +56 -0
  390. package/src/shared/agent-config-types.ts +25 -0
  391. package/src/shared/branding.test.ts +42 -0
  392. package/src/shared/branding.ts +54 -0
  393. package/src/shared/compression.test.ts +85 -0
  394. package/src/shared/compression.ts +42 -0
  395. package/src/shared/coordination-store.test.ts +24 -0
  396. package/src/shared/coordination-store.ts +26 -0
  397. package/src/shared/dev-ports.test.ts +84 -0
  398. package/src/shared/dev-ports.ts +100 -0
  399. package/src/shared/extension-types.ts +45 -0
  400. package/src/shared/fork-presets.ts +54 -0
  401. package/src/shared/harness-types.test.ts +15 -0
  402. package/src/shared/harness-types.ts +21 -0
  403. package/src/shared/log-sink.test.ts +112 -0
  404. package/src/shared/log-sink.ts +185 -0
  405. package/src/shared/mention-pattern.ts +7 -0
  406. package/src/shared/merge-presets.ts +41 -0
  407. package/src/shared/nats-subjects.test.ts +61 -0
  408. package/src/shared/nats-subjects.ts +131 -0
  409. package/src/shared/permission-policy.ts +136 -0
  410. package/src/shared/ports.ts +3 -0
  411. package/src/shared/preset-types.ts +15 -0
  412. package/src/shared/profile-types.ts +49 -0
  413. package/src/shared/projectFileUrl.ts +36 -0
  414. package/src/shared/protocol.ts +153 -0
  415. package/src/shared/pty-instance.ts +43 -0
  416. package/src/shared/puggy/diagnostics/result.ts +18 -0
  417. package/src/shared/puggy/expressions/evaluate.ts +292 -0
  418. package/src/shared/puggy/index.test.ts +101 -0
  419. package/src/shared/puggy/index.ts +69 -0
  420. package/src/shared/puggy/parser/ast.ts +110 -0
  421. package/src/shared/puggy/parser/parser.ts +624 -0
  422. package/src/shared/puggy/renderer/html.ts +447 -0
  423. package/src/shared/runner-protocol.test.ts +277 -0
  424. package/src/shared/runner-protocol.ts +210 -0
  425. package/src/shared/runner-team-types.ts +73 -0
  426. package/src/shared/runtime-types.ts +48 -0
  427. package/src/shared/sandbox-types.ts +53 -0
  428. package/src/shared/tailwind-build.test.ts +12 -0
  429. package/src/shared/tinkaria-system-prompt.ts +101 -0
  430. package/src/shared/tools.test.ts +335 -0
  431. package/src/shared/tools.ts +397 -0
  432. package/src/shared/transcript-entries.ts +27 -0
  433. package/src/shared/transcript-render.test.ts +225 -0
  434. package/src/shared/transcript-render.ts +467 -0
  435. package/src/shared/types.ts +1031 -0
  436. package/src/shared/vite-config.test.ts +47 -0
  437. package/src/shared/web-context.test.ts +110 -0
  438. package/src/shared/web-context.ts +76 -0
  439. package/src/shared/workflow-types.ts +51 -0
  440. package/src/shared/workspace-types.ts +100 -0
@@ -0,0 +1,1018 @@
1
+ import { afterEach, describe, test, expect } from "bun:test"
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { NatsServer } from "@lagz0ne/nats-embedded"
6
+ import { connect, type NatsConnection } from "@nats-io/transport-node"
7
+ import { registerCommandResponders, type RegisterRespondersArgs } from "./nats-responders"
8
+ import { commandSubject } from "../shared/nats-subjects"
9
+ import { EventStore } from "./event-store"
10
+
11
+ const encoder = new TextEncoder()
12
+ const decoder = new TextDecoder()
13
+
14
+ function encode(data: unknown): Uint8Array {
15
+ return encoder.encode(JSON.stringify(data))
16
+ }
17
+
18
+ function decode<T>(data: Uint8Array): T {
19
+ return JSON.parse(decoder.decode(data)) as T
20
+ }
21
+
22
+ interface CommandResponse {
23
+ ok: boolean
24
+ result?: unknown
25
+ error?: string
26
+ }
27
+
28
+ // --- Mocks ---
29
+
30
+ function createMockStore() {
31
+ return {
32
+ state: {},
33
+ openProject: async (_path: string, _title?: string) => ({ id: "proj-1" }),
34
+ getProject: (id: string) => (id === "proj-1" ? { id: "proj-1", localPath: "/tmp/test-project" } : null),
35
+ getChat: (id: string) => (id === "chat-1"
36
+ ? { id: "chat-1", workspaceId: "proj-1", provider: "codex", sessionToken: "session-1" }
37
+ : null),
38
+ removeProject: async () => {},
39
+ createChat: async (_workspaceId: string) => ({ id: "chat-1" }),
40
+ renameChat: async () => {},
41
+ deleteChat: async () => {},
42
+ listChatsByProject: () => [{ id: "chat-1" }],
43
+ getMessages: () => [],
44
+ getMessageCount: async () => 0,
45
+ }
46
+ }
47
+
48
+ function createMockAgent() {
49
+ return {
50
+ send: async () => ({ chatId: "chat-1" }),
51
+ queue: async () => ({ chatId: "chat-1", queued: true }),
52
+ cancel: async () => {},
53
+ disposeChat: async () => {},
54
+ respondTool: async () => {},
55
+ getActiveStatuses: () => new Map(),
56
+ }
57
+ }
58
+
59
+ function createMockTerminals() {
60
+ return {
61
+ createTerminal: (opts: { terminalId: string }) => ({
62
+ terminalId: opts.terminalId,
63
+ title: "bash",
64
+ cwd: "/tmp",
65
+ shell: "/bin/bash",
66
+ cols: 80,
67
+ rows: 24,
68
+ scrollback: 1000,
69
+ serializedState: "",
70
+ status: "running",
71
+ exitCode: null,
72
+ }),
73
+ write: () => {},
74
+ resize: () => {},
75
+ close: () => {},
76
+ closeByCwd: () => {},
77
+ getSnapshot: () => null,
78
+ onEvent: () => () => {},
79
+ }
80
+ }
81
+
82
+ function createMockUpdateManager() {
83
+ const snapshot = {
84
+ currentVersion: "1.0.0",
85
+ latestVersion: "1.1.0",
86
+ status: "available" as const,
87
+ updateAvailable: true,
88
+ lastCheckedAt: Date.now(),
89
+ error: null,
90
+ installAction: "restart" as const,
91
+ }
92
+ return {
93
+ checkForUpdates: async () => snapshot,
94
+ installUpdate: async () => ({ success: true, version: "1.1.0" }),
95
+ getSnapshot: () => snapshot,
96
+ onChange: () => () => {},
97
+ }
98
+ }
99
+
100
+ // --- Test infrastructure ---
101
+
102
+ let server: NatsServer | null = null
103
+ let serverNc: NatsConnection | null = null
104
+ let clientNc: NatsConnection | null = null
105
+ let disposeFn: (() => void) | null = null
106
+ let tempDir: string | null = null
107
+
108
+ afterEach(async () => {
109
+ disposeFn?.()
110
+ disposeFn = null
111
+ if (clientNc) {
112
+ await clientNc.drain()
113
+ clientNc = null
114
+ }
115
+ if (serverNc) {
116
+ await serverNc.drain()
117
+ serverNc = null
118
+ }
119
+ if (server) {
120
+ await server.stop()
121
+ server = null
122
+ }
123
+ if (tempDir) {
124
+ await rm(tempDir, { recursive: true, force: true })
125
+ tempDir = null
126
+ }
127
+ })
128
+
129
+ async function setup(overrides?: Partial<RegisterRespondersArgs>) {
130
+ server = await NatsServer.start()
131
+ serverNc = await connect({ servers: server.url })
132
+ clientNc = await connect({ servers: server.url })
133
+
134
+ const args: RegisterRespondersArgs = {
135
+ nc: serverNc,
136
+ store: createMockStore() as never,
137
+ agent: createMockAgent() as never,
138
+ terminals: createMockTerminals() as never,
139
+ refreshDiscovery: async () => [],
140
+ getDiscoveredProjects: () => [],
141
+ machineDisplayName: "Test Machine",
142
+ updateManager: createMockUpdateManager() as never,
143
+ publisher: {
144
+ addSubscription: () => {},
145
+ removeSubscription: () => {},
146
+ getSnapshot: async () => null,
147
+ broadcastSnapshots: async () => {},
148
+ publishChatMessage: () => {},
149
+ dispose: () => {},
150
+ },
151
+ onStateChange: () => {},
152
+ directoryPolicy: null,
153
+ repoManager: null,
154
+ clonePolicy: null,
155
+ workflowEngine: null,
156
+ workflowStore: null,
157
+ sandboxManager: null,
158
+ runtimeRegistry: null,
159
+ ...overrides,
160
+ }
161
+
162
+ const { dispose } = registerCommandResponders(args)
163
+ disposeFn = dispose
164
+
165
+ // Allow subscription to propagate
166
+ await serverNc.flush()
167
+
168
+ return { clientNc: clientNc!, args }
169
+ }
170
+
171
+ async function sendCommand(nc: NatsConnection, command: unknown): Promise<CommandResponse> {
172
+ const msg = await nc.request(commandSubject((command as { type: string }).type), encode(command), { timeout: 2000 })
173
+ return decode<CommandResponse>(msg.data)
174
+ }
175
+
176
+ // --- Tests ---
177
+
178
+ describe("nats-responders", () => {
179
+ test("system.ping responds ok", async () => {
180
+ const { clientNc } = await setup()
181
+ const res = await sendCommand(clientNc, { type: "system.ping" })
182
+ expect(res.ok).toBe(true)
183
+ })
184
+
185
+ test("system.ping does not trigger onStateChange", async () => {
186
+ let changed = false
187
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
188
+ await sendCommand(clientNc, { type: "system.ping" })
189
+ expect(changed).toBe(false)
190
+ })
191
+
192
+
193
+ test("system.readLocalFilePreview returns local file content", async () => {
194
+ tempDir = await mkdtemp(path.join(tmpdir(), "kanna-preview-"))
195
+ const filePath = path.join(tempDir, "README.md")
196
+ await writeFile(filePath, "# Preview\n")
197
+
198
+ const { clientNc } = await setup()
199
+ const res = await sendCommand(clientNc, { type: "system.readLocalFilePreview", localPath: filePath })
200
+
201
+ expect(res.ok).toBe(true)
202
+ expect(res.result).toEqual({
203
+ localPath: filePath,
204
+ content: "# Preview\n",
205
+ })
206
+ })
207
+
208
+ test("system.readLocalFilePreview does not trigger onStateChange", async () => {
209
+ tempDir = await mkdtemp(path.join(tmpdir(), "kanna-preview-"))
210
+ const filePath = path.join(tempDir, "README.md")
211
+ await writeFile(filePath, "# Preview\n")
212
+
213
+ let changed = false
214
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
215
+ await sendCommand(clientNc, { type: "system.readLocalFilePreview", localPath: filePath })
216
+ expect(changed).toBe(false)
217
+ })
218
+
219
+ test("chat.getSessionRuntime returns null when the session file cannot be inspected", async () => {
220
+ const { clientNc } = await setup()
221
+ const res = await sendCommand(clientNc, { type: "chat.getSessionRuntime", chatId: "chat-1" })
222
+
223
+ expect(res.ok).toBe(true)
224
+ expect(res.result).toEqual({ runtime: null })
225
+ })
226
+
227
+ test("chat.getSessionRuntime does not trigger onStateChange", async () => {
228
+ let changed = false
229
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
230
+ await sendCommand(clientNc, { type: "chat.getSessionRuntime", chatId: "chat-1" })
231
+ expect(changed).toBe(false)
232
+ })
233
+
234
+ test("chat.generateForkPrompt returns a derived prompt from transcript context", async () => {
235
+ const generateCalls: Array<{ intent: string; entries: unknown[]; cwd: string; preset?: string }> = []
236
+ const { clientNc } = await setup({
237
+ store: {
238
+ ...createMockStore(),
239
+ getMessages: () => [
240
+ { kind: "user_prompt", content: "Investigate auth race", _id: "1", createdAt: 1 },
241
+ { kind: "assistant_text", text: "Likely around session restore", _id: "2", createdAt: 2 },
242
+ ],
243
+ } as never,
244
+ generateForkPrompt: async (intent, entries, cwd, preset) => {
245
+ generateCalls.push({ intent, entries, cwd, preset })
246
+ return "## Objective\nFix the auth race"
247
+ },
248
+ })
249
+
250
+ const res = await sendCommand(clientNc, {
251
+ type: "chat.generateForkPrompt",
252
+ chatId: "chat-1",
253
+ intent: "Focus on the regression test",
254
+ preset: "tests",
255
+ })
256
+
257
+ expect(res.ok).toBe(true)
258
+ expect(res.result).toEqual({ prompt: "## Objective\nFix the auth race" })
259
+ expect(generateCalls).toEqual([
260
+ {
261
+ intent: "Focus on the regression test",
262
+ entries: [
263
+ { kind: "user_prompt", content: "Investigate auth race", _id: "1", createdAt: 1 },
264
+ { kind: "assistant_text", text: "Likely around session restore", _id: "2", createdAt: 2 },
265
+ ],
266
+ cwd: "/tmp/test-project",
267
+ preset: "tests",
268
+ },
269
+ ])
270
+ })
271
+
272
+ test("chat.generateMergePrompt returns a synthesized prompt from multiple sessions", async () => {
273
+ const mergeCalls: Array<{ intent: string; sessions: Array<{ chatId: string; entries: unknown[] }>; cwd: string; preset?: string }> = []
274
+ const multiChatStore = {
275
+ ...createMockStore(),
276
+ getChat: (id: string) => {
277
+ if (id === "chat-1" || id === "chat-2") return { id, workspaceId: "proj-1", provider: "claude", sessionToken: `session-${id}` }
278
+ return null
279
+ },
280
+ getMessages: (chatId: string) => {
281
+ if (chatId === "chat-1") return [{ kind: "user_prompt", content: "Session 1 work", _id: "1", createdAt: 1 }]
282
+ if (chatId === "chat-2") return [{ kind: "assistant_text", text: "Session 2 output", _id: "2", createdAt: 2 }]
283
+ return []
284
+ },
285
+ }
286
+
287
+ const { clientNc } = await setup({
288
+ store: multiChatStore as never,
289
+ generateMergePrompt: async (intent, sessions, cwd, preset) => {
290
+ mergeCalls.push({ intent, sessions, cwd, preset })
291
+ return "## Merged\nCombined context from sessions"
292
+ },
293
+ })
294
+
295
+ const res = await sendCommand(clientNc, {
296
+ type: "chat.generateMergePrompt",
297
+ chatIds: ["chat-1", "chat-2"],
298
+ intent: "Synthesize findings",
299
+ preset: "synthesis",
300
+ })
301
+
302
+ expect(res.ok).toBe(true)
303
+ expect(res.result).toEqual({ prompt: "## Merged\nCombined context from sessions" })
304
+ expect(mergeCalls).toHaveLength(1)
305
+ expect(mergeCalls[0]!.intent).toBe("Synthesize findings")
306
+ expect(mergeCalls[0]!.sessions).toHaveLength(2)
307
+ expect(mergeCalls[0]!.cwd).toBe("/tmp/test-project")
308
+ expect(mergeCalls[0]!.preset).toBe("synthesis")
309
+ })
310
+
311
+ test("chat.generateMergePrompt rejects empty chatIds", async () => {
312
+ const { clientNc } = await setup({
313
+ generateMergePrompt: async () => "should not reach",
314
+ })
315
+
316
+ const res = await sendCommand(clientNc, {
317
+ type: "chat.generateMergePrompt",
318
+ chatIds: [],
319
+ intent: "Merge this",
320
+ })
321
+
322
+ expect(res.ok).toBe(false)
323
+ expect(res.error).toContain("At least 1 session")
324
+ })
325
+
326
+ test("chat.generateMergePrompt does not trigger onStateChange", async () => {
327
+ let changed = false
328
+ const multiChatStore = {
329
+ ...createMockStore(),
330
+ getChat: (id: string) => {
331
+ if (id === "chat-1" || id === "chat-2") return { id, workspaceId: "proj-1", provider: "claude", sessionToken: `session-${id}` }
332
+ return null
333
+ },
334
+ }
335
+ const { clientNc } = await setup({
336
+ store: multiChatStore as never,
337
+ onStateChange: () => { changed = true },
338
+ generateMergePrompt: async () => "merge seed",
339
+ })
340
+ await sendCommand(clientNc, { type: "chat.generateMergePrompt", chatIds: ["chat-1", "chat-2"], intent: "Merge these" })
341
+ expect(changed).toBe(false)
342
+ })
343
+
344
+ test("chat.generateForkPrompt does not trigger onStateChange", async () => {
345
+ let changed = false
346
+ const { clientNc } = await setup({
347
+ onStateChange: () => { changed = true },
348
+ generateForkPrompt: async () => "fork seed",
349
+ })
350
+ await sendCommand(clientNc, { type: "chat.generateForkPrompt", chatId: "chat-1", intent: "Fork this work" })
351
+ expect(changed).toBe(false)
352
+ })
353
+
354
+ test("chat.getRepoStatus returns repo status for the active project", async () => {
355
+ tempDir = await mkdtemp(path.join(tmpdir(), "kanna-repo-status-"))
356
+ const { clientNc } = await setup({
357
+ store: {
358
+ ...createMockStore(),
359
+ getProject: (id: string) => (id === "proj-1" ? { id: "proj-1", localPath: tempDir! } : null),
360
+ } as never,
361
+ })
362
+
363
+ const res = await sendCommand(clientNc, { type: "chat.getRepoStatus", chatId: "chat-1" })
364
+
365
+ expect(res.ok).toBe(true)
366
+ expect(res.result).toEqual({
367
+ repoStatus: {
368
+ localPath: tempDir,
369
+ branch: null,
370
+ stagedCount: 0,
371
+ unstagedCount: 0,
372
+ untrackedCount: 0,
373
+ ahead: 0,
374
+ behind: 0,
375
+ isRepo: false,
376
+ },
377
+ })
378
+ })
379
+
380
+ test("chat.getRepoStatus does not trigger onStateChange", async () => {
381
+ let changed = false
382
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
383
+ await sendCommand(clientNc, { type: "chat.getRepoStatus", chatId: "chat-1" })
384
+ expect(changed).toBe(false)
385
+ })
386
+
387
+
388
+ test("update.check returns snapshot when manager exists", async () => {
389
+ const { clientNc } = await setup()
390
+ const res = await sendCommand(clientNc, { type: "update.check" })
391
+ expect(res.ok).toBe(true)
392
+ const result = res.result as { currentVersion: string; updateAvailable: boolean }
393
+ expect(result.currentVersion).toBe("1.0.0")
394
+ expect(result.updateAvailable).toBe(true)
395
+ })
396
+
397
+ test("update.check returns fallback when manager is null", async () => {
398
+ const { clientNc } = await setup({ updateManager: null })
399
+ const res = await sendCommand(clientNc, { type: "update.check" })
400
+ expect(res.ok).toBe(true)
401
+ const result = res.result as { currentVersion: string; status: string }
402
+ expect(result.currentVersion).toBe("unknown")
403
+ expect(result.status).toBe("error")
404
+ })
405
+
406
+ test("update.check does not trigger onStateChange", async () => {
407
+ let changed = false
408
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
409
+ await sendCommand(clientNc, { type: "update.check" })
410
+ expect(changed).toBe(false)
411
+ })
412
+
413
+ test("update.install returns result", async () => {
414
+ const { clientNc } = await setup()
415
+ const res = await sendCommand(clientNc, { type: "update.install" })
416
+ expect(res.ok).toBe(true)
417
+ expect(res.result).toEqual({ success: true, version: "1.1.0" })
418
+ })
419
+
420
+ test("update.install errors when manager is null", async () => {
421
+ const { clientNc } = await setup({ updateManager: null })
422
+ const res = await sendCommand(clientNc, { type: "update.install" })
423
+ expect(res.ok).toBe(false)
424
+ expect(res.error).toBe("Update manager unavailable.")
425
+ })
426
+
427
+ test("chat.create returns chatId and triggers onStateChange", async () => {
428
+ let changed = false
429
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
430
+ const res = await sendCommand(clientNc, { type: "chat.create", workspaceId: "proj-1" })
431
+ expect(res.ok).toBe(true)
432
+ expect(res.result).toEqual({ chatId: "chat-1" })
433
+ expect(changed).toBe(true)
434
+ })
435
+
436
+ test("chat.create forwards a client-supplied optimistic chatId", async () => {
437
+ let seenWorkspaceId: string | null = null
438
+ let seenChatId: string | null = null
439
+ const { clientNc } = await setup({
440
+ store: {
441
+ ...createMockStore(),
442
+ createChat: async (workspaceId: string, _repoId?: string, chatId?: string) => {
443
+ seenWorkspaceId = workspaceId
444
+ seenChatId = chatId ?? null
445
+ return { id: chatId ?? "chat-1", workspaceId }
446
+ },
447
+ } as never,
448
+ })
449
+
450
+ const res = await sendCommand(clientNc, {
451
+ type: "chat.create",
452
+ workspaceId: "proj-1",
453
+ chatId: "chat-optimistic-1",
454
+ })
455
+
456
+ expect(res.ok).toBe(true)
457
+ expect(res.result).toEqual({ chatId: "chat-optimistic-1" })
458
+ if (seenWorkspaceId === null || seenChatId === null) {
459
+ throw new Error("expected chat.create to forward the optimistic identifiers")
460
+ }
461
+ expect(String(seenWorkspaceId)).toBe("proj-1")
462
+ expect(String(seenChatId)).toBe("chat-optimistic-1")
463
+ })
464
+
465
+ test("chat.rename triggers onStateChange", async () => {
466
+ let changed = false
467
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
468
+ const res = await sendCommand(clientNc, { type: "chat.rename", chatId: "chat-1", title: "New Title" })
469
+ expect(res.ok).toBe(true)
470
+ expect(changed).toBe(true)
471
+ })
472
+
473
+ test("chat.delete disposes agent runtime state then deletes", async () => {
474
+ const disposed: string[] = []
475
+ const deleted: string[] = []
476
+ const mockAgent = {
477
+ ...createMockAgent(),
478
+ disposeChat: async (id: string) => { disposed.push(id) },
479
+ }
480
+ const mockStore = {
481
+ ...createMockStore(),
482
+ deleteChat: async (id: string) => { deleted.push(id) },
483
+ }
484
+ const { clientNc } = await setup({
485
+ agent: mockAgent as never,
486
+ store: mockStore as never,
487
+ })
488
+ const res = await sendCommand(clientNc, { type: "chat.delete", chatId: "chat-99" })
489
+ expect(res.ok).toBe(true)
490
+ expect(disposed).toEqual(["chat-99"])
491
+ expect(deleted).toEqual(["chat-99"])
492
+ })
493
+
494
+ test("chat.send returns agent result", async () => {
495
+ const { clientNc } = await setup()
496
+ const res = await sendCommand(clientNc, {
497
+ type: "chat.send",
498
+ chatId: "chat-1",
499
+ content: "Hello",
500
+ })
501
+ expect(res.ok).toBe(true)
502
+ expect(res.result).toEqual({ chatId: "chat-1" })
503
+ })
504
+
505
+ test("chat.queue returns agent queue result", async () => {
506
+ const queued: unknown[] = []
507
+ const mockAgent = {
508
+ ...createMockAgent(),
509
+ queue: async (cmd: unknown) => {
510
+ queued.push(cmd)
511
+ return { chatId: "chat-1", queued: true }
512
+ },
513
+ }
514
+ const { clientNc } = await setup({ agent: mockAgent as never })
515
+ const res = await sendCommand(clientNc, {
516
+ type: "chat.queue",
517
+ chatId: "chat-1",
518
+ content: "Follow-up",
519
+ })
520
+ expect(res.ok).toBe(true)
521
+ expect(res.result).toEqual({ chatId: "chat-1", queued: true })
522
+ expect(queued).toEqual([{
523
+ type: "chat.queue",
524
+ chatId: "chat-1",
525
+ content: "Follow-up",
526
+ }])
527
+ })
528
+
529
+ test("chat.cancel calls agent.cancel", async () => {
530
+ const cancelled: string[] = []
531
+ const mockAgent = {
532
+ ...createMockAgent(),
533
+ cancel: async (id: string) => { cancelled.push(id) },
534
+ }
535
+ const { clientNc } = await setup({ agent: mockAgent as never })
536
+ const res = await sendCommand(clientNc, { type: "chat.cancel", chatId: "chat-5" })
537
+ expect(res.ok).toBe(true)
538
+ expect(cancelled).toEqual(["chat-5"])
539
+ })
540
+
541
+ test("chat.respondTool calls agent.respondTool", async () => {
542
+ const responses: unknown[] = []
543
+ const mockAgent = {
544
+ ...createMockAgent(),
545
+ respondTool: async (cmd: unknown) => { responses.push(cmd) },
546
+ }
547
+ const { clientNc } = await setup({ agent: mockAgent as never })
548
+ const res = await sendCommand(clientNc, {
549
+ type: "chat.respondTool",
550
+ chatId: "chat-1",
551
+ toolUseId: "tool-1",
552
+ result: { accepted: true },
553
+ })
554
+ expect(res.ok).toBe(true)
555
+ expect(responses).toHaveLength(1)
556
+ })
557
+
558
+ // ── PR5: chat.selectRunner ────────────────────────────────────────────────
559
+
560
+ test("chat.selectRunner calls store.setChatRunner and returns ok", async () => {
561
+ const calls: Array<{ chatId: string; runnerId: string | null }> = []
562
+ const mockStore = {
563
+ ...createMockStore(),
564
+ setChatRunner: async (chatId: string, runnerId: string | null) => {
565
+ calls.push({ chatId, runnerId })
566
+ },
567
+ }
568
+ const { clientNc } = await setup({ store: mockStore as never })
569
+ const res = await sendCommand(clientNc, {
570
+ type: "chat.selectRunner",
571
+ chatId: "chat-1",
572
+ runnerId: "runner-abc",
573
+ })
574
+ expect(res.ok).toBe(true)
575
+ expect(calls).toEqual([{ chatId: "chat-1", runnerId: "runner-abc" }])
576
+ })
577
+
578
+ test("chat.send maps RunnerPickRequired to needsPick result", async () => {
579
+ const { RunnerPickRequired } = await import("./runner-proxy")
580
+ const mockAgent = {
581
+ ...createMockAgent(),
582
+ send: async () => {
583
+ throw new RunnerPickRequired({
584
+ chatId: "chat-pick",
585
+ candidates: [
586
+ { runnerId: "runner-A", state: "online", capabilities: null, isShared: true, incompatible: false, protocolVersion: 1, lastSeenAt: null, pid: null, ownerId: null },
587
+ ],
588
+ reason: "ambiguous",
589
+ })
590
+ },
591
+ }
592
+ const { clientNc } = await setup({ agent: mockAgent as never })
593
+ const res = await sendCommand(clientNc, {
594
+ type: "chat.send",
595
+ chatId: "chat-pick",
596
+ content: "Hello",
597
+ })
598
+ // Must be ok:true with needsPick structure (not ok:false)
599
+ expect(res.ok).toBe(true)
600
+ const result = res.result as { needsPick: boolean; chatId: string; candidates: unknown[]; reason: string }
601
+ expect(result.needsPick).toBe(true)
602
+ expect(result.chatId).toBe("chat-pick")
603
+ expect(result.reason).toBe("ambiguous")
604
+ expect(Array.isArray(result.candidates)).toBe(true)
605
+ expect(result.candidates).toHaveLength(1)
606
+ })
607
+
608
+ test("chat.queue maps RunnerPickRequired to needsPick result", async () => {
609
+ const { RunnerPickRequired } = await import("./runner-proxy")
610
+ const mockAgent = {
611
+ ...createMockAgent(),
612
+ queue: async () => {
613
+ throw new RunnerPickRequired({
614
+ chatId: "chat-pick",
615
+ candidates: [],
616
+ reason: "sticky_offline",
617
+ })
618
+ },
619
+ }
620
+ const { clientNc } = await setup({ agent: mockAgent as never })
621
+ const res = await sendCommand(clientNc, {
622
+ type: "chat.queue",
623
+ chatId: "chat-pick",
624
+ content: "Follow-up",
625
+ })
626
+ expect(res.ok).toBe(true)
627
+ const result = res.result as { needsPick: boolean; reason: string }
628
+ expect(result.needsPick).toBe(true)
629
+ expect(result.reason).toBe("sticky_offline")
630
+ })
631
+
632
+ test("chat.send still propagates non-RunnerPickRequired errors as ok:false", async () => {
633
+ const mockAgent = {
634
+ ...createMockAgent(),
635
+ send: async () => { throw new Error("runner unavailable") },
636
+ }
637
+ const { clientNc } = await setup({ agent: mockAgent as never })
638
+ const res = await sendCommand(clientNc, {
639
+ type: "chat.send",
640
+ chatId: "chat-1",
641
+ content: "Hello",
642
+ })
643
+ expect(res.ok).toBe(false)
644
+ expect(res.error).toBe("runner unavailable")
645
+ })
646
+
647
+ // ─────────────────────────────────────────────────────────────────────────
648
+
649
+ test("project.open returns workspaceId and triggers onStateChange", async () => {
650
+ let changed = false
651
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
652
+ const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/test-project" })
653
+ expect(res.ok).toBe(true)
654
+ expect(res.result).toEqual({ workspaceId: "proj-1" })
655
+ expect(changed).toBe(true)
656
+ })
657
+
658
+ test("project.create returns workspaceId", async () => {
659
+ const { clientNc } = await setup()
660
+ const res = await sendCommand(clientNc, {
661
+ type: "project.create",
662
+ localPath: "/tmp/test-project",
663
+ title: "Test",
664
+ })
665
+ expect(res.ok).toBe(true)
666
+ expect(res.result).toEqual({ workspaceId: "proj-1" })
667
+ })
668
+
669
+ test("project.remove disposes chats and triggers onStateChange", async () => {
670
+ let changed = false
671
+ const disposed: string[] = []
672
+ const mockAgent = {
673
+ ...createMockAgent(),
674
+ disposeChat: async (id: string) => { disposed.push(id) },
675
+ }
676
+ const { clientNc } = await setup({
677
+ agent: mockAgent as never,
678
+ onStateChange: () => { changed = true },
679
+ })
680
+ const res = await sendCommand(clientNc, { type: "project.remove", workspaceId: "proj-1" })
681
+ expect(res.ok).toBe(true)
682
+ expect(disposed).toContain("chat-1")
683
+ expect(changed).toBe(true)
684
+ })
685
+
686
+ test("terminal.create returns snapshot", async () => {
687
+ const { clientNc } = await setup()
688
+ const res = await sendCommand(clientNc, {
689
+ type: "terminal.create",
690
+ workspaceId: "proj-1",
691
+ terminalId: "term-1",
692
+ cols: 80,
693
+ rows: 24,
694
+ scrollback: 1000,
695
+ })
696
+ expect(res.ok).toBe(true)
697
+ const result = res.result as { terminalId: string }
698
+ expect(result.terminalId).toBe("term-1")
699
+ })
700
+
701
+ test("terminal.create errors for missing project", async () => {
702
+ const { clientNc } = await setup()
703
+ const res = await sendCommand(clientNc, {
704
+ type: "terminal.create",
705
+ workspaceId: "nonexistent",
706
+ terminalId: "term-1",
707
+ cols: 80,
708
+ rows: 24,
709
+ scrollback: 1000,
710
+ })
711
+ expect(res.ok).toBe(false)
712
+ expect(res.error).toBe("Project not found")
713
+ })
714
+
715
+ test("terminal.input does not trigger onStateChange", async () => {
716
+ let changed = false
717
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
718
+ const res = await sendCommand(clientNc, { type: "terminal.input", terminalId: "term-1", data: "ls\n" })
719
+ expect(res.ok).toBe(true)
720
+ expect(changed).toBe(false)
721
+ })
722
+
723
+ test("terminal.resize does not trigger onStateChange", async () => {
724
+ let changed = false
725
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
726
+ const res = await sendCommand(clientNc, { type: "terminal.resize", terminalId: "term-1", cols: 120, rows: 40 })
727
+ expect(res.ok).toBe(true)
728
+ expect(changed).toBe(false)
729
+ })
730
+
731
+ test("terminal.close triggers onStateChange (publishes null snapshot)", async () => {
732
+ let changed = false
733
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
734
+ const res = await sendCommand(clientNc, { type: "terminal.close", terminalId: "term-1" })
735
+ expect(res.ok).toBe(true)
736
+ expect(changed).toBe(true)
737
+ })
738
+
739
+ test("invalid JSON payload returns error", async () => {
740
+ const { clientNc } = await setup()
741
+ const msg = await clientNc.request(
742
+ commandSubject("system.ping"),
743
+ encoder.encode("not json{{{"),
744
+ { timeout: 2000 },
745
+ )
746
+ const res = decode<CommandResponse>(msg.data)
747
+ expect(res.ok).toBe(false)
748
+ expect(res.error).toBe("Invalid JSON payload")
749
+ })
750
+
751
+ test("missing command type returns error", async () => {
752
+ const { clientNc } = await setup()
753
+ const msg = await clientNc.request(
754
+ commandSubject("system.ping"),
755
+ encode({ foo: "bar" }),
756
+ { timeout: 2000 },
757
+ )
758
+ const res = decode<CommandResponse>(msg.data)
759
+ expect(res.ok).toBe(false)
760
+ expect(res.error).toBe("Missing command type")
761
+ })
762
+
763
+ test("command handler error returns ok:false with message", async () => {
764
+ const mockStore = {
765
+ ...createMockStore(),
766
+ openProject: async () => { throw new Error("Disk full") },
767
+ }
768
+ const { clientNc } = await setup({ store: mockStore as never })
769
+ const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/fail" })
770
+ expect(res.ok).toBe(false)
771
+ expect(res.error).toBe("Disk full")
772
+ })
773
+
774
+ test("non-Error throw returns stringified message", async () => {
775
+ const mockStore = {
776
+ ...createMockStore(),
777
+ openProject: async () => { throw "string error" },
778
+ }
779
+ const { clientNc } = await setup({ store: mockStore as never })
780
+ const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/fail" })
781
+ expect(res.ok).toBe(false)
782
+ expect(res.error).toBe("string error")
783
+ })
784
+
785
+ test("dispose unsubscribes from commands", async () => {
786
+ const { clientNc } = await setup()
787
+
788
+ // Verify it works before dispose
789
+ const before = await sendCommand(clientNc, { type: "system.ping" })
790
+ expect(before.ok).toBe(true)
791
+
792
+ // Dispose and verify requests timeout
793
+ disposeFn?.()
794
+ disposeFn = null
795
+
796
+ try {
797
+ await clientNc.request(commandSubject("system.ping"), encode({ type: "system.ping" }), { timeout: 300 })
798
+ expect(true).toBe(false) // should not reach
799
+ } catch (error) {
800
+ expect(error).toBeDefined()
801
+ }
802
+ })
803
+
804
+ test("multiple commands in sequence", async () => {
805
+ let changeCount = 0
806
+ const { clientNc } = await setup({ onStateChange: () => { changeCount++ } })
807
+
808
+ const ping = await sendCommand(clientNc, { type: "system.ping" })
809
+ expect(ping.ok).toBe(true)
810
+
811
+ const create = await sendCommand(clientNc, { type: "chat.create", workspaceId: "proj-1" })
812
+ expect(create.ok).toBe(true)
813
+
814
+ const read = await sendCommand(clientNc, { type: "update.check" })
815
+ expect(read.ok).toBe(true)
816
+
817
+ // Only chat.create should have triggered onStateChange
818
+ expect(changeCount).toBe(1)
819
+ })
820
+
821
+ test("snapshot.subscribe registers subscription and returns snapshot", async () => {
822
+ let addCalled = false
823
+ let addedId = ""
824
+ const mockPublisher = {
825
+ addSubscription: (id: string) => { addCalled = true; addedId = id },
826
+ removeSubscription: () => {},
827
+ getSnapshot: async () => ({ mock: "snapshot" }),
828
+ broadcastSnapshots: async () => {},
829
+ publishChatMessage: () => {},
830
+ refreshSessions: async () => {},
831
+ dispose: () => {},
832
+ }
833
+ const { clientNc } = await setup({ publisher: mockPublisher })
834
+ const res = await sendCommand(clientNc, {
835
+ type: "snapshot.subscribe",
836
+ subscriptionId: "sub-1",
837
+ topic: { type: "sidebar" },
838
+ })
839
+ expect(res.ok).toBe(true)
840
+ expect(res.result).toEqual({ mock: "snapshot" })
841
+ expect(addCalled).toBe(true)
842
+ expect(addedId).toBe("sub-1")
843
+ })
844
+
845
+ test("chat.getMessageCount returns the persisted transcript length", async () => {
846
+ const { clientNc } = await setup({
847
+ store: {
848
+ ...createMockStore(),
849
+ getMessageCount: async (chatId: string) => (chatId === "chat-1" ? 3 : 0),
850
+ } as never,
851
+ })
852
+
853
+ const res = await sendCommand(clientNc, {
854
+ type: "chat.getMessageCount",
855
+ chatId: "chat-1",
856
+ })
857
+
858
+ expect(res.ok).toBe(true)
859
+ expect(res.result).toEqual({ messageCount: 3 })
860
+ })
861
+
862
+ test("chat.getRenderUnits returns folded transcript render units", async () => {
863
+ const { clientNc } = await setup({
864
+ store: {
865
+ ...createMockStore(),
866
+ getMessages: async () => [
867
+ { _id: "e1", kind: "assistant_text", createdAt: 1, text: "Checking" },
868
+ {
869
+ _id: "e2",
870
+ kind: "tool_call",
871
+ createdAt: 2,
872
+ tool: {
873
+ kind: "tool",
874
+ toolKind: "bash",
875
+ toolName: "Bash",
876
+ toolId: "tool-1",
877
+ input: { command: "pwd" },
878
+ },
879
+ },
880
+ { _id: "e3", kind: "assistant_text", createdAt: 3, text: "Done" },
881
+ ],
882
+ } as never,
883
+ })
884
+
885
+ const res = await sendCommand(clientNc, {
886
+ type: "chat.getRenderUnits",
887
+ chatId: "chat-1",
888
+ offset: 0,
889
+ limit: 3,
890
+ })
891
+
892
+ expect(res.ok).toBe(true)
893
+ expect((res.result as Array<{ id: string; kind: string }>).map((unit) => [unit.id, unit.kind])).toEqual([
894
+ ["wip:e1:e2", "wip_block"],
895
+ ["assistant_response:e3", "assistant_response"],
896
+ ])
897
+ })
898
+
899
+ test("chat.getExternalSessionMessages returns transcript entries for a provider session", async () => {
900
+ const sessionId = `responders-codex-${Date.now()}`
901
+ const codexSessionsDir = path.join(process.env.HOME ?? "", ".codex", "sessions")
902
+ await mkdir(codexSessionsDir, { recursive: true })
903
+ await writeFile(path.join(codexSessionsDir, `${sessionId}.jsonl`), [
904
+ JSON.stringify({ type: "session_meta", payload: { id: sessionId, cwd: "/tmp/test-project" } }),
905
+ JSON.stringify({ timestamp: "2026-04-13T12:00:00.000Z", type: "event_msg", payload: { type: "agent_message", message: "Loaded external transcript" } }),
906
+ ].join("\n") + "\n")
907
+
908
+ const { clientNc } = await setup()
909
+ const res = await sendCommand(clientNc, {
910
+ type: "chat.getExternalSessionMessages",
911
+ parentChatId: "chat-1",
912
+ sessionId,
913
+ })
914
+
915
+ expect(res.ok).toBe(true)
916
+ expect(res.result).toEqual([
917
+ expect.objectContaining({ kind: "assistant_text", text: "Loaded external transcript" }),
918
+ ])
919
+ })
920
+
921
+ test("snapshot.unsubscribe removes subscription", async () => {
922
+ let removedId = ""
923
+ const mockPublisher = {
924
+ addSubscription: () => {},
925
+ removeSubscription: (id: string) => { removedId = id },
926
+ getSnapshot: async () => null,
927
+ broadcastSnapshots: async () => {},
928
+ publishChatMessage: () => {},
929
+ refreshSessions: async () => {},
930
+ dispose: () => {},
931
+ }
932
+ const { clientNc } = await setup({ publisher: mockPublisher })
933
+ const res = await sendCommand(clientNc, {
934
+ type: "snapshot.unsubscribe",
935
+ subscriptionId: "sub-1",
936
+ })
937
+ expect(res.ok).toBe(true)
938
+ expect(removedId).toBe("sub-1")
939
+ })
940
+
941
+ test("snapshot.subscribe does not trigger onStateChange", async () => {
942
+ let changed = false
943
+ const { clientNc } = await setup({ onStateChange: () => { changed = true } })
944
+ await sendCommand(clientNc, {
945
+ type: "snapshot.subscribe",
946
+ subscriptionId: "sub-1",
947
+ topic: { type: "sidebar" },
948
+ })
949
+ expect(changed).toBe(false)
950
+ })
951
+
952
+ test("snapshot.subscribe with local-projects triggers refresh", async () => {
953
+ let refreshed = false
954
+ const { clientNc } = await setup({
955
+ refreshDiscovery: async () => { refreshed = true; return [] },
956
+ })
957
+ await sendCommand(clientNc, {
958
+ type: "snapshot.subscribe",
959
+ subscriptionId: "sub-lp",
960
+ topic: { type: "local-workspaces" },
961
+ })
962
+ expect(refreshed).toBe(true)
963
+ })
964
+ })
965
+
966
+ describe("nats-responders runner team (US-RTN)", () => {
967
+ // Uses a real EventStore so the command → store → state round-trip is exercised.
968
+ async function setupWithStore() {
969
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-rt-resp-"))
970
+ tempDir = dir
971
+ const store = new EventStore(dir)
972
+ await store.initialize()
973
+ let stateChanges = 0
974
+ const { clientNc } = await setup({
975
+ store: store as never,
976
+ onStateChange: () => { stateChanges += 1 },
977
+ })
978
+ return { clientNc, store, stateChanges: () => stateChanges }
979
+ }
980
+
981
+ test("team.member.save persists and team.member.list returns it", async () => {
982
+ const { clientNc, store } = await setupWithStore()
983
+ const save = await sendCommand(clientNc, { type: "team.member.save", member: { id: "m1", name: "Alice" } })
984
+ expect(save.ok).toBe(true)
985
+ expect(store.state.teamMembers.get("m1")?.name).toBe("Alice")
986
+
987
+ const list = await sendCommand(clientNc, { type: "team.member.list" })
988
+ expect(list.ok).toBe(true)
989
+ expect((list.result as { members: { id: string; name: string }[] }).members).toEqual([{ id: "m1", name: "Alice" }])
990
+ })
991
+
992
+ test("runner.label.set persists name + assignment and triggers a state change", async () => {
993
+ const { clientNc, store, stateChanges } = await setupWithStore()
994
+ const before = stateChanges()
995
+ const res = await sendCommand(clientNc, { type: "runner.label.set", runnerId: "runner-1", name: "Studio Mac", memberId: "m1" })
996
+ expect(res.ok).toBe(true)
997
+ expect(store.state.runnerLabels.get("runner-1")?.name).toBe("Studio Mac")
998
+ expect(store.state.runnerLabels.get("runner-1")?.memberId).toBe("m1")
999
+ // Mutating command must republish snapshots.
1000
+ expect(stateChanges()).toBe(before + 1)
1001
+ })
1002
+
1003
+ test("team.member.remove unassigns runners", async () => {
1004
+ const { clientNc, store } = await setupWithStore()
1005
+ await sendCommand(clientNc, { type: "team.member.save", member: { id: "m1", name: "Alice" } })
1006
+ await sendCommand(clientNc, { type: "runner.label.set", runnerId: "runner-1", name: "Box", memberId: "m1" })
1007
+ await sendCommand(clientNc, { type: "team.member.remove", memberId: "m1" })
1008
+ expect(store.state.teamMembers.get("m1")).toBeUndefined()
1009
+ expect(store.state.runnerLabels.get("runner-1")?.memberId).toBeNull()
1010
+ })
1011
+
1012
+ test("team.member.list is non-mutating (no state change)", async () => {
1013
+ const { clientNc, stateChanges } = await setupWithStore()
1014
+ const before = stateChanges()
1015
+ await sendCommand(clientNc, { type: "team.member.list" })
1016
+ expect(stateChanges()).toBe(before)
1017
+ })
1018
+ })