@desplega.ai/agent-swarm 1.152.1 → 1.153.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 (228) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-4s7q25ft.js → acp-adapter-ahmjxhek.js} +4 -4
  3. package/dist/{actions-cs6rhmvc.js → actions-jzrfpkmn.js} +30 -26
  4. package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
  5. package/dist/{app-j54aq0dj.js → app-03na2vwb.js} +9 -6
  6. package/dist/{assistant-w1pgn7vf.js → assistant-hfkxvhgt.js} +33 -29
  7. package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
  8. package/dist/{boot-reembed-smy30z32.js → boot-reembed-c98xrad3.js} +8 -6
  9. package/dist/{boot-reembed-42vgkrrj.js → boot-reembed-zt3qm9xn.js} +9 -7
  10. package/dist/{boot-scrub-logs-b6s72fsw.js → boot-scrub-logs-jcqd8ksj.js} +6 -4
  11. package/dist/{claude-adapter-fh7rmcj9.js → claude-adapter-mgryg8g3.js} +17 -15
  12. package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
  13. package/dist/{claude-sdk-session-xhynatn9.js → claude-sdk-session-jjf4zss1.js} +17 -15
  14. package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
  15. package/dist/{cli-fm4ytsdk.js → cli-0d9nbqxf.js} +1 -1
  16. package/dist/cli-0x5qzvkg.js +4279 -0
  17. package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
  18. package/dist/{cli-wbyk465z.js → cli-1hwnb4hc.js} +1 -1
  19. package/dist/{cli-sakxd2rg.js → cli-3bcmp015.js} +5 -5
  20. package/dist/cli-3er6gcsn.js +1849 -0
  21. package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
  22. package/dist/{cli-sqmh7jj1.js → cli-5mbmvxnn.js} +5 -5
  23. package/dist/{cli-v9sk4dzw.js → cli-5taqtpze.js} +235 -8
  24. package/dist/{cli-dsyg66vt.js → cli-6ks9xf1n.js} +7 -5
  25. package/dist/cli-7g9s78jf.js +2827 -0
  26. package/dist/{cli-jtvs5m8q.js → cli-7pnz2v6s.js} +2 -2
  27. package/dist/{cli-jytf5mw9.js → cli-83wkg402.js} +2 -2
  28. package/dist/{cli-3pft2z6r.js → cli-a9scg8sv.js} +12 -10
  29. package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
  30. package/dist/{cli-ht5rs63n.js → cli-b2g6mzjc.js} +15 -6
  31. package/dist/{cli-3g61md6v.js → cli-bw0dfwkr.js} +1 -1
  32. package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
  33. package/dist/{cli-ks8a1cgq.js → cli-cdq08v3r.js} +2 -2
  34. package/dist/cli-cgj4hc12.js +18902 -0
  35. package/dist/cli-d6hk10xx.js +14075 -0
  36. package/dist/{cli-bdt7w021.js → cli-djvxhnc4.js} +31 -3324
  37. package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
  38. package/dist/{cli-b4dyetqt.js → cli-eazstkad.js} +2 -2
  39. package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
  40. package/dist/cli-h38x0qhg.js +6912 -0
  41. package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
  42. package/dist/{pi-mono-adapter-jcqt71cg.js → cli-ja7mrygn.js} +67343 -101083
  43. package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
  44. package/dist/{cli-y8tayczk.js → cli-m04pt18h.js} +2 -2
  45. package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
  46. package/dist/{cli-j4nv2wqy.js → cli-mzgyhxbe.js} +10 -4
  47. package/dist/{cli-wfve107r.js → cli-n0q9b7j4.js} +46 -1870
  48. package/dist/cli-n7h1kqpf.js +29 -0
  49. package/dist/{cli-ng4q6a9t.js → cli-nq0kgyxe.js} +7 -7
  50. package/dist/{cli-72tgtzr1.js → cli-qrqhc31j.js} +151 -125
  51. package/dist/{cli-vbk025fm.js → cli-r8tdds6s.js} +1 -1
  52. package/dist/{cli-ch38q5z4.js → cli-rj7de4b2.js} +6788 -2541
  53. package/dist/{cli-ftdjz1mk.js → cli-s5s1f2yn.js} +56 -40
  54. package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
  55. package/dist/{cli-7bwaerjt.js → cli-tdh4hjr2.js} +3 -2
  56. package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
  57. package/dist/{cli-kf8qp2vf.js → cli-vw03hg5t.js} +3 -2
  58. package/dist/{cli-gfez0qmk.js → cli-ybpp1srm.js} +6 -6
  59. package/dist/cli-yn0a417a.js +4069 -0
  60. package/dist/{cli-gxf2ebyz.js → cli-ysc1kbet.js} +2 -2
  61. package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
  62. package/dist/cli-zcsyfw2z.js +3335 -0
  63. package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
  64. package/dist/cli-zy5w9gpt.js +230 -0
  65. package/dist/cli.js +26 -34
  66. package/dist/{codex-adapter-k6yq95sn.js → codex-adapter-8548daap.js} +12 -11
  67. package/dist/{codex-session-runner-3rp84pk8.js → codex-session-runner-2dad8p4x.js} +12 -11
  68. package/dist/{commands-pbr5jzy5.js → commands-355dbpa8.js} +6 -4
  69. package/dist/{db-z0xaa59a.js → db-4jjvwz1s.js} +6 -4
  70. package/dist/{dispatcher-pwpgsj0x.js → dispatcher-ca590nkz.js} +29 -25
  71. package/dist/dsh-adapter-rgvj6ykp.js +19 -0
  72. package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
  73. package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-xtw7tggr.js} +33 -73
  74. package/dist/{google-vertex-fr665tqs.js → google-vertex-zdqpms6n.js} +38 -68
  75. package/dist/{handlers-5mzk4mbg.js → handlers-2y6dqpvf.js} +32 -28
  76. package/dist/{hook-8bz33zee.js → hook-0s8knepr.js} +11 -9
  77. package/dist/{hook-kbh84zb1.js → hook-k0gec254.js} +12 -10
  78. package/dist/{http-pppxg5vr.js → http-jp2nb4zw.js} +300 -237
  79. package/dist/{index-k1hq1qt6.js → index-0wzg3sp3.js} +39 -24
  80. package/dist/{index-68bd9b5f.js → index-3mq6ce93.js} +29 -25
  81. package/dist/{index-z0zwjtkn.js → index-42s7eezq.js} +29 -25
  82. package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
  83. package/dist/{index-y0te09pn.js → index-b56xkbc7.js} +8 -6
  84. package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
  85. package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
  86. package/dist/{index-mjpxsr6k.js → index-s37dbevy.js} +30 -26
  87. package/dist/index-w097eg0x.js +5 -0
  88. package/dist/jiti-loader-b4837mtm.js +34 -0
  89. package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
  90. package/dist/{keepalive-xvmwnh1d.js → keepalive-9pfw7t02.js} +8 -6
  91. package/dist/{lead-pf8nvxt8.js → lead-7vcmcdq4.js} +44 -39
  92. package/dist/{maintenance-jk4q0wgg.js → maintenance-x277jbpd.js} +9 -7
  93. package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
  94. package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
  95. package/dist/{oauth-refresh-sweep-6207x9sj.js → oauth-refresh-sweep-5fwa7th2.js} +7 -5
  96. package/dist/{onboard-b0csgm0q.js → onboard-17sv0f51.js} +103 -35
  97. package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
  98. package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
  99. package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
  100. package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
  101. package/dist/{otel-impl-b6rfehth.js → otel-impl-srjy4be4.js} +4 -4
  102. package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
  103. package/dist/pi-mono-adapter-946a0sdd.js +1316 -0
  104. package/dist/{pricing-refresh-4e201wtd.js → pricing-refresh-vetcm563.js} +8 -6
  105. package/dist/{rbac-roles-mkc5n5qa.js → rbac-roles-cfzgr6r1.js} +8 -6
  106. package/dist/{rbac-roles-zp6e4geh.js → rbac-roles-cmxaddmy.js} +7 -5
  107. package/dist/{render-v2-4fazz9tf.js → render-v2-f9hzff72.js} +9 -6
  108. package/dist/{seed-pricing-thxpn9vn.js → seed-pricing-jggk6rsy.js} +7 -5
  109. package/dist/{setup-wb0z8cat.js → setup-8mfceb9t.js} +4 -5
  110. package/dist/virtual-modules-ax661xp0.js +54 -0
  111. package/dist/{work-objects-h3498kwe.js → work-objects-vdsdcczj.js} +32 -28
  112. package/dist/{worker-2m2tzxp3.js → worker-cd0nhyv1.js} +44 -39
  113. package/openapi.json +36 -14
  114. package/package.json +6 -6
  115. package/src/be/automation-preflight.ts +6 -4
  116. package/src/be/extensions/db.ts +35 -4
  117. package/src/be/models-catalog.ts +2 -0
  118. package/src/be/modelsdev-cache.json +213 -0
  119. package/src/be/rbac-audit.ts +2 -0
  120. package/src/be/seed-pricing.ts +3 -1
  121. package/src/be/swarm-config-guard.ts +1 -0
  122. package/src/commands/onboard/compose-generator.ts +6 -1
  123. package/src/commands/onboard/env-generator.ts +6 -1
  124. package/src/commands/onboard/manifest.ts +1 -0
  125. package/src/commands/onboard/steps/integration-slack.tsx +56 -16
  126. package/src/commands/onboard/steps/review.tsx +5 -0
  127. package/src/commands/onboard/types.ts +6 -0
  128. package/src/commands/onboard.tsx +5 -1
  129. package/src/commands/provider-credentials.ts +14 -2
  130. package/src/extensions/lifecycle.ts +15 -21
  131. package/src/http/agents.ts +2 -2
  132. package/src/http/config.ts +1 -1
  133. package/src/http/core.ts +5 -3
  134. package/src/http/extensions.ts +108 -34
  135. package/src/http/status.ts +22 -10
  136. package/src/prompts/base-prompt.ts +15 -7
  137. package/src/providers/claude-managed-models.ts +8 -0
  138. package/src/providers/codex-models.ts +15 -1
  139. package/src/providers/dsh-adapter.ts +248 -0
  140. package/src/providers/index.ts +5 -1
  141. package/src/providers/modelsdev-reasoning.json +50 -0
  142. package/src/rbac/legacy-policy.ts +15 -1
  143. package/src/rbac/types.ts +2 -0
  144. package/src/server.ts +7 -1
  145. package/src/slack/app.ts +32 -10
  146. package/src/slack/assistant.ts +10 -1
  147. package/src/slack/config.ts +29 -0
  148. package/src/slack/handlers.ts +10 -1
  149. package/src/slack/thread-buffer.ts +14 -3
  150. package/src/telemetry.ts +2 -3
  151. package/src/tests/agents-harness-provider.test.ts +19 -0
  152. package/src/tests/agents-list-model-display.test.ts +60 -1
  153. package/src/tests/base-prompt.test.ts +25 -0
  154. package/src/tests/claude-managed-adapter.test.ts +6 -0
  155. package/src/tests/codex-gpt6-release.test.ts +97 -0
  156. package/src/tests/config-api-only-keys.test.ts +9 -0
  157. package/src/tests/context-window.test.ts +1 -0
  158. package/src/tests/dsh-adapter.test.ts +276 -0
  159. package/src/tests/e2e-nightly-report.test.ts +3 -3
  160. package/src/tests/e2e-redact.test.ts +2 -0
  161. package/src/tests/env-flag.test.ts +6 -0
  162. package/src/tests/extensions-db.test.ts +41 -0
  163. package/src/tests/extensions-http.test.ts +148 -4
  164. package/src/tests/extensions-lifecycle.test.ts +18 -0
  165. package/src/tests/extensions-mcp-tools.test.ts +9 -5
  166. package/src/tests/internal-ai/complete-structured.test.ts +32 -9
  167. package/src/tests/internal-ai/credentials.test.ts +4 -4
  168. package/src/tests/internal-ai/models.test.ts +20 -0
  169. package/src/tests/migration-046-budgets.test.ts +2 -0
  170. package/src/tests/model-control.test.ts +2 -2
  171. package/src/tests/onboard-compose.test.ts +14 -0
  172. package/src/tests/onboard-env.test.ts +17 -0
  173. package/src/tests/onboard-manifest.test.ts +12 -0
  174. package/src/tests/opus-5-5-registry.test.ts +48 -0
  175. package/src/tests/pricing-refresh.test.ts +1 -1
  176. package/src/tests/pricing-routes.test.ts +15 -3
  177. package/src/tests/provider-registration.test.ts +5 -2
  178. package/src/tests/rbac-admission-e2e.test.ts +54 -0
  179. package/src/tests/rbac-e2e-helpers.ts +2 -0
  180. package/src/tests/rbac-engine.test.ts +32 -0
  181. package/src/tests/rbac-wire-e2e.test.ts +33 -0
  182. package/src/tests/reasoning-effort.test.ts +8 -0
  183. package/src/tests/reload-config.test.ts +33 -0
  184. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  185. package/src/tests/slack-assistant.test.ts +134 -0
  186. package/src/tests/slack-config.test.ts +136 -0
  187. package/src/tests/slack-mode-lifecycle.test.ts +69 -0
  188. package/src/tests/status.test.ts +22 -2
  189. package/src/tests/task-tool-preload-mcp.test.ts +3 -1
  190. package/src/tests/telemetry-init.test.ts +18 -0
  191. package/src/tests/workflow-llm.test.ts +3 -3
  192. package/src/tools/extension-common.ts +4 -0
  193. package/src/tools/extension-install.ts +1 -1
  194. package/src/tools/extension-list.ts +4 -3
  195. package/src/types.ts +17 -8
  196. package/src/utils/context-window.ts +1 -0
  197. package/src/utils/credentials.ts +8 -0
  198. package/src/utils/internal-ai/complete-structured.ts +8 -0
  199. package/src/utils/internal-ai/models.ts +5 -5
  200. package/templates/schedules/daily-status-report/content.md +25 -12
  201. package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
  202. package/templates/skills/swarm-extensions/content.md +9 -5
  203. package/dist/cli-4wppfkcm.js +0 -16384
  204. package/dist/cli-55qrx3q8.js +0 -6041
  205. package/dist/cli-cg4vnyh4.js +0 -305
  206. package/dist/cli-jyetr64j.js +0 -52
  207. package/dist/cli-pp9ax9pa.js +0 -520
  208. package/dist/cli-qa66qaf0.js +0 -6076
  209. package/dist/cli-qghpvca2.js +0 -590
  210. package/dist/cli-rembwgt8.js +0 -861
  211. package/dist/cli-tcvj7j2r.js +0 -1971
  212. package/dist/cli-vfcth11k.js +0 -36
  213. package/dist/cli-w1d1bgkk.js +0 -1422
  214. package/dist/cli-y6x0gqtg.js +0 -5977
  215. package/dist/es5-tj3tn4jj.js +0 -844
  216. package/dist/index-1kq5hw2p.js +0 -467
  217. package/dist/index-1p8wdstt.js +0 -7
  218. package/dist/index-8fdjzb2f.js +0 -100
  219. package/dist/index-bxmwzbgw.js +0 -365
  220. package/dist/index-nzerxd5z.js +0 -7
  221. package/dist/index-pyymnzat.js +0 -1168
  222. package/dist/index-qv6atbab.js +0 -619
  223. package/dist/index-rja32qa7.js +0 -801
  224. package/dist/index-tzg5srfp.js +0 -211
  225. package/dist/index-vhncw983.js +0 -90
  226. package/dist/index-xk036zsk.js +0 -859
  227. package/dist/index-zyazbzcb.js +0 -6
  228. package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
@@ -1,9 +1,38 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import type { LiveModelsCatalog } from "../../apps/ui/src/lib/agent-runtime-models";
2
3
  import {
3
4
  getAgentModelDisplay,
4
5
  getAgentModelPresentation,
5
6
  } from "../../apps/ui/src/lib/agents-list-model-display";
6
7
 
8
+ const liveCatalog: LiveModelsCatalog = {
9
+ openrouter: {
10
+ id: "openrouter",
11
+ name: "OpenRouter",
12
+ models: {
13
+ "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
14
+ "deepseek/deepseek-v4.1-flash": {
15
+ id: "deepseek/deepseek-v4.1-flash",
16
+ name: "DeepSeek V4.1 Flash",
17
+ },
18
+ },
19
+ },
20
+ opencode: {
21
+ id: "opencode",
22
+ name: "OpenCode Zen",
23
+ models: {
24
+ "glm-5.3-flash": { id: "glm-5.3-flash", name: "GLM-5.3-Flash" },
25
+ },
26
+ },
27
+ anthropic: {
28
+ id: "anthropic",
29
+ name: "Anthropic",
30
+ models: {
31
+ "claude-opus-5-5": { id: "claude-opus-5-5", name: "Claude Opus 5.5" },
32
+ },
33
+ },
34
+ };
35
+
7
36
  describe("agents list model display", () => {
8
37
  test("shows configured and last-used models when they diverge", () => {
9
38
  const display = getAgentModelDisplay("claude-opus-4-7", "claude-sonnet-4-6");
@@ -43,6 +72,31 @@ describe("agents list model display", () => {
43
72
  });
44
73
  });
45
74
 
75
+ test.each([
76
+ ["openrouter/deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "OpenRouter"],
77
+ ["openrouter/deepseek/deepseek-v4.1-flash", "DeepSeek V4.1 Flash", "OpenRouter"],
78
+ ["opencode/glm-5.3-flash", "GLM-5.3-Flash", "OpenCode Zen"],
79
+ ["claude-opus-5-5", "Claude Opus 5.5", "Anthropic"],
80
+ ])("resolves %s from the live catalog", (model, label, provider) => {
81
+ expect(getAgentModelPresentation(model, liveCatalog)).toMatchObject({
82
+ raw: model,
83
+ label,
84
+ provider,
85
+ });
86
+ });
87
+
88
+ test("title-cases an uncatalogued model id without a maintained label entry", () => {
89
+ expect(
90
+ getAgentModelPresentation("opencode/future-model-v8.2-preview", liveCatalog),
91
+ ).toMatchObject({
92
+ label: "Future Model V8.2 Preview",
93
+ provider: null,
94
+ });
95
+ expect(
96
+ getAgentModelPresentation("openrouter/deepseek/deepseek-v5.1-flash", liveCatalog)?.label,
97
+ ).toBe("Deepseek V5.1 Flash");
98
+ });
99
+
46
100
  test("presents latest Anthropic direct model ids as readable labels", () => {
47
101
  expect(getAgentModelPresentation("claude-fable-5-1")).toMatchObject({
48
102
  label: "Claude Fable 5.1",
@@ -54,6 +108,11 @@ describe("agents list model display", () => {
54
108
  provider: "Anthropic",
55
109
  providerId: "anthropic",
56
110
  });
111
+ expect(getAgentModelPresentation("claude-opus-5-5")).toMatchObject({
112
+ label: "Claude Opus 5.5",
113
+ provider: "Anthropic",
114
+ providerId: "anthropic",
115
+ });
57
116
  expect(getAgentModelPresentation("claude-opus-5")).toMatchObject({
58
117
  label: "Claude Opus 5",
59
118
  provider: "Anthropic",
@@ -75,7 +134,7 @@ describe("agents list model display", () => {
75
134
  providerId: "anthropic",
76
135
  });
77
136
  expect(getAgentModelPresentation("opus")).toMatchObject({
78
- label: "Claude Opus 5",
137
+ label: "Claude Opus 5.5",
79
138
  provider: "Anthropic",
80
139
  providerId: "anthropic",
81
140
  });
@@ -25,6 +25,8 @@ const ENV_KEYS = [
25
25
  "SLACK_DISABLE",
26
26
  "SLACK_BOT_TOKEN",
27
27
  "SLACK_APP_TOKEN",
28
+ "SLACK_MODE",
29
+ "SLACK_SIGNING_SECRET",
28
30
  "STEERING_ENABLED",
29
31
  "AGENT_FS_API_URL",
30
32
  "SCRIPTS_ONLY_MCP",
@@ -561,6 +563,29 @@ describe("getBasePrompt: slack section", () => {
561
563
  expect(result).toContain(SLACK_HEADER);
562
564
  });
563
565
 
566
+ test("a tool capability alone does not bypass the default socket credential gate", async () => {
567
+ const result = await getBasePrompt({
568
+ ...minimalArgs,
569
+ serverCapabilities: ["core", "slack"],
570
+ });
571
+ expect(result).not.toContain(SLACK_HEADER);
572
+ });
573
+
574
+ test("supports the HTTP prompt contract without distributing Slack secrets to the worker", async () => {
575
+ process.env.SLACK_MODE = "http";
576
+ const result = await getBasePrompt({
577
+ ...minimalArgs,
578
+ serverCapabilities: ["core", "slack"],
579
+ });
580
+ expect(result).toContain(SLACK_HEADER);
581
+
582
+ const withoutCapability = await getBasePrompt({
583
+ ...minimalArgs,
584
+ serverCapabilities: ["core"],
585
+ });
586
+ expect(withoutCapability).not.toContain(SLACK_HEADER);
587
+ });
588
+
564
589
  test("a scripts-only worker with a Slack task gets the scripts-only variant only", async () => {
565
590
  enableSlack();
566
591
  const result = await getBasePrompt({
@@ -898,6 +898,12 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
898
898
  cacheReadPerMillion: 0.25,
899
899
  cacheWritePerMillion: 12.5,
900
900
  });
901
+ expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-5-5"]).toEqual({
902
+ inputPerMillion: 4.0,
903
+ outputPerMillion: 20.0,
904
+ cacheReadPerMillion: 0.2,
905
+ cacheWritePerMillion: 5.0,
906
+ });
901
907
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-5"]).toEqual({
902
908
  inputPerMillion: 5.0,
903
909
  outputPerMillion: 25.0,
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { modelGroupsForHarness } from "../../apps/ui/src/lib/agent-runtime-models";
3
+ import { buildModelsCatalog } from "../be/models-catalog";
4
+ import cache from "../be/modelsdev-cache.json";
5
+ import { buildModelsDevSeedRows } from "../be/seed-pricing";
6
+ import {
7
+ CODEX_DEFAULT_MODEL,
8
+ CODEX_MODELS,
9
+ computeCodexCostUsd,
10
+ FALLBACK_CODEX_MODEL_PRICING,
11
+ getCodexContextWindow,
12
+ resolveCodexModel,
13
+ } from "../providers/codex-models";
14
+ import { applyReasoningEffort, reasoningCapability } from "../providers/reasoning-effort";
15
+
16
+ const releases = [
17
+ { id: "gpt-6-sol", label: "GPT-6 Sol", input: 2, output: 10, read: 0.2, write: 2.5 },
18
+ { id: "gpt-6-luna", label: "GPT-6 Luna", input: 0.1, output: 0.5, read: 0.01, write: 0.125 },
19
+ ] as const;
20
+
21
+ for (const release of releases) {
22
+ describe(release.id, () => {
23
+ test("uses the exact bare CLI ID with verified context and base costs", () => {
24
+ expect(CODEX_MODELS).toContain(release.id);
25
+ expect(resolveCodexModel(release.id.toUpperCase())).toBe(release.id);
26
+ expect(getCodexContextWindow(release.id)).toBe(1_050_000);
27
+ expect(cache.openai.models[release.id].limit.output).toBe(128_000);
28
+ expect(FALLBACK_CODEX_MODEL_PRICING[release.id]).toEqual({
29
+ inputPerMillion: release.input,
30
+ cachedInputPerMillion: release.read,
31
+ outputPerMillion: release.output,
32
+ });
33
+ expect(computeCodexCostUsd(release.id, 1_000_000, 250_000, 100_000)).toBeCloseTo(
34
+ release.input * 0.75 + release.read * 0.25 + release.output * 0.1,
35
+ 8,
36
+ );
37
+ });
38
+
39
+ test("exposes the full reasoning range and transports none/max correctly", () => {
40
+ expect(reasoningCapability("codex", release.id)).toEqual({
41
+ supported: true,
42
+ levels: ["off", "low", "medium", "high", "xhigh", "max"],
43
+ default: "medium",
44
+ });
45
+ for (const [effort, value] of [
46
+ ["off", "none"],
47
+ ["max", "max"],
48
+ ] as const) {
49
+ expect(applyReasoningEffort("codex", release.id, effort)).toEqual({
50
+ kind: "codex-config",
51
+ config: { model_reasoning_effort: value },
52
+ });
53
+ }
54
+ });
55
+
56
+ test("seeds all four token classes under canonical Codex IDs", () => {
57
+ const rows = buildModelsDevSeedRows(cache).filter(
58
+ (row) => row.provider === "codex" && row.model === release.id,
59
+ );
60
+ expect(
61
+ Object.fromEntries(rows.map((row) => [row.tokenClass, row.pricePerMillionUsd])),
62
+ ).toEqual({
63
+ input: release.input,
64
+ output: release.output,
65
+ cached_input: release.read,
66
+ cache_write: release.write,
67
+ });
68
+ expect(cache.openai.models[release.id].cost.tiers).toEqual([
69
+ {
70
+ input: release.input * 2,
71
+ output: release.output * 1.5,
72
+ cache_read: release.read * 2,
73
+ cache_write: release.write * 2,
74
+ tier: { type: "context", size: 272000 },
75
+ },
76
+ ]);
77
+ });
78
+
79
+ test("is available through the catalog and direct UI selector", () => {
80
+ const model = buildModelsCatalog(cache).openai?.models[release.id];
81
+ expect(model?.limit?.context).toBe(1_050_000);
82
+ expect(model?.cost).toEqual({ input: release.input, output: release.output });
83
+ const option = modelGroupsForHarness("codex", undefined, undefined)
84
+ .flatMap((group) => group.models)
85
+ .find((entry) => entry.id === release.id);
86
+ expect(option?.label).toBe(release.label);
87
+ expect(option?.reasoningLevels).toEqual(["off", "low", "medium", "high", "xhigh", "max"]);
88
+ });
89
+ });
90
+ }
91
+
92
+ test("release additions preserve rollout-safe defaults and aliases", () => {
93
+ expect(CODEX_DEFAULT_MODEL).toBe("gpt-5.6-terra");
94
+ expect(resolveCodexModel("sonnet")).toBe("gpt-5.6-terra");
95
+ expect(resolveCodexModel("haiku")).toBe("gpt-5.6-luna");
96
+ expect(resolveCodexModel("opus")).toBe("gpt-5.6-sol");
97
+ });
@@ -50,6 +50,12 @@ beforeAll(async () => {
50
50
  value: BOOTSTRAP_VALUE,
51
51
  isSecret: true,
52
52
  });
53
+ await upsertSwarmConfig({
54
+ scope: "global",
55
+ key: "SLACK_SIGNING_SECRET",
56
+ value: "synthetic-api-only-signing-secret",
57
+ isSecret: true,
58
+ });
53
59
  await upsertSwarmConfig({
54
60
  scope: "global",
55
61
  key: "SOME_WORKER_SECRET",
@@ -86,6 +92,7 @@ describe("API-only config keys are never served over HTTP", () => {
86
92
  test("GET /api/config/resolved WITHOUT agentId strips the bootstrap key", async () => {
87
93
  const { configs } = await getJson("/api/config/resolved?includeSecrets=true");
88
94
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
95
+ expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
89
96
  // A legitimate worker secret still comes through.
90
97
  expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
91
98
  });
@@ -95,11 +102,13 @@ describe("API-only config keys are never served over HTTP", () => {
95
102
  `/api/config/resolved?includeSecrets=true&agentId=${agentId}`,
96
103
  );
97
104
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
105
+ expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
98
106
  });
99
107
 
100
108
  test("GET /api/config?scope=global strips the bootstrap key", async () => {
101
109
  const { configs } = await getJson("/api/config?scope=global&includeSecrets=true");
102
110
  expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
111
+ expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
103
112
  expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
104
113
  });
105
114
  });
@@ -18,6 +18,7 @@ describe("getContextWindowSize", () => {
18
18
  });
19
19
 
20
20
  test("returns 1M for opus models", () => {
21
+ expect(getContextWindowSize("claude-opus-5-5")).toBe(1_000_000);
21
22
  expect(getContextWindowSize("claude-opus-5")).toBe(1_000_000);
22
23
  expect(getContextWindowSize("claude-opus-4-8")).toBe(1_000_000);
23
24
  expect(getContextWindowSize("claude-opus-4-7")).toBe(1_000_000);
@@ -0,0 +1,276 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { chmod, mkdtemp, rename, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { checkProviderCredentials } from "../commands/provider-credentials";
6
+ import { createProviderAdapter } from "../providers";
7
+ import { checkDshCredentials, DshAdapter } from "../providers/dsh-adapter";
8
+ import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
9
+ import { DEFAULT_MODEL_TIER_MAP } from "../types";
10
+ import { getModelAwareCredentialVars } from "../utils/credentials";
11
+ import { resolveHarnessProvider } from "../utils/harness-provider";
12
+ import { DEFAULT_OPENROUTER_BASE_URL } from "../utils/openrouter-base-url";
13
+
14
+ const directories: string[] = [];
15
+ afterEach(async () => {
16
+ await Promise.all(
17
+ directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
18
+ );
19
+ });
20
+
21
+ async function fixture(mode = "success"): Promise<ProviderSessionConfig> {
22
+ const cwd = await mkdtemp(join(tmpdir(), "dsh-adapter-test-"));
23
+ directories.push(cwd);
24
+ const binary = join(cwd, "dsh");
25
+ await Bun.write(
26
+ binary,
27
+ `#!${process.execPath}
28
+ const args = process.argv.slice(2);
29
+ const patchPath = args[args.indexOf("--patch") + 1];
30
+ const patch = await Bun.file(patchPath).json();
31
+ const prompt = await Bun.stdin.text();
32
+ await Bun.write("invocation.json", JSON.stringify({ args, patchPath, patch, prompt, cwd: process.cwd() }));
33
+ const emit = (event) => console.log(JSON.stringify(event));
34
+ emit({ type: "session", sessionId: "dsh-test-session" });
35
+ const mode = process.env.DSH_TEST_MODE;
36
+ if (mode === "abort") { setInterval(() => {}, 1000); }
37
+ else {
38
+ if (mode === "malformed") console.log("bad json");
39
+ emit({ type: "status", phase: "turn_end", reason: { kind: mode === "failure" ? "error" : "completed" } });
40
+ if (mode !== "missing-final") {
41
+ // A split UTF-8 character plus a final line without a newline exercises both decoders.
42
+ const line = Buffer.from(JSON.stringify({ type: "final", text: "Done ✓" }));
43
+ const split = line.indexOf(Buffer.from("✓")) + 1;
44
+ process.stdout.write(line.subarray(0, split));
45
+ await Bun.sleep(10);
46
+ process.stdout.write(line.subarray(split));
47
+ }
48
+ if (mode === "failure") { console.error(process.env.DEEPSEEK_API_KEY); process.exitCode = 7; }
49
+ }
50
+ `,
51
+ );
52
+ await chmod(binary, 0o700);
53
+ return {
54
+ prompt: "--not-a-flag\nKeep $() and `quotes` literal.",
55
+ systemPrompt: "System instructions:\n!!js must remain text",
56
+ model: "deepseek-v4-pro",
57
+ role: "worker",
58
+ agentId: "agent-test",
59
+ taskId: "task-test",
60
+ apiUrl: "http://unused.invalid",
61
+ apiKey: "unused",
62
+ cwd,
63
+ logFile: join(cwd, "log.jsonl"),
64
+ env: {
65
+ DSH_BINARY: binary,
66
+ DEEPSEEK_API_KEY: "dsh-test-credential-value",
67
+ OPENROUTER_API_KEY: "",
68
+ OPENROUTER_BASE_URL: "",
69
+ DSH_TEST_MODE: mode,
70
+ },
71
+ };
72
+ }
73
+
74
+ describe("dsh harness", () => {
75
+ test("registry, model defaults, and credential dispatch select dsh", async () => {
76
+ expect(await createProviderAdapter("dsh")).toBeInstanceOf(DshAdapter);
77
+ expect(resolveHarnessProvider({ HARNESS_PROVIDER: "dsh" }, {})).toBe("dsh");
78
+ expect(DEFAULT_MODEL_TIER_MAP.dsh).toEqual(DEFAULT_MODEL_TIER_MAP.pi);
79
+ expect(checkDshCredentials({ OPENAI_API_KEY: "unrelated" }).ready).toBe(false);
80
+ expect(checkDshCredentials({ DEEPSEEK_API_KEY: " " }).ready).toBe(false);
81
+ expect(checkDshCredentials({}).missing).toEqual(["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY"]);
82
+ expect((await checkProviderCredentials("dsh", { OPENROUTER_API_KEY: "present" })).ready).toBe(
83
+ true,
84
+ );
85
+ expect(getModelAwareCredentialVars("dsh", DEFAULT_MODEL_TIER_MAP.dsh.ultra)).toEqual([
86
+ "OPENROUTER_API_KEY",
87
+ ]);
88
+ expect(getModelAwareCredentialVars("dsh", "deepseek-v4-pro")).toEqual(["DEEPSEEK_API_KEY"]);
89
+ expect((await checkProviderCredentials("dsh", { DEEPSEEK_API_KEY: "present" })).ready).toBe(
90
+ true,
91
+ );
92
+ });
93
+
94
+ test("passes prompt on stdin, patches model/system, preserves cwd and cleans up", async () => {
95
+ const config = await fixture();
96
+ const session = await new DshAdapter().createSession(config);
97
+ const events: ProviderEvent[] = [];
98
+ session.onEvent((event) => events.push(event));
99
+ expect(await session.waitForCompletion()).toMatchObject({
100
+ exitCode: 0,
101
+ isError: false,
102
+ output: "Done ✓",
103
+ sessionId: "dsh-test-session",
104
+ });
105
+ const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
106
+ expect(invocation.prompt).toBe(config.prompt);
107
+ expect(invocation.cwd).toBe(config.cwd);
108
+ expect(invocation.args).toEqual([
109
+ "--profile",
110
+ "headless",
111
+ "--patch",
112
+ invocation.patchPath,
113
+ "--json",
114
+ "-",
115
+ ]);
116
+ expect(invocation.patch).toContainEqual({
117
+ id: "agent-default-model",
118
+ config: { provider: "deepseek-official", model: config.model },
119
+ });
120
+ expect(invocation.patch).toContainEqual({
121
+ id: "system-prompt",
122
+ config: { personaPrefix: config.systemPrompt },
123
+ });
124
+ expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
125
+ expect(events).toContainEqual({
126
+ type: "session_init",
127
+ sessionId: "dsh-test-session",
128
+ provider: "dsh",
129
+ });
130
+ });
131
+
132
+ for (const model of [
133
+ DEFAULT_MODEL_TIER_MAP.dsh.regular,
134
+ DEFAULT_MODEL_TIER_MAP.dsh.ultra,
135
+ "openrouter/vendor/new-model",
136
+ ]) {
137
+ test(`routes ${model} through OpenRouter with only its key`, async () => {
138
+ const config = await fixture();
139
+ config.model = model;
140
+ config.env = {
141
+ ...config.env,
142
+ DEEPSEEK_API_KEY: "",
143
+ OPENROUTER_API_KEY: "openrouter-test-key",
144
+ };
145
+ const session = await new DshAdapter().createSession(config);
146
+ expect((await session.waitForCompletion()).isError).toBe(false);
147
+ const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
148
+ const modelId = model.slice("openrouter/".length);
149
+ expect(patch).toContainEqual({
150
+ id: "agent-default-model",
151
+ config: { provider: "openrouter", model: modelId },
152
+ });
153
+ expect(patch).toContainEqual({
154
+ id: "llm-pi-ai",
155
+ config: {
156
+ providers: {
157
+ openrouter: {
158
+ apiKeyEnv: "OPENROUTER_API_KEY",
159
+ baseURL: DEFAULT_OPENROUTER_BASE_URL,
160
+ api: "openai-completions",
161
+ models: [{ id: modelId }],
162
+ },
163
+ },
164
+ },
165
+ });
166
+ expect(JSON.stringify(patch)).not.toContain("openrouter-test-key");
167
+ });
168
+ }
169
+
170
+ test("defaults to OpenRouter and honors its base URL when both keys exist", async () => {
171
+ const config = await fixture();
172
+ config.model = undefined;
173
+ config.env = {
174
+ ...config.env,
175
+ OPENROUTER_API_KEY: "openrouter-test-key",
176
+ OPENROUTER_BASE_URL: " https://gateway.example/proxy/v1/// ",
177
+ };
178
+ const session = await new DshAdapter().createSession(config);
179
+ expect((await session.waitForCompletion()).isError).toBe(false);
180
+ const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
181
+ expect(patch[0].config).toEqual({
182
+ provider: "openrouter",
183
+ model: "deepseek/deepseek-v4.1-flash",
184
+ });
185
+ expect(patch[2].config.providers.openrouter.baseURL).toBe("https://gateway.example/proxy/v1");
186
+ });
187
+
188
+ test("bare models keep direct DeepSeek when both keys exist", async () => {
189
+ const config = await fixture();
190
+ config.env = { ...config.env, OPENROUTER_API_KEY: "openrouter-test-key" };
191
+ const session = await new DshAdapter().createSession(config);
192
+ expect((await session.waitForCompletion()).isError).toBe(false);
193
+ const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
194
+ expect(patch[0].config.provider).toBe("deepseek-official");
195
+ expect(patch[2]).toEqual({ id: "llm-deepseek", config: { apiKeyEnv: "DEEPSEEK_API_KEY" } });
196
+ });
197
+
198
+ test("fails closed for absent or mismatched credentials and empty model IDs", async () => {
199
+ const config = await fixture();
200
+ config.env = { ...config.env, DEEPSEEK_API_KEY: "", OPENROUTER_API_KEY: "" };
201
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow(
202
+ "DEEPSEEK_API_KEY or OPENROUTER_API_KEY",
203
+ );
204
+ config.env.DEEPSEEK_API_KEY = "direct-test-key";
205
+ config.model = DEFAULT_MODEL_TIER_MAP.dsh.regular;
206
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow(
207
+ "requires OPENROUTER_API_KEY",
208
+ );
209
+ config.env = { ...config.env, DEEPSEEK_API_KEY: "", OPENROUTER_API_KEY: "openrouter-test-key" };
210
+ config.model = "deepseek-v4-pro";
211
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow(
212
+ "requires DEEPSEEK_API_KEY",
213
+ );
214
+ config.model = "openrouter/";
215
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow("requires a model ID");
216
+ expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
217
+ });
218
+
219
+ test("fails closed when only npx is installed", async () => {
220
+ const config = await fixture();
221
+ await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
222
+ config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
223
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow(
224
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.1.7-alpha.2",
225
+ );
226
+ expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
227
+ });
228
+
229
+ test("uses a preinstalled dsh from PATH without DSH_BINARY", async () => {
230
+ const config = await fixture();
231
+ config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
232
+ const session = await new DshAdapter().createSession(config);
233
+ expect((await session.waitForCompletion()).isError).toBe(false);
234
+ const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
235
+ expect(invocation.args[0]).toBe("--profile");
236
+ });
237
+
238
+ test("does not fall back when an explicit DSH_BINARY is missing", async () => {
239
+ const config = await fixture();
240
+ config.env = { ...config.env, DSH_BINARY: join(config.cwd, "missing"), PATH: config.cwd };
241
+ await expect(new DshAdapter().createSession(config)).rejects.toThrow();
242
+ expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
243
+ });
244
+
245
+ for (const mode of ["failure", "missing-final", "malformed"]) {
246
+ test(`reports ${mode} as failure even with output or exit zero`, async () => {
247
+ const config = await fixture(mode);
248
+ const session = await new DshAdapter().createSession(config);
249
+ const events: ProviderEvent[] = [];
250
+ session.onEvent((event) => events.push(event));
251
+ const result = await session.waitForCompletion();
252
+ expect(result.isError).toBe(true);
253
+ expect(result.exitCode).toBe(mode === "failure" ? 7 : 1);
254
+ expect(JSON.stringify({ result, events })).not.toContain(config.env!.DEEPSEEK_API_KEY);
255
+ const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
256
+ expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
257
+ });
258
+ }
259
+
260
+ test("abort terminates a running process and removes its patch", async () => {
261
+ const config = await fixture("abort");
262
+ const session = await new DshAdapter().createSession(config);
263
+ await new Promise<void>((resolve) =>
264
+ session.onEvent((event) => {
265
+ if (event.type === "session_init") resolve();
266
+ }),
267
+ );
268
+ await session.abort();
269
+ expect(await session.waitForCompletion()).toMatchObject({
270
+ isError: true,
271
+ failureReason: "dsh session aborted",
272
+ });
273
+ const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
274
+ expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
275
+ });
276
+ });
@@ -85,11 +85,11 @@ describe("nightly report", () => {
85
85
  });
86
86
  expect(report.ok).toBe(true);
87
87
  expect(report.missingLegs).toEqual([]);
88
- expect(report.totalCostUsd).toBeCloseTo(0.0615, 6);
88
+ expect(report.totalCostUsd).toBeCloseTo(0.0738, 6);
89
89
  expect(report.warnings).toEqual([]);
90
90
  const markdown = nightlyMarkdown(report, [], DEFAULT_PROVIDERS);
91
91
  expect(markdown).toContain("## Nightly E2E: PASS");
92
- expect(markdown).toContain("5/5 harness legs passed");
92
+ expect(markdown).toContain("6/6 harness legs passed");
93
93
  expect(markdown).toContain("| claude | claude-model | PASS | 1 |");
94
94
  expect(markdown).not.toContain("### Warnings");
95
95
  });
@@ -159,7 +159,7 @@ describe("nightly report", () => {
159
159
  results: [contractResult(), harnessResult([leg("claude")]), harnessResult([failed])],
160
160
  });
161
161
  expect(report.ok).toBe(false);
162
- expect(report.missingLegs).toEqual(["claude-sdk", "pi", "opencode"]);
162
+ expect(report.missingLegs).toEqual(["claude-sdk", "pi", "opencode", "dsh"]);
163
163
  expect(report.warnings).toContain("pi: no result file from the leg.");
164
164
  expect(report.warnings).toContain("codex: needed 2 attempts.");
165
165
  const markdown = nightlyMarkdown(report, [], DEFAULT_PROVIDERS);
@@ -43,6 +43,7 @@ describe("e2e redaction", () => {
43
43
  const saved = { ...process.env };
44
44
  try {
45
45
  process.env.OPENROUTER_API_KEY = "example-sk-or-v1-known-value-1234";
46
+ process.env.DEEPSEEK_API_KEY = "deepseek-known-value-1234";
46
47
  process.env.CODEX_OAUTH = JSON.stringify({
47
48
  access: "access-token-value-1234",
48
49
  refresh: "refresh-token-value-1234",
@@ -51,6 +52,7 @@ describe("e2e redaction", () => {
51
52
  });
52
53
  const known = knownSecrets(["extra-secret-value"]);
53
54
  expect(known).toContain("example-sk-or-v1-known-value-1234");
55
+ expect(known).toContain("deepseek-known-value-1234");
54
56
  expect(known).toContain("access-token-value-1234");
55
57
  expect(known).toContain("refresh-token-value-1234");
56
58
  expect(known).toContain("extra-secret-value");
@@ -62,6 +62,12 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
62
62
  expect(validateConfigValue("SLACK_THREAD_STEERING_MODE", "now")).toContain("must be one of");
63
63
  });
64
64
 
65
+ test("Slack transport is constrained", () => {
66
+ expect(validateConfigValue("SLACK_MODE", "socket")).toBeNull();
67
+ expect(validateConfigValue("SLACK_MODE", "http")).toBeNull();
68
+ expect(validateConfigValue("SLACK_MODE", "webhook")).toContain("must be one of");
69
+ });
70
+
65
71
  test("Slack reaction shortcode keys accept bare and colon-wrapped names", () => {
66
72
  const keys = [
67
73
  "SLACK_REACTION_ACCEPTED",
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
2
2
  import { closeDb, getDbClient, initDb } from "../be/db";
3
3
  import {
4
4
  deleteExtension,
5
+ ExtensionOwnershipError,
5
6
  getExtensionById,
6
7
  getExtensionFiles,
7
8
  getExtensionVersion,
@@ -67,6 +68,46 @@ describe("extension DB helpers", () => {
67
68
  ).toBe(1);
68
69
  });
69
70
 
71
+ test("concurrent worker installs cannot take ownership of the same name", async () => {
72
+ const bundle = await loadBundleFixture("minimal");
73
+ const results = await Promise.allSettled(
74
+ ["worker-a", "worker-b"].map((agentId) =>
75
+ installExtension({ ...bundle, agentId, ownerOnly: agentId }),
76
+ ),
77
+ );
78
+ expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
79
+ const rejected = results.find((result) => result.status === "rejected");
80
+ expect(rejected?.status === "rejected" && rejected.reason).toBeInstanceOf(
81
+ ExtensionOwnershipError,
82
+ );
83
+ const winner = results.find((result) => result.status === "fulfilled");
84
+ if (winner?.status !== "fulfilled") throw new Error("missing winning install");
85
+ expect(await listExtensionVersions(winner.value.extension.id)).toHaveLength(1);
86
+ });
87
+
88
+ test("worker metadata writes and deletion recheck disabled ownership at the database write", async () => {
89
+ const installed = await installExtension({
90
+ ...(await loadBundleFixture("minimal")),
91
+ agentId: "owner",
92
+ });
93
+ const id = installed.extension.id;
94
+ await expect(
95
+ updateExtensionMeta(id, { priority: 7, ownerOnly: "other" }),
96
+ ).rejects.toBeInstanceOf(ExtensionOwnershipError);
97
+ await expect(deleteExtension(id, "other")).rejects.toBeInstanceOf(ExtensionOwnershipError);
98
+ await setExtensionState(id, { enabled: true, status: "enabled" });
99
+ await expect(
100
+ updateExtensionMeta(id, { priority: 7, ownerOnly: "owner" }),
101
+ ).rejects.toBeInstanceOf(ExtensionOwnershipError);
102
+ await expect(deleteExtension(id, "owner")).rejects.toBeInstanceOf(ExtensionOwnershipError);
103
+ expect(await getExtensionById(id)).toMatchObject({ enabled: true, priority: 100 });
104
+ await setExtensionState(id, { enabled: false, status: "disabled" });
105
+ expect(await updateExtensionMeta(id, { priority: 7, ownerOnly: "owner" })).toMatchObject({
106
+ priority: 7,
107
+ });
108
+ expect(await deleteExtension(id, "owner")).toBe(true);
109
+ });
110
+
70
111
  test("matching bundles dedupe and changed hooks create a complete snapshot", async () => {
71
112
  const bundle = await loadBundleFixture("minimal");
72
113
  const first = await installExtension(bundle);