shadow-claw 1.23.4 → 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 (255) hide show
  1. package/README.md +42 -5
  2. package/bin/cli.mjs +166 -0
  3. package/bin/cli.test.mjs +60 -0
  4. package/bin/commands/backup.mjs +70 -0
  5. package/bin/commands/clients.mjs +41 -0
  6. package/bin/commands/peer-id.mjs +76 -0
  7. package/bin/commands/peer-id.test.mjs +104 -0
  8. package/bin/commands/send.mjs +51 -0
  9. package/bin/commands/tasks.mjs +59 -0
  10. package/bin/commands/webrtc-listen.mjs +254 -0
  11. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  12. package/bin/rolldown.config.test.mjs +401 -8
  13. package/bin/utils/control-client.mjs +243 -0
  14. package/bin/utils/control-client.test.mjs +83 -0
  15. package/bin/utils/ulid.mjs +45 -0
  16. package/bin/utils/webrtc-control-client.mjs +699 -0
  17. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  18. package/dist/electron/main.cjs +1 -1
  19. package/dist/public/AGENTS.md +5 -3
  20. package/dist/public/README.md +42 -5
  21. package/dist/public/agent.worker.js +1 -1
  22. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  23. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  24. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  25. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  26. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  27. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  28. package/dist/public/config-CrHchneq.js +1 -0
  29. package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  30. package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
  31. package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
  32. package/dist/public/docs/README.md +3 -1
  33. package/dist/public/docs/architecture/overview.md +12 -12
  34. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  35. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
  36. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  37. package/dist/public/docs/example/article/index.html +1 -1
  38. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  39. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  40. package/dist/public/docs/publishing/index.html +1 -1
  41. package/dist/public/docs/skill-creator/index.html +1 -1
  42. package/dist/public/docs/subsystems/backup.md +56 -0
  43. package/dist/public/docs/subsystems/cli.md +124 -0
  44. package/dist/public/docs/subsystems/control-plane.md +158 -0
  45. package/dist/public/docs/subsystems/notifications.md +54 -2
  46. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  47. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  48. package/dist/public/e2e/README.md +5 -1
  49. package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
  50. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  51. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  52. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  53. package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
  54. package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  55. package/dist/public/index.html +1 -1
  56. package/dist/public/index.js +1 -1
  57. package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  58. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  59. package/dist/public/llms.txt +1 -1
  60. package/dist/public/main/index.html +1 -1
  61. package/dist/public/main/memory/index.html +1 -1
  62. package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
  63. package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
  64. package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
  65. package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
  66. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  67. package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
  68. package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
  69. package/dist/public/service-worker/push-handler.js +1 -1
  70. package/dist/public/service-worker.js +1 -1
  71. package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
  72. package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
  73. package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  74. package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  75. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  76. package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  77. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  78. package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
  79. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  80. package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
  81. package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  82. package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  83. package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  84. package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  85. package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  86. package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  87. package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
  88. package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  89. package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
  90. package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  91. package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
  92. package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  93. package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
  94. package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  95. package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  96. package/dist/server.js +286 -189
  97. package/package.json +2 -1
  98. package/src/__mocks__/jszip.cjs +15 -5
  99. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  100. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  101. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  102. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  103. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  104. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  105. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  106. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  107. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  108. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  109. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  110. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  111. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  112. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
  113. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  114. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  115. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  116. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  117. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  118. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  119. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  120. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  121. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  122. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  123. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  124. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  125. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  126. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
  127. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
  128. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  129. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  130. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  131. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  132. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  133. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  134. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  135. package/src/config/config.ts +10 -0
  136. package/src/core/backup-controller.test.ts +101 -0
  137. package/src/core/backup-controller.ts +137 -0
  138. package/src/core/control-plane-client.test.ts +408 -0
  139. package/src/core/control-plane-client.ts +425 -0
  140. package/src/core/orchestrator/orchestrator.ts +4 -0
  141. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  142. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  143. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  144. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  145. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  146. package/src/core/utils/initControlPlane.test.ts +276 -0
  147. package/src/core/utils/initControlPlane.ts +437 -0
  148. package/src/core/utils/initializeApp.ts +18 -0
  149. package/src/db/getSession.test.ts +13 -1
  150. package/src/server/app.test.ts +12 -0
  151. package/src/server/app.ts +18 -8
  152. package/src/server/client-registry.test.ts +250 -0
  153. package/src/server/client-registry.ts +477 -0
  154. package/src/server/config.ts +15 -0
  155. package/src/server/control-plane-types.ts +120 -0
  156. package/src/server/control-plane.test.ts +472 -0
  157. package/src/server/control-plane.ts +581 -0
  158. package/src/server/routes/backup.test.ts +263 -0
  159. package/src/server/routes/backup.ts +262 -0
  160. package/src/server/routes/integrations-email.test.ts +532 -0
  161. package/src/server/routes/llamafile.test.ts +172 -48
  162. package/src/server/routes/mesh-llm.test.ts +192 -0
  163. package/src/server/routes/ollama.test.ts +49 -0
  164. package/src/server/server-peer.test.ts +86 -0
  165. package/src/server/server-peer.ts +330 -0
  166. package/src/server/server.ts +35 -0
  167. package/src/server/services/llamafile-manager.test.ts +230 -6
  168. package/src/server/services/transformers-runtime.test.ts +186 -18
  169. package/src/server/webrtc.test.ts +21 -0
  170. package/src/server/webrtc.ts +72 -0
  171. package/src/service-worker/fetch-proxy.test.ts +64 -1
  172. package/src/service-worker/push-handler.ts +39 -0
  173. package/src/shell/vm.test.ts +744 -12
  174. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  175. package/src/storage/getGroupFile.test.ts +66 -0
  176. package/src/storage/memoryStorage.test.ts +82 -2
  177. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  178. package/src/stores/chat-ui.test.ts +20 -0
  179. package/src/stores/orchestrator.test.ts +134 -0
  180. package/src/stores/toast.test.ts +50 -0
  181. package/src/stores/tools.test.ts +147 -10
  182. package/src/subsystems/channels/imessage.test.ts +59 -0
  183. package/src/subsystems/channels/peerjs.test.ts +500 -62
  184. package/src/subsystems/channels/peerjs.ts +71 -4
  185. package/src/subsystems/channels/telegram.test.ts +177 -0
  186. package/src/subsystems/email/catalog.test.ts +1 -0
  187. package/src/subsystems/email/connections.test.ts +39 -1
  188. package/src/subsystems/git/credentials.test.ts +160 -825
  189. package/src/subsystems/git/git.test.ts +463 -0
  190. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  191. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  192. package/src/subsystems/notifications/push-routes.ts +23 -0
  193. package/src/subsystems/notifications/push-store.ts +1 -1
  194. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  195. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  196. package/src/subsystems/providers/model-registry.test.ts +115 -0
  197. package/src/subsystems/providers/providers.test.ts +94 -1
  198. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  199. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  200. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  201. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  202. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  203. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  204. package/src/subsystems/tools/cron.test.ts +64 -0
  205. package/src/subsystems/tools/declarative.test.ts +255 -89
  206. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  207. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  208. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  209. package/src/testing/jest-setup.ts +328 -40
  210. package/src/testing/mock-v86.mjs +49 -0
  211. package/src/ui/a2ui/types.ts +4 -3
  212. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  213. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  214. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  215. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  216. package/src/worker/tools/email/email.test.ts +193 -8
  217. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  218. package/src/worker/tools/git/git.test.ts +214 -3
  219. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  220. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  221. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  222. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  223. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  224. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  225. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  226. package/src/worker/tools/ui/javascript.test.ts +116 -0
  227. package/src/worker/tools/ui/list-components.test.ts +10 -0
  228. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  229. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  230. package/src/worker/tools/ui/render-component.test.ts +346 -0
  231. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  232. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  233. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  234. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  235. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  236. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  237. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  238. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  239. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  240. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  241. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  242. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  243. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  244. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  245. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  246. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  247. package/src/worker/utils/rate-limit.test.ts +199 -0
  248. package/src/worker/utils/tool-state.test.ts +29 -0
  249. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  250. package/dist/public/config-BYAuxx3j.js +0 -1
  251. package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
  252. package/dist/public/file-viewer-CcZYw-PS.js +0 -1
  253. package/dist/public/orchestrator-o9Fj67BN.js +0 -15
  254. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  255. package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
@@ -0,0 +1,279 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ describe("Transformers.js Provider", () => {
4
+ let mockWorkerInstance: any;
5
+ let mockGetConfig: jest.Mock<any>;
6
+ let mockExecuteTool: jest.Mock<any>;
7
+ let mockSetPostHandler: jest.Mock<any>;
8
+
9
+ beforeEach(() => {
10
+ jest.resetModules();
11
+
12
+ mockGetConfig = jest.fn().mockResolvedValue("auto" as never);
13
+ mockExecuteTool = jest
14
+ .fn()
15
+ .mockResolvedValue("tool success result" as never);
16
+ mockSetPostHandler = jest.fn();
17
+
18
+ mockWorkerInstance = {
19
+ postMessage: jest.fn((msg: any) => {
20
+ const { type, payload } = msg;
21
+ if (type === "load") {
22
+ setTimeout(() => {
23
+ mockWorkerInstance.onmessage?.({
24
+ data: {
25
+ type: "progress",
26
+ payload: {
27
+ groupId: payload.groupId,
28
+ status: "progress",
29
+ loaded: 50,
30
+ total: 100,
31
+ },
32
+ },
33
+ });
34
+ mockWorkerInstance.onmessage?.({
35
+ data: {
36
+ type: "progress",
37
+ payload: { groupId: payload.groupId, status: "done" },
38
+ },
39
+ });
40
+ }, 0);
41
+ } else if (type === "generate") {
42
+ setTimeout(() => {
43
+ mockWorkerInstance.onmessage?.({
44
+ data: {
45
+ type: "chunk",
46
+ payload: { groupId: payload.groupId, text: "Hello " },
47
+ },
48
+ });
49
+ mockWorkerInstance.onmessage?.({
50
+ data: {
51
+ type: "thinking-chunk",
52
+ payload: {
53
+ groupId: payload.groupId,
54
+ text: "thinking deeply...",
55
+ },
56
+ },
57
+ });
58
+ mockWorkerInstance.onmessage?.({
59
+ data: {
60
+ type: "done",
61
+ payload: {
62
+ groupId: payload.groupId,
63
+ text: "Hello world response",
64
+ },
65
+ },
66
+ });
67
+ }, 0);
68
+ }
69
+ }),
70
+ onmessage: null,
71
+ };
72
+
73
+ (globalThis as any).Worker = jest
74
+ .fn()
75
+ .mockImplementation(() => mockWorkerInstance);
76
+
77
+ jest.unstable_mockModule("../../db/getConfig.js", () => ({
78
+ getConfig: mockGetConfig,
79
+ }));
80
+
81
+ jest.unstable_mockModule("../../worker/utils/executeTool.js", () => ({
82
+ executeTool: mockExecuteTool,
83
+ }));
84
+
85
+ jest.unstable_mockModule("../../worker/utils/post.js", () => ({
86
+ post: jest.fn(),
87
+ setPostHandler: mockSetPostHandler,
88
+ }));
89
+ });
90
+
91
+ afterEach(() => {
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ it("loads model, streams chunks and completes simple response", async () => {
96
+ const { invokeWithTransformersJs } =
97
+ await import("./transformers-js-provider.js");
98
+
99
+ const emitted: any[] = [];
100
+ const emit = (msg: any) => {
101
+ emitted.push(msg);
102
+ };
103
+
104
+ const messages = [
105
+ {
106
+ role: "user",
107
+ content: [
108
+ { type: "text", text: "Hello" },
109
+ {
110
+ type: "attachment",
111
+ mediaType: "image",
112
+ fileName: "pic.png",
113
+ mimeType: "image/png",
114
+ },
115
+ ],
116
+ },
117
+ ];
118
+
119
+ await invokeWithTransformersJs(
120
+ {} as any,
121
+ "g1",
122
+ "You are a helpful assistant.",
123
+ messages,
124
+ 2048,
125
+ emit,
126
+ undefined,
127
+ [],
128
+ "Xenova/Qwen1.5-0.5B-Chat",
129
+ );
130
+
131
+ expect(emitted.some((e) => e.type === "model-download-progress")).toBe(
132
+ true,
133
+ );
134
+ expect(emitted.some((e) => e.type === "streaming-start")).toBe(true);
135
+ expect(
136
+ emitted.some(
137
+ (e) => e.type === "streaming-chunk" && e.payload.text === "Hello ",
138
+ ),
139
+ ).toBe(true);
140
+ expect(emitted.some((e) => e.type === "streaming-done")).toBe(true);
141
+ expect(
142
+ emitted.some(
143
+ (e) =>
144
+ e.type === "response" && e.payload.text === "Hello world response",
145
+ ),
146
+ ).toBe(true);
147
+ });
148
+
149
+ it("handles XML <tool_call> output, executes tool, and iterates loop", async () => {
150
+ let callCount = 0;
151
+ mockWorkerInstance.postMessage = jest.fn((msg: any) => {
152
+ const { type, payload } = msg;
153
+ if (type === "load") {
154
+ setTimeout(() => {
155
+ mockWorkerInstance.onmessage?.({
156
+ data: {
157
+ type: "progress",
158
+ payload: { groupId: payload.groupId, status: "done" },
159
+ },
160
+ });
161
+ }, 0);
162
+ } else if (type === "generate") {
163
+ callCount++;
164
+ setTimeout(() => {
165
+ if (callCount === 1) {
166
+ mockWorkerInstance.onmessage?.({
167
+ data: {
168
+ type: "done",
169
+ payload: {
170
+ groupId: payload.groupId,
171
+ text: '<tool_call>{"name": "web_search", "arguments": {"query": "weather"}}</tool_call>',
172
+ },
173
+ },
174
+ });
175
+ } else {
176
+ mockWorkerInstance.onmessage?.({
177
+ data: {
178
+ type: "done",
179
+ payload: {
180
+ groupId: payload.groupId,
181
+ text: '{"type":"response","response":"The weather is sunny."}',
182
+ },
183
+ },
184
+ });
185
+ }
186
+ }, 0);
187
+ }
188
+ });
189
+
190
+ const { invokeWithTransformersJs } =
191
+ await import("./transformers-js-provider.js");
192
+
193
+ const emitted: any[] = [];
194
+ const emit = (msg: any) => {
195
+ emitted.push(msg);
196
+ };
197
+
198
+ const tools: any[] = [
199
+ {
200
+ name: "web_search",
201
+ description: "Search web",
202
+ input_schema: { type: "object", properties: {} },
203
+ },
204
+ ];
205
+
206
+ await invokeWithTransformersJs(
207
+ {} as any,
208
+ "g2",
209
+ "System prompt",
210
+ [{ role: "user", content: "What is the weather?" }],
211
+ 1024,
212
+ emit,
213
+ undefined,
214
+ tools,
215
+ "model-id",
216
+ );
217
+
218
+ expect(mockExecuteTool).toHaveBeenCalledWith(
219
+ {},
220
+ "web_search",
221
+ { query: "weather" },
222
+ "g2",
223
+ expect.objectContaining({ allowedTools: tools }),
224
+ );
225
+ expect(
226
+ emitted.some(
227
+ (e) =>
228
+ e.type === "response" && e.payload.text === "The weather is sunny.",
229
+ ),
230
+ ).toBe(true);
231
+ });
232
+
233
+ it("handles worker error rejection during generation", async () => {
234
+ mockWorkerInstance.postMessage = jest.fn((msg: any) => {
235
+ const { type, payload } = msg;
236
+ if (type === "load") {
237
+ setTimeout(() => {
238
+ mockWorkerInstance.onmessage?.({
239
+ data: {
240
+ type: "progress",
241
+ payload: { groupId: payload.groupId, status: "done" },
242
+ },
243
+ });
244
+ }, 0);
245
+ } else if (type === "generate") {
246
+ setTimeout(() => {
247
+ mockWorkerInstance.onmessage?.({
248
+ data: {
249
+ type: "error",
250
+ payload: { groupId: payload.groupId, error: "Out of memory" },
251
+ },
252
+ });
253
+ }, 0);
254
+ }
255
+ });
256
+
257
+ const { invokeWithTransformersJs } =
258
+ await import("./transformers-js-provider.js");
259
+
260
+ const emitted: any[] = [];
261
+ const emit = (msg: any) => {
262
+ emitted.push(msg);
263
+ };
264
+
265
+ await expect(
266
+ invokeWithTransformersJs(
267
+ {} as any,
268
+ "g3",
269
+ "System prompt",
270
+ [{ role: "user", content: "Hi" }],
271
+ 512,
272
+ emit,
273
+ undefined,
274
+ [],
275
+ "model-id",
276
+ ),
277
+ ).rejects.toBe("Out of memory");
278
+ });
279
+ });
@@ -2,8 +2,8 @@ import { jest } from "@jest/globals";
2
2
  import { createModelCacheFetch } from "./createModelCacheFetch.js";
3
3
 
4
4
  // Mock Blob and Response for Jest/jsdom
5
- if (typeof global !== "undefined") {
6
- (global as any).Blob = class MockBlob {
5
+ if (typeof globalThis !== "undefined") {
6
+ (globalThis as any).Blob = class MockBlob {
7
7
  private parts: any[];
8
8
  public size: number;
9
9
  constructor(parts: any[]) {
@@ -2,8 +2,8 @@ import { jest } from "@jest/globals";
2
2
  import { downloadModelToCache } from "./downloadModelToCache.js";
3
3
 
4
4
  // jsdom does not implement Blob fully, so we mock it for the tests
5
- if (typeof global !== "undefined") {
6
- (global as any).Blob = class MockBlob {
5
+ if (typeof globalThis !== "undefined") {
6
+ (globalThis as any).Blob = class MockBlob {
7
7
  private parts: any[];
8
8
  public size: number;
9
9
  constructor(parts: any[]) {
@@ -2,8 +2,8 @@ import { jest } from "@jest/globals";
2
2
  import { loadModelStream } from "./loadModelStream.js";
3
3
 
4
4
  // Mock Blob and Response for Jest/jsdom
5
- if (typeof global !== "undefined") {
6
- (global as any).Blob = class MockBlob {
5
+ if (typeof globalThis !== "undefined") {
6
+ (globalThis as any).Blob = class MockBlob {
7
7
  private parts: any[];
8
8
  public size: number;
9
9
  constructor(parts: any[]) {
@@ -0,0 +1,101 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ const mockLoadSkill = jest.fn() as any;
4
+
5
+ jest.unstable_mockModule("./discoverSkills.js", () => ({
6
+ loadSkill: mockLoadSkill,
7
+ }));
8
+
9
+ const { executeActivateSkill } = await import("./activateSkill.js");
10
+
11
+ describe("executeActivateSkill", () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ it("returns error when skill name is missing or empty", async () => {
17
+ const res1 = await executeActivateSkill({} as any, {}, "group-1");
18
+ expect(res1).toBe("Error: activate_skill requires a skill name.");
19
+
20
+ const res2 = await executeActivateSkill(
21
+ {} as any,
22
+ { name: " " },
23
+ "group-1",
24
+ );
25
+ expect(res2).toBe("Error: activate_skill requires a skill name.");
26
+ });
27
+
28
+ it("returns error when skill is not found", async () => {
29
+ mockLoadSkill.mockResolvedValue(null);
30
+
31
+ const res = await executeActivateSkill(
32
+ {} as any,
33
+ { name: "my-skill" },
34
+ "group-1",
35
+ );
36
+ expect(res).toBe(
37
+ 'Error: skill "my-skill" was not found in this conversation workspace.',
38
+ );
39
+ });
40
+
41
+ it("returns error when skill disables model invocation", async () => {
42
+ mockLoadSkill.mockResolvedValue({
43
+ name: "secret-skill",
44
+ disableModelInvocation: true,
45
+ });
46
+
47
+ const res = await executeActivateSkill(
48
+ {} as any,
49
+ { name: "secret-skill" },
50
+ "group-1",
51
+ );
52
+ expect(res).toBe(
53
+ 'Error: skill "secret-skill" is not available for model-driven activation.',
54
+ );
55
+ });
56
+
57
+ it("formats skill content without resources", async () => {
58
+ mockLoadSkill.mockResolvedValue({
59
+ name: "summarize",
60
+ body: "Instructions for summarizing texts.",
61
+ basePath: ".agents/skills/summarize",
62
+ resources: [],
63
+ });
64
+
65
+ const res = await executeActivateSkill(
66
+ {} as any,
67
+ { name: "summarize" },
68
+ "group-1",
69
+ );
70
+ expect(res).toContain('<skill_content name="summarize">');
71
+ expect(res).toContain("Instructions for summarizing texts.");
72
+ expect(res).toContain("Skill directory: .agents/skills/summarize");
73
+ expect(res).not.toContain("<skill_resources>");
74
+ expect(res).toContain("</skill_content>");
75
+ });
76
+
77
+ it("formats skill content with fallback body and resources", async () => {
78
+ mockLoadSkill.mockResolvedValue({
79
+ name: "data-analysis",
80
+ body: "",
81
+ basePath: ".agents/skills/data-analysis",
82
+ resources: [
83
+ ".agents/skills/data-analysis/template.py",
84
+ ".agents/skills/data-analysis/data.csv",
85
+ ],
86
+ });
87
+
88
+ const res = await executeActivateSkill(
89
+ {} as any,
90
+ { name: "data-analysis" },
91
+ "group-1",
92
+ );
93
+ expect(res).toContain("(This skill has no instructions.)");
94
+ expect(res).toContain("<skill_resources>");
95
+ expect(res).toContain(
96
+ "<file>.agents/skills/data-analysis/template.py</file>",
97
+ );
98
+ expect(res).toContain("<file>.agents/skills/data-analysis/data.csv</file>");
99
+ expect(res).toContain("</skill_resources>");
100
+ });
101
+ });
@@ -0,0 +1,251 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ const mockGetGroupDir = jest.fn() as any;
4
+ const mockReadGroupFile = jest.fn() as any;
5
+
6
+ jest.unstable_mockModule("../../storage/getGroupDir.js", () => ({
7
+ getGroupDir: mockGetGroupDir,
8
+ }));
9
+
10
+ jest.unstable_mockModule("../../storage/readGroupFile.js", () => ({
11
+ readGroupFile: mockReadGroupFile,
12
+ }));
13
+
14
+ const { discoverSkills, loadSkill } = await import("./discoverSkills.js");
15
+ const { DEFAULT_GROUP_ID } = await import("../../config/config.js");
16
+
17
+ function createMockDirectoryHandle(
18
+ name: string,
19
+ children: Record<string, any> = {},
20
+ ) {
21
+ return {
22
+ kind: "directory",
23
+ name,
24
+ getDirectoryHandle: jest.fn(async (childName: string) => {
25
+ const child = children[childName];
26
+ if (!child || child.kind !== "directory") {
27
+ throw new Error(`Directory not found: ${childName}`);
28
+ }
29
+ return child;
30
+ }),
31
+ entries: async function* () {
32
+ for (const [childName, child] of Object.entries(children)) {
33
+ yield [childName, child];
34
+ }
35
+ },
36
+ };
37
+ }
38
+
39
+ function createMockFileHandle(name: string) {
40
+ return {
41
+ kind: "file",
42
+ name,
43
+ };
44
+ }
45
+
46
+ describe("discoverSkills and loadSkill", () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+
51
+ it("returns empty results when .agents or .agents/skills does not exist", async () => {
52
+ const root = createMockDirectoryHandle("root", {});
53
+ mockGetGroupDir.mockResolvedValue(root);
54
+
55
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
56
+ expect(result.skills).toEqual([]);
57
+ expect(result.diagnostics).toEqual([]);
58
+ });
59
+
60
+ it("discovers skills from .agents/skills directory hierarchy", async () => {
61
+ const skillA = createMockDirectoryHandle("skill-a", {
62
+ "SKILL.md": createMockFileHandle("SKILL.md"),
63
+ });
64
+ const skillsDir = createMockDirectoryHandle("skills", {
65
+ "skill-a": skillA,
66
+ });
67
+ const agentsDir = createMockDirectoryHandle(".agents", {
68
+ skills: skillsDir,
69
+ });
70
+ const root = createMockDirectoryHandle("root", {
71
+ ".agents": agentsDir,
72
+ });
73
+
74
+ mockGetGroupDir.mockResolvedValue(root);
75
+ mockReadGroupFile.mockResolvedValue(`---
76
+ name: skill-a
77
+ description: First test skill
78
+ ---
79
+ # Skill A Content
80
+ `);
81
+
82
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
83
+ expect(result.skills).toHaveLength(1);
84
+ expect(result.skills[0].name).toBe("skill-a");
85
+ expect(result.skills[0].description).toBe("First test skill");
86
+ expect(result.skills[0].path).toBe(".agents/skills/skill-a/SKILL.md");
87
+ expect(result.diagnostics).toEqual([]);
88
+ });
89
+
90
+ it("records diagnostic when duplicate skill names exist in current group", async () => {
91
+ const skillA1 = createMockDirectoryHandle("skill-a1", {
92
+ "SKILL.md": createMockFileHandle("SKILL.md"),
93
+ });
94
+ const skillA2 = createMockDirectoryHandle("skill-a2", {
95
+ "SKILL.md": createMockFileHandle("SKILL.md"),
96
+ });
97
+ const skillsDir = createMockDirectoryHandle("skills", {
98
+ "skill-a1": skillA1,
99
+ "skill-a2": skillA2,
100
+ });
101
+ const agentsDir = createMockDirectoryHandle(".agents", {
102
+ skills: skillsDir,
103
+ });
104
+ const root = createMockDirectoryHandle("root", {
105
+ ".agents": agentsDir,
106
+ });
107
+
108
+ mockGetGroupDir.mockResolvedValue(root);
109
+ // Both return the same name "duplicate-skill"
110
+ mockReadGroupFile.mockResolvedValue(`---
111
+ name: duplicate-skill
112
+ description: Dup skill
113
+ ---
114
+ Body
115
+ `);
116
+
117
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
118
+ expect(result.skills).toHaveLength(1);
119
+ expect(result.diagnostics).toHaveLength(1);
120
+ expect(result.diagnostics[0].message).toContain(
121
+ "Duplicate skill name: duplicate-skill",
122
+ );
123
+ });
124
+
125
+ it("records diagnostic when skill parsing fails (invalid frontmatter)", async () => {
126
+ const brokenSkill = createMockDirectoryHandle("broken", {
127
+ "SKILL.md": createMockFileHandle("SKILL.md"),
128
+ });
129
+ const skillsDir = createMockDirectoryHandle("skills", {
130
+ broken: brokenSkill,
131
+ });
132
+ const agentsDir = createMockDirectoryHandle(".agents", {
133
+ skills: skillsDir,
134
+ });
135
+ const root = createMockDirectoryHandle("root", {
136
+ ".agents": agentsDir,
137
+ });
138
+
139
+ mockGetGroupDir.mockResolvedValue(root);
140
+ mockReadGroupFile.mockResolvedValue(`invalid content without frontmatter`);
141
+
142
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
143
+ expect(result.skills).toHaveLength(0);
144
+ expect(result.diagnostics).toHaveLength(1);
145
+ expect(result.diagnostics[0].message).toBeTruthy();
146
+ });
147
+
148
+ it("stops and adds diagnostic when visited directories exceeds limit", async () => {
149
+ const deepChildren: Record<string, any> = {};
150
+ for (let i = 0; i <= 2001; i++) {
151
+ deepChildren[`dir_${i}`] = createMockDirectoryHandle(`dir_${i}`, {});
152
+ }
153
+
154
+ const skillsDir = createMockDirectoryHandle("skills", deepChildren);
155
+ const agentsDir = createMockDirectoryHandle(".agents", {
156
+ skills: skillsDir,
157
+ });
158
+ const root = createMockDirectoryHandle("root", {
159
+ ".agents": agentsDir,
160
+ });
161
+
162
+ mockGetGroupDir.mockResolvedValue(root);
163
+
164
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
165
+ expect(
166
+ result.diagnostics.some((d) =>
167
+ d.message.includes("stopped after 2000 directories"),
168
+ ),
169
+ ).toBe(true);
170
+ });
171
+
172
+ it("scans custom group first then fallback main group", async () => {
173
+ const rootCustom = createMockDirectoryHandle("rootCustom", {});
174
+ const skillDefault = createMockDirectoryHandle("default-skill", {
175
+ "SKILL.md": createMockFileHandle("SKILL.md"),
176
+ });
177
+ const skillsDir = createMockDirectoryHandle("skills", {
178
+ "default-skill": skillDefault,
179
+ });
180
+ const agentsDir = createMockDirectoryHandle(".agents", {
181
+ skills: skillsDir,
182
+ });
183
+ const rootDefault = createMockDirectoryHandle("rootDefault", {
184
+ ".agents": agentsDir,
185
+ });
186
+
187
+ mockGetGroupDir.mockImplementation(async (_db: any, gId: string) => {
188
+ if (gId === "custom-group") return rootCustom;
189
+ return rootDefault;
190
+ });
191
+
192
+ mockReadGroupFile.mockResolvedValue(`---
193
+ name: default-skill
194
+ description: Default fallback
195
+ ---
196
+ Body
197
+ `);
198
+
199
+ const result = await discoverSkills({} as any, "custom-group");
200
+ expect(result.skills).toHaveLength(1);
201
+ expect(result.skills[0].name).toBe("default-skill");
202
+ });
203
+
204
+ describe("loadSkill", () => {
205
+ it("returns null if skill is not found", async () => {
206
+ const root = createMockDirectoryHandle("root", {});
207
+ mockGetGroupDir.mockResolvedValue(root);
208
+
209
+ const skill = await loadSkill({} as any, DEFAULT_GROUP_ID, "nonexistent");
210
+ expect(skill).toBeNull();
211
+ });
212
+
213
+ it("loads and refreshes skill with resources", async () => {
214
+ const helperFile = createMockFileHandle("helper.py");
215
+ const subFolder = createMockDirectoryHandle("scripts", {
216
+ "run.sh": createMockFileHandle("run.sh"),
217
+ });
218
+ const skillDir = createMockDirectoryHandle("my-skill", {
219
+ "SKILL.md": createMockFileHandle("SKILL.md"),
220
+ "helper.py": helperFile,
221
+ scripts: subFolder,
222
+ });
223
+ const skillsDir = createMockDirectoryHandle("skills", {
224
+ "my-skill": skillDir,
225
+ });
226
+ const agentsDir = createMockDirectoryHandle(".agents", {
227
+ skills: skillsDir,
228
+ });
229
+ const root = createMockDirectoryHandle("root", {
230
+ ".agents": agentsDir,
231
+ });
232
+
233
+ mockGetGroupDir.mockResolvedValue(root);
234
+ mockReadGroupFile.mockResolvedValue(`---
235
+ name: my-skill
236
+ description: Full loaded skill
237
+ ---
238
+ Loaded body instructions
239
+ `);
240
+
241
+ const loaded = await loadSkill({} as any, DEFAULT_GROUP_ID, "my-skill");
242
+ expect(loaded).not.toBeNull();
243
+ expect(loaded?.name).toBe("my-skill");
244
+ expect(loaded?.body).toBe("Loaded body instructions");
245
+ expect(loaded?.resources).toEqual([
246
+ ".agents/skills/my-skill/helper.py",
247
+ ".agents/skills/my-skill/scripts/run.sh",
248
+ ]);
249
+ });
250
+ });
251
+ });