@desplega.ai/agent-swarm 1.166.0 → 1.168.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 (221) hide show
  1. package/dist/acp-adapter-zhejfecx.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
  13. package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
  14. package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
  15. package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
  18. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  19. package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
  20. package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
  21. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  22. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  23. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  24. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  25. package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
  30. package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
  31. package/dist/cli-a35hzdve.js +33 -0
  32. package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
  33. package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
  34. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
  37. package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
  38. package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
  39. package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
  40. package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
  41. package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
  42. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  43. package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
  44. package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
  45. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  46. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  47. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  48. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  49. package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
  50. package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
  51. package/dist/cli-nkqjj42q.js +104 -0
  52. package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
  53. package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
  54. package/dist/cli-q1vnz8wv.js +96 -0
  55. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  56. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  57. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  58. package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
  59. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  60. package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
  61. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  62. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  63. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  67. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  68. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  69. package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
  70. package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +28 -28
  73. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  74. package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
  75. package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
  76. package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
  82. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  83. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  84. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  85. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  86. package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
  88. package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
  89. package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
  90. package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
  91. package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
  92. package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
  93. package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
  94. package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
  95. package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
  98. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  99. package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
  100. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  101. package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
  102. package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
  104. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  105. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  106. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  107. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  108. package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
  109. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  110. package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
  111. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  112. package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
  113. package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
  119. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  120. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  121. package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
  123. package/openapi.json +72 -1
  124. package/package.json +10 -10
  125. package/src/be/memory/providers/openai-embedding.ts +7 -2
  126. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  127. package/src/be/modelsdev-cache.json +61 -0
  128. package/src/be/script-connections.ts +40 -6
  129. package/src/be/seed-pricing.ts +84 -53
  130. package/src/be/swarm-config-guard.ts +14 -0
  131. package/src/commands/provider-credentials.ts +28 -5
  132. package/src/commands/runner.ts +7 -12
  133. package/src/extensions/catalog.generated.json +17 -0
  134. package/src/heartbeat/heartbeat.ts +91 -19
  135. package/src/hooks/hook.ts +72 -27
  136. package/src/http/mcp.ts +5 -0
  137. package/src/http/onboarding.ts +2 -0
  138. package/src/http/script-connections.ts +35 -0
  139. package/src/providers/acp-targets.ts +10 -5
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/claude-adapter.ts +32 -0
  142. package/src/providers/claude-background-keepalive.ts +100 -0
  143. package/src/providers/claude-sdk-session.ts +6 -0
  144. package/src/providers/codex-adapter.ts +30 -3
  145. package/src/providers/codex-oauth/auth-json.ts +21 -4
  146. package/src/providers/codex-oauth/types.ts +2 -0
  147. package/src/providers/dsh-adapter.ts +11 -2
  148. package/src/providers/grok-adapter.ts +18 -2
  149. package/src/providers/opencode-adapter.ts +21 -5
  150. package/src/providers/pi-mono-extension.ts +56 -25
  151. package/src/providers/pricing-sources.md +2 -0
  152. package/src/scheduler/scheduler.ts +82 -13
  153. package/src/scripts-runtime/executors/registry.ts +1 -1
  154. package/src/server.ts +63 -3
  155. package/src/tests/acp-adapter.test.ts +152 -12
  156. package/src/tests/ci-unit-test-report.test.ts +73 -0
  157. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  158. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  159. package/src/tests/claude-background-keepalive.test.ts +139 -0
  160. package/src/tests/claude-default-route.test.ts +75 -0
  161. package/src/tests/claude-hook-output.test.ts +233 -0
  162. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  163. package/src/tests/codex-adapter.test.ts +166 -26
  164. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  165. package/src/tests/codex-oauth.test.ts +4 -1
  166. package/src/tests/connection-embedded-auth.test.ts +51 -0
  167. package/src/tests/context-window.test.ts +2 -2
  168. package/src/tests/credential-check.test.ts +51 -1
  169. package/src/tests/docs-index-footprint.test.js +3 -3
  170. package/src/tests/dsh-adapter.test.ts +24 -2
  171. package/src/tests/enabled-tools.test.ts +269 -0
  172. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  173. package/src/tests/extensions-catalog.test.ts +1 -0
  174. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  175. package/src/tests/grok-adapter.test.ts +13 -0
  176. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  177. package/src/tests/heartbeat-checklist.test.ts +158 -1
  178. package/src/tests/onboarding.test.ts +44 -0
  179. package/src/tests/opencode-adapter.test.ts +66 -3
  180. package/src/tests/opencode-plugin.test.ts +37 -0
  181. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  182. package/src/tests/openrouter-base-url.test.ts +30 -39
  183. package/src/tests/pi-mono-extension.test.ts +161 -2
  184. package/src/tests/pricing-refresh.test.ts +70 -1
  185. package/src/tests/reasoning-effort.test.ts +3 -1
  186. package/src/tests/schedule-target-type.test.ts +136 -0
  187. package/src/tests/script-connections-http.test.ts +45 -0
  188. package/src/tests/script-connections-mcp.test.ts +41 -0
  189. package/src/tests/script-executor-registry.test.ts +18 -0
  190. package/src/tests/send-task-self-send.test.ts +107 -0
  191. package/src/tests/telemetry-trigger.test.ts +12 -1
  192. package/src/tests/workflow-llm.test.ts +14 -0
  193. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  194. package/src/tools/request-human-input.ts +7 -2
  195. package/src/tools/script-connections/tool.ts +35 -3
  196. package/src/tools/send-task.ts +9 -1
  197. package/src/utils/enabled-tools.ts +27 -0
  198. package/src/utils/internal-ai/complete-structured.ts +11 -3
  199. package/src/utils/openrouter-base-url.ts +23 -0
  200. package/src/workflows/executors/raw-llm.ts +1 -0
  201. package/src/workflows/executors/system-one-decision.ts +2 -0
  202. package/src/workflows/executors/validate.ts +1 -0
  203. package/src/workflows/executors/workflow-llm.ts +9 -2
  204. package/templates/extensions/README.md +1 -0
  205. package/templates/extensions/slack-reply-gate/README.md +98 -0
  206. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  207. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  208. package/templates/skills/agent-browser/SKILL.md +2 -0
  209. package/templates/skills/agent-browser/content.md +2 -0
  210. package/templates/skills/code-quality/content.md +1 -0
  211. package/templates/skills/heartbeat-runbook/content.md +1 -1
  212. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  213. package/templates/skills/swarm-scripts/content.md +10 -0
  214. package/templates/skills/work-on-task/SKILL.md +4 -0
  215. package/templates/skills/work-on-task/content.md +4 -0
  216. package/templates/skills/workflow-iterate/content.md +9 -0
  217. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  218. package/dist/cli-3dpj7atn.js +0 -11
  219. package/dist/cli-gvcgmx69.js +0 -183
  220. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  221. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * Covers the shared resolver (`src/utils/openrouter-base-url.ts`), the pi
5
5
  * models.json override writer (`ensureOpenRouterModelsOverride`) including
6
- * its composition through pi-coding-agent's ModelRuntime, and the opencode
7
- * per-task config injection (`applyOpenRouterBaseUrlOverride`).
6
+ * its composition through pi-coding-agent's ModelRuntime, and attribution host gating.
8
7
  */
9
8
 
10
9
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
@@ -12,10 +11,12 @@ import { mkdtempSync, rmSync } from "node:fs";
12
11
  import { tmpdir } from "node:os";
13
12
  import { join } from "node:path";
14
13
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
15
- import type { Config } from "@opencode-ai/sdk";
16
- import { applyOpenRouterBaseUrlOverride } from "../providers/opencode-adapter";
17
14
  import { ensureOpenRouterModelsOverride } from "../providers/pi-mono-adapter";
18
- import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
15
+ import {
16
+ DEFAULT_OPENROUTER_BASE_URL,
17
+ getOpenRouterAttributionHeaders,
18
+ getOpenRouterBaseUrl,
19
+ } from "../utils/openrouter-base-url";
19
20
 
20
21
  const GATEWAY = "https://control-plane.example/proxy/v1";
21
22
 
@@ -220,40 +221,30 @@ describe("ensureOpenRouterModelsOverride (pi)", () => {
220
221
  });
221
222
  });
222
223
 
223
- describe("applyOpenRouterBaseUrlOverride (opencode)", () => {
224
- test("no-op when OPENROUTER_BASE_URL is unset", () => {
225
- const config: Config = { model: "openrouter/google/gemini-3-flash-preview" };
226
- applyOpenRouterBaseUrlOverride(config, {});
227
- expect(config.provider).toBeUndefined();
228
- });
229
-
230
- test("sets provider.openrouter.options.baseURL when the env is set", () => {
231
- const config: Config = { model: "openrouter/google/gemini-3-flash-preview" };
232
- applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
233
- expect(config.provider?.openrouter?.options?.baseURL).toBe(GATEWAY);
234
- });
235
-
236
- test("preserves existing provider config (e.g. reasoning model options)", () => {
237
- const config: Config = {
238
- model: "openrouter/google/gemini-3-flash-preview",
239
- provider: {
240
- openrouter: {
241
- models: {
242
- "google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
243
- },
244
- options: { apiKey: "sk-keep" },
245
- },
246
- anthropic: { options: { apiKey: "sk-ant" } },
247
- },
248
- };
249
- applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
250
- expect(config.provider?.openrouter?.options).toEqual({
251
- apiKey: "sk-keep",
252
- baseURL: GATEWAY,
253
- });
254
- expect(config.provider?.openrouter?.models).toEqual({
255
- "google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
224
+ describe("getOpenRouterAttributionHeaders", () => {
225
+ test.each([
226
+ "https://openrouter.ai/api/v1",
227
+ "https://api.openrouter.ai/v1",
228
+ ])("attributes direct OpenRouter host %s", (url) => {
229
+ expect(getOpenRouterAttributionHeaders(url, {})).toEqual({
230
+ "HTTP-Referer": "https://agent-swarm.dev",
231
+ "X-OpenRouter-Title": "Agent Swarm",
232
+ "X-OpenRouter-Categories": "personal-agent,cloud-agent",
256
233
  });
257
- expect(config.provider?.anthropic).toEqual({ options: { apiKey: "sk-ant" } });
234
+ });
235
+ test.each([
236
+ "http://localhost:1234/v1",
237
+ "https://evil-openrouter.ai",
238
+ "https://openrouter.ai.evil.com",
239
+ "invalid",
240
+ ])("does not attribute gateway, lookalike or invalid URL %s", (url) => {
241
+ expect(getOpenRouterAttributionHeaders(url, {})).toEqual({});
242
+ });
243
+ test.each(["false", "FALSE", "FaLsE", "0"])("honors opt-out %s", (value) => {
244
+ expect(
245
+ getOpenRouterAttributionHeaders("https://openrouter.ai", {
246
+ OPENROUTER_APP_ATTRIBUTION: value,
247
+ }),
248
+ ).toEqual({});
258
249
  });
259
250
  });
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts`.
2
+ * Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts`,
3
+ * plus the extension's advisory nudges (delivered with `pi.sendMessage`).
3
4
  *
4
5
  * Plan: thoughts/taras/plans/2026-05-10-fix-session-summarization-workers.md
5
6
  * → Phase 1 § "Test coverage"
@@ -19,8 +20,14 @@
19
20
  */
20
21
 
21
22
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
23
+ import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
24
+ import { clearToolHistory } from "../hooks/tool-loop-detection";
22
25
  import type { SummarizeSessionForPiDeps, SwarmHooksConfig } from "../providers/pi-mono-extension";
23
- import { summarizeSessionForPi } from "../providers/pi-mono-extension";
26
+ import {
27
+ createSwarmHooksExtension,
28
+ SWARM_NUDGE_CUSTOM_TYPE,
29
+ summarizeSessionForPi,
30
+ } from "../providers/pi-mono-extension";
24
31
 
25
32
  // ── helpers ───────────────────────────────────────────────────────────────────
26
33
 
@@ -345,3 +352,155 @@ describe("summarizeSessionForPi", () => {
345
352
  expect(consoleErrors.length).toBe(0);
346
353
  });
347
354
  });
355
+
356
+ describe("pi provider header attribution", () => {
357
+ test("uses request model URL and task env, replacing pi attribution", async () => {
358
+ let handler:
359
+ | ((event: { headers: Record<string, string> }, ctx: { model: { baseUrl: string } }) => void)
360
+ | undefined;
361
+ const pi = {
362
+ on: (event: string, callback: unknown) => {
363
+ if (event === "before_provider_headers") handler = callback as NonNullable<typeof handler>;
364
+ },
365
+ } as unknown as ExtensionAPI;
366
+ const config = { ...makeConfig(), env: {} as Record<string, string | undefined> };
367
+ await createSwarmHooksExtension(config)(pi);
368
+ expect(handler).toBeDefined();
369
+ const headers = {
370
+ "HTTP-Referer": "https://pi.dev",
371
+ "X-OpenRouter-Title": "pi",
372
+ "X-Custom": "keep",
373
+ };
374
+ handler!({ headers }, { model: { baseUrl: "https://openrouter.ai/api" } });
375
+ expect(headers).toEqual({
376
+ "HTTP-Referer": "https://agent-swarm.dev",
377
+ "X-OpenRouter-Title": "Agent Swarm",
378
+ "X-OpenRouter-Categories": "personal-agent,cloud-agent",
379
+ "X-Custom": "keep",
380
+ });
381
+ config.env.OPENROUTER_APP_ATTRIBUTION = "false";
382
+ const optedOut = { "X-Custom": "keep" };
383
+ handler!({ headers: optedOut }, { model: { baseUrl: "https://openrouter.ai/api" } });
384
+ expect(optedOut).toEqual({ "X-Custom": "keep" });
385
+ config.env = {};
386
+ const gateway = {};
387
+ handler!({ headers: gateway }, { model: { baseUrl: "http://localhost:1234/v1" } });
388
+ expect(gateway).toEqual({});
389
+ });
390
+ });
391
+
392
+ // ── advisory nudges reach the model via pi.sendMessage ───────────────────────
393
+
394
+ type Handler = (event: unknown, ctx: unknown) => Promise<unknown> | unknown;
395
+ type SentMessage = { customType: string; content: Array<{ type: string; text: string }> };
396
+
397
+ /** Minimal stand-in for pi's ExtensionAPI: records handlers and sent messages. */
398
+ function loadExtension(config: SwarmHooksConfig) {
399
+ const handlers = new Map<string, Handler>();
400
+ const sent: SentMessage[] = [];
401
+ const pi = {
402
+ on: (event: string, handler: Handler) => {
403
+ handlers.set(event, handler);
404
+ },
405
+ sendMessage: (message: SentMessage) => {
406
+ sent.push(message);
407
+ },
408
+ registerMcpServer: () => {},
409
+ };
410
+ createSwarmHooksExtension(config)(pi as unknown as Parameters<ExtensionFactory>[0]);
411
+ const ctx = { getContextUsage: () => undefined };
412
+ const fire = (event: string, payload: unknown = {}) => {
413
+ const handler = handlers.get(event);
414
+ if (!handler) throw new Error(`no handler for ${event}`);
415
+ return handler({ type: event, ...(payload as object) }, ctx);
416
+ };
417
+ const texts = () =>
418
+ sent.map((m) => {
419
+ expect(m.customType).toBe(SWARM_NUDGE_CUSTOM_TYPE);
420
+ return m.content.map((c) => c.text).join("");
421
+ });
422
+ return { fire, texts };
423
+ }
424
+
425
+ function jsonResponse(body: unknown): FetchHandlerResp {
426
+ return { ok: true, status: 200, text: async () => "", json: async () => body };
427
+ }
428
+
429
+ describe("createSwarmHooksExtension nudges", () => {
430
+ test("loop warning on tool_call is sent to the model", async () => {
431
+ const config = { ...makeConfig(), taskId: `task-pi-loop-${crypto.randomUUID()}` };
432
+ const { fire, texts } = loadExtension(config);
433
+ try {
434
+ for (let i = 0; i < 8; i++) {
435
+ const result = await fire("tool_call", { toolName: "bash", input: { command: "ls" } });
436
+ expect(result).toBeUndefined();
437
+ }
438
+ const warnings = texts().filter((t) => t.startsWith("Warning: "));
439
+ expect(warnings.length).toBe(1);
440
+ expect(warnings[0]).toContain("bash");
441
+ } finally {
442
+ await clearToolHistory(config.taskId);
443
+ }
444
+ });
445
+
446
+ test("lead send-task reminder on tool_result is sent to the model", async () => {
447
+ const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
448
+ await fire("tool_result", { toolName: "mcp__agent-swarm__send-task", input: {} });
449
+ expect(texts()).toEqual([
450
+ "Task sent successfully. Monitor progress using the get-task-details tool periodically.",
451
+ ]);
452
+ });
453
+
454
+ test("lead concurrent context on session_start is sent to the model", async () => {
455
+ fetchHandler = async (url) =>
456
+ url.includes("/api/concurrent-context")
457
+ ? jsonResponse({
458
+ processingInboxMessages: [
459
+ { content: "deploy the thing", source: "slack", createdAt: "now" },
460
+ ],
461
+ recentTaskDelegations: [],
462
+ activeSwarmTasks: [],
463
+ })
464
+ : jsonResponse({});
465
+ const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
466
+ await fire("session_start");
467
+ expect(texts().length).toBe(1);
468
+ expect(texts()[0]).toContain("=== CONCURRENT SESSION AWARENESS ===");
469
+ expect(texts()[0]).toContain("deploy the thing");
470
+ });
471
+
472
+ test("goal reminder is sent after compaction, not before every LLM call", async () => {
473
+ fetchHandler = async (url) =>
474
+ url.endsWith("/api/tasks/task-pi-1")
475
+ ? jsonResponse({ id: "task-pi-1", task: "ship the fix", progress: "tests green" })
476
+ : jsonResponse({});
477
+ const { fire, texts } = loadExtension(makeConfig());
478
+
479
+ await fire("context", { messages: [] });
480
+ expect(texts()).toEqual([]);
481
+
482
+ await fire("session_compact");
483
+ expect(texts()).toEqual([
484
+ [
485
+ "=== GOAL REMINDER (injected before context compaction) ===",
486
+ "Task ID: task-pi-1",
487
+ "Task: ship the fix",
488
+ "Current Progress: tests green",
489
+ "=== Continue working on this task after compaction ===",
490
+ ].join("\n"),
491
+ ]);
492
+ });
493
+
494
+ test("cancelled task on input skips the turn and leaves the nudge in the session", async () => {
495
+ fetchHandler = async (url) =>
496
+ url.includes("/cancelled-tasks")
497
+ ? jsonResponse({ cancelled: [{ id: "task-pi-1", failureReason: "lead stopped it" }] })
498
+ : jsonResponse({});
499
+ const { fire, texts } = loadExtension(makeConfig());
500
+ const result = await fire("input", { text: "go", source: "rpc" });
501
+ expect(result).toEqual({ action: "handled" });
502
+ expect(texts()).toEqual([
503
+ "🛑 TASK CANCELLED: lead stopped it. Stop working and use store-progress to acknowledge.",
504
+ ]);
505
+ });
506
+ });
@@ -2,8 +2,9 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, getActivePricingRow, getDbClient, getLogsByEventType, initDb } from "../be/db";
4
4
  import { getModelsCatalog, resetModelsCatalogForTests } from "../be/models-catalog";
5
- import type { ModelsDevCache } from "../be/modelsdev-cache";
5
+ import { loadModelsDevCache, type ModelsDevCache } from "../be/modelsdev-cache";
6
6
  import { refreshPricingFromModelsDev } from "../be/pricing-refresh";
7
+ import { buildModelsDevSeedRows, buildPricingSeedRows } from "../be/seed-pricing";
7
8
 
8
9
  const TEST_DB_PATH = "./test-pricing-refresh.sqlite";
9
10
 
@@ -201,6 +202,74 @@ describe("models.dev runtime pricing refresh", () => {
201
202
  expect(rows.map((row) => row.effective_from)).toEqual([2_000, 3_000]);
202
203
  });
203
204
 
205
+ test("refreshes when dated snapshots collapse onto one amp key at different prices", async () => {
206
+ // models.dev's live gpt-4o family. Amp strips the dated suffix, so all four
207
+ // land on amp|gpt-4o; the 2024-05-13 snapshot is priced higher. Its key
208
+ // comes first, as upstream serves it, so a first-wins builder would pick it.
209
+ const cache: ModelsDevCache = {
210
+ openai: {
211
+ models: {
212
+ "gpt-4o-2024-05-13": { cost: { input: 5, output: 15 } },
213
+ "gpt-4o": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
214
+ "gpt-4o-2024-08-06": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
215
+ "gpt-4o-2024-11-20": { cost: { input: 2.5, output: 10, cache_read: 1.25 } },
216
+ },
217
+ },
218
+ };
219
+
220
+ const result = await refreshPricingFromModelsDev({
221
+ now: 1_000,
222
+ fetchImpl: async () => responseFor(cache, '"etag-collapse"'),
223
+ });
224
+
225
+ expect(result.status).toBe("refreshed");
226
+ expect(await getLogsByEventType("pricing.refresh.failed")).toHaveLength(0);
227
+ // The undated alias wins every token class.
228
+ expect((await getActivePricingRow("amp", "gpt-4o", "input", 1_000))?.pricePerMillionUsd).toBe(
229
+ 2.5,
230
+ );
231
+ expect((await getActivePricingRow("amp", "gpt-4o", "output", 1_000))?.pricePerMillionUsd).toBe(
232
+ 10,
233
+ );
234
+ expect(
235
+ (await getActivePricingRow("amp", "gpt-4o", "cached_input", 1_000))?.pricePerMillionUsd,
236
+ ).toBe(1.25);
237
+ // Providers that keep the dated id still price each snapshot on its own.
238
+ expect(
239
+ (await getActivePricingRow("codex", "gpt-4o-2024-05-13", "input", 1_000))?.pricePerMillionUsd,
240
+ ).toBe(5);
241
+ });
242
+
243
+ test("picks the newest dated snapshot when no undated alias exists", () => {
244
+ const rows = buildModelsDevSeedRows({
245
+ openai: {
246
+ models: {
247
+ "gpt-x-2025-08-07": { cost: { input: 2, output: 8 } },
248
+ "gpt-x-2025-01-01": { cost: { input: 1, output: 4 } },
249
+ },
250
+ },
251
+ }).filter((row) => row.provider === "amp");
252
+
253
+ expect(rows).toEqual([
254
+ { provider: "amp", model: "gpt-x", tokenClass: "input", pricePerMillionUsd: 2 },
255
+ { provider: "amp", model: "gpt-x", tokenClass: "output", pricePerMillionUsd: 8 },
256
+ ]);
257
+ });
258
+
259
+ test("the vendored models.dev snapshot yields one row per (provider, model, tokenClass)", () => {
260
+ const cache = loadModelsDevCache();
261
+ expect(cache).not.toBeNull();
262
+ const rows = buildPricingSeedRows(cache);
263
+ const seen = new Set<string>();
264
+ const duplicates: string[] = [];
265
+ for (const row of rows) {
266
+ const key = `${row.provider}|${row.model}|${row.tokenClass}`;
267
+ if (seen.has(key)) duplicates.push(key);
268
+ seen.add(key);
269
+ }
270
+ expect(duplicates).toEqual([]);
271
+ });
272
+
204
273
  test("writes scrubbed audit log entries for successful refreshes", async () => {
205
274
  await refreshPricingFromModelsDev({
206
275
  now: 1_000,
@@ -305,7 +305,9 @@ describe("Claude CLI shortnames (the tier defaults) take effort like the model t
305
305
  expect(reasoningCapability("claude", "sonnet")).toEqual(
306
306
  reasoningCapability("claude", "claude-sonnet-5-5"),
307
307
  );
308
- expect(reasoningCapability("claude", "haiku").levels).toContain("off");
308
+ expect(reasoningCapability("claude", "haiku")).toEqual(
309
+ reasoningCapability("claude", "claude-haiku-5-5"),
310
+ );
309
311
  expect(reasoningCapability("claude", "fable").supported).toBe(true);
310
312
  });
311
313
 
@@ -33,6 +33,7 @@ import {
33
33
  preflightAutomation,
34
34
  renderAutomationTokens,
35
35
  } from "../be/automation-preflight";
36
+ import * as dbModule from "../be/db";
36
37
  import {
37
38
  closeDb,
38
39
  createAgent,
@@ -58,6 +59,7 @@ import {
58
59
  startScheduler,
59
60
  stopScheduler,
60
61
  } from "../scheduler/scheduler";
62
+ import * as scriptLoader from "../scripts-runtime/loader";
61
63
  import type { Workflow, WorkflowDefinition } from "../types";
62
64
  import { InProcessEventBus } from "../workflows/event-bus";
63
65
  import { BaseExecutor, type ExecutorResult } from "../workflows/executors/base";
@@ -148,6 +150,38 @@ async function saveGlobalScript(name: string, source: string) {
148
150
  });
149
151
  }
150
152
 
153
+ type ScheduleScriptRunRow = {
154
+ agentId: string;
155
+ scriptName: string;
156
+ source: string;
157
+ kind: string;
158
+ status: string;
159
+ output: string | null;
160
+ error: string | null;
161
+ finishedAt: string | null;
162
+ };
163
+
164
+ async function scriptRunsFor(scriptName: string): Promise<ScheduleScriptRunRow[]> {
165
+ return getDbClient().query<ScheduleScriptRunRow>(
166
+ "SELECT agentId, scriptName, source, kind, status, output, error, finishedAt FROM script_runs WHERE scriptName = ?",
167
+ [scriptName],
168
+ );
169
+ }
170
+
171
+ type RunScriptOutput = Awaited<ReturnType<typeof scriptLoader.runScript>>;
172
+
173
+ function fakeRunOutput(overrides: Partial<RunScriptOutput>): RunScriptOutput {
174
+ return {
175
+ result: undefined,
176
+ stdout: "",
177
+ stderr: "",
178
+ exitCode: 0,
179
+ durationMs: 1,
180
+ truncated: { stdout: false, stderr: false },
181
+ ...overrides,
182
+ } as RunScriptOutput;
183
+ }
184
+
151
185
  beforeAll(async () => {
152
186
  savedEnv = { ...process.env };
153
187
  await removeDbFiles();
@@ -718,6 +752,11 @@ describe("dispatchScheduleTarget — script target", () => {
718
752
  const result = await dispatchScheduleTarget(schedule);
719
753
  expect(result.triggeredWorkflows).toBe(false);
720
754
  expect(result.task).toBeUndefined();
755
+
756
+ const runs = await scriptRunsFor("schedule-target-type-echo");
757
+ expect(runs).toHaveLength(1);
758
+ expect(runs[0]?.status).toBe("completed");
759
+ expect(JSON.parse(runs[0]?.output ?? "null")).toEqual({ received: { hello: "world" } });
721
760
  }, 15_000);
722
761
 
723
762
  skip("scheduled script runs receive ctx.api and ctx.mcp connections", async () => {
@@ -794,9 +833,106 @@ describe("dispatchScheduleTarget — script target", () => {
794
833
  });
795
834
 
796
835
  await expect(dispatchScheduleTarget(schedule)).rejects.toThrow();
836
+
837
+ const runs = await scriptRunsFor("schedule-target-type-throws");
838
+ expect(runs).toHaveLength(1);
839
+ expect(runs[0]?.status).toBe("failed");
840
+ expect(runs[0]?.error).toContain("boom");
797
841
  }, 15_000);
798
842
  });
799
843
 
844
+ describe("dispatchScheduleTarget — script target records a script_runs row", () => {
845
+ async function scheduleFor(scriptName: string) {
846
+ await saveGlobalScript(scriptName, `export default async () => ({ ok: true });`);
847
+ return createScheduledTask({
848
+ name: `record-${scriptName}`,
849
+ intervalMs: 60_000,
850
+ targetType: "script",
851
+ scriptName,
852
+ createdByAgentId: agentId,
853
+ });
854
+ }
855
+
856
+ test("a completed scheduled run writes a completed inline row that points at the script version", async () => {
857
+ const scriptName = `record-ok-${crypto.randomUUID()}`;
858
+ const schedule = await scheduleFor(scriptName);
859
+ const run = spyOn(scriptLoader, "runScript").mockResolvedValue(
860
+ fakeRunOutput({ result: { polled: 3 } }),
861
+ );
862
+ try {
863
+ await dispatchScheduleTarget(schedule);
864
+ } finally {
865
+ run.mockRestore();
866
+ }
867
+
868
+ const runs = await scriptRunsFor(scriptName);
869
+ expect(runs).toHaveLength(1);
870
+ const row = runs[0];
871
+ expect(row?.kind).toBe("inline");
872
+ expect(row?.status).toBe("completed");
873
+ expect(row?.agentId).toBe(agentId);
874
+ expect(row?.finishedAt).toBeTruthy();
875
+ expect(row?.error).toBeNull();
876
+ expect(JSON.parse(row?.output ?? "null")).toEqual({ polled: 3 });
877
+ expect(row?.source).toContain(`'${scriptName}' v1`);
878
+ expect(row?.source).toContain(`by schedule '${schedule.name}'`);
879
+ expect(row?.source).not.toContain("export default");
880
+ });
881
+
882
+ test("a failed scheduled run writes a failed row and still throws", async () => {
883
+ const scriptName = `record-fail-${crypto.randomUUID()}`;
884
+ const schedule = await scheduleFor(scriptName);
885
+ const run = spyOn(scriptLoader, "runScript").mockResolvedValue(
886
+ fakeRunOutput({ exitCode: 1, stderr: "upstream 503" }),
887
+ );
888
+ try {
889
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("upstream 503");
890
+ } finally {
891
+ run.mockRestore();
892
+ }
893
+
894
+ const runs = await scriptRunsFor(scriptName);
895
+ expect(runs).toHaveLength(1);
896
+ expect(runs[0]?.status).toBe("failed");
897
+ expect(runs[0]?.error).toBe("upstream 503");
898
+ });
899
+
900
+ test("a runtime that throws before returning writes a failed row and rethrows", async () => {
901
+ const scriptName = `record-throw-${crypto.randomUUID()}`;
902
+ const schedule = await scheduleFor(scriptName);
903
+ const run = spyOn(scriptLoader, "runScript").mockRejectedValue(new Error("spawn failed"));
904
+ try {
905
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("spawn failed");
906
+ } finally {
907
+ run.mockRestore();
908
+ }
909
+
910
+ const runs = await scriptRunsFor(scriptName);
911
+ expect(runs.map((r) => [r.status, r.error])).toEqual([["failed", "spawn failed"]]);
912
+ });
913
+
914
+ test("a persistence error changes neither outcome", async () => {
915
+ const scriptName = `record-dberr-${crypto.randomUUID()}`;
916
+ const schedule = await scheduleFor(scriptName);
917
+ const record = spyOn(dbModule, "recordInlineScriptRun").mockRejectedValue(
918
+ new Error("database is locked"),
919
+ );
920
+ const run = spyOn(scriptLoader, "runScript")
921
+ .mockResolvedValueOnce(fakeRunOutput({ result: { ok: true } }))
922
+ .mockResolvedValueOnce(fakeRunOutput({ exitCode: 1, stderr: "real failure" }));
923
+ try {
924
+ const result = await dispatchScheduleTarget(schedule);
925
+ expect(result.triggeredWorkflows).toBe(false);
926
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("real failure");
927
+ expect(record).toHaveBeenCalledTimes(2);
928
+ } finally {
929
+ run.mockRestore();
930
+ record.mockRestore();
931
+ }
932
+ expect(await scriptRunsFor(scriptName)).toHaveLength(0);
933
+ });
934
+ });
935
+
800
936
  // ─── HTTP route cross-field validation ────────────────────────────────────────
801
937
 
802
938
  function makeHttpReq(
@@ -254,6 +254,51 @@ describe("/api/script-connections HTTP", () => {
254
254
  expect(await res.json()).toEqual({ error: "Only the lead can manage script connections." });
255
255
  });
256
256
 
257
+ test("DELETE removes a connection, its managed binding, and its inline secret", async () => {
258
+ const created = await dispatch("/api/script-connections", {
259
+ method: "POST",
260
+ agentId: leadAgentId,
261
+ body: {
262
+ kind: "openapi",
263
+ slug: "deleteHttpVendor",
264
+ baseUrl: "https://api.vendor.test",
265
+ allowedHosts: ["api.vendor.test"],
266
+ auth: { type: "bearer", secret: "example-http-delete-tok" },
267
+ openapiSpecJson: inlineOpenApiSpec(),
268
+ },
269
+ });
270
+ expect(created.status).toBe(200);
271
+ const { connection } = (await created.json()) as { connection: { id: string } };
272
+
273
+ const forbidden = await dispatch(`/api/script-connections/${connection.id}`, {
274
+ method: "DELETE",
275
+ agentId: workerAgentId,
276
+ });
277
+ expect(forbidden.status).toBe(403);
278
+
279
+ const res = await dispatch(`/api/script-connections/${connection.id}`, {
280
+ method: "DELETE",
281
+ agentId: leadAgentId,
282
+ });
283
+ expect(res.status).toBe(200);
284
+ expect(await res.json()).toEqual({ deleted: true, id: connection.id });
285
+
286
+ const counts = await getDbClient().get<{ conns: number; bindings: number; secrets: number }>(
287
+ `SELECT
288
+ (SELECT COUNT(*) FROM script_connections WHERE id = ?) AS conns,
289
+ (SELECT COUNT(*) FROM script_credential_bindings WHERE managed_by_connection_id = ?) AS bindings,
290
+ (SELECT COUNT(*) FROM swarm_config WHERE key = 'connection.deleteHttpVendor.secret') AS secrets`,
291
+ [connection.id, connection.id],
292
+ );
293
+ expect(counts).toEqual({ conns: 0, bindings: 0, secrets: 0 });
294
+
295
+ const missing = await dispatch(`/api/script-connections/${connection.id}`, {
296
+ method: "DELETE",
297
+ agentId: leadAgentId,
298
+ });
299
+ expect(missing.status).toBe(404);
300
+ });
301
+
257
302
  test("list returns connections without secrets", async () => {
258
303
  await upsertSwarmConfig({
259
304
  scope: "global",
@@ -907,3 +907,44 @@ describe("script MCP connections", () => {
907
907
  }
908
908
  });
909
909
  });
910
+
911
+ describe("script-connections MCP delete", () => {
912
+ test("lead deletes a connection and its inline secret; unknown id and non-lead fail", async () => {
913
+ const lead = await createAgent({ name: "mcp-delete-lead", isLead: true, status: "idle" });
914
+ const worker = await createAgent({ name: "mcp-delete-worker", isLead: false, status: "idle" });
915
+ const connection = await upsertScriptConnection({
916
+ slug: "mcpDeleteVendor",
917
+ kind: "graphql",
918
+ baseUrl: "https://api.vendor.test/graphql",
919
+ allowedHosts: ["api.vendor.test"],
920
+ auth: { type: "bearer", secret: "example-mcp-delete-tok" },
921
+ });
922
+
923
+ const denied = (await scriptConnectionsTool().handler(
924
+ { action: "delete", id: connection.id },
925
+ meta(worker.id),
926
+ )) as ToolResult;
927
+ expect(denied.structuredContent.success).toBe(false);
928
+ expect(await getScriptConnectionById(connection.id)).not.toBeNull();
929
+
930
+ const result = (await scriptConnectionsTool().handler(
931
+ { action: "delete", id: connection.id },
932
+ meta(lead.id),
933
+ )) as ToolResult;
934
+ expect(result.structuredContent.success).toBe(true);
935
+ expect(result.structuredContent.connections.map((c) => c.id)).not.toContain(connection.id);
936
+ expect(await getScriptConnectionById(connection.id)).toBeNull();
937
+ const secretRow = await getDbClient().get<{ key: string }>(
938
+ "SELECT key FROM swarm_config WHERE key = ?",
939
+ ["connection.mcpDeleteVendor.secret"],
940
+ );
941
+ expect(secretRow).toBeNull();
942
+
943
+ const again = (await scriptConnectionsTool().handler(
944
+ { action: "delete", id: connection.id },
945
+ meta(lead.id),
946
+ )) as ToolResult;
947
+ expect(again.structuredContent.success).toBe(false);
948
+ expect(again.structuredContent.message).toContain("not found");
949
+ });
950
+ });
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { validateConfigValue } from "../be/swarm-config-guard";
2
3
  import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
3
4
  import { QuickJSScriptExecutor } from "../scripts-runtime/executors/quickjs";
4
5
  import { getScriptExecutor } from "../scripts-runtime/executors/registry";
@@ -20,3 +21,20 @@ describe("getScriptExecutor", () => {
20
21
  expect(() => getScriptExecutor("e2b")).toThrow("Available: native, quickjs");
21
22
  });
22
23
  });
24
+
25
+ describe("SCRIPT_EXECUTOR configuration", () => {
26
+ test("every accepted value resolves to its executor", () => {
27
+ for (const value of ["native", "quickjs", " native ", " quickjs "]) {
28
+ expect(validateConfigValue("SCRIPT_EXECUTOR", value)).toBeNull();
29
+ expect(getScriptExecutor(value).name).toBe(value.trim());
30
+ }
31
+ });
32
+
33
+ test("rejects unsupported names, blank values and non-string values", () => {
34
+ for (const value of ["e2b", "Native", "QUICKJS", "", " ", null, undefined, 1, true]) {
35
+ expect(validateConfigValue("SCRIPT_EXECUTOR", value)).toBe(
36
+ "Invalid SCRIPT_EXECUTOR value (must be one of: native, quickjs)",
37
+ );
38
+ }
39
+ });
40
+ });