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
@@ -1,902 +1,237 @@
1
1
  import { jest } from "@jest/globals";
2
2
 
3
+ const mockDb = {} as any;
4
+ let storedConfig: Record<string, any> = {};
5
+
3
6
  jest.unstable_mockModule("../../db/getConfig.js", () => ({
4
- getConfig: (jest.fn() as any).mockResolvedValue(undefined),
7
+ getConfig: jest.fn(async (_db: any, key: string) => storedConfig[key]),
5
8
  }));
6
9
 
7
10
  jest.unstable_mockModule("../../db/setConfig.js", () => ({
8
- setConfig: jest.fn<any>().mockResolvedValue(undefined),
11
+ setConfig: jest.fn(async (_db: any, key: string, value: any) => {
12
+ storedConfig[key] = value;
13
+ }),
9
14
  }));
10
15
 
11
16
  jest.unstable_mockModule("../../security/crypto.js", () => ({
12
- decryptValue: jest.fn(async (v) => `decrypted:${v}`),
13
- encryptValue: jest.fn(async (v) => `encrypted:${v}`),
17
+ encryptValue: jest.fn(async (val: string) => `enc:${val}`),
18
+ decryptValue: jest.fn(async (val: string) =>
19
+ val.startsWith("enc:") ? val.slice(4) : val,
20
+ ),
14
21
  }));
15
22
 
16
23
  jest.unstable_mockModule("../../config/config.js", () => ({
17
24
  CONFIG_KEYS: {
18
25
  GIT_ACCOUNTS: "git_accounts",
19
- GIT_AUTHOR_EMAIL: "git_author_email",
20
- GIT_AUTHOR_NAME: "git_author_name",
21
26
  GIT_DEFAULT_ACCOUNT: "git_default_account",
22
- GIT_PASSWORD: "git_password",
23
27
  GIT_TOKEN: "git_token",
24
28
  GIT_USERNAME: "git_username",
29
+ GIT_PASSWORD: "git_password",
30
+ GIT_AUTHOR_NAME: "git_author_name",
31
+ GIT_AUTHOR_EMAIL: "git_author_email",
25
32
  },
26
- getProviderTokenAuthScheme: (
27
- providerId,
28
- authMode,
29
- serviceType = "http_api",
30
- ) => {
31
- const schemes = {
32
- github: {
33
- default: {
34
- oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
35
- token: { headerName: "Authorization", headerPrefix: "token " },
36
- },
37
- },
38
- gitlab: {
39
- byServiceType: {
40
- git_remote: {
41
- oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
42
- token: { headerName: "Authorization", headerPrefix: "Bearer " },
43
- },
44
- },
45
- default: {
46
- oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
47
- token: { headerName: "PRIVATE-TOKEN", headerPrefix: "" },
48
- },
49
- },
50
- azure_devops: {
51
- byServiceType: {
52
- git_remote: {
53
- oauth: { headerName: "Authorization", headerPrefix: "Basic " },
54
- token: { headerName: "Authorization", headerPrefix: "Basic " },
55
- },
56
- },
57
- default: {
58
- oauth: { headerName: "Authorization", headerPrefix: "Bearer " },
59
- token: { headerName: "Authorization", headerPrefix: "Bearer " },
60
- },
61
- },
62
- };
63
-
64
- const provider = schemes[providerId];
65
- if (!provider) {
66
- return null;
33
+ getProviderTokenAuthScheme: jest.fn((providerId: string) => {
34
+ if (providerId === "azure_devops") {
35
+ return { headerName: "Authorization", headerPrefix: "Basic " };
67
36
  }
68
-
69
- return (
70
- provider.byServiceType?.[serviceType]?.[authMode] ||
71
- provider.default?.[authMode] ||
72
- null
73
- );
74
- },
37
+ return { headerName: "Authorization", headerPrefix: "Bearer " };
38
+ }),
75
39
  }));
76
40
 
77
- const { getConfig } = await import("../../db/getConfig.js");
78
- const { setConfig } = await import("../../db/setConfig.js");
79
- const { resolveGitCredentials, detectProvider, buildAuthHeaders } =
41
+ const { detectProvider, buildAuthHeaders, resolveGitCredentials } =
80
42
  await import("./credentials.js");
81
43
 
82
- const mockGetConfig = getConfig as any;
83
- const mockSetConfig = setConfig as any;
84
-
85
- function setupConfig(map: any) {
86
- (mockGetConfig as any).mockImplementation(
87
- async (_db: any, key: any) => map[key],
88
- );
89
- }
90
-
91
- const fakeDb: any = {} as any;
92
-
93
- describe("resolveGitCredentials", () => {
44
+ describe("git credentials", () => {
94
45
  beforeEach(() => {
95
- mockGetConfig.mockReset();
96
- mockSetConfig.mockReset();
97
- (mockGetConfig as any).mockResolvedValue(undefined);
46
+ jest.clearAllMocks();
47
+ storedConfig = {};
98
48
  });
99
49
 
100
- it("returns empty creds when nothing is configured", async () => {
101
- const result = await resolveGitCredentials(fakeDb);
102
- expect(result).toEqual({
103
- authMode: "token",
104
- authorEmail: undefined,
105
- authorName: undefined,
106
- password: undefined,
107
- provider: "generic",
108
- token: undefined,
109
- username: undefined,
50
+ describe("detectProvider", () => {
51
+ it("detects GitHub", () => {
52
+ expect(detectProvider("github.com")).toBe("github");
53
+ expect(detectProvider("https://github.com/owner/repo")).toBe("github");
110
54
  });
111
- });
112
-
113
- // ── Legacy single-account fallback ─────────────────────────────
114
- describe("legacy fallback (no accounts configured)", () => {
115
- it("returns decrypted token from legacy GIT_TOKEN", async () => {
116
- setupConfig({ git_token: "enc-tok" });
117
55
 
118
- const result = await resolveGitCredentials(fakeDb);
119
- expect(result.token).toBe("decrypted:enc-tok");
56
+ it("detects Azure DevOps", () => {
57
+ expect(detectProvider("dev.azure.com")).toBe("azure-devops");
58
+ expect(detectProvider("myorg.visualstudio.com")).toBe("azure-devops");
120
59
  });
121
60
 
122
- it("returns username and decrypted password from legacy keys", async () => {
123
- setupConfig({
124
- git_password: "enc-pw",
125
- git_username: "alice",
126
- });
127
-
128
- const result = await resolveGitCredentials(fakeDb);
129
- expect(result.username).toBe("alice");
130
- expect(result.password).toBe("decrypted:enc-pw");
61
+ it("detects GitLab", () => {
62
+ expect(detectProvider("gitlab.com")).toBe("gitlab");
63
+ expect(detectProvider("https://gitlab.example.com/repo")).toBe("gitlab");
131
64
  });
132
65
 
133
- it("returns author name and email from legacy keys", async () => {
134
- setupConfig({
135
- git_author_email: "alice@example.com",
136
- git_author_name: "Alice",
137
- });
138
-
139
- const result = await resolveGitCredentials(fakeDb);
140
- expect(result.authorName).toBe("Alice");
141
- expect(result.authorEmail).toBe("alice@example.com");
66
+ it("falls back to generic for other hosts", () => {
67
+ expect(detectProvider("bitbucket.org")).toBe("generic");
68
+ expect(detectProvider("")).toBe("generic");
142
69
  });
143
70
  });
144
71
 
145
- // ── Multi-account with host pattern matching ───────────────────
146
- describe("multi-account resolution", () => {
147
- const githubAccount: any = {
148
- authorEmail: "gh@example.com",
149
- authorName: "GH User",
150
- hostPattern: "github.com",
151
- id: "acct-1",
152
- label: "GitHub",
153
- password: "",
154
- token: "enc-gh-tok",
155
- username: "",
156
- };
157
-
158
- const azureAccount: any = {
159
- authorEmail: "az@example.com",
160
- authorName: "AZ User",
161
- hostPattern: "dev.azure.com",
162
- id: "acct-2",
163
- label: "ADO",
164
- password: "enc-az-pw",
165
- token: "enc-az-tok",
166
- username: "azuser",
167
- };
168
-
169
- it("matches account by URL hostname", async () => {
170
- setupConfig({
171
- git_accounts: [githubAccount, azureAccount],
172
- git_default_account: "acct-1",
173
- });
174
-
175
- const result = await resolveGitCredentials(
176
- fakeDb,
177
- "https://github.com/org/repo.git",
178
- );
179
-
180
- expect(result.token).toBe("decrypted:enc-gh-tok");
181
- expect(result.authorName).toBe("GH User");
182
- expect(result.authorEmail).toBe("gh@example.com");
72
+ describe("buildAuthHeaders", () => {
73
+ it("returns empty object when reauthRequired is true", () => {
74
+ expect(
75
+ buildAuthHeaders({
76
+ reauthRequired: true,
77
+ token: "secret-token",
78
+ provider: "github",
79
+ }),
80
+ ).toEqual({});
183
81
  });
184
82
 
185
- it("matches Azure account by URL hostname", async () => {
186
- setupConfig({
187
- git_accounts: [githubAccount, azureAccount],
188
- git_default_account: "acct-1",
83
+ it("builds Authorization header for Azure DevOps basic/token", () => {
84
+ const headers = buildAuthHeaders({
85
+ provider: "azure-devops",
86
+ token: "pat123",
189
87
  });
190
-
191
- const result = await resolveGitCredentials(
192
- fakeDb,
193
- "https://dev.azure.com/org/project/_git/repo",
194
- );
195
-
196
- expect(result.authorName).toBe("AZ User");
197
- expect(result.password).toBe("decrypted:enc-az-pw");
198
- expect(result.token).toBe("decrypted:enc-az-tok");
199
- expect(result.username).toBe("azuser");
88
+ expect(headers.Authorization).toBe(`Basic ${btoa(":pat123")}`);
200
89
  });
201
90
 
202
- it("does not fall back to unrelated default account when URL has not match", async () => {
203
- setupConfig({
204
- git_accounts: [githubAccount, azureAccount],
205
- git_default_account: "acct-2",
91
+ it("builds Authorization header for Azure DevOps username/password", () => {
92
+ const headers = buildAuthHeaders({
93
+ provider: "azure-devops",
94
+ username: "user1",
95
+ password: "pwd",
206
96
  });
207
-
208
- const result = await resolveGitCredentials(
209
- fakeDb,
210
- "https://gitlab.com/org/repo.git",
211
- );
212
-
213
- // Should NOT send Azure credentials to GitLab -- that will cause 401s on
214
- // public repos. Instead, return empty creds for anonymous clones
215
- expect(result.token).toBeUndefined();
216
- expect(result.username).toBeUndefined();
217
- expect(result.password).toBeUndefined();
97
+ expect(headers.Authorization).toBe(`Basic ${btoa("user1:pwd")}`);
218
98
  });
219
99
 
220
- it("falls back to default account when no URL is provided", async () => {
221
- setupConfig({
222
- git_accounts: [githubAccount, azureAccount],
223
- git_default_account: "acct-1",
100
+ it("builds Bearer Authorization header for general tokens", () => {
101
+ const headers = buildAuthHeaders({
102
+ provider: "github",
103
+ token: "ghp_123456",
224
104
  });
225
-
226
- const result = await resolveGitCredentials(fakeDb);
227
- expect(result.token).toBe("decrypted:enc-gh-tok");
228
- expect(result.authorName).toBe("GH User");
105
+ expect(headers.Authorization).toBe("Bearer ghp_123456");
229
106
  });
230
107
 
231
- it("falls back to first account if no default is set", async () => {
232
- setupConfig({
233
- git_accounts: [githubAccount, azureAccount],
234
- });
235
-
236
- const result = await resolveGitCredentials(fakeDb);
237
- expect(result.token).toBe("decrypted:enc-gh-tok");
108
+ it("returns empty object when no token or credentials provided", () => {
109
+ expect(buildAuthHeaders({ provider: "github" })).toEqual({});
238
110
  });
111
+ });
239
112
 
240
- it("skips empty token and password fields", async () => {
241
- const noTokenAccount: any = {
242
- authorEmail: "",
243
- authorName: "",
244
- hostPattern: "example.com",
245
- id: "acct-3",
246
- label: "No Token",
247
- password: "",
248
- token: "",
249
- username: "user",
250
- };
251
-
252
- setupConfig({
253
- git_accounts: [noTokenAccount],
254
- git_default_account: "acct-3",
255
- });
256
-
257
- const result = await resolveGitCredentials(
258
- fakeDb,
259
- "https://example.com/repo.git",
260
- );
261
-
262
- expect(result.token).toBeUndefined();
263
- expect(result.username).toBe("user");
264
- expect(result.password).toBeUndefined();
265
- });
266
-
267
- it("supports partial hostPattern matching (substring of hostname)", async () => {
268
- const account: any = {
269
- authorEmail: "",
270
- authorName: "",
271
- hostPattern: "gitlab.mycompany.com",
272
- id: "acct-4",
273
- label: "Custom GitLab",
274
- password: "",
275
- token: "enc-custom-tok",
276
- username: "",
277
- };
278
-
279
- setupConfig({
280
- git_accounts: [account],
281
- });
282
-
283
- const result = await resolveGitCredentials(
284
- fakeDb,
285
- "https://gitlab.mycompany.com/group/project.git",
286
- );
287
-
288
- expect(result.token).toBe("decrypted:enc-custom-tok");
289
- });
290
-
291
- it("supports selecting an explicit git account id", async () => {
292
- const patAccount: any = {
293
- authMode: "token",
294
- authorEmail: "",
295
- authorName: "",
296
- hostPattern: "github.com",
297
- id: "acct-pat",
298
- label: "GitHub PAT",
299
- password: "",
300
- token: "enc-pat-tok",
301
- username: "",
302
- };
303
-
304
- const oauthAccount: any = {
305
- authMode: "oauth",
306
- authorEmail: "",
307
- authorName: "",
308
- hostPattern: "github.com",
309
- id: "acct-oauth",
310
- label: "GitHub OAuth",
311
- password: "",
312
- token: "enc-oauth-tok",
313
- username: "",
314
- };
113
+ describe("resolveGitCredentials", () => {
114
+ it("resolves from legacy single-key config when no accounts configured", async () => {
115
+ storedConfig["git_token"] = "enc:legacy-token";
116
+ storedConfig["git_username"] = "legacy-user";
117
+ storedConfig["git_author_name"] = "Legacy Author";
118
+ storedConfig["git_author_email"] = "legacy@example.com";
315
119
 
316
- setupConfig({
317
- git_accounts: [patAccount, oauthAccount],
318
- });
319
-
320
- const result = await resolveGitCredentials(
321
- fakeDb,
120
+ const creds = await resolveGitCredentials(
121
+ mockDb,
322
122
  "https://github.com/org/repo.git",
323
- { accountId: "acct-oauth" },
324
123
  );
325
-
326
- expect(result.token).toBe("decrypted:enc-oauth-tok");
327
- expect(result.authMode).toBe("oauth");
328
- expect(result.accountId).toBe("acct-oauth");
124
+ expect(creds.token).toBe("legacy-token");
125
+ expect(creds.username).toBe("legacy-user");
126
+ expect(creds.authorName).toBe("Legacy Author");
127
+ expect(creds.authorEmail).toBe("legacy@example.com");
128
+ expect(creds.provider).toBe("generic");
329
129
  });
330
130
 
331
- it("prefers matching auth mode when host patterns tie", async () => {
332
- const patAccount: any = {
333
- authMode: "token",
334
- authorEmail: "",
335
- authorName: "",
336
- hostPattern: "github.com",
337
- id: "acct-pat",
338
- label: "GitHub PAT",
339
- password: "",
340
- token: "enc-pat-tok",
341
- username: "",
342
- };
343
-
344
- const oauthAccount: any = {
345
- authMode: "oauth",
346
- authorEmail: "",
347
- authorName: "",
348
- hostPattern: "github.com",
349
- id: "acct-oauth",
350
- label: "GitHub OAuth",
351
- password: "",
352
- token: "enc-oauth-tok",
353
- username: "",
354
- };
355
-
356
- setupConfig({
357
- git_accounts: [patAccount, oauthAccount],
358
- });
131
+ it("resolves and decrypts credentials for matching host URL with longest pattern match", async () => {
132
+ storedConfig["git_accounts"] = [
133
+ {
134
+ id: "general-azure",
135
+ hostPattern: "dev.azure.com",
136
+ provider: "azure-devops",
137
+ token: "enc:general-token",
138
+ },
139
+ {
140
+ id: "specific-azure",
141
+ hostPattern: "dev.azure.com/specific-org",
142
+ provider: "azure-devops",
143
+ token: "enc:specific-token",
144
+ },
145
+ ];
359
146
 
360
- const result = await resolveGitCredentials(
361
- fakeDb,
362
- "https://github.com/org/repo.git",
363
- { authMode: "oauth" },
147
+ const creds = await resolveGitCredentials(
148
+ mockDb,
149
+ "https://dev.azure.com/specific-org/project/_git/repo",
364
150
  );
365
151
 
366
- expect(result.token).toBe("decrypted:enc-oauth-tok");
367
- expect(result.authMode).toBe("oauth");
368
- expect(result.accountId).toBe("acct-oauth");
152
+ expect(creds.accountId).toBe("specific-azure");
153
+ expect(creds.token).toBe("specific-token");
154
+ expect(creds.provider).toBe("azure-devops");
369
155
  });
370
156
 
371
- it("refreshes expiring OAuth Git tokens before returning credentials", async () => {
372
- const oauthAccount: any = {
373
- accessTokenExpiresAt: Date.now() - 1_000,
374
- authMode: "oauth",
375
- authorEmail: "",
376
- authorName: "",
377
- hostPattern: "github.com",
378
- id: "oauth-1",
379
- label: "GitHub OAuth",
380
- oauthClientId: "client-123",
381
- oauthProviderId: "github",
382
- password: "",
383
- refreshToken: "enc-refresh",
384
- scopes: ["repo"],
385
- token: "enc-old-access",
386
- username: "",
387
- };
388
-
389
- setupConfig({
390
- git_accounts: [oauthAccount],
391
- });
392
-
393
- (global as any).fetch = (jest.fn() as any).mockResolvedValue({
394
- json: async () => ({
395
- accessToken: "new-access-token",
396
- expiresIn: 3600,
397
- refreshToken: "new-refresh-token",
398
- scope: "repo read:user",
399
- tokenType: "Bearer",
400
- }),
401
- ok: true,
402
- });
403
-
404
- const result = await resolveGitCredentials(
405
- fakeDb,
406
- "https://github.com/org/repo.git",
407
- { authMode: "oauth" },
408
- );
157
+ it("resolves specific account when accountId option is passed", async () => {
158
+ storedConfig["git_accounts"] = [
159
+ {
160
+ id: "acct-1",
161
+ hostPattern: "github.com",
162
+ token: "enc:tok-1",
163
+ },
164
+ {
165
+ id: "acct-2",
166
+ hostPattern: "github.com",
167
+ token: "enc:tok-2",
168
+ },
169
+ ];
409
170
 
410
- expect(result.token).toBe("decrypted:encrypted:new-access-token");
411
- expect(result.authMode).toBe("oauth");
412
- expect(mockSetConfig).toHaveBeenCalledWith(
413
- fakeDb,
414
- "git_accounts",
415
- expect.arrayContaining([
416
- expect.objectContaining({
417
- id: "oauth-1",
418
- refreshToken: "encrypted:new-refresh-token",
419
- token: "encrypted:new-access-token",
420
- }),
421
- ]),
171
+ const creds = await resolveGitCredentials(
172
+ mockDb,
173
+ "https://github.com/foo/bar",
174
+ {
175
+ accountId: "acct-2",
176
+ },
422
177
  );
178
+ expect(creds.accountId).toBe("acct-2");
179
+ expect(creds.token).toBe("tok-2");
423
180
  });
424
181
 
425
- it("forces Git OAuth refresh when forceRefresh option is true", async () => {
426
- const oauthAccount: any = {
427
- accessTokenExpiresAt: Date.now() + 60 * 60 * 1000,
428
- authMode: "oauth",
429
- authorEmail: "",
430
- authorName: "",
431
- hostPattern: "github.com",
432
- id: "oauth-2",
433
- label: "GitHub OAuth",
434
- oauthClientId: "client-123",
435
- oauthProviderId: "github",
436
- password: "",
437
- refreshToken: "enc-refresh",
438
- scopes: ["repo"],
439
- token: "enc-old-access",
440
- username: "",
441
- };
442
-
443
- setupConfig({
444
- git_accounts: [oauthAccount],
445
- });
446
-
447
- (global as any).fetch = (jest.fn() as any).mockResolvedValue({
448
- json: async () => ({
449
- accessToken: "forced-access-token",
450
- expiresIn: 1800,
451
- }),
452
- ok: true,
453
- });
454
-
455
- const result = await resolveGitCredentials(
456
- fakeDb,
457
- "https://github.com/org/repo.git",
458
- { authMode: "oauth", forceRefresh: true },
459
- );
182
+ it("resolves default account when URL is omitted or matches default host", async () => {
183
+ storedConfig["git_default_account"] = "default-acct";
184
+ storedConfig["git_accounts"] = [
185
+ {
186
+ id: "default-acct",
187
+ hostPattern: "github.com",
188
+ provider: "github",
189
+ token: "enc:default-tok",
190
+ },
191
+ ];
460
192
 
461
- expect(result.token).toBe("decrypted:encrypted:forced-access-token");
462
- expect((global as any).fetch).toHaveBeenCalledWith(
463
- "/oauth/refresh",
464
- expect.objectContaining({ method: "POST" }),
465
- );
193
+ const creds = await resolveGitCredentials(mockDb);
194
+ expect(creds.accountId).toBe("default-acct");
195
+ expect(creds.token).toBe("default-tok");
466
196
  });
467
197
 
468
- it("marks Git OAuth account as reauth-required after repeated refresh failures", async () => {
469
- const oauthAccount: any = {
470
- accessTokenExpiresAt: Date.now() - 1_000,
471
- authMode: "oauth",
472
- authorEmail: "",
473
- authorName: "",
474
- hostPattern: "github.com",
475
- id: "oauth-3",
476
- label: "GitHub OAuth",
477
- oauthClientId: "client-123",
478
- oauthProviderId: "github",
479
- password: "",
480
- refreshToken: "enc-refresh",
481
- token: "enc-old-access",
482
- username: "",
483
- };
484
-
485
- setupConfig({
486
- git_accounts: [oauthAccount],
487
- });
488
-
489
- (global as any).fetch = (jest.fn() as any).mockResolvedValue({
490
- json: async () => ({ error: "invalid_grant" }),
491
- ok: false,
492
- status: 401,
493
- });
494
-
495
- await resolveGitCredentials(fakeDb, "https://github.com/org/repo.git", {
496
- authMode: "oauth",
497
- });
498
-
499
- await resolveGitCredentials(fakeDb, "https://github.com/org/repo.git", {
500
- authMode: "oauth",
501
- });
502
-
503
- const third = await resolveGitCredentials(
504
- fakeDb,
505
- "https://github.com/org/repo.git",
198
+ it("handles oauth reauth required state", async () => {
199
+ storedConfig["git_accounts"] = [
506
200
  {
201
+ id: "oauth-acct",
202
+ hostPattern: "github.com",
203
+ provider: "github",
507
204
  authMode: "oauth",
205
+ oauthReauthRequired: true,
206
+ authorEmail: "oauth@example.com",
508
207
  },
509
- );
208
+ ];
510
209
 
511
- expect(third.reauthRequired).toBe(true);
512
- expect(mockSetConfig).toHaveBeenLastCalledWith(
513
- fakeDb,
514
- "git_accounts",
515
- expect.arrayContaining([
516
- expect.objectContaining({
517
- id: "oauth-3",
518
- oauthReauthRequired: true,
519
- oauthRefreshFailureCount: 3,
520
- }),
521
- ]),
210
+ const creds = await resolveGitCredentials(
211
+ mockDb,
212
+ "https://github.com/foo/bar",
522
213
  );
214
+ expect(creds.reauthRequired).toBe(true);
215
+ expect(creds.authMode).toBe("oauth");
216
+ expect(creds.authorEmail).toBe("oauth@example.com");
523
217
  });
524
218
 
525
- // ── Path-based host patterns (e.g. ADO orgs) ─────────
526
- describe("path-based host patterns", () => {
527
- const exampleOrg2Account: any = {
528
- id: "acct-exampleOrg2",
529
- label: "ADO - ExampleOrg2",
530
- hostPattern: "dev.azure.com/exampleOrg2",
531
- token: "enc-exampleOrg2-tok",
532
- username: "",
533
- password: "",
534
- authorName: "ExampleOrg2 Dev",
535
- authorEmail: "dev@exampleOrg2.example.com",
536
- };
537
-
538
- const exampleOrg1Account: any = {
539
- id: "acct-exampleOrg1",
540
- label: "ADO - ExampleOrg1",
541
- hostPattern: "dev.azure.com/exampleOrg1",
542
- token: "enc-exampleOrg1-tok",
543
- username: "",
544
- password: "",
545
- authorName: "ExampleOrg1 Dev",
546
- authorEmail: "dev@exampleOrg1.example.com",
547
- };
548
-
549
- it("matches account with host+path pattern against URL", async () => {
550
- setupConfig({
551
- git_accounts: [exampleOrg2Account, exampleOrg1Account],
552
- });
553
-
554
- const result = await resolveGitCredentials(
555
- fakeDb,
556
- "https://dev.azure.com/exampleOrg2/project/_git/repo",
557
- );
558
-
559
- expect(result.token).toBe("decrypted:enc-exampleOrg2-tok");
560
- expect(result.authorName).toBe("ExampleOrg2 Dev");
561
- });
562
-
563
- it("matches second account with different org path", async () => {
564
- setupConfig({
565
- git_accounts: [exampleOrg2Account, exampleOrg1Account],
566
- });
567
-
568
- const result = await resolveGitCredentials(
569
- fakeDb,
570
- "https://dev.azure.com/exampleOrg1/project/_git/repo",
571
- );
572
-
573
- expect(result.token).toBe("decrypted:enc-exampleOrg1-tok");
574
- expect(result.authorName).toBe("ExampleOrg1 Dev");
575
- });
576
-
577
- it("prefers more specific path pattern over domain-only pattern", async () => {
578
- const domainOnlyAccount: any = {
579
- id: "acct-az-generic",
580
- label: "ADO (generic)",
581
- hostPattern: "dev.azure.com",
582
- token: "enc-generic-tok",
583
- username: "",
584
- password: "",
585
- authorName: "",
586
- authorEmail: "",
587
- };
588
-
589
- setupConfig({
590
- git_accounts: [domainOnlyAccount, exampleOrg2Account],
591
- });
592
-
593
- const result = await resolveGitCredentials(
594
- fakeDb,
595
- "https://dev.azure.com/exampleOrg2/project/_git/repo",
596
- );
597
-
598
- // Should prefer the more specific path-based match
599
- expect(result.token).toBe("decrypted:enc-exampleOrg2-tok");
600
- expect(result.authorName).toBe("ExampleOrg2 Dev");
601
- });
602
-
603
- it("falls back to domain-only pattern when path does not match", async () => {
604
- const domainOnlyAccount: any = {
605
- id: "acct-az-generic",
606
- label: "ADO (generic)",
607
- hostPattern: "dev.azure.com",
608
- token: "enc-generic-tok",
609
- username: "",
610
- password: "",
611
- authorName: "",
612
- authorEmail: "",
613
- };
614
-
615
- setupConfig({
616
- git_accounts: [exampleOrg2Account, domainOnlyAccount],
617
- });
618
-
619
- const result = await resolveGitCredentials(
620
- fakeDb,
621
- "https://dev.azure.com/other-org/project/_git/repo",
622
- );
623
- expect(result.token).toBe("decrypted:enc-generic-tok");
624
- });
625
-
626
- it("detects azure-devops provider from path-based host pattern", () => {
627
- expect(detectProvider("dev.azure.com/exampleOrg2")).toBe(
628
- "azure-devops",
629
- );
630
- expect(detectProvider("dev.azure.com/exampleOrg1")).toBe(
631
- "azure-devops",
632
- );
633
- });
634
-
635
- it("builds correct auth headers from path-based host pattern", () => {
636
- const headers = buildAuthHeaders({
637
- token: "pat123",
638
- hostPattern: "dev.azure.com/exampleOrg2",
639
- });
640
- const expected = btoa(":pat123");
641
- expect(headers).toEqual({
642
- Authorization: `Basic ${expected}`,
643
- });
644
- });
645
- });
646
- });
647
-
648
- // ── Priority: accounts over legacy ─────────────────────────────
649
- describe("priority", () => {
650
- it("uses accounts over legacy when both exist", async () => {
651
- const account: any = {
652
- id: "acct-1",
653
- label: "GitHub",
654
- hostPattern: "github.com",
655
- token: "enc-new-tok",
656
- username: "",
657
- password: "",
658
- authorName: "",
659
- authorEmail: "",
660
- };
661
-
662
- setupConfig({
663
- git_accounts: [account],
664
- git_default_account: "acct-1",
665
- git_token: "enc-old-tok",
666
- git_username: "old-user",
667
- });
219
+ it("returns empty default object when accounts exist but none match url", async () => {
220
+ storedConfig["git_accounts"] = [
221
+ {
222
+ id: "acct-github",
223
+ hostPattern: "github.com",
224
+ token: "enc:gh-tok",
225
+ },
226
+ ];
668
227
 
669
- const result = await resolveGitCredentials(
670
- fakeDb,
671
- "https://github.com/org/repo.git",
228
+ const creds = await resolveGitCredentials(
229
+ mockDb,
230
+ "https://gitlab.com/other/repo",
672
231
  );
673
- expect(result.token).toBe("decrypted:enc-new-tok");
674
- });
675
- });
676
- });
677
-
678
- // ── detectProvider ─────────────────────────────────────────────
679
- describe("detectProvider", () => {
680
- it("detects github from hostPattern", () => {
681
- expect(detectProvider("github.com")).toBe("github");
682
- });
683
-
684
- it("detects github enterprise", () => {
685
- expect(detectProvider("github.mycompany.com")).toBe("github");
686
- });
687
-
688
- it("detects azure-devops from dev.azure.com", () => {
689
- expect(detectProvider("dev.azure.com")).toBe("azure-devops");
690
- });
691
-
692
- it("detects azure-devops from visualstudio.com", () => {
693
- expect(detectProvider("myorg.visualstudio.com")).toBe("azure-devops");
694
- });
695
-
696
- it("detects gitlab from gitlab.com", () => {
697
- expect(detectProvider("gitlab.com")).toBe("gitlab");
698
- });
699
-
700
- it("detects gitlab from self-hosted", () => {
701
- expect(detectProvider("gitlab.mycompany.com")).toBe("gitlab");
702
- });
703
-
704
- it("returns generic for unknown hosts", () => {
705
- expect(detectProvider("bitbucket.org")).toBe("generic");
706
- expect(detectProvider("example.com")).toBe("generic");
707
- });
708
-
709
- it("returns generic for empty string", () => {
710
- expect(detectProvider("")).toBe("generic");
711
- });
712
- });
713
-
714
- // ── buildAuthHeaders ───────────────────────────────────────────
715
- describe("buildAuthHeaders", () => {
716
- it("returns token header for github provider", () => {
717
- const headers = buildAuthHeaders({
718
- token: "fake",
719
- provider: "github",
720
- });
721
- expect(headers).toEqual({
722
- Authorization: "token fake",
723
- });
724
- });
725
-
726
- it("returns Basic header for azure-devops with token", () => {
727
- const headers = buildAuthHeaders({
728
- token: "azure-fake",
729
- provider: "azure-devops",
730
- });
731
- // ADO: Basic base64(:PAT) — empty username, PAT as password
732
- const expected = btoa(":azure-fake");
733
- expect(headers).toEqual({
734
- Authorization: `Basic ${expected}`,
735
- });
736
- });
737
-
738
- it("returns Basic header for azure-devops with username+password", () => {
739
- const headers = buildAuthHeaders({
740
- username: "azuser",
741
- password: "azpass",
742
- provider: "azure-devops",
743
- });
744
-
745
- const expected = btoa("azuser:azpass");
746
- expect(headers).toEqual({
747
- Authorization: `Basic ${expected}`,
748
- });
749
- });
750
-
751
- it("prefers token over username+password for azure-devops", () => {
752
- const headers = buildAuthHeaders({
753
- token: "azure-fake",
754
- username: "azuser",
755
- password: "azpass",
756
- provider: "azure-devops",
232
+ expect(creds.accountId).toBeUndefined();
233
+ expect(creds.token).toBeUndefined();
234
+ expect(creds.provider).toBe("generic");
757
235
  });
758
-
759
- const expected = btoa(":azure-fake");
760
- expect(headers).toEqual({
761
- Authorization: `Basic ${expected}`,
762
- });
763
- });
764
-
765
- it("returns Bearer header for gitlab provider", () => {
766
- const headers = buildAuthHeaders({
767
- token: "glpat-fake",
768
- provider: "gitlab",
769
- });
770
- expect(headers).toEqual({
771
- Authorization: "Bearer glpat-fake",
772
- });
773
- });
774
-
775
- it("returns Bearer header for generic provider", () => {
776
- const headers = buildAuthHeaders({
777
- token: "some-token",
778
- provider: "generic",
779
- });
780
- expect(headers).toEqual({
781
- Authorization: "Bearer some-token",
782
- });
783
- });
784
-
785
- it("returns empty object when no credentials", () => {
786
- const headers = buildAuthHeaders({ provider: "github" });
787
- expect(headers).toEqual({} as any);
788
- });
789
-
790
- it("returns empty object when token is undefined", () => {
791
- const headers = buildAuthHeaders({
792
- token: undefined,
793
- provider: "github",
794
- });
795
- expect(headers).toEqual({} as any);
796
- });
797
-
798
- it("infers provider from hostPattern when provider not set", () => {
799
- const headers = buildAuthHeaders({
800
- token: "pat123",
801
- hostPattern: "dev.azure.com",
802
- });
803
-
804
- const expected = btoa(":pat123");
805
- expect(headers).toEqual({
806
- Authorization: `Basic ${expected}`,
807
- });
808
- });
809
-
810
- it("explicit provider overrides hostPattern inference", () => {
811
- const headers = buildAuthHeaders({
812
- token: "pat123",
813
- provider: "github",
814
- hostPattern: "dev.azure.com",
815
- });
816
- expect(headers).toEqual({
817
- Authorization: "token pat123",
818
- });
819
- });
820
- });
821
-
822
- // ── resolveGitCredentials returns provider ─────────────────────
823
- describe("resolveGitCredentials — provider field", () => {
824
- beforeEach(() => {
825
- mockGetConfig.mockReset();
826
- (mockGetConfig as any).mockResolvedValue(undefined);
827
- });
828
-
829
- it("returns provider from account hostPattern", async () => {
830
- const azureAccount: any = {
831
- id: "acct-az",
832
- label: "ADO",
833
- hostPattern: "dev.azure.com",
834
- token: "enc-tok",
835
- username: "",
836
- password: "",
837
- authorName: "",
838
- authorEmail: "",
839
- };
840
- setupConfig({
841
- git_accounts: [azureAccount],
842
- });
843
-
844
- const result = await resolveGitCredentials(
845
- fakeDb,
846
- "https://dev.azure.com/org/project/_git/repo",
847
- );
848
- expect(result.provider).toBe("azure-devops");
849
- });
850
-
851
- it("returns explicit provider from account when set", async () => {
852
- const customAccount: any = {
853
- id: "acct-custom",
854
- label: "Custom",
855
- hostPattern: "example.com",
856
- provider: "gitlab",
857
- token: "enc-tok",
858
- username: "",
859
- password: "",
860
- authorName: "",
861
- authorEmail: "",
862
- };
863
- setupConfig({
864
- git_accounts: [customAccount],
865
- });
866
-
867
- const result = await resolveGitCredentials(
868
- fakeDb,
869
- "https://example.com/repo.git",
870
- );
871
- expect(result.provider).toBe("gitlab");
872
- });
873
-
874
- it("returns generic provider for legacy credentials", async () => {
875
- setupConfig({ git_token: "enc-tok" });
876
- const result = await resolveGitCredentials(fakeDb);
877
- expect(result.provider).toBe("generic");
878
- });
879
-
880
- it("returns hostPattern in resolved credentials", async () => {
881
- const ghAccount: any = {
882
- id: "acct-gh",
883
- label: "GitHub",
884
- hostPattern: "github.com",
885
- token: "enc-tok",
886
- username: "",
887
- password: "",
888
- authorName: "",
889
- authorEmail: "",
890
- };
891
- setupConfig({
892
- git_accounts: [ghAccount],
893
- });
894
-
895
- const result = await resolveGitCredentials(
896
- fakeDb,
897
- "https://github.com/org/repo",
898
- );
899
- expect(result.hostPattern).toBe("github.com");
900
- expect(result.provider).toBe("github");
901
236
  });
902
237
  });