@desplega.ai/agent-swarm 2.6.0 → 2.7.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 (213) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-ma1js5n7.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-1vhmtvg9.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-sxqqvmdk.js} +3 -3
  5. package/dist/{app-k8a5cgpp.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-a16433gr.js → assistant-yx1p6g82.js} +20 -20
  7. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-8mawd4gw.js} +3 -3
  10. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-wqt4hfkq.js} +7 -7
  12. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-rqv98sea.js} +2 -2
  14. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-f29f3b2x.js} +7 -7
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-k21gx5ht.js → cli-1hhbzkbb.js} +6 -6
  17. package/dist/{cli-v49fkghp.js → cli-1m9x8dr5.js} +1 -1
  18. package/dist/{cli-24j1jeeb.js → cli-2pvzmas1.js} +3 -3
  19. package/dist/{cli-w2656ka1.js → cli-2y7v133v.js} +7 -7
  20. package/dist/{cli-g8ka5j4j.js → cli-48gxjmcz.js} +94 -36
  21. package/dist/{cli-2sjhpsns.js → cli-54sj64fc.js} +1 -1
  22. package/dist/{cli-aj8djyqt.js → cli-60sty27p.js} +2 -2
  23. package/dist/{cli-4hxrf0p4.js → cli-6a9qfa8b.js} +308 -47
  24. package/dist/{cli-n7asmce6.js → cli-6mgdba42.js} +3 -3
  25. package/dist/{cli-gf95q3bm.js → cli-6srv653c.js} +3 -3
  26. package/dist/{cli-9abdgn5t.js → cli-86ehn4s5.js} +3 -3
  27. package/dist/{cli-bqztfyve.js → cli-8mbc9za9.js} +1 -1
  28. package/dist/{cli-a2rr0fmg.js → cli-8wmj18cz.js} +1 -1
  29. package/dist/{cli-c2habvsf.js → cli-9gsm2rzp.js} +8 -8
  30. package/dist/{cli-4rf0f5vg.js → cli-9pshx6rd.js} +4 -2
  31. package/dist/{cli-abkm71vr.js → cli-a6ds2epe.js} +1 -1
  32. package/dist/{cli-esakngh8.js → cli-a89zdqz9.js} +1 -1
  33. package/dist/{cli-3ndmpxxv.js → cli-ay40dnj3.js} +2 -2
  34. package/dist/{cli-kf4jree9.js → cli-aypd09nf.js} +1 -1
  35. package/dist/{cli-6y51ryva.js → cli-b3yzyc57.js} +1 -1
  36. package/dist/{cli-ag2yrnt5.js → cli-c668vvcf.js} +2 -2
  37. package/dist/{cli-5sckpqcx.js → cli-d20qvpr2.js} +1 -1
  38. package/dist/{cli-a34pr3w1.js → cli-e1595bgx.js} +2 -2
  39. package/dist/{cli-m70bajzr.js → cli-f12mr20z.js} +1 -1
  40. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  41. package/dist/{cli-mvjz5an3.js → cli-f4dbh7a5.js} +7 -7
  42. package/dist/{cli-az25fn5f.js → cli-gsfk58nw.js} +2 -2
  43. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  44. package/dist/{cli-cz37jqss.js → cli-h5jeg46p.js} +2 -2
  45. package/dist/{cli-emhjcj2y.js → cli-he9gbdx0.js} +60 -10
  46. package/dist/{cli-3fwqy71j.js → cli-j0ppywdr.js} +38 -31
  47. package/dist/{cli-5rj83bam.js → cli-j3y0x2n4.js} +34 -33
  48. package/dist/{cli-3gfvmzht.js → cli-j9bpmykh.js} +73 -25
  49. package/dist/{cli-m5a9m0ak.js → cli-kv7sbc2c.js} +3 -3
  50. package/dist/{cli-g20jjwt4.js → cli-m30k9777.js} +12 -12
  51. package/dist/{cli-683y0b3a.js → cli-m5e802r7.js} +2 -2
  52. package/dist/{cli-nn6vx2mq.js → cli-ptfq202x.js} +5 -3
  53. package/dist/{cli-d2dpf95n.js → cli-qj442fws.js} +1 -1
  54. package/dist/{cli-03gg9pd1.js → cli-rd5m70gj.js} +4 -4
  55. package/dist/{cli-gnsb1zxa.js → cli-s09j9vp5.js} +1 -1
  56. package/dist/{cli-zqne1qcj.js → cli-s0mgx214.js} +2 -2
  57. package/dist/{cli-sqma6vw1.js → cli-sgqm2582.js} +5 -5
  58. package/dist/{cli-nq677csy.js → cli-sjctjr48.js} +1 -1
  59. package/dist/{cli-3aqd4jaa.js → cli-twctnkng.js} +3 -3
  60. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  61. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  62. package/dist/{cli-art7c1zh.js → cli-wqr7cea4.js} +4 -4
  63. package/dist/{cli-hfeb68z4.js → cli-x37r7qwf.js} +11 -3
  64. package/dist/{cli-ffvsj2qw.js → cli-yd2nreqw.js} +2 -2
  65. package/dist/{cli-ca6banwz.js → cli-ymjpkxgn.js} +6 -6
  66. package/dist/{cli-mtnj4fwa.js → cli-yxbsmx56.js} +5 -5
  67. package/dist/cli.js +18 -16
  68. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-zk59x2zh.js} +5 -5
  69. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  70. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-kh6s4j3n.js} +5 -5
  71. package/dist/{commands-jqmgk54b.js → commands-68et1qv2.js} +2 -2
  72. package/dist/{controller-z3jq589f.js → controller-fyrbnzg4.js} +2 -2
  73. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-nep9h2xv.js} +19 -19
  74. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-yss3pg7t.js} +1 -1
  75. package/dist/{db-w36wwb5g.js → db-28gjyvxj.js} +2 -2
  76. package/dist/{db-ppv2tze7.js → db-8ftwvzba.js} +3 -3
  77. package/dist/{dispatcher-pv5h77vf.js → dispatcher-hayexmkr.js} +17 -17
  78. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-46wawff9.js} +1 -1
  79. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  80. package/dist/{execute-jev839y1.js → execute-cjrfxfnm.js} +1 -1
  81. package/dist/{external-channel-y6nkzhnn.js → external-channel-xbmxcg09.js} +2 -2
  82. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-ag4wks2a.js} +7 -7
  83. package/dist/handlers-tzmevqb5.js +41 -0
  84. package/dist/{heartbeat-vn31xwdk.js → heartbeat-psfvcgwp.js} +20 -20
  85. package/dist/{hook-6d0zbvvt.js → hook-8pz04697.js} +5 -5
  86. package/dist/{hook-cpjs8q51.js → hook-cyq4mryn.js} +6 -6
  87. package/dist/{http-9c6medda.js → http-haj1nd6k.js} +109 -114
  88. package/dist/{index-qhjf2htm.js → index-jwt2bj6f.js} +261 -96
  89. package/dist/{index-g6k9tctj.js → index-q52f47pe.js} +1 -1
  90. package/dist/{index-pkma1yf8.js → index-syvhnvtw.js} +1 -1
  91. package/dist/{index-yqgbfgaj.js → index-y1ajx04a.js} +20 -20
  92. package/dist/{index-k0yt1tvq.js → index-z5h22pes.js} +17 -17
  93. package/dist/{index-38t35h0n.js → index-zyq8ct52.js} +17 -17
  94. package/dist/{keepalive-em0117bp.js → keepalive-xdvsamvj.js} +3 -3
  95. package/dist/{lead-23e7z59t.js → lead-hqg57kdp.js} +35 -35
  96. package/dist/{maintenance-kbme3qy9.js → maintenance-4y2xzh3a.js} +1 -1
  97. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-jd4r7zbs.js} +8 -3
  98. package/dist/{onboard-p6q0vz97.js → onboard-18447q9q.js} +5 -5
  99. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-26b2frdf.js} +5 -5
  100. package/dist/{otel-impl-54qhqh1z.js → otel-impl-f9tgr8yw.js} +1 -1
  101. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-qn6qbga1.js} +33 -12
  102. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-dw2tgb53.js} +3 -3
  103. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-aygm2760.js} +2 -2
  104. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-s4nwj2m3.js} +1 -1
  105. package/dist/{render-v2-tjay5hhf.js → render-v2-yfvdfr0x.js} +3 -3
  106. package/dist/{resume-8ayf37g5.js → resume-1bmvjbf3.js} +7 -7
  107. package/dist/{runtime-hqe5cqcd.js → runtime-f7tpzedy.js} +1 -1
  108. package/dist/{session-access-fdhp987s.js → session-access-r5garej6.js} +4 -4
  109. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-424wzv0r.js} +2 -2
  110. package/dist/{setup-4rrmqcq3.js → setup-8tygvxkq.js} +1 -1
  111. package/dist/{virtual-modules-52km00d6.js → virtual-modules-xsw323b3.js} +2 -2
  112. package/dist/{work-objects-p28fgd70.js → work-objects-ggnghpgf.js} +20 -20
  113. package/dist/{worker-e3w7vye1.js → worker-sqjd1nqf.js} +35 -35
  114. package/openapi.json +69 -1
  115. package/package.json +5 -3
  116. package/src/be/automation-preflight.ts +2 -0
  117. package/src/be/db/api-keys.ts +9 -0
  118. package/src/be/db/workflow-runs.ts +36 -5
  119. package/src/be/db.ts +80 -22
  120. package/src/be/memory/task-learning.ts +155 -0
  121. package/src/be/migrations/245_script_executor.sql +3 -0
  122. package/src/be/oauth-refresh-sweep.ts +2 -1
  123. package/src/be/scripts/boot-reembed.ts +2 -0
  124. package/src/be/scripts/db.ts +14 -4
  125. package/src/be/scripts/embeddings.ts +2 -0
  126. package/src/be/scripts/run-saved.ts +1 -0
  127. package/src/be/scripts/typecheck.ts +9 -3
  128. package/src/be/seed-skills/index.ts +7 -0
  129. package/src/commands/onboard/compose-generator.ts +5 -3
  130. package/src/commands/provider-credentials.ts +2 -1
  131. package/src/heartbeat/heartbeat.ts +14 -1
  132. package/src/heartbeat/script-runner-probe.ts +47 -0
  133. package/src/http/api-keys.ts +24 -9
  134. package/src/http/idempotency-key.ts +1 -1
  135. package/src/http/index.ts +3 -3
  136. package/src/http/pages-public.ts +38 -5
  137. package/src/http/scripts.ts +27 -0
  138. package/src/http/workflows.ts +10 -0
  139. package/src/http/x.ts +5 -0
  140. package/src/prompts/base-prompt.ts +21 -1
  141. package/src/prompts/session-templates.ts +17 -3
  142. package/src/providers/amp-adapter.ts +1 -1
  143. package/src/providers/dsh-adapter.ts +1 -1
  144. package/src/providers/pi-mono-adapter.ts +41 -11
  145. package/src/scheduler/scheduler.ts +2 -1
  146. package/src/scripts-runtime/eval-harness.ts +8 -1
  147. package/src/scripts-runtime/executors/native.ts +22 -16
  148. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  149. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  150. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  151. package/src/scripts-runtime/executors/registry.ts +32 -0
  152. package/src/scripts-runtime/executors/types.ts +7 -0
  153. package/src/scripts-runtime/host-validated-args.ts +133 -0
  154. package/src/scripts-runtime/loader.ts +30 -3
  155. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  156. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  157. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  158. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  159. package/src/tests/base-prompt.test.ts +70 -2
  160. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  161. package/src/tests/credential-check.test.ts +4 -2
  162. package/src/tests/docs-index.test.ts +135 -0
  163. package/src/tests/dsh-adapter.test.ts +1 -1
  164. package/src/tests/fetch-resolved-env.test.ts +16 -2
  165. package/src/tests/heartbeat-checklist.test.ts +89 -0
  166. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  167. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  168. package/src/tests/onboard-compose.test.ts +20 -0
  169. package/src/tests/opencode-adapter.test.ts +2 -1
  170. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  171. package/src/tests/pi-codemode.test.ts +2 -1
  172. package/src/tests/pi-mono-adapter.test.ts +54 -0
  173. package/src/tests/pi-native-mcp.test.ts +13 -1
  174. package/src/tests/pi-tool-exposure.test.ts +8 -8
  175. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  176. package/src/tests/prompt-template-session.test.ts +4 -3
  177. package/src/tests/request-composition-derive.test.ts +329 -0
  178. package/src/tests/script-connections.test.ts +2 -0
  179. package/src/tests/script-executor-conformance.test.ts +36 -1
  180. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  181. package/src/tests/script-executor-quickjs.test.ts +140 -0
  182. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  183. package/src/tests/script-per-script-executor.test.ts +415 -0
  184. package/src/tests/scripts-external-api.test.ts +45 -0
  185. package/src/tests/sdk-allowlist.test.ts +3 -2
  186. package/src/tests/self-improvement.test.ts +4 -4
  187. package/src/tests/store-progress-learning.test.ts +288 -0
  188. package/src/tests/ui-logs-parser.test.ts +180 -0
  189. package/src/tests/workflow-engine-v2.test.ts +116 -1
  190. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  191. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  192. package/src/tests/workflow-swarm-script.test.ts +34 -0
  193. package/src/tools/script-common.ts +1 -0
  194. package/src/tools/script-run.ts +7 -0
  195. package/src/tools/script-upsert.ts +7 -0
  196. package/src/tools/store-progress.ts +67 -1
  197. package/src/tools/workflows/trigger-workflow.ts +104 -10
  198. package/src/types.ts +12 -1
  199. package/src/utils/credentials.ts +5 -2
  200. package/src/workflows/engine.ts +18 -16
  201. package/src/workflows/executors/swarm-script.ts +5 -0
  202. package/src/workflows/foreach-driver.ts +5 -11
  203. package/src/workflows/node-timeout.ts +30 -0
  204. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  205. package/templates/community/linear-delivery-loop/README.md +38 -0
  206. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  207. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  208. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  209. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  210. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  211. package/templates/skills/swarm-docs/config.json +14 -0
  212. package/templates/skills/swarm-docs/content.md +144 -0
  213. package/dist/handlers-eqsnmv95.js +0 -41
@@ -1323,10 +1323,12 @@ describe("NaN and Helmcode live probes", () => {
1323
1323
  // ─── Migration 055: report composition + opt-out ────────────────────────────
1324
1324
 
1325
1325
  describe("isCredCheckDisabled", () => {
1326
- test("true only when CRED_CHECK_DISABLE === '1'", () => {
1326
+ test("true when CRED_CHECK_DISABLE is '1' or 'true' (case-insensitive)", () => {
1327
1327
  expect(isCredCheckDisabled({})).toBe(false);
1328
1328
  expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "0" })).toBe(false);
1329
- expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "true" })).toBe(false);
1329
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "false" })).toBe(false);
1330
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "true" })).toBe(true);
1331
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "TRUE" })).toBe(true);
1330
1332
  expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "1" })).toBe(true);
1331
1333
  });
1332
1334
  });
@@ -0,0 +1,135 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ collectDocsPages,
7
+ coverageErrors,
8
+ INDEX_END,
9
+ INDEX_START,
10
+ renderDocsIndex,
11
+ replaceIndexBlock,
12
+ SKILL_CONTENT_PATH,
13
+ } from "../../scripts/build-docs-index";
14
+ import { loadSeedSkills } from "../be/seed-skills";
15
+
16
+ const page = (frontmatter: string, body = "Body.\n") => `---\n${frontmatter}\n---\n\n${body}`;
17
+
18
+ describe("docs index (real docs tree)", () => {
19
+ test("every playbook is in the index and the coverage rules pass", async () => {
20
+ const pages = await collectDocsPages();
21
+ expect(coverageErrors(pages)).toEqual([]);
22
+
23
+ const index = renderDocsIndex(pages);
24
+ const playbooks = pages.filter((p) => p.path.startsWith("playbooks/"));
25
+ expect(playbooks.length).toBeGreaterThan(10);
26
+ for (const playbook of playbooks) {
27
+ expect(index).toContain(`(https://docs.agent-swarm.dev/docs/${playbook.slug})`);
28
+ }
29
+ // Positive control: a known playbook carries its trigger.
30
+ expect(index).toContain(
31
+ "[Open Oncall](https://docs.agent-swarm.dev/docs/playbooks/open-oncall). When: ",
32
+ );
33
+ // Negative control: opted-out pages are reference only, release notes are skipped.
34
+ expect(index).toContain("(https://docs.agent-swarm.dev/docs/playbooks). Reference.");
35
+ expect(index).not.toContain("/docs/releases/");
36
+ });
37
+
38
+ test("the committed skill body matches the generator", async () => {
39
+ const content = await Bun.file(SKILL_CONTENT_PATH).text();
40
+ const regenerated = replaceIndexBlock(content, renderDocsIndex(await collectDocsPages()));
41
+ expect(content).toBe(regenerated);
42
+ });
43
+
44
+ test("the generator is deterministic", async () => {
45
+ const first = renderDocsIndex(await collectDocsPages());
46
+ const second = renderDocsIndex(await collectDocsPages());
47
+ expect(second).toBe(first);
48
+ });
49
+
50
+ test("the seeded skill is a system default and carries the index", async () => {
51
+ const skill = (await loadSeedSkills()).find((s) => s.name === "swarm-docs");
52
+ expect(skill?.systemDefault).toBe(true);
53
+ expect(skill?.content).toContain("https://docs.agent-swarm.dev/docs/playbooks/open-oncall");
54
+ });
55
+ });
56
+
57
+ describe("docs index (fixture tree)", () => {
58
+ let root: string;
59
+
60
+ beforeAll(async () => {
61
+ root = await mkdtemp(join(tmpdir(), "docs-index-"));
62
+ await mkdir(join(root, "playbooks"), { recursive: true });
63
+ await mkdir(join(root, "guides"), { recursive: true });
64
+ await mkdir(join(root, "releases"), { recursive: true });
65
+ await Bun.write(join(root, "meta.json"), JSON.stringify({ pages: ["index", "playbooks"] }));
66
+ await Bun.write(join(root, "index.mdx"), page("title: Home"));
67
+ await Bun.write(
68
+ join(root, "playbooks", "meta.json"),
69
+ JSON.stringify({ pages: ["index", "b", "---Separator---", "a"] }),
70
+ );
71
+ await Bun.write(
72
+ join(root, "playbooks", "index.mdx"),
73
+ page("title: Playbooks\nagent:\n offer: false"),
74
+ );
75
+ await Bun.write(
76
+ join(root, "playbooks", "a.mdx"),
77
+ page('title: A\nagent:\n when: "Someone wants A"\n template: "community/a"'),
78
+ );
79
+ await Bun.write(join(root, "playbooks", "b.mdx"), page("title: B"));
80
+ await Bun.write(
81
+ join(root, "guides", "g.mdx"),
82
+ page("title: G\nagent:\n offer: true\n notWhen: never"),
83
+ );
84
+ await Bun.write(join(root, "releases", "r.mdx"), page("title: R"));
85
+ });
86
+
87
+ afterAll(async () => {
88
+ await rm(root, { recursive: true, force: true });
89
+ });
90
+
91
+ test("orders by meta.json, appends unlisted folders, and skips releases", async () => {
92
+ const pages = await collectDocsPages(root);
93
+ expect(pages.map((p) => p.path)).toEqual([
94
+ "index",
95
+ "playbooks/index",
96
+ "playbooks/b",
97
+ "playbooks/a",
98
+ "guides/g",
99
+ ]);
100
+ expect(pages.map((p) => p.slug)).toEqual([
101
+ "",
102
+ "playbooks",
103
+ "playbooks/b",
104
+ "playbooks/a",
105
+ "guides/g",
106
+ ]);
107
+ });
108
+
109
+ test("a playbook without when, and an offered guide without when, fail coverage", async () => {
110
+ const errors = coverageErrors(await collectDocsPages(root));
111
+ expect(errors).toEqual([
112
+ "playbooks/b: playbook needs agent.when (or agent.offer: false)",
113
+ "guides/g: agent.offer is true but agent.when is missing",
114
+ ]);
115
+ });
116
+
117
+ test("renders triggers and templates for offered pages only", async () => {
118
+ const index = renderDocsIndex(await collectDocsPages(root));
119
+ expect(index).toContain(
120
+ "- [A](https://docs.agent-swarm.dev/docs/playbooks/a). When: Someone wants A. Template: `community/a`.",
121
+ );
122
+ expect(index).toContain(
123
+ "- [Playbooks](https://docs.agent-swarm.dev/docs/playbooks). Reference.",
124
+ );
125
+ expect(index).toContain("- [Home](https://docs.agent-swarm.dev/docs)");
126
+ });
127
+
128
+ test("replaceIndexBlock swaps only the marked block and rejects missing markers", () => {
129
+ const content = `before\n${INDEX_START}\nold\n${INDEX_END}\nafter\n`;
130
+ expect(replaceIndexBlock(content, `${INDEX_START}\nnew\n${INDEX_END}`)).toBe(
131
+ `before\n${INDEX_START}\nnew\n${INDEX_END}\nafter\n`,
132
+ );
133
+ expect(() => replaceIndexBlock("no markers", "x")).toThrow();
134
+ });
135
+ });
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
376
376
  await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
377
377
  config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
378
378
  await expect(new DshAdapter().createSession(config)).rejects.toThrow(
379
- "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.3",
379
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.4",
380
380
  );
381
381
  expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
382
382
  });
@@ -41,8 +41,12 @@ const defaultMockResponse: MockResponse = {
41
41
  const mockResponsesByAgentId = new Map<string, MockResponse>();
42
42
  const requestedRepoIds = new Map<string, string | null>();
43
43
  const requestedTaskIds = new Map<string, string | null>();
44
- /** Every `GET /api/keys/available` query string observed by the mock server, most recent last. */
44
+ /**
45
+ * Every `GET /api/keys/available` query string observed by the mock server,
46
+ * most recent last, without `keySuffixes` (recorded in `keysAvailableSuffixes`).
47
+ */
45
48
  const keysAvailableRequests: string[] = [];
49
+ const keysAvailableSuffixes: Array<string | null> = [];
46
50
 
47
51
  beforeAll(() => {
48
52
  server = Bun.serve({
@@ -63,7 +67,10 @@ beforeAll(() => {
63
67
  }
64
68
 
65
69
  if (url.pathname === "/api/keys/available") {
66
- keysAvailableRequests.push(url.search);
70
+ const params = new URLSearchParams(url.search);
71
+ keysAvailableSuffixes.push(params.get("keySuffixes"));
72
+ params.delete("keySuffixes");
73
+ keysAvailableRequests.push(`?${params}`);
67
74
  const totalKeys = Number(url.searchParams.get("totalKeys") ?? "1");
68
75
  const availableIndices = Array.from({ length: totalKeys }, (_, i) => i);
69
76
  return Response.json({ success: true, availableIndices, totalKeys });
@@ -455,6 +462,13 @@ describe("fetchResolvedEnv", () => {
455
462
  expect(keysAvailableRequests).toEqual([
456
463
  "?keyType=CLAUDE_CODE_OAUTH_TOKEN&totalKeys=32&model=fable",
457
464
  ]);
465
+ // The pool's own suffixes, in order, so status is matched by key (issue #2425).
466
+ expect(keysAvailableSuffixes.at(-1)).toBe(
467
+ Object.values(envWithOauthPool(32))[0]!
468
+ .split(",")
469
+ .map((key) => key.slice(-5))
470
+ .join(","),
471
+ );
458
472
  });
459
473
 
460
474
  test("model: 'claude-fable-5-1' issues the same model=fable", async () => {
@@ -24,6 +24,7 @@ import {
24
24
  upsertSwarmConfig,
25
25
  } from "../be/db";
26
26
  import { getScript, upsertScriptByName } from "../be/scripts/db";
27
+ import * as savedScriptRunner from "../be/scripts/run-saved";
27
28
  import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
28
29
  import {
29
30
  checkHeartbeatChecklist,
@@ -624,6 +625,66 @@ describe("Heartbeat Checklist", () => {
624
625
  });
625
626
  });
626
627
 
628
+ for (const quiet of [true, false]) {
629
+ test(`healthy probe preserves the saved gate quiet=${quiet} verdict`, async () => {
630
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
631
+ await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
632
+ const saved = spyOn(savedScriptRunner, "runSavedScriptAsAgent").mockResolvedValue({
633
+ exitCode: 0,
634
+ result: { quiet, reason: "existing verdict" },
635
+ stdout: "",
636
+ stderr: "",
637
+ durationMs: 1,
638
+ truncated: { stdout: false, stderr: false },
639
+ });
640
+ try {
641
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
642
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
643
+ );
644
+ await seedLead();
645
+ await checkHeartbeatChecklist();
646
+ expect(saved).toHaveBeenCalledTimes(1);
647
+ const tasks = await checklistTasks();
648
+ expect(tasks).toHaveLength(quiet ? 0 : 1);
649
+ if (!quiet) expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
650
+ } finally {
651
+ saved.mockRestore();
652
+ }
653
+ });
654
+ }
655
+
656
+ for (const mode of ["shadow", "enforce"] as const) {
657
+ test(`${mode}: an unhealthy probe produces a wake checklist`, async () => {
658
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = mode;
659
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
660
+ defaultChecklistGateRunner(name, agentId, signal, {
661
+ probe: async () => ({ quiet: false, reason: "script-runner-unhealthy: timeout" }),
662
+ }),
663
+ );
664
+ await seedLead();
665
+ await checkHeartbeatChecklist();
666
+ const tasks = await checklistTasks();
667
+ expect(tasks).toHaveLength(1);
668
+ expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
669
+ });
670
+ }
671
+
672
+ test("a bug in the probe produces wake rather than error", async () => {
673
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
674
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
675
+ defaultChecklistGateRunner(name, agentId, signal, {
676
+ probe: async () => {
677
+ throw new Error("probe bug");
678
+ },
679
+ }),
680
+ );
681
+ await seedLead();
682
+ await checkHeartbeatChecklist();
683
+ const tasks = await checklistTasks();
684
+ expect(tasks).toHaveLength(1);
685
+ expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
686
+ });
687
+
627
688
  test("shadow mode (default): a quiet verdict still creates the task, tagged quiet", async () => {
628
689
  useGate({ quiet: true, reason: "no change", summary: "nothing moved" });
629
690
  const lead = await seedLead();
@@ -703,6 +764,9 @@ describe("Heartbeat Checklist", () => {
703
764
  test("enforce mode: a missing global script fails open through the real runner", async () => {
704
765
  process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
705
766
  process.env.HEARTBEAT_CHECKLIST_GATE_SCRIPT = "no-such-gate-script";
767
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
768
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
769
+ );
706
770
  await seedLead();
707
771
 
708
772
  await checkHeartbeatChecklist();
@@ -739,6 +803,7 @@ describe("Heartbeat Checklist", () => {
739
803
  let run: Promise<unknown> | undefined;
740
804
  setChecklistGateRunnerForTests((scriptName, leadAgentId, signal) => {
741
805
  run = defaultChecklistGateRunner(scriptName, leadAgentId, signal, {
806
+ probe: async () => null,
742
807
  buildCredentials: () => {
743
808
  setupStarted.resolve();
744
809
  return new Promise((resolve) => {
@@ -798,6 +863,9 @@ describe("Heartbeat Checklist", () => {
798
863
  "export default async function run() { await new Promise((r) => setTimeout(r, 30_000)); return { quiet: true }; }",
799
864
  );
800
865
  setChecklistGateTimeoutMsForTests(1_500);
866
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
867
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
868
+ );
801
869
  await seedLead();
802
870
 
803
871
  const started = Date.now();
@@ -808,6 +876,27 @@ describe("Heartbeat Checklist", () => {
808
876
  expect(tasks).toHaveLength(1);
809
877
  expect(tasks[0]!.tags).toContain("heartbeat-gate:error");
810
878
  });
879
+
880
+ test("enforce mode: the real probe passes with no swarm API key, using the Lead's run token", async () => {
881
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
882
+ await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
883
+ const savedKeys = { swarm: process.env.AGENT_SWARM_API_KEY, legacy: process.env.API_KEY };
884
+ process.env.AGENT_SWARM_API_KEY = "";
885
+ process.env.API_KEY = "";
886
+ try {
887
+ await seedLead();
888
+ await checkHeartbeatChecklist();
889
+ expect(await checklistTasks()).toHaveLength(0);
890
+ } finally {
891
+ for (const [name, value] of [
892
+ ["AGENT_SWARM_API_KEY", savedKeys.swarm],
893
+ ["API_KEY", savedKeys.legacy],
894
+ ] as const) {
895
+ if (value === undefined) delete process.env[name];
896
+ else process.env[name] = value;
897
+ }
898
+ }
899
+ });
811
900
  });
812
901
 
813
902
  // ==========================================================================
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import { probeScriptRunner } from "../heartbeat/script-runner-probe";
3
+ import type { RunScriptOutput } from "../scripts-runtime/loader";
4
+
5
+ const ok: RunScriptOutput = {
6
+ result: true,
7
+ stdout: "",
8
+ stderr: "",
9
+ truncated: { stdout: false, stderr: false },
10
+ durationMs: 1,
11
+ exitCode: 0,
12
+ };
13
+
14
+ afterEach(() => mock.restore());
15
+
16
+ describe("heartbeat script runner probe", () => {
17
+ test("success leaves the gate verdict unchanged and clears its deadline", async () => {
18
+ const clear = spyOn(globalThis, "clearTimeout");
19
+ const runner = mock(async () => ok);
20
+ expect(await probeScriptRunner("lead", runner)).toBeNull();
21
+ expect(runner).toHaveBeenCalledWith({
22
+ source: "export default async function () { return true; }",
23
+ agentId: "lead",
24
+ fsMode: "none",
25
+ timeoutMs: 10_000,
26
+ signal: expect.any(AbortSignal),
27
+ });
28
+ expect(clear).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ test("timeout wakes without waiting for a stalled runner and aborts it", async () => {
32
+ let expire: (() => void) | undefined;
33
+ spyOn(globalThis, "setTimeout").mockImplementation(((callback: () => void, ms: number) => {
34
+ expect(ms).toBe(10_000);
35
+ expire = callback;
36
+ return 1;
37
+ }) as typeof setTimeout);
38
+ const clear = spyOn(globalThis, "clearTimeout").mockImplementation(() => {});
39
+ let signal: AbortSignal | undefined;
40
+ let rejectRun: ((error: Error) => void) | undefined;
41
+ const result = probeScriptRunner("lead", async (input) => {
42
+ signal = input.signal;
43
+ return new Promise<RunScriptOutput>((_, reject) => {
44
+ rejectRun = reject;
45
+ });
46
+ });
47
+ expire!();
48
+ expect(await result).toEqual({
49
+ quiet: false,
50
+ reason: "script-runner-unhealthy: timeout after 10000ms",
51
+ });
52
+ expect(signal?.aborted).toBe(true);
53
+ expect(clear).toHaveBeenCalledTimes(1);
54
+ // A runner that rejects after the deadline remains handled by Promise.race.
55
+ rejectRun!(new Error("late rejection"));
56
+ await Promise.resolve();
57
+ });
58
+
59
+ test("a thrown runner error wakes", async () => {
60
+ expect(
61
+ await probeScriptRunner("lead", async () => {
62
+ throw new Error("ECONNRESET");
63
+ }),
64
+ ).toEqual({
65
+ quiet: false,
66
+ reason: "script-runner-unhealthy: ECONNRESET",
67
+ });
68
+ });
69
+
70
+ for (const [label, failure, reason] of [
71
+ ["nonzero exit", { exitCode: 1 }, "script exited with code 1"],
72
+ ["runner error", { error: "killed" }, "killed"],
73
+ ["runtime error", { runtimeError: { message: "runtime failed" } }, "runtime failed"],
74
+ ] as const) {
75
+ test(`${label} wakes`, async () => {
76
+ const result = await probeScriptRunner(
77
+ "lead",
78
+ async () => ({ ...ok, ...failure }) as RunScriptOutput,
79
+ );
80
+ expect(result).toEqual({ quiet: false, reason: `script-runner-unhealthy: ${reason}` });
81
+ });
82
+ }
83
+ });
@@ -8,7 +8,11 @@ import {
8
8
  storeOAuthTokens,
9
9
  upsertOAuthApp,
10
10
  } from "../be/db-queries/oauth";
11
- import { sweepOAuthTokenRefresh } from "../be/oauth-refresh-sweep";
11
+ import {
12
+ startOAuthRefreshSweep,
13
+ stopOAuthRefreshSweep,
14
+ sweepOAuthTokenRefresh,
15
+ } from "../be/oauth-refresh-sweep";
12
16
 
13
17
  async function authorizationStatus(provider: string): Promise<string | undefined> {
14
18
  const id = await getDefaultAuthorizationIdForProvider(provider);
@@ -242,3 +246,32 @@ describe("sweepOAuthTokenRefresh", () => {
242
246
  expect((await getOAuthTokens("vendor_a"))?.accessToken).toBe("example-new-access-token");
243
247
  });
244
248
  });
249
+
250
+ describe("startOAuthRefreshSweep", () => {
251
+ const originalDisable = process.env.OAUTH_REFRESH_SWEEP_DISABLE;
252
+
253
+ afterAll(async () => {
254
+ await stopOAuthRefreshSweep();
255
+ if (originalDisable === undefined) {
256
+ delete process.env.OAUTH_REFRESH_SWEEP_DISABLE;
257
+ } else {
258
+ process.env.OAUTH_REFRESH_SWEEP_DISABLE = originalDisable;
259
+ }
260
+ });
261
+
262
+ for (const val of ["true", "1", "TRUE"]) {
263
+ test(`does not start when OAUTH_REFRESH_SWEEP_DISABLE=${val}`, async () => {
264
+ process.env.OAUTH_REFRESH_SWEEP_DISABLE = val;
265
+ const logs: string[] = [];
266
+ const origLog = console.log;
267
+ console.log = (...args: unknown[]) => logs.push(args.map(String).join(" "));
268
+ try {
269
+ startOAuthRefreshSweep();
270
+ } finally {
271
+ console.log = origLog;
272
+ await stopOAuthRefreshSweep();
273
+ }
274
+ expect(logs).toEqual(["[oauth-refresh-sweep] Disabled via OAUTH_REFRESH_SWEEP_DISABLE"]);
275
+ });
276
+ }
277
+ });
@@ -136,6 +136,26 @@ describe("generateCompose", () => {
136
136
  expect(yaml).not.toContain("pull_policy: always");
137
137
  });
138
138
 
139
+ test.each([
140
+ "docker",
141
+ "podman",
142
+ ] as const)("%s agent services mount volumes where the worker reads and writes", (containerEngine) => {
143
+ const doc = Bun.YAML.parse(generateCompose({ ...devState, containerEngine })) as {
144
+ services: Record<string, { volumes?: string[] }>;
145
+ };
146
+ const agents = Object.entries(doc.services).filter(([name]) => name !== "swarm-api");
147
+
148
+ expect(agents.map(([name]) => name)).toEqual(["lead", "worker-coder-1", "worker-coder-2"]);
149
+ for (const [name, service] of agents) {
150
+ expect(service.volumes).toEqual([
151
+ "swarm_logs:/logs",
152
+ "swarm_shared:/workspace/shared",
153
+ `swarm_${name.replaceAll("-", "_")}:/workspace/personal`,
154
+ ]);
155
+ }
156
+ expect(doc.services["swarm-api"]?.volumes).toEqual(["swarm_data:/app/data"]);
157
+ });
158
+
139
159
  test("dev preset produces 3 agent services + 1 API service", () => {
140
160
  const yaml = generateCompose(devState);
141
161
  // Only count service definitions in the services section (before volumes:)
@@ -675,7 +675,7 @@ describe("OpencodeSession — persisted rows render in the dashboard", () => {
675
675
  const { items } = normalizeSessionLogs(rows);
676
676
 
677
677
  const visible = items
678
- .filter((item) => item.kind !== "lifecycle")
678
+ .filter((item) => item.kind !== "lifecycle" && item.kind !== "call_usage")
679
679
  .map((item) =>
680
680
  item.kind === "text"
681
681
  ? `${item.role} text: ${item.text}`
@@ -695,6 +695,7 @@ describe("OpencodeSession — persisted rows render in the dashboard", () => {
695
695
  "system text: opencode error: provider overloaded",
696
696
  ]);
697
697
  expect(items.some((item) => item.kind === "unknown")).toBe(false);
698
+ expect(items.filter((item) => item.kind === "call_usage")).toHaveLength(1);
698
699
  });
699
700
  });
700
701
 
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `PAGES_FRAME_ANCESTORS` widens `frame-ancestors` on `/p/:id` for PUBLIC
3
+ * pages only, so an outside site (for example a blog) can iframe a chart.
4
+ * Authed and password pages keep the app-origin-only header, and an unset
5
+ * env leaves every header unchanged.
6
+ */
7
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
8
+ import crypto from "node:crypto";
9
+ import { unlink } from "node:fs/promises";
10
+ import {
11
+ createServer as createHttpServer,
12
+ type IncomingMessage,
13
+ type Server,
14
+ type ServerResponse,
15
+ } from "node:http";
16
+ import { closeDb, initDb } from "../be/db";
17
+ import { handlePages } from "../http/pages";
18
+ import { getPageFrameAncestors, handlePagesPublic } from "../http/pages-public";
19
+ import { getPathSegments, parseQueryParams } from "../http/utils";
20
+ import { signPageSession } from "../utils/page-session";
21
+ import { listenOnFreePort } from "./test-net";
22
+
23
+ const TEST_DB_PATH = "./test-pages-frame-ancestors.sqlite";
24
+ const EMBEDDERS = "https://www.agent-swarm.dev https://agent-swarm.dev";
25
+ let BASE = "";
26
+
27
+ function createTestServer(): Server {
28
+ return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
29
+ const pathSegments = getPathSegments(req.url || "");
30
+ const queryParams = parseQueryParams(req.url || "");
31
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
32
+ if (await handlePagesPublic(req, res, pathSegments, queryParams)) return;
33
+ if (await handlePages(req, res, pathSegments, queryParams, myAgentId)) return;
34
+ res.writeHead(404);
35
+ res.end("not found");
36
+ });
37
+ }
38
+
39
+ function frameAncestors(res: Response): string {
40
+ const csp = res.headers.get("content-security-policy") ?? "";
41
+ return csp.split(";").find((d) => d.trim().startsWith("frame-ancestors ")) ?? "";
42
+ }
43
+
44
+ describe("PAGES_FRAME_ANCESTORS on /p/:id", () => {
45
+ let server: Server;
46
+ const agentId = crypto.randomUUID();
47
+ const headers = { "Content-Type": "application/json", "X-Agent-ID": agentId };
48
+ const ORIG = {
49
+ PAGES_FRAME_ANCESTORS: process.env.PAGES_FRAME_ANCESTORS,
50
+ APP_URL: process.env.APP_URL,
51
+ DASHBOARD_URL: process.env.DASHBOARD_URL,
52
+ PAGE_SESSION_SECRET: process.env.PAGE_SESSION_SECRET,
53
+ };
54
+
55
+ function restore(key: keyof typeof ORIG): void {
56
+ if (ORIG[key] === undefined) delete process.env[key];
57
+ else process.env[key] = ORIG[key];
58
+ }
59
+
60
+ async function createPage(
61
+ slug: string,
62
+ authMode: "public" | "authed" | "password",
63
+ ): Promise<string> {
64
+ const post = await fetch(`${BASE}/api/pages`, {
65
+ method: "POST",
66
+ headers,
67
+ body: JSON.stringify({
68
+ slug,
69
+ title: slug,
70
+ contentType: "text/html",
71
+ authMode,
72
+ ...(authMode === "password" ? { password: "swordfish" } : {}),
73
+ body: "<!doctype html><html><head></head><body><h1>chart</h1></body></html>",
74
+ }),
75
+ });
76
+ expect(post.status).toBe(201);
77
+ const { id } = (await post.json()) as { id: string };
78
+ return id;
79
+ }
80
+
81
+ beforeAll(async () => {
82
+ process.env.PAGE_SESSION_SECRET = "example-test-frame-ancestors-secret";
83
+ process.env.APP_URL = "https://app.example.test";
84
+ delete process.env.DASHBOARD_URL;
85
+ for (const suffix of ["", "-wal", "-shm"]) {
86
+ try {
87
+ await unlink(`${TEST_DB_PATH}${suffix}`);
88
+ } catch {}
89
+ }
90
+ initDb(TEST_DB_PATH);
91
+ server = createTestServer();
92
+ const port = await listenOnFreePort(server);
93
+ BASE = `http://localhost:${port}`;
94
+ });
95
+
96
+ afterEach(() => restore("PAGES_FRAME_ANCESTORS"));
97
+
98
+ afterAll(async () => {
99
+ await new Promise<void>((resolve) => server.close(() => resolve()));
100
+ closeDb();
101
+ for (const key of Object.keys(ORIG) as (keyof typeof ORIG)[]) restore(key);
102
+ for (const suffix of ["", "-wal", "-shm"]) {
103
+ try {
104
+ await unlink(`${TEST_DB_PATH}${suffix}`);
105
+ } catch {}
106
+ }
107
+ });
108
+
109
+ test("public page gets the configured origins", async () => {
110
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
111
+ const id = await createPage("public-embed", "public");
112
+ const res = await fetch(`${BASE}/p/${id}`);
113
+ expect(res.status).toBe(200);
114
+ const fa = frameAncestors(res);
115
+ expect(fa).toContain("'self'");
116
+ expect(fa).toContain("https://app.example.test");
117
+ expect(fa).toContain("https://www.agent-swarm.dev");
118
+ expect(fa).toContain("https://agent-swarm.dev");
119
+ });
120
+
121
+ test("authed page keeps the app-origin-only header", async () => {
122
+ const id = await createPage("authed-embed", "authed");
123
+ const exp = Math.floor(Date.now() / 1000) + 3600;
124
+ const cookie = `page_session_${id}=${await signPageSession({ pageId: id, exp })}`;
125
+
126
+ const before = frameAncestors(await fetch(`${BASE}/p/${id}`, { headers: { cookie } }));
127
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
128
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { cookie } });
129
+ expect(res.status).toBe(200);
130
+ expect(frameAncestors(res)).toBe(before);
131
+ expect(frameAncestors(res)).not.toContain("agent-swarm.dev");
132
+ });
133
+
134
+ test("password page keeps the app-origin-only header", async () => {
135
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
136
+ const id = await createPage("password-embed", "password");
137
+ const res = await fetch(`${BASE}/p/${id}?key=swordfish`);
138
+ expect(res.status).toBe(200);
139
+ expect(frameAncestors(res)).not.toContain("agent-swarm.dev");
140
+ });
141
+
142
+ test("unset env leaves the public header unchanged", async () => {
143
+ const id = await createPage("public-unset", "public");
144
+ delete process.env.PAGES_FRAME_ANCESTORS;
145
+ const unset = frameAncestors(await fetch(`${BASE}/p/${id}`));
146
+ process.env.PAGES_FRAME_ANCESTORS = "";
147
+ const blank = frameAncestors(await fetch(`${BASE}/p/${id}`));
148
+ expect(unset).toContain("https://app.example.test");
149
+ expect(unset).not.toContain("agent-swarm.dev");
150
+ expect(blank).toBe(unset);
151
+ });
152
+
153
+ test("parser accepts comma or space lists and drops invalid entries", () => {
154
+ process.env.PAGES_FRAME_ANCESTORS =
155
+ "https://www.agent-swarm.dev/, https://*.example.com:8443 blog.example.org,* 'none' https://evil.test;script-src javascript:alert(1) https://x.test/path";
156
+ expect(getPageFrameAncestors()).toEqual([
157
+ "https://www.agent-swarm.dev",
158
+ "https://*.example.com:8443",
159
+ "blog.example.org",
160
+ ]);
161
+ });
162
+ });
@@ -104,7 +104,8 @@ describe("PiMonoAdapter.createSession — codemode", () => {
104
104
  process.env.OPENROUTER_BASE_URL = "http://127.0.0.1:9/api/v1";
105
105
  delete process.env.PI_CODEMODE;
106
106
  delete process.env.PI_CODEMODE_MODELS;
107
- delete process.env.PI_TOOL_DEFERRAL;
107
+ // Deferral off, so every swarm tool shows up as an active tool name.
108
+ process.env.PI_TOOL_DEFERRAL = "false";
108
109
  });
109
110
  afterEach(() => {
110
111
  for (const key of envKeys) {