@desplega.ai/agent-swarm 2.5.4 → 2.6.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 (271) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-a16433gr.js} +29 -25
  7. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-w75maga2.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-55207973.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-3cm35gey.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-nw5yd8cp.js} +11 -11
  15. package/dist/{cli-c61mbdds.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-jbnn4vxb.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-pzatvw94.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-x0f2v69c.js → cli-3aqd4jaa.js} +12 -7
  23. package/dist/{cli-gbfxp48k.js → cli-3fwqy71j.js} +15 -15
  24. package/dist/{cli-7p4jxgtt.js → cli-3gfvmzht.js} +6 -6
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-asq5ydm4.js → cli-4hxrf0p4.js} +131 -50
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-k4tjsdx9.js → cli-5rj83bam.js} +272 -195
  29. package/dist/{cli-sbwsxvtd.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-ethgy7ha.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-hh5we352.js → cli-9abdgn5t.js} +7 -7
  37. package/dist/{cli-ff1tm9n9.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-c952c7ac.js → cli-a34pr3w1.js} +2 -2
  39. package/dist/{cli-s682zgqz.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-bvpgr9ny.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-gwhjmh8g.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-5bnsc0e1.js → cli-az25fn5f.js} +4 -4
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-1r5ad7j5.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-64mh9mf3.js → cli-ca6banwz.js} +69 -17
  48. package/dist/{cli-25n3mv5d.js → cli-cz37jqss.js} +5 -5
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-gn5af0x5.js → cli-emhjcj2y.js} +11 -11
  51. package/dist/{cli-6a1xg1p7.js → cli-esakngh8.js} +2 -2
  52. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  53. package/dist/{cli-4b2z6n0m.js → cli-ffvsj2qw.js} +17 -8
  54. package/dist/{cli-abs8ndtr.js → cli-g20jjwt4.js} +26 -26
  55. package/dist/{cli-jb30dzvk.js → cli-g8ka5j4j.js} +446 -164
  56. package/dist/{cli-19202kew.js → cli-gf95q3bm.js} +5 -5
  57. package/dist/{cli-eftzyh9y.js → cli-gnsb1zxa.js} +2 -2
  58. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  59. package/dist/{cli-e42vss7b.js → cli-hfeb68z4.js} +5 -5
  60. package/dist/{cli-g9vd9ece.js → cli-k21gx5ht.js} +14 -16
  61. package/dist/{cli-tsma7p8v.js → cli-kf4jree9.js} +2 -2
  62. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  63. package/dist/{cli-wx2nj19m.js → cli-m5a9m0ak.js} +6 -6
  64. package/dist/{cli-621ce7kw.js → cli-m70bajzr.js} +3 -3
  65. package/dist/{cli-9j129brq.js → cli-mtnj4fwa.js} +6 -6
  66. package/dist/{cli-xzfq4at3.js → cli-mvjz5an3.js} +15 -11
  67. package/dist/{cli-277c31dw.js → cli-n7asmce6.js} +225 -3
  68. package/dist/{cli-xtrz6bzs.js → cli-nn6vx2mq.js} +1 -1
  69. package/dist/{cli-2655k16q.js → cli-nq677csy.js} +1 -1
  70. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  71. package/dist/{cli-wghjkr0e.js → cli-sqma6vw1.js} +10 -7
  72. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  73. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  74. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  75. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  76. package/dist/{cli-m4ecdxvz.js → cli-zqne1qcj.js} +7 -7
  77. package/dist/cli.js +17 -17
  78. package/dist/{codex-adapter-wch12q99.js → codex-adapter-d95q4qsn.js} +7 -7
  79. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  80. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-gdwd8h0c.js} +8 -8
  81. package/dist/{commands-9849dkj6.js → commands-jqmgk54b.js} +2 -2
  82. package/dist/{controller-ja0sd95p.js → controller-z3jq589f.js} +4 -4
  83. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-r2za39ef.js} +23 -23
  84. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-4mr4jgkw.js} +2 -2
  85. package/dist/{db-47vj40dy.js → db-ppv2tze7.js} +3 -3
  86. package/dist/{db-w759qmx0.js → db-w36wwb5g.js} +4 -2
  87. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  88. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-pv5h77vf.js} +20 -20
  89. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  90. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-d4pdpb3r.js} +2 -2
  91. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  92. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  93. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  94. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  95. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  96. package/dist/{external-channel-e2s028da.js → external-channel-y6nkzhnn.js} +2 -2
  97. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-t08b9ntm.js} +10 -10
  98. package/dist/handlers-eqsnmv95.js +41 -0
  99. package/dist/{heartbeat-w3mntghj.js → heartbeat-vn31xwdk.js} +23 -23
  100. package/dist/{hook-g7kvv6x9.js → hook-6d0zbvvt.js} +6 -6
  101. package/dist/{hook-3jjywnpb.js → hook-cpjs8q51.js} +8 -8
  102. package/dist/{http-tkp7d4kb.js → http-9c6medda.js} +856 -99
  103. package/dist/{index-xdaahthb.js → index-38t35h0n.js} +20 -20
  104. package/dist/{index-3n8m9ea2.js → index-g6k9tctj.js} +1 -1
  105. package/dist/{index-1ef1myq2.js → index-k0yt1tvq.js} +20 -20
  106. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  107. package/dist/{index-q7m26kvs.js → index-qhjf2htm.js} +13 -10
  108. package/dist/index-yqgbfgaj.js +43 -0
  109. package/dist/{keepalive-v4ta8wgg.js → keepalive-em0117bp.js} +4 -4
  110. package/dist/lead-23e7z59t.js +79 -0
  111. package/dist/{maintenance-84n1ex21.js → maintenance-kbme3qy9.js} +1 -1
  112. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  113. package/dist/{onboard-sfmpe4np.js → onboard-p6q0vz97.js} +3 -3
  114. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-esv3k9yn.js} +7 -7
  115. package/dist/{otel-impl-v6cc0zve.js → otel-impl-54qhqh1z.js} +2 -2
  116. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-mymbwhj0.js} +10 -10
  117. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-yb75wv33.js} +3 -3
  118. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-1vp2mw4p.js} +2 -2
  119. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-dexhc9x8.js} +1 -1
  120. package/dist/render-v2-tjay5hhf.js +16 -0
  121. package/dist/{resume-frdtgfa0.js → resume-8ayf37g5.js} +9 -9
  122. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  123. package/dist/{session-access-fpez4y5q.js → session-access-fdhp987s.js} +4 -4
  124. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  125. package/dist/{setup-mq6m3m8q.js → setup-4rrmqcq3.js} +1 -1
  126. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  127. package/dist/{work-objects-kfahhjsq.js → work-objects-p28fgd70.js} +23 -23
  128. package/dist/worker-e3w7vye1.js +79 -0
  129. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  130. package/openapi.json +1274 -32
  131. package/package.json +1 -1
  132. package/src/assets/source-key.ts +95 -0
  133. package/src/be/db/api-keys.ts +80 -0
  134. package/src/be/db/session-labels.ts +28 -0
  135. package/src/be/db/session-moves.ts +5 -6
  136. package/src/be/db/session-tree.ts +153 -14
  137. package/src/be/db/tasks/read.ts +11 -0
  138. package/src/be/db/tasks/write.ts +6 -1
  139. package/src/be/db/workflow-runs.ts +183 -1
  140. package/src/be/db-retention.ts +61 -6
  141. package/src/be/db.ts +59 -114
  142. package/src/be/memory/providers/openai-embedding.ts +5 -1
  143. package/src/be/migrations/244_api_key_validation.sql +4 -0
  144. package/src/be/onboarding.ts +4 -0
  145. package/src/be/scripts/typecheck.ts +1 -1
  146. package/src/be/session-source-backfill.ts +163 -0
  147. package/src/be/swarm-config-guard.ts +1 -0
  148. package/src/comb/review-batch.ts +28 -1
  149. package/src/comb/templates.ts +7 -2
  150. package/src/commands/claude-managed-setup.ts +7 -1
  151. package/src/commands/credential-refresh.ts +76 -1
  152. package/src/commands/credential-wait.ts +2 -2
  153. package/src/commands/provider-credentials.ts +210 -170
  154. package/src/commands/runner.ts +25 -4
  155. package/src/extensions/apply-task-create.ts +8 -1
  156. package/src/extensions/contract-types.generated.ts +1 -1
  157. package/src/extensions/contract.ts +5 -0
  158. package/src/http/all-routes.ts +2 -0
  159. package/src/http/api-keys.ts +53 -0
  160. package/src/http/assets.ts +15 -0
  161. package/src/http/codex-oauth-accounts.ts +191 -0
  162. package/src/http/comb.ts +15 -2
  163. package/src/http/config.ts +1 -1
  164. package/src/http/core.ts +6 -0
  165. package/src/http/fleet-benchmark.ts +5 -2
  166. package/src/http/index.ts +11 -0
  167. package/src/http/integrations.ts +5 -1
  168. package/src/http/kv.ts +8 -3
  169. package/src/http/onboarding.ts +6 -1
  170. package/src/http/pool-keys.ts +354 -0
  171. package/src/http/sessions.ts +37 -12
  172. package/src/http/stats.ts +1 -0
  173. package/src/http/status.ts +26 -12
  174. package/src/http/tasks.ts +3 -0
  175. package/src/http/used-by.ts +7 -2
  176. package/src/http/workflows.ts +78 -9
  177. package/src/integrations/chat-sdk/inbound.ts +27 -0
  178. package/src/kv-reserved-namespaces.ts +19 -0
  179. package/src/providers/acp-targets.ts +1 -1
  180. package/src/providers/amp-adapter.ts +1 -1
  181. package/src/providers/claude-adapter.ts +5 -1
  182. package/src/providers/claude-managed-adapter.ts +16 -9
  183. package/src/providers/codex-adapter.ts +71 -11
  184. package/src/scheduler/scheduler.ts +3 -2
  185. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  186. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  189. package/src/slack/assistant.ts +7 -0
  190. package/src/slack/channel-labels.ts +74 -0
  191. package/src/slack/handlers.ts +8 -0
  192. package/src/slack/inbound-files.ts +5 -2
  193. package/src/slack/thread-buffer.ts +4 -0
  194. package/src/tasks/sibling-awareness.ts +12 -3
  195. package/src/telemetry-context.ts +5 -6
  196. package/src/telemetry-identity.ts +3 -3
  197. package/src/tests/api-keys-validation.test.ts +170 -0
  198. package/src/tests/claude-managed-adapter.test.ts +46 -0
  199. package/src/tests/claude-routes-e2e.test.ts +7 -2
  200. package/src/tests/codex-credential-pools.test.ts +120 -0
  201. package/src/tests/codex-gateway-route.test.ts +165 -0
  202. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  203. package/src/tests/comb-review-batch.test.ts +63 -2
  204. package/src/tests/config-pool-keys.test.ts +533 -0
  205. package/src/tests/credential-check.test.ts +290 -2
  206. package/src/tests/credential-refresh.test.ts +172 -1
  207. package/src/tests/db-retention.test.ts +9 -3
  208. package/src/tests/db-task-read-contract.test.ts +1 -0
  209. package/src/tests/db-task-write-contract.test.ts +1 -3
  210. package/src/tests/docs-index-footprint.test.js +3 -3
  211. package/src/tests/extensions-pre-task.test.ts +53 -1
  212. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  213. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  214. package/src/tests/fleet-benchmark.test.ts +8 -0
  215. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  216. package/src/tests/integrations-http.test.ts +32 -2
  217. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  218. package/src/tests/internal-ai/credentials.test.ts +23 -0
  219. package/src/tests/kv-http.test.ts +32 -0
  220. package/src/tests/kv-tool.test.ts +25 -0
  221. package/src/tests/multi-runtime-registration.test.ts +12 -12
  222. package/src/tests/onboarding.test.ts +108 -1
  223. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  224. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  225. package/src/tests/org-website.test.ts +36 -0
  226. package/src/tests/provider-key-probes.test.ts +181 -0
  227. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  228. package/src/tests/rbac-e2e-helpers.ts +36 -11
  229. package/src/tests/script-connections.test.ts +2 -0
  230. package/src/tests/script-runs-retention.test.ts +212 -0
  231. package/src/tests/sdk-allowlist.test.ts +39 -0
  232. package/src/tests/session-source-backfill.test.ts +429 -0
  233. package/src/tests/session-source-keys.test.ts +236 -0
  234. package/src/tests/session-source-labels.test.ts +419 -0
  235. package/src/tests/sessions-detail-batch.test.ts +2 -0
  236. package/src/tests/sessions-move.test.ts +131 -0
  237. package/src/tests/sessions-tree.test.ts +299 -2
  238. package/src/tests/slack-inbound-files.test.ts +103 -23
  239. package/src/tests/status.test.ts +101 -18
  240. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  241. package/src/tests/test-net.ts +31 -0
  242. package/src/tests/transcription.test.ts +8 -0
  243. package/src/tests/used-by-route.test.ts +11 -0
  244. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  245. package/src/tests/workflow-http-v2.test.ts +131 -0
  246. package/src/tests/workflow-step-order.test.ts +92 -0
  247. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  248. package/src/tools/get-tasks.ts +8 -0
  249. package/src/tools/kv/kv-read-auth.ts +4 -2
  250. package/src/tools/task-action.ts +1 -1
  251. package/src/tools/workflows/get-workflow-run.ts +14 -4
  252. package/src/types.ts +24 -0
  253. package/src/utils/company-profile.ts +51 -1
  254. package/src/utils/credentials.ts +5 -2
  255. package/src/utils/internal-ai/complete-structured.ts +5 -0
  256. package/src/utils/internal-ai/credentials.ts +4 -3
  257. package/src/utils/provider-key-probes.ts +328 -0
  258. package/src/utils/transcription.ts +4 -1
  259. package/src/workflows/executors/sub-workflow.ts +8 -1
  260. package/templates/ai-toolbox.manifest.json +2 -2
  261. package/templates/skills/researching/content.md +2 -0
  262. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  263. package/templates/skills/swarm-scripts/content.md +1 -0
  264. package/dist/cli-7421jzsw.js +0 -84
  265. package/dist/cli-e45e4m6b.js +0 -413
  266. package/dist/cli-zw88zjba.js +0 -307
  267. package/dist/handlers-wy1dr6ja.js +0 -41
  268. package/dist/index-nmm79qgr.js +0 -43
  269. package/dist/lead-sawnyqxk.js +0 -79
  270. package/dist/render-v2-pjwxvnkw.js +0 -12
  271. package/dist/worker-v5e5rxj3.js +0 -79
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "record-task-create",
3
+ "description": "Records task creation events",
4
+ "version": "1.0.0",
5
+ "runtime": "api",
6
+ "assets": { "hooks": "hooks.ts" }
7
+ }
@@ -326,6 +326,14 @@ describe("GET /api/fleet-benchmark", () => {
326
326
  expect(fetched).toHaveLength(1);
327
327
  });
328
328
 
329
+ test("a use case the catalog does not list yet compares against the whole fleet", async () => {
330
+ await addTasks(10, "completed", 1);
331
+ await setProfile("2-10", "content");
332
+ const response = await buildFleetBenchmarkResponse(NOW);
333
+ expect(response).toMatchObject({ state: "ready", level: "full", peerGroup: null });
334
+ expect(fetched).toEqual(["http://proxy.test/v1/fleet/benchmarks?window=30d"]);
335
+ });
336
+
329
337
  test("full profile tolerates absent or null peer_group from the proxy", async () => {
330
338
  await addTasks(10, "completed", 1);
331
339
  await setProfile();
@@ -0,0 +1,98 @@
1
+ import { afterAll, beforeAll, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import {
5
+ closeDb,
6
+ createWorkflow,
7
+ createWorkflowRun,
8
+ createWorkflowRunStep,
9
+ initDb,
10
+ updateWorkflowRun,
11
+ updateWorkflowRunStep,
12
+ } from "../be/db";
13
+ import { registerGetWorkflowRunTool } from "../tools/workflows/get-workflow-run";
14
+
15
+ const dbPath = `./test-get-workflow-run-tool-${process.pid}.sqlite`;
16
+
17
+ type ToolResult = {
18
+ isError?: boolean;
19
+ structuredContent: {
20
+ message: string;
21
+ run?: Record<string, unknown>;
22
+ steps?: Array<Record<string, unknown>>;
23
+ };
24
+ };
25
+ let handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
26
+ let runId: string;
27
+
28
+ async function removeDb() {
29
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ await removeDb();
34
+ initDb(dbPath);
35
+ const workflow = await createWorkflow({
36
+ name: "get-workflow-run-tool",
37
+ definition: { nodes: [{ id: "poll", type: "notify", config: { template: "x" } }] },
38
+ });
39
+ runId = crypto.randomUUID();
40
+ await createWorkflowRun({ id: runId, workflowId: workflow.id });
41
+ const context = { poll: { attempts: 2 }, blob: "x".repeat(2_000) };
42
+ for (let i = 0; i < 2; i++) {
43
+ const step = await createWorkflowRunStep({
44
+ id: crypto.randomUUID(),
45
+ runId,
46
+ nodeId: "poll",
47
+ nodeType: "notify",
48
+ input: { ...context, iteration: i },
49
+ });
50
+ await updateWorkflowRunStep(step.id, {
51
+ status: "completed",
52
+ output: { iteration: i },
53
+ retryCount: i,
54
+ finishedAt: new Date().toISOString(),
55
+ });
56
+ }
57
+ await updateWorkflowRun(runId, { status: "completed", context });
58
+
59
+ const server = new McpServer({ name: "get-workflow-run-tool", version: "1.0.0" });
60
+ registerGetWorkflowRunTool(server);
61
+ handler = (server as unknown as { _registeredTools: Record<string, { handler: typeof handler }> })
62
+ ._registeredTools["get-workflow-run"]!.handler;
63
+ });
64
+
65
+ afterAll(async () => {
66
+ closeDb();
67
+ await removeDb();
68
+ });
69
+
70
+ const extra = { sessionId: "get-workflow-run-tool", requestInfo: { headers: {} } };
71
+
72
+ test("defaults to the full run with context and step inputs", async () => {
73
+ const result = await handler({ id: runId }, extra);
74
+ expect(result.isError).not.toBe(true);
75
+ const { run, steps } = result.structuredContent;
76
+ expect(run?.context).toBeDefined();
77
+ expect(steps).toHaveLength(2);
78
+ for (const step of steps!) expect(step.input).toBeDefined();
79
+ });
80
+
81
+ test("fields=slim drops context and step inputs, keeps outputs and retry fields", async () => {
82
+ const result = await handler({ id: runId, fields: "slim" }, extra);
83
+ expect(result.isError).not.toBe(true);
84
+ const { run, steps } = result.structuredContent;
85
+ expect(run?.status).toBe("completed");
86
+ expect(run?.context).toBeUndefined();
87
+ expect(steps).toHaveLength(2);
88
+ for (const step of steps!) {
89
+ expect(step.input).toBeUndefined();
90
+ expect(step.output).toBeDefined();
91
+ expect(typeof step.retryCount).toBe("number");
92
+ }
93
+ });
94
+
95
+ test("fields=slim reports a missing run as an error", async () => {
96
+ const result = await handler({ id: crypto.randomUUID(), fields: "slim" }, extra);
97
+ expect(result.isError).toBe(true);
98
+ });
@@ -21,6 +21,7 @@ const TEST_DB_PATH = "./test-integrations-http.sqlite";
21
21
 
22
22
  interface FakeClientLog {
23
23
  retrieveCalls: string[];
24
+ apiKeys: string[];
24
25
  retrieveResult?: { name?: string | null; model?: string | null };
25
26
  retrieveError?: Error;
26
27
  }
@@ -60,7 +61,7 @@ async function clearManagedConfigRows() {
60
61
  describe("POST /api/integrations/claude-managed/test", () => {
61
62
  let server: Server;
62
63
  let baseUrl = "";
63
- const log: FakeClientLog = { retrieveCalls: [] };
64
+ const log: FakeClientLog = { retrieveCalls: [], apiKeys: [] };
64
65
  const savedEnv = {
65
66
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
66
67
  MANAGED_AGENT_ID: process.env.MANAGED_AGENT_ID,
@@ -71,7 +72,10 @@ describe("POST /api/integrations/claude-managed/test", () => {
71
72
  initDb(TEST_DB_PATH);
72
73
 
73
74
  const handler = createIntegrationsHandler({
74
- buildClient: () => buildFakeClient(log),
75
+ buildClient: (apiKey) => {
76
+ log.apiKeys.push(apiKey);
77
+ return buildFakeClient(log);
78
+ },
75
79
  });
76
80
 
77
81
  server = createHttpServer(async (req, res) => {
@@ -101,6 +105,7 @@ describe("POST /api/integrations/claude-managed/test", () => {
101
105
 
102
106
  beforeEach(async () => {
103
107
  log.retrieveCalls = [];
108
+ log.apiKeys = [];
104
109
  log.retrieveResult = undefined;
105
110
  log.retrieveError = undefined;
106
111
  await clearManagedConfigRows();
@@ -138,6 +143,31 @@ describe("POST /api/integrations/claude-managed/test", () => {
138
143
  expect(log.retrieveCalls).toEqual(["agent_abc123"]);
139
144
  });
140
145
 
146
+ test("a comma pool sends one key to Anthropic", async () => {
147
+ await upsertSwarmConfig({
148
+ scope: "global",
149
+ key: "ANTHROPIC_API_KEY",
150
+ value: "sk-ant-first,sk-ant-second",
151
+ isSecret: true,
152
+ });
153
+ await upsertSwarmConfig({
154
+ scope: "global",
155
+ key: "MANAGED_AGENT_ID",
156
+ value: "agent_abc123",
157
+ isSecret: false,
158
+ });
159
+
160
+ const res = await fetch(`${baseUrl}/api/integrations/claude-managed/test`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: "{}",
164
+ });
165
+
166
+ expect((await res.json()) as { ok: boolean }).toMatchObject({ ok: true });
167
+ expect(log.apiKeys).toHaveLength(1);
168
+ expect(["sk-ant-first", "sk-ant-second"]).toContain(log.apiKeys[0]);
169
+ });
170
+
141
171
  test("missing-config path — returns ok:false with helpful error", async () => {
142
172
  // No swarm_config rows, no env. resolveConfigValue should return null
143
173
  // for both keys and short-circuit before calling Anthropic.
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
2
  import { Type } from "typebox";
3
3
  import { z } from "zod";
4
4
  import { SummaryWithRatingsSchema } from "../../be/memory/raters/llm.js";
@@ -501,6 +501,42 @@ describe("completeStructured", () => {
501
501
  });
502
502
 
503
503
  describe("defaultSpawnClaudeCli", () => {
504
+ for (const mirrored of [false, true]) {
505
+ test(`selects single credentials before CLI spawn (mirror=${mirrored})`, async () => {
506
+ const keys = [
507
+ "CLAUDE_CODE_OAUTH_TOKEN",
508
+ "AGENT_SWARM_CLAUDE_OAUTH_TOKEN",
509
+ "ANTHROPIC_API_KEY",
510
+ ];
511
+ const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
512
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = mirrored ? "" : "example-first,example-second";
513
+ process.env.AGENT_SWARM_CLAUDE_OAUTH_TOKEN = "example-first,example-second";
514
+ process.env.ANTHROPIC_API_KEY = "example-first,example-second";
515
+ let capturedEnv: Record<string, string | undefined> | undefined;
516
+ const spawn = spyOn(Bun, "spawn").mockImplementation(((
517
+ ...args: Parameters<typeof Bun.spawn>
518
+ ) => {
519
+ const options = args[0] as Bun.SpawnOptions.OptionsObject;
520
+ capturedEnv = options.env;
521
+ throw new Error("test stops before CLI spawn");
522
+ }) as typeof Bun.spawn);
523
+ try {
524
+ await expect(defaultSpawnClaudeCli("prompt", "haiku")).rejects.toThrow(
525
+ "test stops before CLI spawn",
526
+ );
527
+ for (const key of ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]) {
528
+ expect(["example-first", "example-second"]).toContain(capturedEnv?.[key]);
529
+ }
530
+ } finally {
531
+ spawn.mockRestore();
532
+ for (const [key, value] of Object.entries(saved)) {
533
+ if (value === undefined) delete process.env[key];
534
+ else process.env[key] = value;
535
+ }
536
+ }
537
+ });
538
+ }
539
+
504
540
  test("sets SKIP_SESSION_SUMMARY=1 in the child env (Stop-hook recursion guard)", async () => {
505
541
  // Without this guard, the spawned `claude -p` summarizer session fires the
506
542
  // same global Stop hook on exit, which spawns another summarizer claude,
@@ -25,6 +25,29 @@ function makeOpts(
25
25
  }
26
26
 
27
27
  describe("resolveCredential", () => {
28
+ for (const [keyVar, kind] of [
29
+ ["OPENROUTER_API_KEY", "openrouter"],
30
+ ["ANTHROPIC_API_KEY", "anthropic"],
31
+ ["OPENAI_API_KEY", "openai"],
32
+ ] as const) {
33
+ for (const source of ["env", "fallback"] as const) {
34
+ test(`${keyVar} selects one pool member from ${source}`, async () => {
35
+ const pool = "example-first, example-second";
36
+ const cred = await resolveCredential(
37
+ makeOpts({
38
+ env: source === "env" ? { [keyVar]: pool } : {},
39
+ _getEnvApiKey: (provider) =>
40
+ source === "fallback" && provider === kind ? pool : undefined,
41
+ }),
42
+ );
43
+ expect(cred?.kind).toBe(kind);
44
+ expect(
45
+ cred && "apiKey" in cred && ["example-first", "example-second"].includes(cred.apiKey),
46
+ ).toBe(true);
47
+ });
48
+ }
49
+ }
50
+
28
51
  test("OPENROUTER_API_KEY wins", async () => {
29
52
  const cred = await resolveCredential(makeOpts({ env: { OPENROUTER_API_KEY: "or-1" } }));
30
53
  expect(cred).not.toBeNull();
@@ -362,6 +362,38 @@ describe("/api/kv REST — auth on writes", () => {
362
362
  expect(intruderWrite.status).toBe(403);
363
363
  });
364
364
 
365
+ test("session source labels are private: generic callers cannot read, list, or write them", async () => {
366
+ const namespace = "session-labels";
367
+ const key = encodeURIComponent("shared/slack/d0private1/");
368
+ await upsertKv({
369
+ namespace,
370
+ key: "shared/slack/d0private1/",
371
+ value: "DM · Lena",
372
+ valueType: "string",
373
+ });
374
+ for (const as of [agentId, leadAgentId]) {
375
+ expect((await authedFetch(`/api/kv/_/${namespace}/${key}`, { agentId: as })).status).toBe(
376
+ 403,
377
+ );
378
+ expect((await authedFetch(`/api/kv/_/${namespace}`, { agentId: as })).status).toBe(403);
379
+ for (const method of ["PUT", "DELETE"]) {
380
+ const res = await authedFetch(`/api/kv/_/${namespace}/${key}`, {
381
+ method,
382
+ body:
383
+ method === "PUT" ? JSON.stringify({ value: "forged", valueType: "string" }) : undefined,
384
+ agentId: as,
385
+ });
386
+ expect(res.status).toBe(403);
387
+ }
388
+ }
389
+ expect(
390
+ await getDbClient().get<{ value: string }>(
391
+ "SELECT value FROM kv_entries WHERE namespace = ? AND key = ?",
392
+ [namespace, "shared/slack/d0private1/"],
393
+ ),
394
+ ).toEqual({ value: "DM · Lena" });
395
+ });
396
+
365
397
  test("403 when writing to task:page:* without an X-Page-Id header", async () => {
366
398
  const ns = "task:page:abc";
367
399
  const res = await authedFetch(`/api/kv/_/${encodeURIComponent(ns)}/k`, {
@@ -133,6 +133,31 @@ describe("kv MCP tools", () => {
133
133
  ).toBeNull();
134
134
  });
135
135
 
136
+ test("kv-get, kv-list, and kv-set refuse the private session-labels namespace, even for lead", async () => {
137
+ const namespace = "session-labels";
138
+ const key = "shared/slack/d0private1/";
139
+ await getDbClient().run(
140
+ `INSERT INTO kv_entries (namespace, key, value, value_type, created_at, updated_at)
141
+ VALUES (?, ?, 'DM · Lena', 'string', 0, 0)`,
142
+ [namespace, key],
143
+ );
144
+ const tools = buildServer();
145
+ for (const result of [
146
+ await tools.get.handler({ namespace, key }, meta(lead)),
147
+ await tools.list.handler({ namespace }, meta(lead)),
148
+ await tools.set.handler({ namespace, key, value: "forged" }, meta(lead)),
149
+ ] as StructuredResult<{ success: boolean; message: string }>[]) {
150
+ expect(result.structuredContent.success).toBe(false);
151
+ expect(result.structuredContent.message).toContain("reserved for session source labels");
152
+ }
153
+ expect(
154
+ await getDbClient().get("SELECT value FROM kv_entries WHERE namespace = ? AND key = ?", [
155
+ namespace,
156
+ key,
157
+ ]),
158
+ ).toEqual({ value: "DM · Lena" });
159
+ });
160
+
136
161
  test("kv-get returns entry=null for missing keys", async () => {
137
162
  const tools = buildServer();
138
163
  const getRes = (await tools.get.handler({ key: "nope" }, meta(agentA))) as StructuredResult<{
@@ -2995,11 +2995,19 @@ describe("post-task credential recovery without a restart", () => {
2995
2995
  ready: true,
2996
2996
  lastRefreshAt: 0,
2997
2997
  inFlight: false,
2998
+ fingerprint: null,
2999
+ rejectedKeys: 0,
2998
3000
  };
2999
3001
  const check = spyOn(CREDENTIAL_PROVIDER_CHECKERS, "claude");
3000
3002
  const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
3001
- // The real Claude checker and live-test use OAuth presence, no upstream I/O.
3002
3003
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-credential-recovery";
3004
+ // The subscription live test calls Anthropic's OAuth profile: answer it locally.
3005
+ const realFetch = globalThis.fetch;
3006
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
3007
+ const url = input instanceof Request ? input.url : String(input);
3008
+ if (url.startsWith("https://api.anthropic.com/")) return new Response("{}");
3009
+ return realFetch(input, init);
3010
+ }) as typeof fetch;
3003
3011
  try {
3004
3012
  // A live provider swap produces a real not-ready post-task snapshot.
3005
3013
  await refreshCredentialStatus(api, refresh, "claude", {}, 1);
@@ -3021,6 +3029,8 @@ describe("post-task credential recovery without a restart", () => {
3021
3029
  await refreshCredentialStatus(api, refresh, "claude", {}, CREDENTIAL_RETRY_INTERVAL_MS + 1);
3022
3030
  expect(check).toHaveBeenCalledTimes(2);
3023
3031
  expect((await getAgentById(id))?.status).toBe("waiting_for_credentials");
3032
+ // Credentials resolve. A changed credential is checked at once, inside
3033
+ // the retry throttle. Same runner state, provider, and runtime identity.
3024
3034
  const env = { CLAUDE_CODE_OAUTH_TOKEN: "test-credential-recovery" };
3025
3035
  await refreshCredentialStatus(
3026
3036
  api,
@@ -3029,17 +3039,6 @@ describe("post-task credential recovery without a restart", () => {
3029
3039
  env,
3030
3040
  2 * CREDENTIAL_RETRY_INTERVAL_MS,
3031
3041
  );
3032
- expect(check).toHaveBeenCalledTimes(2);
3033
- expect((await getAgentById(id))?.status).toBe("waiting_for_credentials");
3034
-
3035
- // Credentials resolve. Same runner state, provider, and runtime identity.
3036
- await refreshCredentialStatus(
3037
- api,
3038
- refresh,
3039
- "claude",
3040
- env,
3041
- 2 * CREDENTIAL_RETRY_INTERVAL_MS + 1,
3042
- );
3043
3042
  expect(check).toHaveBeenCalledTimes(3);
3044
3043
  expect((await getAgentById(id))?.status).toBe("idle");
3045
3044
  expect((await getAgentById(id))?.credStatus?.ready).toBe(true);
@@ -3058,6 +3057,7 @@ describe("post-task credential recovery without a restart", () => {
3058
3057
  );
3059
3058
  expect(check).toHaveBeenCalledTimes(3);
3060
3059
  } finally {
3060
+ globalThis.fetch = realFetch;
3061
3061
  check.mockRestore();
3062
3062
  if (originalOAuth === undefined) delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
3063
3063
  else process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
@@ -362,7 +362,14 @@ describe("onboarding state", () => {
362
362
  signals: { providers: Array<Record<string, unknown>> };
363
363
  };
364
364
  expect(body.signals.providers).toEqual([
365
- { provider: "claude", state: "verified", workers: 2, verifiedWorkers: 2 },
365
+ {
366
+ provider: "claude",
367
+ state: "verified",
368
+ workers: 2,
369
+ verifiedWorkers: 2,
370
+ error: null,
371
+ warning: null,
372
+ },
366
373
  ]);
367
374
  expect(body.state.steps.ai).toMatchObject({
368
375
  status: "done",
@@ -370,6 +377,57 @@ describe("onboarding state", () => {
370
377
  });
371
378
  });
372
379
 
380
+ test("GET carries the latest live test error and the pool warning", async () => {
381
+ const report = (liveTest: {
382
+ ok: boolean;
383
+ error: string | null;
384
+ latency_ms: number;
385
+ testedAt: number;
386
+ warning?: string;
387
+ }) => ({
388
+ ready: true,
389
+ missing: [],
390
+ satisfiedBy: "env" as const,
391
+ hint: null,
392
+ liveTest,
393
+ reportedAt: Date.now(),
394
+ reportKind: "boot" as const,
395
+ });
396
+ const failing = await createAgent({ name: "codex-worker", isLead: false, status: "idle" });
397
+ await setAgentHarnessProvider(failing.id, "codex");
398
+ await updateAgentCredStatus(
399
+ failing.id,
400
+ report({ ok: false, error: "HTTP 401: bad key", latency_ms: 5, testedAt: Date.now() }),
401
+ );
402
+ const warned = await createAgent({ name: "pi-worker", isLead: false, status: "idle" });
403
+ await setAgentHarnessProvider(warned.id, "pi");
404
+ await updateAgentCredStatus(
405
+ warned.id,
406
+ report({
407
+ ok: true,
408
+ error: null,
409
+ latency_ms: 5,
410
+ testedAt: Date.now(),
411
+ warning: "1 of 2 keys failed: key ending abcde (HTTP 401).",
412
+ }),
413
+ );
414
+
415
+ const body = (await (await request("/api/onboarding")).json()) as {
416
+ signals: { providers: Array<Record<string, unknown>> };
417
+ };
418
+ const byProvider = Object.fromEntries(body.signals.providers.map((p) => [p.provider, p]));
419
+ expect(byProvider.codex).toMatchObject({
420
+ state: "configured",
421
+ error: "HTTP 401: bad key",
422
+ warning: null,
423
+ });
424
+ expect(byProvider.pi).toMatchObject({
425
+ state: "verified",
426
+ error: null,
427
+ warning: "1 of 2 keys failed: key ending abcde (HTTP 401).",
428
+ });
429
+ });
430
+
373
431
  test("GET derives integrations from environment configuration", async () => {
374
432
  process.env.GITHUB_TOKEN = "example-github-token";
375
433
  const body = (await (await request("/api/onboarding")).json()) as {
@@ -853,6 +911,55 @@ describe("onboarding memory probe", () => {
853
911
  expect((await getState()).steps.memory.status).toBe("todo");
854
912
  });
855
913
 
914
+ test("probes and saves the first member of a reused OpenAI pool", async () => {
915
+ const originalFetch = globalThis.fetch;
916
+ let authorization: string | null = null;
917
+ process.env.OPENAI_API_KEY = "example-first, example-second";
918
+ globalThis.fetch = (async (input, init) => {
919
+ const req = new Request(input, init);
920
+ if (new URL(req.url).hostname !== "api.openai.com") return originalFetch(input, init);
921
+ authorization = req.headers.get("authorization");
922
+ return embeddingResponse(512);
923
+ }) as typeof fetch;
924
+ try {
925
+ const response = await request("/api/onboarding/memory", {
926
+ method: "POST",
927
+ body: { preset: "openai", reuseKey: "OPENAI_API_KEY" },
928
+ });
929
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
930
+ expect(authorization).toBe("Bearer example-first");
931
+ const configs = await getSwarmConfigs({ scope: "global", key: "EMBEDDING_API_KEY" });
932
+ expect(configs[0]?.value).toBe("example-first");
933
+ } finally {
934
+ globalThis.fetch = originalFetch;
935
+ }
936
+ });
937
+
938
+ for (const keyVar of ["EMBEDDING_API_KEY", "OPENAI_API_KEY"]) {
939
+ test(`existing memory probes the first member of ${keyVar}`, async () => {
940
+ const originalFetch = globalThis.fetch;
941
+ let authorization: string | null = null;
942
+ process.env[keyVar] = "example-first, example-second";
943
+ process.env.EMBEDDING_API_BASE_URL = "https://api.openai.com/v1";
944
+ globalThis.fetch = (async (input, init) => {
945
+ const req = new Request(input, init);
946
+ if (new URL(req.url).hostname !== "api.openai.com") return originalFetch(input, init);
947
+ authorization = req.headers.get("authorization");
948
+ return embeddingResponse(512);
949
+ }) as typeof fetch;
950
+ try {
951
+ const response = await request("/api/onboarding/memory", {
952
+ method: "POST",
953
+ body: { preset: "existing" },
954
+ });
955
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
956
+ expect(authorization).toBe("Bearer example-first");
957
+ } finally {
958
+ globalThis.fetch = originalFetch;
959
+ }
960
+ });
961
+ }
962
+
856
963
  test("does not send a reused OpenAI key to another host", async () => {
857
964
  let hits = 0;
858
965
  const fake = startEmbeddingServer(() => {
@@ -0,0 +1,35 @@
1
+ import { expect, test } from "bun:test";
2
+ import { OpenAIEmbeddingProvider } from "../be/memory/providers/openai-embedding";
3
+
4
+ for (const source of ["config", "EMBEDDING_API_KEY", "OPENAI_API_KEY"] as const) {
5
+ test(`embedding selects one key from ${source}`, async () => {
6
+ const originalFetch = globalThis.fetch;
7
+ const saved = {
8
+ EMBEDDING_API_KEY: process.env.EMBEDDING_API_KEY,
9
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
10
+ };
11
+ delete process.env.EMBEDDING_API_KEY;
12
+ delete process.env.OPENAI_API_KEY;
13
+ const pool = "example-first, example-second";
14
+ if (source !== "config") process.env[source] = pool;
15
+ let authorization: string | null = null;
16
+ globalThis.fetch = (async (input, init) => {
17
+ authorization = new Request(input, init).headers.get("authorization");
18
+ return Response.json({ data: [{ embedding: [0.5] }] });
19
+ }) as typeof fetch;
20
+ try {
21
+ const provider = new OpenAIEmbeddingProvider({
22
+ dimensions: 1,
23
+ ...(source === "config" ? { apiKey: pool } : {}),
24
+ });
25
+ expect(await provider.embed("test input")).toEqual(new Float32Array([0.5]));
26
+ expect(["Bearer example-first", "Bearer example-second"]).toContain(authorization);
27
+ } finally {
28
+ globalThis.fetch = originalFetch;
29
+ for (const [key, value] of Object.entries(saved)) {
30
+ if (value === undefined) delete process.env[key];
31
+ else process.env[key] = value;
32
+ }
33
+ }
34
+ });
35
+ }
@@ -164,12 +164,12 @@ describe("workflow raw-llm and validate nodes", () => {
164
164
  });
165
165
  });
166
166
 
167
- describe("OpenRouter credential check (GET /models)", () => {
167
+ describe("OpenRouter credential check (GET /key)", () => {
168
168
  for (const c of CASES) {
169
169
  test(`${c.name}`, async () => {
170
170
  setEnv(c.env);
171
171
  await validateProviderCredentials("grok", { OPENROUTER_API_KEY: "example-or-key" });
172
- expect(requests[0]?.url.endsWith("/models")).toBe(true);
172
+ expect(requests[0]?.url.endsWith("/key")).toBe(true);
173
173
  expectAttribution(requests[0]?.headers, c.attributed);
174
174
  });
175
175
  }
@@ -5,6 +5,8 @@ import {
5
5
  COMPANY_ACQUISITION_SOURCES,
6
6
  COMPANY_TEAM_SIZES,
7
7
  COMPANY_USE_CASES,
8
+ reportedUseCase,
9
+ reportedUserFunction,
8
10
  } from "../utils/company-profile";
9
11
  import {
10
12
  isFilledOrgLogoUrl,
@@ -157,6 +159,40 @@ describe("company profile config guards", () => {
157
159
  }
158
160
  });
159
161
 
162
+ describe("profile values the telemetry catalog does not list yet", () => {
163
+ const input = {
164
+ orgId: "org_0123456789abcdef",
165
+ installationId: "install_0123456789abcdef",
166
+ isCloud: false,
167
+ isE2b: false,
168
+ swarmVersion: "1.0.0",
169
+ actor: { userId: "user-1" },
170
+ };
171
+
172
+ test("the config guards accept them", () => {
173
+ for (const value of ["content", "research", "seo", "admin"]) {
174
+ expect(validateConfigValue("SWARM_ORG_USE_CASE", value)).toBeNull();
175
+ }
176
+ });
177
+
178
+ test("stay out of the telemetry context, as if unset", () => {
179
+ for (const useCase of ["content", "research", "seo", "admin"]) {
180
+ expect(reportedUseCase(useCase)).toBeUndefined();
181
+ expect(buildContext({ ...input, useCase })).not.toHaveProperty("use_case");
182
+ }
183
+ for (const userFunction of ["marketing", "sales", "research"]) {
184
+ expect(reportedUserFunction(userFunction)).toBeUndefined();
185
+ expect(buildContext({ ...input, userFunction })).not.toHaveProperty("user_function");
186
+ }
187
+ });
188
+
189
+ test("the catalog values still report", () => {
190
+ const context = buildContext({ ...input, useCase: "docs", userFunction: "founder" });
191
+ expect(context.use_case).toBe("docs");
192
+ expect(context.user_function).toBe("founder");
193
+ });
194
+ });
195
+
160
196
  describe("org_website in the telemetry context", () => {
161
197
  const input = {
162
198
  orgId: "org_0123456789abcdef",