shadow-claw 1.23.3 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +42 -5
  2. package/bin/assert-version-bump.mjs +60 -38
  3. package/bin/assert-version-bump.test.mjs +74 -11
  4. package/bin/cli.mjs +166 -0
  5. package/bin/cli.test.mjs +60 -0
  6. package/bin/commands/backup.mjs +70 -0
  7. package/bin/commands/clients.mjs +41 -0
  8. package/bin/commands/peer-id.mjs +76 -0
  9. package/bin/commands/peer-id.test.mjs +104 -0
  10. package/bin/commands/send.mjs +51 -0
  11. package/bin/commands/tasks.mjs +59 -0
  12. package/bin/commands/webrtc-listen.mjs +254 -0
  13. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  14. package/bin/rolldown.config.test.mjs +401 -8
  15. package/bin/utils/control-client.mjs +243 -0
  16. package/bin/utils/control-client.test.mjs +83 -0
  17. package/bin/utils/ulid.mjs +45 -0
  18. package/bin/utils/webrtc-control-client.mjs +699 -0
  19. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  20. package/dist/electron/main.cjs +1 -1
  21. package/dist/public/AGENTS.md +5 -3
  22. package/dist/public/README.md +42 -5
  23. package/dist/public/agent.worker.js +6 -6
  24. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  25. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  26. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  27. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  28. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  29. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  30. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  31. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  32. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  33. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  34. package/dist/public/config-CrHchneq.js +1 -0
  35. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  36. package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
  37. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
  38. package/dist/public/docs/README.md +3 -1
  39. package/dist/public/docs/architecture/overview.md +12 -12
  40. package/dist/public/docs/architecture/storage.md +5 -2
  41. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  42. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
  43. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  44. package/dist/public/docs/example/article/index.html +2 -2
  45. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  46. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  47. package/dist/public/docs/publishing/index.html +19 -76
  48. package/dist/public/docs/skill-creator/index.html +2 -2
  49. package/dist/public/docs/subsystems/backup.md +56 -0
  50. package/dist/public/docs/subsystems/cli.md +124 -0
  51. package/dist/public/docs/subsystems/control-plane.md +158 -0
  52. package/dist/public/docs/subsystems/notifications.md +54 -2
  53. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  54. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  55. package/dist/public/e2e/README.md +5 -1
  56. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
  57. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  58. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  59. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  60. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  61. package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
  62. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  63. package/dist/public/index.html +2 -2
  64. package/dist/public/index.js +1 -1
  65. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  66. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  67. package/dist/public/llms.txt +1 -1
  68. package/dist/public/main/index.html +2 -2
  69. package/dist/public/main/memory/index.html +2 -2
  70. package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
  71. package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
  72. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
  73. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
  74. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  75. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  76. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
  77. package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
  78. package/dist/public/service-worker/fetch-proxy.js +1 -1
  79. package/dist/public/service-worker/push-handler.js +1 -1
  80. package/dist/public/service-worker.js +1 -1
  81. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
  82. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
  83. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  84. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  85. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  86. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  87. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  88. package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
  89. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  90. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
  91. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  92. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  93. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  94. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  95. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  96. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  97. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
  98. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  99. package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
  100. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  101. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
  102. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  103. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
  104. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  105. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  106. package/dist/public/static-main-manifest.json +1 -1
  107. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  108. package/dist/server.js +286 -189
  109. package/package.json +2 -1
  110. package/src/__mocks__/jszip.cjs +15 -5
  111. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  112. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  113. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  114. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  115. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  116. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  117. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  118. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  119. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  120. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  121. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  122. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  123. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  124. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  125. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
  126. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  127. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  128. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  129. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  130. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  131. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  132. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  133. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  134. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  135. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  136. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  137. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  138. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  139. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  140. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
  141. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  142. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  143. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  144. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
  145. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  146. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  147. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  148. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  149. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  150. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  151. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  152. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  153. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  154. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  155. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  156. package/src/config/config.test.ts +9 -0
  157. package/src/config/config.ts +15 -0
  158. package/src/core/app-routes.test.ts +56 -0
  159. package/src/core/app-routes.ts +8 -1
  160. package/src/core/backup-controller.test.ts +101 -0
  161. package/src/core/backup-controller.ts +137 -0
  162. package/src/core/control-plane-client.test.ts +408 -0
  163. package/src/core/control-plane-client.ts +425 -0
  164. package/src/core/orchestrator/orchestrator.ts +4 -0
  165. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  166. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  167. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  168. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  169. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  170. package/src/core/utils/initControlPlane.test.ts +276 -0
  171. package/src/core/utils/initControlPlane.ts +437 -0
  172. package/src/core/utils/initializeApp.ts +18 -0
  173. package/src/db/getSession.test.ts +13 -1
  174. package/src/db/openDatabase.test.ts +10 -1
  175. package/src/db/openDatabase.ts +3 -1
  176. package/src/server/app.test.ts +12 -0
  177. package/src/server/app.ts +18 -8
  178. package/src/server/client-registry.test.ts +250 -0
  179. package/src/server/client-registry.ts +477 -0
  180. package/src/server/config.ts +15 -0
  181. package/src/server/control-plane-types.ts +120 -0
  182. package/src/server/control-plane.test.ts +472 -0
  183. package/src/server/control-plane.ts +581 -0
  184. package/src/server/routes/backup.test.ts +263 -0
  185. package/src/server/routes/backup.ts +262 -0
  186. package/src/server/routes/integrations-email.test.ts +532 -0
  187. package/src/server/routes/llamafile.test.ts +172 -48
  188. package/src/server/routes/mesh-llm.test.ts +192 -0
  189. package/src/server/routes/ollama.test.ts +49 -0
  190. package/src/server/server-peer.test.ts +86 -0
  191. package/src/server/server-peer.ts +330 -0
  192. package/src/server/server.ts +35 -0
  193. package/src/server/services/llamafile-manager.test.ts +230 -6
  194. package/src/server/services/transformers-runtime.test.ts +186 -18
  195. package/src/server/webrtc.test.ts +21 -0
  196. package/src/server/webrtc.ts +72 -0
  197. package/src/service-worker/fetch-proxy.test.ts +64 -1
  198. package/src/service-worker/push-handler.ts +39 -0
  199. package/src/shell/vm.test.ts +744 -12
  200. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  201. package/src/storage/getGroupFile.test.ts +66 -0
  202. package/src/storage/memoryStorage.test.ts +82 -2
  203. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  204. package/src/storage/migrateLegacyOpfs.ts +117 -0
  205. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  206. package/src/stores/chat-ui.test.ts +20 -0
  207. package/src/stores/orchestrator.test.ts +134 -0
  208. package/src/stores/toast.test.ts +50 -0
  209. package/src/stores/tools.test.ts +147 -10
  210. package/src/subsystems/channels/imessage.test.ts +59 -0
  211. package/src/subsystems/channels/peerjs.test.ts +500 -62
  212. package/src/subsystems/channels/peerjs.ts +71 -4
  213. package/src/subsystems/channels/telegram.test.ts +177 -0
  214. package/src/subsystems/email/catalog.test.ts +1 -0
  215. package/src/subsystems/email/connections.test.ts +39 -1
  216. package/src/subsystems/git/credentials.test.ts +160 -825
  217. package/src/subsystems/git/git.test.ts +463 -0
  218. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  219. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  220. package/src/subsystems/notifications/push-routes.ts +23 -0
  221. package/src/subsystems/notifications/push-store.ts +1 -1
  222. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  223. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  224. package/src/subsystems/providers/model-registry.test.ts +115 -0
  225. package/src/subsystems/providers/providers.test.ts +94 -1
  226. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  227. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  228. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  229. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  230. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  231. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  232. package/src/subsystems/tools/cron.test.ts +64 -0
  233. package/src/subsystems/tools/declarative.test.ts +255 -89
  234. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  235. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  236. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  237. package/src/testing/jest-setup.ts +328 -40
  238. package/src/testing/mock-v86.mjs +49 -0
  239. package/src/ui/a2ui/types.ts +4 -3
  240. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  241. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  242. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  243. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  244. package/src/worker/tools/email/email.test.ts +193 -8
  245. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  246. package/src/worker/tools/git/git.test.ts +214 -3
  247. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  248. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  249. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  250. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  251. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  252. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  253. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  254. package/src/worker/tools/ui/javascript.test.ts +116 -0
  255. package/src/worker/tools/ui/list-components.test.ts +10 -0
  256. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  257. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  258. package/src/worker/tools/ui/render-component.test.ts +346 -0
  259. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  260. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  261. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  262. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  263. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  264. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  265. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  266. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  267. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  268. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  269. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  270. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  271. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  272. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  273. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  274. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  275. package/src/worker/utils/rate-limit.test.ts +199 -0
  276. package/src/worker/utils/tool-state.test.ts +29 -0
  277. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  278. package/dist/public/config-AL77QAx4.js +0 -1
  279. package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
  280. package/dist/public/file-viewer-CCSABMsE.js +0 -1
  281. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  282. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  283. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  284. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  285. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  286. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
@@ -4,7 +4,6 @@ import { jest } from "@jest/globals";
4
4
  // Minimal PeerJS mocks
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
- // type DataHandler = (data: unknown) => void;
8
7
  type EventHandler = (...args: any[]) => void;
9
8
 
10
9
  class MockDataConnection {
@@ -44,13 +43,11 @@ class MockPeer {
44
43
 
45
44
  constructor(id: string, _opts?: unknown) {
46
45
  this.id = id;
47
- // Simulate async "open" event so start() gets our id
48
46
  setTimeout(() => this.emit("open", id), 0);
49
47
  }
50
48
 
51
49
  connect(remotePeerId: string): MockDataConnection {
52
50
  const conn = new MockDataConnection(remotePeerId);
53
- // Simulate open
54
51
  setTimeout(() => conn.emit("open"), 0);
55
52
 
56
53
  return conn;
@@ -74,7 +71,6 @@ class MockPeer {
74
71
  }
75
72
  }
76
73
 
77
- // Mock the peerjs module
78
74
  let lastPeerInstance: MockPeer | null = null;
79
75
 
80
76
  jest.unstable_mockModule("peerjs", () => ({
@@ -90,7 +86,7 @@ jest.unstable_mockModule("../../db/db.js", () => ({
90
86
  }));
91
87
 
92
88
  jest.unstable_mockModule("../../storage/readGroupFileBytes.js", () => ({
93
- readGroupFileBytes: jest.fn(async () => new Uint8Array()),
89
+ readGroupFileBytes: jest.fn(async () => new Uint8Array([1, 2, 3])),
94
90
  }));
95
91
 
96
92
  jest.unstable_mockModule("../../storage/writeGroupFileBytes.js", () => ({
@@ -103,26 +99,16 @@ jest.unstable_mockModule("../../utils/utils.js", () => ({
103
99
 
104
100
  const { PeerJsChannel } = await import("./peerjs.js");
105
101
 
106
- // ---------------------------------------------------------------------------
107
- // Helpers
108
- // ---------------------------------------------------------------------------
109
-
110
102
  function flushMicrotasks(): Promise<void> {
111
103
  return new Promise((r) => setTimeout(r, 10));
112
104
  }
113
105
 
114
- // ---------------------------------------------------------------------------
115
- // Tests
116
- // ---------------------------------------------------------------------------
117
-
118
106
  describe("PeerJsChannel", () => {
119
107
  beforeEach(() => {
120
108
  lastPeerInstance = null;
121
109
  jest.clearAllMocks();
122
110
  });
123
111
 
124
- // ----- configure -----
125
-
126
112
  describe("configure()", () => {
127
113
  it("stores myPeerId and trustedPeerIds", () => {
128
114
  const ch = new PeerJsChannel();
@@ -145,8 +131,6 @@ describe("PeerJsChannel", () => {
145
131
  });
146
132
  });
147
133
 
148
- // ----- start / stop -----
149
-
150
134
  describe("start() / stop()", () => {
151
135
  it("creates a Peer on start", async () => {
152
136
  const ch = new PeerJsChannel();
@@ -170,7 +154,7 @@ describe("PeerJsChannel", () => {
170
154
  it("does not start without configure being called first", async () => {
171
155
  const { Peer } = await import("peerjs");
172
156
  const ch = new PeerJsChannel();
173
- ch.start(); // no myPeerId set — should be a no-op
157
+ ch.start();
174
158
  await new Promise((r) => setTimeout(r, 0));
175
159
  expect(Peer).not.toHaveBeenCalled();
176
160
  });
@@ -186,17 +170,8 @@ describe("PeerJsChannel", () => {
186
170
  });
187
171
  });
188
172
 
189
- // ----- onMessage -----
190
-
191
- describe("onMessage()", () => {
192
- it("stores the message callback", () => {
193
- const ch = new PeerJsChannel();
194
- const cb = jest.fn();
195
- ch.onMessage(cb);
196
- expect(ch.messageCallback).toBe(cb);
197
- });
198
-
199
- it("fires callback on inbound chat message", async () => {
173
+ describe("onMessage() and typing callbacks", () => {
174
+ it("stores the message callback and fires on chat message", async () => {
200
175
  const ch = new PeerJsChannel();
201
176
  const cb = jest.fn();
202
177
  ch.onMessage(cb);
@@ -205,12 +180,10 @@ describe("PeerJsChannel", () => {
205
180
 
206
181
  await flushMicrotasks();
207
182
 
208
- // Simulate incoming connection
209
183
  const incomingConn = new MockDataConnection("remote-peer");
210
184
  lastPeerInstance!.emit("connection", incomingConn);
211
185
  incomingConn.emit("open");
212
186
 
213
- // Simulate inbound chat data
214
187
  incomingConn.emit("data", { type: "chat", text: "hello" });
215
188
 
216
189
  expect(cb).toHaveBeenCalledWith(
@@ -223,10 +196,10 @@ describe("PeerJsChannel", () => {
223
196
  );
224
197
  });
225
198
 
226
- it("ignores non-chat messages", async () => {
199
+ it("fires onTyping callback when typing event received", async () => {
227
200
  const ch = new PeerJsChannel();
228
- const cb = jest.fn();
229
- ch.onMessage(cb);
201
+ const typingCb = jest.fn();
202
+ ch.onTyping(typingCb);
230
203
  ch.configure("my-id", []);
231
204
  ch.start();
232
205
 
@@ -235,9 +208,10 @@ describe("PeerJsChannel", () => {
235
208
  const incomingConn = new MockDataConnection("remote-peer");
236
209
  lastPeerInstance!.emit("connection", incomingConn);
237
210
  incomingConn.emit("open");
211
+
238
212
  incomingConn.emit("data", { type: "typing", typing: true });
239
213
 
240
- expect(cb).not.toHaveBeenCalled();
214
+ expect(typingCb).toHaveBeenCalledWith("peer:remote-peer", true);
241
215
  });
242
216
 
243
217
  it("rejects messages from untrusted peers when trustedPeerIds is set", async () => {
@@ -277,9 +251,7 @@ describe("PeerJsChannel", () => {
277
251
  });
278
252
  });
279
253
 
280
- // ----- send -----
281
-
282
- describe("send()", () => {
254
+ describe("send() and setTyping()", () => {
283
255
  it("sends a chat message over an open connection", async () => {
284
256
  const ch = new PeerJsChannel();
285
257
  ch.configure("my-id", []);
@@ -287,14 +259,12 @@ describe("PeerJsChannel", () => {
287
259
 
288
260
  await flushMicrotasks();
289
261
 
290
- // Simulate remote peer connecting to us first (so we have a conn)
291
262
  const incomingConn = new MockDataConnection("remote-peer");
292
263
  lastPeerInstance!.emit("connection", incomingConn);
293
264
  incomingConn.emit("open");
294
265
 
295
266
  await ch.send("peer:remote-peer", "hello world");
296
267
 
297
- // Find the SendMessage call (skip GetAgentCard)
298
268
  const calls = (incomingConn.send as jest.Mock).mock.calls;
299
269
  const sendMsgCall = calls.find(
300
270
  (c: any[]) => c[0]?.method === "SendMessage",
@@ -309,39 +279,38 @@ describe("PeerJsChannel", () => {
309
279
  ]);
310
280
  });
311
281
 
312
- it("strips peer: prefix to get remote peer id", async () => {
282
+ it("sends a chat message with attachments", async () => {
313
283
  const ch = new PeerJsChannel();
314
284
  ch.configure("my-id", []);
315
285
  ch.start();
316
286
 
317
287
  await flushMicrotasks();
318
288
 
319
- const incomingConn = new MockDataConnection("abc-123");
289
+ const incomingConn = new MockDataConnection("remote-peer");
320
290
  lastPeerInstance!.emit("connection", incomingConn);
321
291
  incomingConn.emit("open");
322
292
 
323
- await ch.send("peer:abc-123", "test");
293
+ await ch.send("peer:remote-peer", "check this file", [
294
+ {
295
+ path: "images/test.png",
296
+ mimeType: "image/png",
297
+ size: 3,
298
+ } as any,
299
+ ]);
324
300
 
325
- // Find the SendMessage call (skip GetAgentCard)
326
301
  const calls = (incomingConn.send as jest.Mock).mock.calls;
327
302
  const sendMsgCall = calls.find(
328
303
  (c: any[]) => c[0]?.method === "SendMessage",
329
304
  );
330
305
  expect(sendMsgCall).toBeDefined();
331
- const envelope = sendMsgCall![0] as any;
332
- expect(envelope.jsonrpc).toBe("2.0");
333
- expect(envelope.method).toBe("SendMessage");
334
- expect(envelope.params.message.role).toBe("ROLE_AGENT");
335
- expect(envelope.params.message.parts).toEqual([
336
- { kind: "text", text: "test" },
337
- ]);
306
+ expect(
307
+ (sendMsgCall![0] as any).params.message.parts.some(
308
+ (p: any) => p.kind === "file",
309
+ ),
310
+ ).toBe(true);
338
311
  });
339
- });
340
-
341
- // ----- setTyping -----
342
312
 
343
- describe("setTyping()", () => {
344
- it("sends a typing message when typing=true", async () => {
313
+ it("sends typing notifications", async () => {
345
314
  const ch = new PeerJsChannel();
346
315
  ch.configure("my-id", []);
347
316
  ch.start();
@@ -353,13 +322,19 @@ describe("PeerJsChannel", () => {
353
322
  incomingConn.emit("open");
354
323
 
355
324
  ch.setTyping("peer:remote-peer", true);
356
-
357
325
  expect(incomingConn.send).toHaveBeenCalledWith(
358
326
  expect.objectContaining({ type: "typing", typing: true }),
359
327
  );
328
+
329
+ ch.setTyping("peer:remote-peer", false);
330
+ expect(incomingConn.send).toHaveBeenCalledWith(
331
+ expect.objectContaining({ type: "typing", typing: false }),
332
+ );
360
333
  });
334
+ });
361
335
 
362
- it("sends a typing=false message when typing stops", async () => {
336
+ describe("A2A JSON-RPC request handling", () => {
337
+ it("responds to GetAgentCard request", async () => {
363
338
  const ch = new PeerJsChannel();
364
339
  ch.configure("my-id", []);
365
340
  ch.start();
@@ -370,15 +345,51 @@ describe("PeerJsChannel", () => {
370
345
  lastPeerInstance!.emit("connection", incomingConn);
371
346
  incomingConn.emit("open");
372
347
 
373
- ch.setTyping("peer:remote-peer", false);
348
+ incomingConn.emit("data", {
349
+ jsonrpc: "2.0",
350
+ id: "req-1",
351
+ method: "GetAgentCard",
352
+ });
374
353
 
375
354
  expect(incomingConn.send).toHaveBeenCalledWith(
376
- expect.objectContaining({ type: "typing", typing: false }),
355
+ expect.objectContaining({
356
+ jsonrpc: "2.0",
357
+ id: "req-1",
358
+ result: expect.objectContaining({
359
+ name: "my-id",
360
+ }),
361
+ }),
377
362
  );
378
363
  });
379
- });
380
364
 
381
- // ----- isConfigured -----
365
+ it("responds to unknown method with METHOD_NOT_FOUND", async () => {
366
+ const ch = new PeerJsChannel();
367
+ ch.configure("my-id", []);
368
+ ch.start();
369
+
370
+ await flushMicrotasks();
371
+
372
+ const incomingConn = new MockDataConnection("remote-peer");
373
+ lastPeerInstance!.emit("connection", incomingConn);
374
+ incomingConn.emit("open");
375
+
376
+ incomingConn.emit("data", {
377
+ jsonrpc: "2.0",
378
+ id: "req-unknown",
379
+ method: "NonExistentMethod",
380
+ });
381
+
382
+ expect(incomingConn.send).toHaveBeenCalledWith(
383
+ expect.objectContaining({
384
+ jsonrpc: "2.0",
385
+ id: "req-unknown",
386
+ error: expect.objectContaining({
387
+ code: -32601,
388
+ }),
389
+ }),
390
+ );
391
+ });
392
+ });
382
393
 
383
394
  describe("isConfigured()", () => {
384
395
  it("returns false before configure", () => {
@@ -392,4 +403,431 @@ describe("PeerJsChannel", () => {
392
403
  expect(ch.isConfigured()).toBe(true);
393
404
  });
394
405
  });
406
+
407
+ describe("A2UI & room notification methods", () => {
408
+ it("sends A2UI surface envelopes and A2UI actions", async () => {
409
+ const ch = new PeerJsChannel();
410
+ ch.configure("my-id", []);
411
+ ch.start();
412
+
413
+ await flushMicrotasks();
414
+
415
+ const incomingConn = new MockDataConnection("remote-peer");
416
+ lastPeerInstance!.emit("connection", incomingConn);
417
+ incomingConn.emit("open");
418
+
419
+ // 1. sendA2UI
420
+ await ch.sendA2UI("peer:remote-peer", {
421
+ surfaces: { main: { type: "text", content: "Hello A2UI" } },
422
+ } as any);
423
+
424
+ expect(incomingConn.send).toHaveBeenCalledWith(
425
+ expect.objectContaining({
426
+ jsonrpc: "2.0",
427
+ method: "message/send",
428
+ params: expect.objectContaining({
429
+ message: expect.objectContaining({
430
+ role: "agent",
431
+ parts: [
432
+ expect.objectContaining({
433
+ kind: "a2ui",
434
+ }),
435
+ ],
436
+ }),
437
+ }),
438
+ }),
439
+ );
440
+
441
+ // 2. sendA2UIAction
442
+ await ch.sendA2UIAction("peer:remote-peer", {
443
+ surfaceId: "main",
444
+ action: "submit",
445
+ payload: { text: "Clicked" },
446
+ } as any);
447
+
448
+ expect(incomingConn.send).toHaveBeenCalledWith(
449
+ expect.objectContaining({
450
+ jsonrpc: "2.0",
451
+ method: "message/send",
452
+ params: expect.objectContaining({
453
+ message: expect.objectContaining({
454
+ role: "user",
455
+ parts: [
456
+ expect.objectContaining({
457
+ kind: "a2ui-action",
458
+ }),
459
+ ],
460
+ }),
461
+ }),
462
+ }),
463
+ );
464
+ });
465
+
466
+ it("sends room notifications and dispatches to handler", async () => {
467
+ const ch = new PeerJsChannel();
468
+ ch.configure("my-id", []);
469
+ ch.start();
470
+
471
+ await flushMicrotasks();
472
+
473
+ const incomingConn = new MockDataConnection("remote-peer");
474
+ lastPeerInstance!.emit("connection", incomingConn);
475
+ incomingConn.emit("open");
476
+
477
+ // 1. sendRoomNotification
478
+ const sent = ch.sendRoomNotification("remote-peer", {
479
+ jsonrpc: "2.0",
480
+ method: "room/join",
481
+ params: { roomId: "room-123" },
482
+ });
483
+ expect(sent).toBe(true);
484
+ expect(incomingConn.send).toHaveBeenCalledWith(
485
+ expect.objectContaining({
486
+ method: "room/join",
487
+ }),
488
+ );
489
+
490
+ // 2. setRoomNotificationHandler & receive notification
491
+ const roomHandler = jest.fn();
492
+ ch.setRoomNotificationHandler(roomHandler);
493
+
494
+ incomingConn.emit("data", {
495
+ jsonrpc: "2.0",
496
+ method: "room/message",
497
+ params: { text: "Hello Room" },
498
+ });
499
+
500
+ expect(roomHandler).toHaveBeenCalledWith("remote-peer", "room/message", {
501
+ text: "Hello Room",
502
+ });
503
+ });
504
+
505
+ it("manages task lifecycle and notifies onTaskComplete", async () => {
506
+ const ch = new PeerJsChannel();
507
+ ch.configure("my-id", []);
508
+ ch.start();
509
+
510
+ await flushMicrotasks();
511
+
512
+ const incomingConn = new MockDataConnection("remote-peer");
513
+ lastPeerInstance!.emit("connection", incomingConn);
514
+ incomingConn.emit("open");
515
+
516
+ const onComplete = jest.fn();
517
+ ch.onTaskComplete(onComplete);
518
+
519
+ // Verify task manager creation
520
+ const tm = ch.getTaskManager("remote-peer");
521
+ expect(tm).toBeDefined();
522
+
523
+ // Emit terminal task status update from remote peer
524
+ incomingConn.emit("data", {
525
+ jsonrpc: "2.0",
526
+ method: "tasks/statusUpdate",
527
+ params: {
528
+ taskId: "t-1",
529
+ contextId: "c-1",
530
+ status: {
531
+ state: "TASK_STATE_COMPLETED",
532
+ },
533
+ },
534
+ });
535
+
536
+ expect(onComplete).toHaveBeenCalledWith("peer:remote-peer");
537
+ });
538
+
539
+ it("tracks peer connection status with isPeerConnected", async () => {
540
+ const ch = new PeerJsChannel();
541
+ ch.configure("my-id", []);
542
+ ch.start();
543
+
544
+ await flushMicrotasks();
545
+
546
+ expect(ch.isPeerConnected("remote-peer")).toBe(false);
547
+
548
+ const incomingConn = new MockDataConnection("remote-peer");
549
+ lastPeerInstance!.emit("connection", incomingConn);
550
+ incomingConn.emit("open");
551
+
552
+ expect(ch.isPeerConnected("remote-peer")).toBe(true);
553
+
554
+ ch.stop();
555
+ expect(ch.isPeerConnected("remote-peer")).toBe(false);
556
+ });
557
+
558
+ it("processes inbound A2A envelopes with file attachments and remapping", async () => {
559
+ const ch = new PeerJsChannel();
560
+ ch.configure("my-id", []);
561
+ ch.start();
562
+
563
+ await flushMicrotasks();
564
+
565
+ const incomingConn = new MockDataConnection("remote-peer");
566
+ lastPeerInstance!.emit("connection", incomingConn);
567
+ incomingConn.emit("open");
568
+
569
+ const messageCallback = jest.fn();
570
+ ch.onMessage(messageCallback);
571
+
572
+ // Inbound A2A envelope with text and file part
573
+ incomingConn.emit("data", {
574
+ jsonrpc: "2.0",
575
+ method: "message/send",
576
+ params: {
577
+ message: {
578
+ role: "user",
579
+ parts: [
580
+ { kind: "text", text: "Here is the report [doc](doc.pdf)" },
581
+ {
582
+ kind: "file",
583
+ name: "doc.pdf",
584
+ mimeType: "application/pdf",
585
+ size: 1024,
586
+ },
587
+ ],
588
+ },
589
+ },
590
+ });
591
+
592
+ expect(messageCallback).toHaveBeenCalledWith(
593
+ expect.objectContaining({
594
+ groupId: "peer:remote-peer",
595
+ sender: "remote-peer",
596
+ content: "Here is the report [doc](doc.pdf)",
597
+ attachments: [
598
+ expect.objectContaining({
599
+ fileName: "doc.pdf",
600
+ mimeType: "application/pdf",
601
+ size: 1024,
602
+ }),
603
+ ],
604
+ }),
605
+ );
606
+ });
607
+
608
+ it("completes active task when present and returns false when absent", async () => {
609
+ const ch = new PeerJsChannel();
610
+ ch.configure("my-id", []);
611
+ expect(ch.completeActiveTask("peer:remote-peer")).toBe(false);
612
+
613
+ const tm = ch.getTaskManager("remote-peer");
614
+ expect(ch.completeActiveTask("peer:remote-peer")).toBe(false);
615
+
616
+ // Create a task
617
+ tm.handleSendMessage({
618
+ message: {
619
+ messageId: "msg-123",
620
+ role: "user" as any,
621
+ parts: [{ text: "do task" }],
622
+ },
623
+ });
624
+
625
+ expect(ch.completeActiveTask("peer:remote-peer")).toBe(true);
626
+ });
627
+
628
+ it("handles connectPeer, setTyping, and room notifications", async () => {
629
+ const ch = new PeerJsChannel();
630
+ ch.configure("my-id", []);
631
+ ch.start();
632
+
633
+ await flushMicrotasks();
634
+
635
+ ch.connectPeer("remote-peer-2");
636
+ expect(ch.isPeerConnected("remote-peer-2")).toBe(true);
637
+
638
+ ch.setTyping("peer:remote-peer-2", true);
639
+ const conn = (ch as any).connections.get("remote-peer-2");
640
+ expect(conn.send).toHaveBeenCalledWith({ type: "typing", typing: true });
641
+
642
+ // sendRoomNotification with open connection
643
+ const sent = ch.sendRoomNotification("remote-peer-2", {
644
+ jsonrpc: "2.0",
645
+ method: "room/custom",
646
+ params: { ok: true },
647
+ });
648
+ expect(sent).toBe(true);
649
+
650
+ // sendRoomNotification when connection opens later
651
+ const freshConn = new MockDataConnection("remote-peer-3");
652
+ freshConn.open = false;
653
+ (lastPeerInstance as any).connect = jest.fn(() => freshConn);
654
+
655
+ const queued = ch.sendRoomNotification("remote-peer-3", {
656
+ jsonrpc: "2.0",
657
+ method: "room/queued",
658
+ params: {},
659
+ });
660
+ expect(queued).toBe(true);
661
+
662
+ freshConn.open = true;
663
+ freshConn.emit("open");
664
+ await flushMicrotasks();
665
+ expect(freshConn.send).toHaveBeenCalledWith({
666
+ jsonrpc: "2.0",
667
+ method: "room/queued",
668
+ params: {},
669
+ });
670
+ });
671
+
672
+ it("tracks peerjs-dc-handle-chunk event on globalThis", async () => {
673
+ new PeerJsChannel();
674
+ const customEvent = new CustomEvent("peerjs-dc-handle-chunk", {
675
+ detail: {
676
+ chunkInfo: { count: 1, total: 3 },
677
+ },
678
+ });
679
+ globalThis.dispatchEvent(customEvent);
680
+
681
+ const { transferProgressSignal } = await import("./peerjs.js");
682
+ expect(transferProgressSignal.get()).toEqual({
683
+ count: 2,
684
+ total: 3,
685
+ direction: "receive",
686
+ });
687
+ });
688
+
689
+ it("sends file attachments and markdown links over DataConnection", async () => {
690
+ const ch = new PeerJsChannel();
691
+ ch.configure("my-id", []);
692
+ ch.start();
693
+
694
+ await flushMicrotasks();
695
+
696
+ const incomingConn = new MockDataConnection("remote-peer-files");
697
+ lastPeerInstance!.emit("connection", incomingConn);
698
+ incomingConn.emit("open");
699
+
700
+ await ch.send("peer:remote-peer-files", "See [notes](notes.txt)", [
701
+ { path: "image.png", mimeType: "image/png" } as any,
702
+ ]);
703
+
704
+ expect(incomingConn.send).toHaveBeenCalledWith(
705
+ expect.objectContaining({
706
+ type: "__file_header",
707
+ name: "image.png",
708
+ mimeType: "image/png",
709
+ }),
710
+ );
711
+ });
712
+
713
+ it("sends A2UI and A2UIAction envelopes", async () => {
714
+ const ch = new PeerJsChannel();
715
+ ch.configure("my-id", []);
716
+ ch.start();
717
+
718
+ await flushMicrotasks();
719
+
720
+ const incomingConn = new MockDataConnection("remote-peer-a2ui");
721
+ lastPeerInstance!.emit("connection", incomingConn);
722
+ incomingConn.emit("open");
723
+
724
+ // 1. sendA2UI
725
+ await ch.sendA2UI("peer:remote-peer-a2ui", {
726
+ surfaceId: "s1",
727
+ components: [],
728
+ } as any);
729
+ expect(incomingConn.send).toHaveBeenCalledWith(
730
+ expect.objectContaining({
731
+ method: "message/send",
732
+ }),
733
+ );
734
+
735
+ // 2. sendA2UIAction
736
+ await ch.sendA2UIAction("peer:remote-peer-a2ui", {
737
+ actionId: "a1",
738
+ surfaceId: "s1",
739
+ componentId: "c1",
740
+ } as any);
741
+ expect(incomingConn.send).toHaveBeenCalledWith(
742
+ expect.objectContaining({
743
+ method: "message/send",
744
+ }),
745
+ );
746
+ });
747
+
748
+ it("handles inbound A2A JSON-RPC requests, errors, and responses", async () => {
749
+ const ch = new PeerJsChannel();
750
+ ch.configure("my-id", []);
751
+ ch.start();
752
+
753
+ await flushMicrotasks();
754
+
755
+ const incomingConn = new MockDataConnection("remote-peer-rpc");
756
+ lastPeerInstance!.emit("connection", incomingConn);
757
+ incomingConn.emit("open");
758
+
759
+ // 1. Unknown method
760
+ incomingConn.emit("data", {
761
+ jsonrpc: "2.0",
762
+ id: "req-1",
763
+ method: "unknown/method",
764
+ params: {},
765
+ });
766
+ expect(incomingConn.send).toHaveBeenCalledWith(
767
+ expect.objectContaining({
768
+ id: "req-1",
769
+ error: expect.objectContaining({ code: -32601 }),
770
+ }),
771
+ );
772
+
773
+ // 2. GetTask missing params
774
+ incomingConn.emit("data", {
775
+ jsonrpc: "2.0",
776
+ id: "req-2",
777
+ method: "GetTask",
778
+ params: {},
779
+ });
780
+ expect(incomingConn.send).toHaveBeenCalledWith(
781
+ expect.objectContaining({
782
+ id: "req-2",
783
+ error: expect.objectContaining({ code: -32602 }),
784
+ }),
785
+ );
786
+
787
+ // 3. CancelTask missing params
788
+ incomingConn.emit("data", {
789
+ jsonrpc: "2.0",
790
+ id: "req-3",
791
+ method: "CancelTask",
792
+ params: {},
793
+ });
794
+ expect(incomingConn.send).toHaveBeenCalledWith(
795
+ expect.objectContaining({
796
+ id: "req-3",
797
+ error: expect.objectContaining({ code: -32602 }),
798
+ }),
799
+ );
800
+
801
+ // 4. Pending request resolution
802
+ const resolveFn = jest.fn();
803
+ (ch as any)._pendingRequests.set("req-test", {
804
+ resolve: resolveFn,
805
+ reject: jest.fn(),
806
+ timer: setTimeout(() => {}, 10000),
807
+ });
808
+ incomingConn.emit("data", {
809
+ jsonrpc: "2.0",
810
+ id: "req-test",
811
+ result: { ok: true },
812
+ });
813
+ expect(resolveFn).toHaveBeenCalledWith(
814
+ expect.objectContaining({ result: { ok: true } }),
815
+ );
816
+ });
817
+
818
+ it("supports wildcard and prefix trusted peer matching", async () => {
819
+ const ch = new PeerJsChannel();
820
+ ch.trustedPeerIds = new Set(["cli-*", "server-node"]);
821
+
822
+ expect((ch as any)._isTrustedPeer("cli-01m1cke479mr186s4hbp4a3c3h")).toBe(
823
+ true,
824
+ );
825
+ expect((ch as any)._isTrustedPeer("server-node")).toBe(true);
826
+ expect((ch as any)._isTrustedPeer("random-untrusted-peer")).toBe(false);
827
+
828
+ ch.trustedPeerIds = new Set(["cli"]);
829
+ expect((ch as any)._isTrustedPeer("cli-12345")).toBe(true);
830
+ expect((ch as any)._isTrustedPeer("other-12345")).toBe(false);
831
+ });
832
+ });
395
833
  });