@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -0,0 +1,299 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ isTranscriptionConfigured,
4
+ NOT_TRANSCRIBED_NOTE,
5
+ resolveTranscriptionConfig,
6
+ TRANSCRIPTION_ENV_KEYS,
7
+ TranscriptionError,
8
+ transcribeAudio,
9
+ transcribeAudioNotes,
10
+ } from "../utils/transcription";
11
+
12
+ // Includes the provider keys the fallback reads: a worker shell often has
13
+ // OPENAI_API_KEY or ELEVENLABS_API_KEY set, which would leak into every test.
14
+ const ENV_KEYS = TRANSCRIPTION_ENV_KEYS;
15
+ const savedEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
16
+
17
+ type Captured = {
18
+ path: string;
19
+ authorization: string | null;
20
+ xiApiKey: string | null;
21
+ fields: string[];
22
+ model: FormDataEntryValue | null;
23
+ modelId: FormDataEntryValue | null;
24
+ filename: string;
25
+ };
26
+
27
+ const captured: Captured[] = [];
28
+ // The file part's Content-Type as sent. Bun's `formData()` re-derives a File's
29
+ // type from its extension (`.webm` reads back as video/webm), so read it raw.
30
+ const sentFileTypes: string[] = [];
31
+ let reply: () => Response = () => Response.json({ text: " hello " });
32
+ let stt: ReturnType<typeof Bun.serve>;
33
+
34
+ beforeAll(() => {
35
+ for (const key of ENV_KEYS) delete process.env[key];
36
+ stt = Bun.serve({
37
+ port: 0,
38
+ hostname: "127.0.0.1",
39
+ async fetch(req) {
40
+ const raw = await req.clone().text();
41
+ sentFileTypes.push(
42
+ /name="file"; filename="[^"]*"\r\nContent-Type: ([^\r\n]+)/.exec(raw)?.[1] ?? "",
43
+ );
44
+ const form = await req.formData();
45
+ captured.push({
46
+ path: new URL(req.url).pathname,
47
+ authorization: req.headers.get("authorization"),
48
+ xiApiKey: req.headers.get("xi-api-key"),
49
+ fields: [...form.keys()].sort(),
50
+ model: form.get("model"),
51
+ modelId: form.get("model_id"),
52
+ filename: (form.get("file") as File).name,
53
+ });
54
+ return reply();
55
+ },
56
+ });
57
+ });
58
+
59
+ afterAll(() => {
60
+ stt.stop(true);
61
+ for (const [key, value] of savedEnv) {
62
+ if (value === undefined) delete process.env[key];
63
+ else process.env[key] = value;
64
+ }
65
+ });
66
+
67
+ afterEach(() => {
68
+ captured.length = 0;
69
+ sentFileTypes.length = 0;
70
+ reply = () => Response.json({ text: " hello " });
71
+ for (const key of ENV_KEYS) delete process.env[key];
72
+ });
73
+
74
+ function configure(provider?: string) {
75
+ process.env.TRANSCRIPTION_API_KEY = "stt-test-key-123456";
76
+ process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
77
+ if (provider) process.env.TRANSCRIPTION_PROVIDER = provider;
78
+ }
79
+
80
+ const audio = {
81
+ data: new Blob([new Uint8Array([1, 2, 3])]),
82
+ filename: "note.ogg",
83
+ mimeType: "audio/ogg",
84
+ };
85
+
86
+ describe("transcribeAudio request shape", () => {
87
+ test("elevenlabs posts to /speech-to-text with xi-api-key and model_id", async () => {
88
+ configure("elevenlabs");
89
+ expect(await transcribeAudio(audio)).toBe("hello");
90
+ expect(captured).toEqual([
91
+ {
92
+ path: "/v1/speech-to-text",
93
+ authorization: null,
94
+ xiApiKey: "stt-test-key-123456",
95
+ fields: ["file", "model_id"],
96
+ model: null,
97
+ modelId: "scribe_v2",
98
+ filename: "note.ogg",
99
+ },
100
+ ]);
101
+ });
102
+
103
+ test("elevenlabs honours TRANSCRIPTION_MODEL in model_id", async () => {
104
+ configure("ElevenLabs");
105
+ process.env.TRANSCRIPTION_MODEL = "scribe_v1";
106
+ await transcribeAudio(audio);
107
+ expect(captured[0]).toMatchObject({ fields: ["file", "model_id"], modelId: "scribe_v1" });
108
+ });
109
+
110
+ test("negative control: the default provider keeps the OpenAI shape", async () => {
111
+ configure();
112
+ expect(await transcribeAudio(audio)).toBe("hello");
113
+ expect(captured).toEqual([
114
+ {
115
+ path: "/v1/audio/transcriptions",
116
+ authorization: "Bearer stt-test-key-123456",
117
+ xiApiKey: null,
118
+ fields: ["file", "model"],
119
+ model: "whisper-1",
120
+ modelId: null,
121
+ filename: "note.ogg",
122
+ },
123
+ ]);
124
+ });
125
+
126
+ test("an unknown provider fails before any request", async () => {
127
+ configure("deepgram");
128
+ await expect(transcribeAudio(audio)).rejects.toThrow(
129
+ new TranscriptionError(
130
+ 'TRANSCRIPTION_PROVIDER "deepgram" is not supported (use openai, groq, elevenlabs)',
131
+ ),
132
+ );
133
+ expect(captured).toHaveLength(0);
134
+ });
135
+
136
+ test("an elevenlabs error detail is surfaced with the key scrubbed", async () => {
137
+ configure("elevenlabs");
138
+ reply = () =>
139
+ Response.json(
140
+ { detail: { status: "invalid_api_key", message: "Invalid key stt-test-key-123456" } },
141
+ { status: 401 },
142
+ );
143
+ await expect(transcribeAudio(audio)).rejects.toThrow(
144
+ "Invalid key [REDACTED:TRANSCRIPTION_API_KEY]",
145
+ );
146
+ });
147
+ });
148
+
149
+ describe("provider key fallback", () => {
150
+ // Built at runtime so the secret scanner does not read them as keys.
151
+ const fake = (provider: string) => [provider, "fixture", "value"].join("-");
152
+ const ELEVEN_FAKE = fake("eleven");
153
+ const OPENAI_FAKE = fake("openai");
154
+ const GROQ_FAKE = fake("groq");
155
+
156
+ // Auto-detected keys always go to the provider's public endpoint, so record
157
+ // that URL and forward the request to the local stub on the same path.
158
+ const realFetch = globalThis.fetch;
159
+ const calledUrls: string[] = [];
160
+ beforeEach(() => {
161
+ calledUrls.length = 0;
162
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
163
+ const url = new URL(String(input));
164
+ calledUrls.push(url.href);
165
+ return realFetch(`http://127.0.0.1:${stt.port}${url.pathname}`, init);
166
+ }) as typeof fetch;
167
+ });
168
+ afterEach(() => {
169
+ globalThis.fetch = realFetch;
170
+ });
171
+
172
+ test("no key at all means not configured", async () => {
173
+ expect(resolveTranscriptionConfig()).toBeNull();
174
+ expect(isTranscriptionConfigured()).toBe(false);
175
+ const notes = await transcribeAudioNotes([{ key: "a", name: "a.ogg", data: audio.data }], "t");
176
+ expect(notes.get("a")).toBe(NOT_TRANSCRIBED_NOTE);
177
+ expect(NOT_TRANSCRIBED_NOTE).toBe(
178
+ "(not transcribed: no transcription provider configured, set TRANSCRIPTION_API_KEY or one of ELEVENLABS_API_KEY, OPENAI_API_KEY, GROQ_API_KEY)",
179
+ );
180
+ expect(calledUrls).toHaveLength(0);
181
+ });
182
+
183
+ test("ELEVENLABS_API_KEY alone uses the elevenlabs shape", async () => {
184
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
185
+ expect(resolveTranscriptionConfig()).toMatchObject({
186
+ provider: "elevenlabs",
187
+ keyEnv: "ELEVENLABS_API_KEY",
188
+ source: "auto",
189
+ });
190
+ expect(await transcribeAudio(audio)).toBe("hello");
191
+ expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
192
+ expect(captured[0]).toMatchObject({
193
+ xiApiKey: ELEVEN_FAKE,
194
+ authorization: null,
195
+ modelId: "scribe_v2",
196
+ });
197
+ });
198
+
199
+ test("OPENAI_API_KEY alone uses the openai shape", async () => {
200
+ process.env.OPENAI_API_KEY = OPENAI_FAKE;
201
+ expect(resolveTranscriptionConfig()).toMatchObject({ provider: "openai", source: "auto" });
202
+ await transcribeAudio(audio);
203
+ expect(calledUrls).toEqual(["https://api.openai.com/v1/audio/transcriptions"]);
204
+ expect(captured[0]).toMatchObject({
205
+ authorization: `Bearer ${OPENAI_FAKE}`,
206
+ model: "whisper-1",
207
+ });
208
+ });
209
+
210
+ test("GROQ_API_KEY alone uses Groq's OpenAI-compatible endpoint and whisper model", async () => {
211
+ process.env.GROQ_API_KEY = GROQ_FAKE;
212
+ expect(resolveTranscriptionConfig()).toMatchObject({
213
+ provider: "groq",
214
+ keyEnv: "GROQ_API_KEY",
215
+ });
216
+ await transcribeAudio(audio);
217
+ expect(calledUrls).toEqual(["https://api.groq.com/openai/v1/audio/transcriptions"]);
218
+ expect(captured[0]).toMatchObject({
219
+ authorization: `Bearer ${GROQ_FAKE}`,
220
+ model: "whisper-large-v3-turbo",
221
+ });
222
+ });
223
+
224
+ test("ElevenLabs wins over OpenAI and Groq when all are set", async () => {
225
+ process.env.OPENAI_API_KEY = OPENAI_FAKE;
226
+ process.env.GROQ_API_KEY = GROQ_FAKE;
227
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
228
+ await transcribeAudio(audio);
229
+ expect(captured[0]).toMatchObject({ xiApiKey: ELEVEN_FAKE, authorization: null });
230
+ });
231
+
232
+ test("OpenAI wins over Groq", () => {
233
+ process.env.GROQ_API_KEY = GROQ_FAKE;
234
+ process.env.OPENAI_API_KEY = OPENAI_FAKE;
235
+ expect(resolveTranscriptionConfig()?.provider).toBe("openai");
236
+ });
237
+
238
+ test("an explicit TRANSCRIPTION_API_KEY overrides both detected keys", async () => {
239
+ process.env.OPENAI_API_KEY = OPENAI_FAKE;
240
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
241
+ configure();
242
+ expect(resolveTranscriptionConfig()).toMatchObject({
243
+ provider: "openai",
244
+ keyEnv: "TRANSCRIPTION_API_KEY",
245
+ source: "explicit",
246
+ });
247
+ await transcribeAudio(audio);
248
+ // The explicit key honours TRANSCRIPTION_BASE_URL.
249
+ expect(calledUrls).toEqual([`http://127.0.0.1:${stt.port}/v1/audio/transcriptions`]);
250
+ expect(captured[0]).toMatchObject({
251
+ authorization: "Bearer stt-test-key-123456",
252
+ xiApiKey: null,
253
+ });
254
+ });
255
+
256
+ test("an auto-detected key never goes to TRANSCRIPTION_BASE_URL", async () => {
257
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
258
+ process.env.TRANSCRIPTION_BASE_URL = "https://collector.example/v1";
259
+ await transcribeAudio(audio);
260
+ expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
261
+ });
262
+
263
+ test("a pinned provider uses only its own key", async () => {
264
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
265
+ process.env.OPENAI_API_KEY = OPENAI_FAKE;
266
+ process.env.TRANSCRIPTION_PROVIDER = "openai";
267
+ await transcribeAudio(audio);
268
+ expect(captured[0]).toMatchObject({ authorization: `Bearer ${OPENAI_FAKE}` });
269
+
270
+ delete process.env.OPENAI_API_KEY;
271
+ expect(isTranscriptionConfigured()).toBe(false);
272
+ });
273
+
274
+ test("a detected key is scrubbed from errors under its own name", async () => {
275
+ process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
276
+ reply = () => Response.json({ detail: `bad key ${ELEVEN_FAKE}` }, { status: 401 });
277
+ await expect(transcribeAudio(audio)).rejects.toThrow("bad key [REDACTED:ELEVENLABS_API_KEY]");
278
+ });
279
+
280
+ // The composer's mic button records webm in Chrome and Firefox; the upload
281
+ // route drops `;codecs=` before it gets here. Each provider reads the format
282
+ // from the file name, and all three list webm as supported.
283
+ test.each([
284
+ ["elevenlabs", "ELEVENLABS_API_KEY", "https://api.elevenlabs.io/v1/speech-to-text"],
285
+ ["openai", "OPENAI_API_KEY", "https://api.openai.com/v1/audio/transcriptions"],
286
+ ["groq", "GROQ_API_KEY", "https://api.groq.com/openai/v1/audio/transcriptions"],
287
+ ])("a recorded webm clip reaches %s as a .webm audio/webm file", async (name, keyEnv, url) => {
288
+ process.env[keyEnv] = fake(name);
289
+ const clip = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], { type: "audio/webm" });
290
+ const notes = await transcribeAudioNotes(
291
+ [{ key: "c", name: "recording-2026-10-21-143005.webm", data: clip, mimeType: "audio/webm" }],
292
+ "t",
293
+ );
294
+ expect(notes.get("c")).toBe("[Transcript of recording-2026-10-21-143005.webm]\nhello");
295
+ expect(calledUrls).toEqual([url]);
296
+ expect(captured[0]).toMatchObject({ filename: "recording-2026-10-21-143005.webm" });
297
+ expect(sentFileTypes).toEqual(["audio/webm"]);
298
+ });
299
+ });
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "bun:test";
2
+ import { parseWorkerTokenArgs, runWorkerToken } from "../commands/worker-token";
3
+
4
+ const env = { MCP_BASE_URL: "http://swarm.test", AGENT_SWARM_API_KEY: "operator-key-for-test" };
5
+
6
+ function fakeFetch(agents: { id: string; name: string }[]) {
7
+ const calls: { url: string; method: string; body?: unknown }[] = [];
8
+ const impl = (async (url: string, init?: RequestInit) => {
9
+ calls.push({
10
+ url,
11
+ method: init?.method ?? "GET",
12
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
13
+ });
14
+ if (url.endsWith("/api/agents")) return Response.json({ agents });
15
+ if (init?.method === "DELETE") return Response.json({ revoked: 1 });
16
+ return Response.json({ plaintext: "minted-value", expiresAt: null, agentCreated: true });
17
+ }) as unknown as typeof fetch;
18
+ return { calls, impl };
19
+ }
20
+
21
+ function capture() {
22
+ const out: string[] = [];
23
+ const err: string[] = [];
24
+ return { out, err, stdout: (l: string) => out.push(l), stderr: (l: string) => err.push(l) };
25
+ }
26
+
27
+ test("argument parsing requires one action and exactly one agent selector", () => {
28
+ expect(parseWorkerTokenArgs(["create", "--name", "w"]).error).toBeUndefined();
29
+ expect(parseWorkerTokenArgs(["create"]).error).toContain("exactly one");
30
+ expect(parseWorkerTokenArgs(["create", "--name", "w", "--agent-id", "a"]).error).toContain(
31
+ "exactly one",
32
+ );
33
+ expect(parseWorkerTokenArgs(["--name", "w"]).error).toContain("Missing action");
34
+ expect(parseWorkerTokenArgs(["create", "--name", "w", "--ttl-days", "0"]).error).toContain(
35
+ "--ttl-days",
36
+ );
37
+ expect(parseWorkerTokenArgs(["create", "--bogus"]).error).toContain("Unknown argument");
38
+ });
39
+
40
+ test("create by an unused name makes the agent and prints only the token to stdout", async () => {
41
+ const { calls, impl } = fakeFetch([]);
42
+ const io = capture();
43
+ const code = await runWorkerToken(["create", "--name", "w1", "--ttl-days", "2"], {
44
+ env,
45
+ fetch: impl,
46
+ ...io,
47
+ });
48
+ expect(code).toBe(0);
49
+ expect(io.out).toEqual(["minted-value"]);
50
+ expect(io.err.join("\n")).not.toContain("minted-value");
51
+ const mint = calls[1]!;
52
+ expect(mint.method).toBe("POST");
53
+ expect(mint.url).toMatch(/^http:\/\/swarm\.test\/api\/agents\/[0-9a-f-]{36}\/token$/);
54
+ expect(mint.body).toEqual({ name: "w1", ttlMs: 2 * 86_400_000 });
55
+ });
56
+
57
+ test("rotate and revoke resolve an existing name; an ambiguous name fails", async () => {
58
+ const agents = [{ id: "agent-1", name: "w1" }];
59
+ for (const [action, method, suffix] of [
60
+ ["rotate", "POST", "/rotate"],
61
+ ["revoke", "DELETE", ""],
62
+ ] as const) {
63
+ const { calls, impl } = fakeFetch(agents);
64
+ expect(await runWorkerToken([action, "--name", "w1"], { env, fetch: impl, ...capture() })).toBe(
65
+ 0,
66
+ );
67
+ expect(calls[1]).toMatchObject({
68
+ method,
69
+ url: `http://swarm.test/api/agents/agent-1/token${suffix}`,
70
+ });
71
+ }
72
+ const { impl } = fakeFetch([...agents, { id: "agent-2", name: "w1" }]);
73
+ const io = capture();
74
+ expect(await runWorkerToken(["rotate", "--name", "w1"], { env, fetch: impl, ...io })).toBe(1);
75
+ expect(io.err.join("\n")).toContain("Several agents");
76
+ });
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
984
984
  // ─── Registry Wiring ─────────────────────────────────────────
985
985
 
986
986
  describe("createExecutorRegistry", () => {
987
- test("registers all 14 executors (9 instant + 5 async)", () => {
987
+ test("registers all 15 executors (10 instant + 5 async)", () => {
988
988
  const registry = createExecutorRegistry(mockDeps);
989
989
  const types = registry.types();
990
990
 
@@ -993,6 +993,7 @@ describe("createExecutorRegistry", () => {
993
993
  expect(types).toContain("notify");
994
994
  expect(types).toContain("raw-llm");
995
995
  expect(types).toContain("system-one-decision");
996
+ expect(types).toContain("system-one-extraction");
996
997
  expect(types).toContain("script");
997
998
  expect(types).toContain("swarm-script");
998
999
  expect(types).toContain("vcs");
@@ -1002,7 +1003,7 @@ describe("createExecutorRegistry", () => {
1002
1003
  expect(types).toContain("human-in-the-loop");
1003
1004
  expect(types).toContain("wait");
1004
1005
  expect(types).toContain("sub-workflow");
1005
- expect(types).toHaveLength(14);
1006
+ expect(types).toHaveLength(15);
1006
1007
  });
1007
1008
 
1008
1009
  test("instant executors have mode instant, async executors have mode async", () => {
@@ -1012,6 +1013,7 @@ describe("createExecutorRegistry", () => {
1012
1013
  "code-match",
1013
1014
  "notify",
1014
1015
  "raw-llm",
1016
+ "system-one-extraction",
1015
1017
  "script",
1016
1018
  "swarm-script",
1017
1019
  "vcs",
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
4
+ import { SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS, validateConfigValue } from "../be/swarm-config-guard";
4
5
  import type { WorkflowNode } from "../types";
5
6
  import { validateDefinition } from "../workflows/definition";
6
7
  import { InProcessEventBus } from "../workflows/event-bus";
@@ -11,6 +12,7 @@ import {
11
12
  SystemOneDecisionExecutor,
12
13
  type SystemOneDecisionExecutorOptions,
13
14
  } from "../workflows/executors/system-one-decision";
15
+ import { SYSTEM_ONE_PROVIDER_IDS } from "../workflows/executors/system-one-providers";
14
16
  import { interpolate } from "../workflows/template";
15
17
 
16
18
  const TEST_DB_PATH = "./test-workflow-system-one-decision-providers.sqlite";
@@ -50,7 +52,7 @@ afterAll(async () => {
50
52
 
51
53
  beforeEach(async () => {
52
54
  await getDbClient().run(
53
- "DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN')",
55
+ "DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN', 'SYSTEM_ONE_DEFAULT_PROVIDER')",
54
56
  );
55
57
  });
56
58
 
@@ -708,6 +710,101 @@ describe("system-one-decision openai provider", () => {
708
710
  });
709
711
  });
710
712
 
713
+ // ─── Default provider ───────────────────────────────────────
714
+
715
+ describe("system-one-decision default provider", () => {
716
+ const TYPESAFE_KEY = "typesafe-example-test-key-0123456789";
717
+ const jevBody = () =>
718
+ jsonResponse({
719
+ model: "jev",
720
+ answers: systemOneAnswers,
721
+ usage: { input_tokens: 1, output_tokens: 0 },
722
+ });
723
+ const setDefault = (value: string) =>
724
+ upsertSwarmConfig({ scope: "global", key: "SYSTEM_ONE_DEFAULT_PROVIDER", value });
725
+ const withTypesafeKey = {
726
+ getApiKey: async (id: string) =>
727
+ id === "typesafe" ? TYPESAFE_KEY : id === "cloudflare" ? CF_TOKEN : null,
728
+ };
729
+
730
+ test("unset, a node with no provider still calls typesafe", async () => {
731
+ const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
732
+ const result = await run(executor, config());
733
+ expect(result.status).toBe("success");
734
+ expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
735
+ expect(calls[0]?.body.model).toBe("jev-latest");
736
+ // The schema leaves the provider unset; the executor resolves it per run.
737
+ expect(SystemOneDecisionConfigSchema.parse(config()).provider).toBeUndefined();
738
+ });
739
+
740
+ test("set to cloudflare, a node with no provider calls Clef with no workflow edit", async () => {
741
+ await setDefault("cloudflare");
742
+ const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())], withTypesafeKey);
743
+ const result = await run(executor, config());
744
+ expect(result.status).toBe("success");
745
+ expect(calls[0]?.url).toBe(`${CF_BASE}/clef`);
746
+ expect((calls[0]?.init.headers as Record<string, string>).authorization).toBe(
747
+ `Bearer ${CF_TOKEN}`,
748
+ );
749
+ expect(result.output).toMatchObject({ model: "clef", answers: systemOneAnswers as never });
750
+ });
751
+
752
+ test("a node that names its provider keeps it", async () => {
753
+ await setDefault("cloudflare");
754
+ const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
755
+ expect((await run(executor, config({ provider: "typesafe" }))).status).toBe("success");
756
+ expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
757
+ });
758
+
759
+ test("readiness checks the default provider's credentials", async () => {
760
+ await setDefault("cloudflare");
761
+ const { executor } = makeExecutor([], { getApiKey: async () => null });
762
+ const problems = await executor.checkReadiness([{ id: "a", config: config() }]);
763
+ expect(problems).toHaveLength(1);
764
+ expect(problems[0]?.nodeIds).toEqual(["a"]);
765
+ expect(problems[0]?.message).toContain("CLOUDFLARE_API_TOKEN is not configured");
766
+ });
767
+
768
+ test("the environment is read when no row is stored; a stored row wins", async () => {
769
+ const env = { SYSTEM_ONE_DEFAULT_PROVIDER: "cloudflare" };
770
+ const fromEnv = makeExecutor([jsonResponse(clefEnvelope())], { ...withTypesafeKey, env });
771
+ expect((await run(fromEnv.executor, config())).status).toBe("success");
772
+ expect(fromEnv.calls[0]?.url).toBe(`${CF_BASE}/clef`);
773
+
774
+ await setDefault("typesafe");
775
+ const stored = makeExecutor([jevBody()], { ...withTypesafeKey, env });
776
+ expect((await run(stored.executor, config())).status).toBe("success");
777
+ expect(stored.calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
778
+ });
779
+
780
+ test("a defaulted provider's limits are checked at run, and nothing is sent", async () => {
781
+ await setDefault("cloudflare");
782
+ const qs: Record<string, unknown> = {};
783
+ const rs: Record<string, unknown> = {};
784
+ for (let i = 0; i < 65; i++) {
785
+ qs[`q${i}`] = { type: "noul", instructions: `Question ${i}?` };
786
+ rs[`q${i}`] = { type: "noul" };
787
+ }
788
+ const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())]);
789
+ const result = await run(executor, config({ questions: qs, returns: rs }));
790
+ expect(result.status).toBe("failed");
791
+ expect(result.error).toContain("at most 64 questions per node; this node has 65");
792
+ expect(calls).toHaveLength(0);
793
+ });
794
+
795
+ test("the config guard accepts exactly the decide hosts", () => {
796
+ expect([...SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS].sort()).toEqual(
797
+ [...SYSTEM_ONE_PROVIDER_IDS].sort(),
798
+ );
799
+ for (const id of SYSTEM_ONE_PROVIDER_IDS) {
800
+ expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", id)).toBeNull();
801
+ }
802
+ expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", "jev")).toContain(
803
+ "Invalid SYSTEM_ONE_DEFAULT_PROVIDER value",
804
+ );
805
+ });
806
+ });
807
+
711
808
  // ─── Existing providers ─────────────────────────────────────
712
809
 
713
810
  describe("system-one-decision SystemOne wire is unchanged", () => {
@@ -227,7 +227,8 @@ describe("system-one-decision config schema", () => {
227
227
  questions: { fit: { type: "noul", instructions: "Is it real?" } },
228
228
  returns: { fit: { type: "noul" } },
229
229
  });
230
- expect(parsed.provider).toBe("typesafe");
230
+ // Unset: the executor reads SYSTEM_ONE_DEFAULT_PROVIDER at run time (typesafe when unset).
231
+ expect(parsed.provider).toBeUndefined();
231
232
  // The model default belongs to the provider, so the schema leaves it unset.
232
233
  expect(parsed.model).toBeUndefined();
233
234
  expect(parsed.timeoutMs).toBe(30_000);