@desplega.ai/agent-swarm 2.4.0 → 2.4.2

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 (239) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
  15. package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
  16. package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
  17. package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
  20. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  21. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  22. package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
  23. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  24. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  25. package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
  26. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  27. package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
  28. package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
  29. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  30. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  31. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  32. package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
  33. package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
  34. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
  37. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  38. package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
  39. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  40. package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
  41. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  42. package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
  43. package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
  44. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  45. package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
  46. package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
  47. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
  49. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  50. package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
  51. package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
  52. package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
  53. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
  56. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  57. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  58. package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
  59. package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
  60. package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
  61. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  62. package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
  63. package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
  64. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  65. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  66. package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
  73. package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
  74. package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +20 -20
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
  87. package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
  99. package/dist/handlers-dkcarkd0.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
  104. package/dist/index-5jecgp3n.js +41 -0
  105. package/dist/index-aex0eba9.js +40 -0
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
  108. package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
  109. package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
  111. package/dist/lead-b1da0f97.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
  119. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
  120. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
  128. package/dist/worker-c1e68hpa.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +491 -52
  131. package/package.json +3 -3
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/catalog.generated.json +3 -2
  134. package/src/apps/definition.ts +152 -4
  135. package/src/apps/row-store.ts +23 -9
  136. package/src/apps/schema-migrate.ts +14 -6
  137. package/src/apps/script-types.ts +2 -1
  138. package/src/apps/sync.ts +20 -19
  139. package/src/be/db/tasks/read.ts +112 -0
  140. package/src/be/db.ts +16 -4
  141. package/src/be/fleet-benchmark-stats.ts +10 -5
  142. package/src/be/models-catalog.ts +4 -0
  143. package/src/be/onboarding.ts +6 -0
  144. package/src/be/scripts/typecheck.ts +13 -0
  145. package/src/be/swarm-config-guard.ts +25 -1
  146. package/src/be/telemetry-enable.ts +1 -1
  147. package/src/commands/provider-credentials.ts +12 -2
  148. package/src/commands/runner.ts +3 -0
  149. package/src/heartbeat/heartbeat.ts +2 -2
  150. package/src/hooks/codex-hook.ts +6 -2
  151. package/src/hooks/hook.ts +7 -0
  152. package/src/hooks/pr-body-guard.ts +38 -11
  153. package/src/hooks/pr-room-guard.ts +88 -0
  154. package/src/http/agents.ts +1 -0
  155. package/src/http/apps.ts +99 -183
  156. package/src/http/fleet-benchmark.ts +79 -30
  157. package/src/http/global-config.ts +7 -0
  158. package/src/http/ranking.ts +24 -40
  159. package/src/http/status.ts +49 -43
  160. package/src/http/tasks.ts +49 -1
  161. package/src/http/telemetry-ui.ts +1 -1
  162. package/src/http/used-by.ts +55 -19
  163. package/src/http/users.ts +2 -2
  164. package/src/providers/acp-targets.ts +1 -1
  165. package/src/providers/amp-adapter.ts +1 -1
  166. package/src/providers/pi-mono-adapter.ts +15 -0
  167. package/src/providers/reasoning-effort.ts +2 -1
  168. package/src/providers/types.ts +2 -0
  169. package/src/ranking/telemetry-ranking.ts +5 -33
  170. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  173. package/src/server.ts +4 -0
  174. package/src/telemetry-context.ts +19 -1
  175. package/src/telemetry-contract/EVENTS.md +42 -8
  176. package/src/telemetry-contract/SOURCE.json +6 -5
  177. package/src/telemetry-contract/catalog.json +225 -5
  178. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  179. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  180. package/src/telemetry-contract/types.gen.ts +225 -27
  181. package/src/telemetry-identity.ts +47 -9
  182. package/src/telemetry-snapshot.ts +9 -1
  183. package/src/telemetry-ui-events.ts +9 -2
  184. package/src/telemetry.ts +29 -6
  185. package/src/tests/app-action-run-tool.test.ts +178 -0
  186. package/src/tests/app-row-tools.test.ts +385 -0
  187. package/src/tests/apps-json-column.test.ts +611 -0
  188. package/src/tests/apps-script-types.test.ts +2 -0
  189. package/src/tests/apps-sync-engine.test.ts +42 -0
  190. package/src/tests/check-pr-body.test.ts +0 -23
  191. package/src/tests/credential-check.test.ts +38 -0
  192. package/src/tests/db-task-read-contract.test.ts +4 -0
  193. package/src/tests/db-task-write-contract.test.ts +2 -0
  194. package/src/tests/docs-index-footprint.test.js +2 -2
  195. package/src/tests/fleet-benchmark.test.ts +122 -2
  196. package/src/tests/http-users.test.ts +62 -1
  197. package/src/tests/model-catalog-table.test.ts +63 -0
  198. package/src/tests/model-profiles-api.test.ts +19 -7
  199. package/src/tests/models-catalog.test.ts +50 -0
  200. package/src/tests/onboarding.test.ts +21 -0
  201. package/src/tests/org-website.test.ts +62 -0
  202. package/src/tests/pi-mono-adapter.test.ts +23 -0
  203. package/src/tests/pr-body-leaks.test.ts +4 -0
  204. package/src/tests/pr-room-guard.test.ts +242 -0
  205. package/src/tests/ranking-listing.test.ts +8 -23
  206. package/src/tests/ranking-route.test.ts +49 -47
  207. package/src/tests/reasoning-effort.test.ts +39 -3
  208. package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
  209. package/src/tests/status.test.ts +158 -9
  210. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  211. package/src/tests/task-lineage.test.ts +180 -0
  212. package/src/tests/telemetry-context.test.ts +128 -2
  213. package/src/tests/telemetry-init.test.ts +62 -5
  214. package/src/tests/telemetry-ui-route.test.ts +123 -0
  215. package/src/tests/tool-annotations.test.ts +3 -2
  216. package/src/tests/used-by-route.test.ts +144 -10
  217. package/src/tests/user-token-rest-auth.test.ts +38 -0
  218. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  219. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  220. package/src/tools/app-action-run.ts +63 -0
  221. package/src/tools/app-rows.ts +239 -0
  222. package/src/tools/model-catalog/index.ts +1 -1
  223. package/src/tools/tool-config.ts +8 -0
  224. package/src/tools/utils.ts +1 -1
  225. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  226. package/src/types.ts +17 -0
  227. package/src/utils/company-profile.ts +24 -0
  228. package/src/utils/org-website.ts +58 -21
  229. package/src/workflows/recovery.ts +10 -22
  230. package/src/workflows/resume.ts +16 -0
  231. package/src/workflows/task-step-routing.ts +47 -7
  232. package/templates/skills/apps/SKILL.md +44 -4
  233. package/templates/skills/apps/content.md +44 -4
  234. package/dist/handlers-zbs90bny.js +0 -39
  235. package/dist/index-8pwc1jc8.js +0 -40
  236. package/dist/index-nxa6me0q.js +0 -41
  237. package/dist/lead-td8h2sfn.js +0 -78
  238. package/dist/worker-wr6xgfr2.js +0 -78
  239. package/src/tests/check-ranking-entry.test.ts +0 -146
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  checkPrBody,
4
4
  isFixTitle,
5
- isRankingEntryPr,
6
5
  pickedChoices,
7
6
  prBodyWarnings,
8
7
  proseWordCount,
@@ -35,28 +34,6 @@ const TEMPLATE = `<!-- top comment -->
35
34
 
36
35
  const URGENT = "## Urgency\n- [ ] asap\n- [x] nice to have\n";
37
36
 
38
- describe("isRankingEntryPr", () => {
39
- test("exempts only a one-file entry PR with the matching title", () => {
40
- expect(
41
- isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.dev.json"]),
42
- ).toBe(true);
43
- expect(
44
- isRankingEntryPr("chore(ranking): remove acme.dev", ["community/ranking/acme.dev.json"]),
45
- ).toBe(true);
46
- expect(
47
- isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.com.json"]),
48
- ).toBe(false);
49
- expect(
50
- isRankingEntryPr("chore(ranking): add acme.dev", [
51
- "community/ranking/acme.dev.json",
52
- "src/server.ts",
53
- ]),
54
- ).toBe(false);
55
- expect(isRankingEntryPr("feat: add acme.dev", ["community/ranking/acme.dev.json"])).toBe(false);
56
- expect(isRankingEntryPr("chore(ranking): add acme.dev", undefined)).toBe(false);
57
- });
58
- });
59
-
60
37
  describe("check-pr-body", () => {
61
38
  test("template markers set when a section is required", () => {
62
39
  expect(templateSections(TEMPLATE)).toEqual([
@@ -236,6 +236,26 @@ describe("checkPiMonoCredentials", () => {
236
236
  ).toBe(false);
237
237
  });
238
238
 
239
+ test("strict: MODEL_OVERRIDE=opencode-go/... requires OPENCODE_API_KEY", async () => {
240
+ const env = { MODEL_OVERRIDE: "opencode-go/deepseek-v4.1-flash" };
241
+ expect(
242
+ (
243
+ await checkPiMonoCredentials(
244
+ { ...env, OPENCODE_API_KEY: "x" },
245
+ { homeDir: HOME, fs: noFiles },
246
+ )
247
+ ).ready,
248
+ ).toBe(true);
249
+ expect(
250
+ (
251
+ await checkPiMonoCredentials(
252
+ { ...env, ANTHROPIC_API_KEY: "x" },
253
+ { homeDir: HOME, fs: noFiles },
254
+ )
255
+ ).ready,
256
+ ).toBe(false);
257
+ });
258
+
239
259
  test("strict: MODEL_OVERRIDE=google/... accepts GEMINI_API_KEY", async () => {
240
260
  const env = {
241
261
  MODEL_OVERRIDE: "google/gemini-3-flash-preview",
@@ -1078,6 +1098,24 @@ describe("isCredCheckDisabled", () => {
1078
1098
  describe("buildCredStatusReport", () => {
1079
1099
  const HOME = "/home/worker";
1080
1100
 
1101
+ test("pi reports the opencode-go ids its pi-ai resolves", async () => {
1102
+ const snap = await buildCredStatusReport(
1103
+ "pi",
1104
+ { HOME },
1105
+ { homeDir: HOME, fs: fsWith(new Set([`${HOME}/.pi/agent/auth.json`])) },
1106
+ "boot",
1107
+ );
1108
+ expect(snap.opencodeGo?.models).toContain("deepseek-v4.1-flash");
1109
+ expect(snap.opencodeGo?.models).not.toContain("deepseek-v4-flash");
1110
+ const other = await buildCredStatusReport(
1111
+ "opencode",
1112
+ { HOME },
1113
+ { homeDir: HOME, fs: fsWith(new Set([`${HOME}/.local/share/opencode/auth.json`])) },
1114
+ "boot",
1115
+ );
1116
+ expect(other.opencodeGo).toBeNull();
1117
+ });
1118
+
1081
1119
  for (const [provider, authPath] of [
1082
1120
  ["pi", ".pi/agent/auth.json"],
1083
1121
  ["opencode", ".local/share/opencode/auth.json"],
@@ -111,6 +111,7 @@ value:ReservedAgentRoleError
111
111
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
112
112
  value:SKILL_FILE_LIMITS
113
113
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
114
+ value:TASK_LINEAGE_LIMITS
114
115
  value:UNATTRIBUTED_USER_ID
115
116
  value:__resetSqliteVecExtensionPathCacheForTests
116
117
  value:abandonSlackOutcomeDelivery
@@ -412,6 +413,7 @@ value:getSystemDefaultSkills
412
413
  value:getTaskAttachments
413
414
  value:getTaskById
414
415
  value:getTaskByWorkflowRunStepId
416
+ value:getTaskLineage
415
417
  value:getTaskStats
416
418
  value:getTasksByAgentId
417
419
  value:getTasksByStatus
@@ -612,6 +614,7 @@ type:ExistingTrackerContextWorkReason
612
614
  type:PendingChatRelay
613
615
  type:TaskFilters
614
616
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
617
+ value:TASK_LINEAGE_LIMITS
615
618
  value:findCompletedTaskInThread
616
619
  value:findExistingLinearTrackerContextWork
617
620
  value:findRecentCancelledTaskInThread
@@ -639,6 +642,7 @@ value:getSlackTasksMissingTree
639
642
  value:getSupersededTasksWithUnsettledDependents
640
643
  value:getSupersededTasksWithoutResume
641
644
  value:getTaskById
645
+ value:getTaskLineage
642
646
  value:getTaskStats
643
647
  value:getTasksByAgentId
644
648
  value:getTasksByStatus
@@ -134,6 +134,7 @@ value:ReservedAgentRoleError
134
134
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
135
135
  value:SKILL_FILE_LIMITS
136
136
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
137
+ value:TASK_LINEAGE_LIMITS
137
138
  value:UNATTRIBUTED_USER_ID
138
139
  value:__resetSqliteVecExtensionPathCacheForTests
139
140
  value:abandonSlackOutcomeDelivery
@@ -435,6 +436,7 @@ value:getSystemDefaultSkills
435
436
  value:getTaskAttachments
436
437
  value:getTaskById
437
438
  value:getTaskByWorkflowRunStepId
439
+ value:getTaskLineage
438
440
  value:getTaskStats
439
441
  value:getTasksByAgentId
440
442
  value:getTasksByStatus
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(436);
107
- expect(generatedKeys).toHaveLength(436);
106
+ expect(operationKeys).toHaveLength(437);
107
+ expect(generatedKeys).toHaveLength(437);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -209,11 +209,37 @@ describe("GET /api/fleet-benchmark", () => {
209
209
  }
210
210
  }
211
211
 
212
- test("telemetry off: no proxy call and no local scan", async () => {
212
+ test("telemetry off: local 30-day counts without an ingest anchor or proxy call", async () => {
213
213
  process.env.ANONYMIZED_TELEMETRY = "false";
214
+ await db.getDbClient().run("DELETE FROM swarm_config WHERE key = 'telemetry_first_ingest'");
215
+ await addTasks(9, "completed", 3);
216
+ await addTasks(1, "failed", 3);
217
+ await addTasks(5, "pending", 3);
218
+ await addTasks(40, "completed", 45);
219
+ await setProfile();
214
220
  const response = await buildFleetBenchmarkResponse(NOW);
215
221
  expect(response.state).toBe("telemetry_off");
216
- expect(response.local).toBeNull();
222
+ expect(response.local).toEqual({
223
+ tasksPerWeek: 3.5,
224
+ taskSuccessRate: 0.9,
225
+ finishedTasks: 10,
226
+ agents: 3,
227
+ bucket: "2-5",
228
+ });
229
+ expect(response.level).toBe("full");
230
+ expect(response.peerGroup).toBeNull();
231
+ expect(response.comparisons).toEqual([]);
232
+ expect(fetched).toEqual([]);
233
+ });
234
+
235
+ test("telemetry off on a fresh swarm returns zero counts", async () => {
236
+ process.env.ANONYMIZED_TELEMETRY = "false";
237
+ await db.getDbClient().run("DELETE FROM swarm_config WHERE key = 'telemetry_first_ingest'");
238
+ expect(await buildFleetBenchmarkResponse(NOW)).toMatchObject({
239
+ state: "telemetry_off",
240
+ level: "basic",
241
+ local: { tasksPerWeek: 0, taskSuccessRate: null, finishedTasks: 0 },
242
+ });
217
243
  expect(fetched).toEqual([]);
218
244
  });
219
245
 
@@ -227,6 +253,19 @@ describe("GET /api/fleet-benchmark", () => {
227
253
  expect(fetched).toEqual([]);
228
254
  });
229
255
 
256
+ test("an empty stored name overrides the deployment name", async () => {
257
+ const saved = process.env.SWARM_ORG_NAME;
258
+ process.env.SWARM_ORG_NAME = "Deployment name";
259
+ try {
260
+ await db.upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_NAME", value: "" });
261
+ expect((await buildFleetBenchmarkResponse(NOW)).state).toBe("no_org_name");
262
+ expect(fetched).toEqual([]);
263
+ } finally {
264
+ if (saved === undefined) delete process.env.SWARM_ORG_NAME;
265
+ else process.env.SWARM_ORG_NAME = saved;
266
+ }
267
+ });
268
+
230
269
  test("fewer than 10 finished tasks: local numbers only", async () => {
231
270
  await addTasks(9, "completed", 3);
232
271
  await addTasks(5, "pending", 3);
@@ -242,6 +281,8 @@ describe("GET /api/fleet-benchmark", () => {
242
281
  await addTasks(40, "completed", 45); // Outside the window.
243
282
  const response = await buildFleetBenchmarkResponse(NOW);
244
283
  expect(response.state).toBe("ready");
284
+ expect(response.level).toBe("basic");
285
+ expect(response.peerGroup).toBeNull();
245
286
  expect(response.local).toEqual({
246
287
  tasksPerWeek: 14,
247
288
  taskSuccessRate: 0.95,
@@ -259,6 +300,85 @@ describe("GET /api/fleet-benchmark", () => {
259
300
  expect(fetched).toHaveLength(1);
260
301
  });
261
302
 
303
+ async function setProfile(teamSize = "2-10", useCase = "features"): Promise<void> {
304
+ await db.upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "acme.com" });
305
+ await db.upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_TEAM_SIZE", value: teamSize });
306
+ await db.upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_USE_CASE", value: useCase });
307
+ }
308
+
309
+ test("full profile requests peers and returns the proxy group", async () => {
310
+ await addTasks(10, "completed", 1);
311
+ await setProfile();
312
+ proxyBody = {
313
+ ...FLEET,
314
+ peer_group: { team_size: "2-10", use_case: "features", orgs: 6 },
315
+ };
316
+ const response = await buildFleetBenchmarkResponse(NOW);
317
+ expect(response).toMatchObject({
318
+ state: "ready",
319
+ level: "full",
320
+ peerGroup: { teamSize: "2-10", useCase: "features", orgs: 6 },
321
+ });
322
+ expect(fetched).toEqual([
323
+ "http://proxy.test/v1/fleet/benchmarks?window=30d&team_size=2-10&use_case=features",
324
+ ]);
325
+ await buildFleetBenchmarkResponse(NOW);
326
+ expect(fetched).toHaveLength(1);
327
+ });
328
+
329
+ test("full profile tolerates absent or null peer_group from the proxy", async () => {
330
+ await addTasks(10, "completed", 1);
331
+ await setProfile();
332
+ for (const peer_group of [undefined, null]) {
333
+ proxyBody = { ...FLEET, peer_group };
334
+ const route = await freshRoute();
335
+ const response = await route.buildFleetBenchmarkResponse(NOW);
336
+ expect(response).toMatchObject({ state: "ready", level: "full", peerGroup: null });
337
+ }
338
+ });
339
+
340
+ test("cache follows profile changes and cleared fields return to basic", async () => {
341
+ await addTasks(10, "completed", 1);
342
+ await buildFleetBenchmarkResponse(NOW);
343
+ await setProfile();
344
+ await buildFleetBenchmarkResponse(NOW);
345
+ await setProfile("11-50", "bugs");
346
+ await buildFleetBenchmarkResponse(NOW);
347
+ await setProfile("11-50", "review");
348
+ await buildFleetBenchmarkResponse(NOW);
349
+ expect(fetched).toHaveLength(4);
350
+ expect(fetched[2]).toContain("team_size=11-50&use_case=bugs");
351
+ expect(fetched[3]).toContain("team_size=11-50&use_case=review");
352
+ await db.upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "" });
353
+ const response = await buildFleetBenchmarkResponse(NOW);
354
+ expect(response.level).toBe("basic");
355
+ expect(fetched.at(-1)).toBe("http://proxy.test/v1/fleet/benchmarks?window=30d");
356
+ });
357
+
358
+ test("partial profile stays basic and sends no peer filters", async () => {
359
+ await addTasks(10, "completed", 1);
360
+ for (const key of ["SWARM_ORG_WEBSITE", "SWARM_ORG_TEAM_SIZE", "SWARM_ORG_USE_CASE"]) {
361
+ await setProfile();
362
+ await db.upsertSwarmConfig({ scope: "global", key, value: "" });
363
+ const route = await freshRoute();
364
+ expect((await route.buildFleetBenchmarkResponse(NOW)).level).toBe("basic");
365
+ expect(fetched.at(-1)).toBe("http://proxy.test/v1/fleet/benchmarks?window=30d");
366
+ }
367
+ });
368
+
369
+ test("enabling telemetry never reuses local-only counts for comparison", async () => {
370
+ await addTasks(10, "completed", 1);
371
+ await db.getDbClient().run("DELETE FROM swarm_config WHERE key = 'telemetry_first_ingest'");
372
+ process.env.ANONYMIZED_TELEMETRY = "false";
373
+ expect((await buildFleetBenchmarkResponse(NOW)).local?.finishedTasks).toBe(10);
374
+ process.env.ANONYMIZED_TELEMETRY = "true";
375
+ expect(await buildFleetBenchmarkResponse(NOW)).toMatchObject({
376
+ state: "unavailable",
377
+ local: null,
378
+ });
379
+ expect(fetched).toEqual([]);
380
+ });
381
+
262
382
  async function mintInstallId(daysAgo: number): Promise<void> {
263
383
  await db
264
384
  .getDbClient()
@@ -160,12 +160,39 @@ describe("GET /api/users", () => {
160
160
  });
161
161
 
162
162
  describe("POST /api/users", () => {
163
+ test("email is optional, and valid email addresses are accepted", async () => {
164
+ for (const email of [undefined, "taras+swarm@example.com"]) {
165
+ const response = await authedFetch("/api/users", {
166
+ method: "POST",
167
+ body: JSON.stringify({ name: "Optional email", email, role: "engineer" }),
168
+ });
169
+ expect(response.status).toBe(200);
170
+ const { user } = await response.json();
171
+ expect(user.email ?? null).toBe(email ?? null);
172
+ expect(user.role).toBe("engineer");
173
+ }
174
+ });
175
+
176
+ test("invalid email is rejected before a user is created", async () => {
177
+ for (const email of ["", "not-an-email", "tester@dev", null]) {
178
+ const response = await authedFetch("/api/users", {
179
+ method: "POST",
180
+ body: JSON.stringify({ name: "Invalid email", email }),
181
+ });
182
+ expect(response.status).toBe(400);
183
+ expect(await response.text()).toContain("email");
184
+ }
185
+ expect(await getDbClient().get<{ n: number }>("SELECT COUNT(*) AS n FROM users")).toEqual({
186
+ n: 0,
187
+ });
188
+ });
189
+
163
190
  test("creates user + links identities + budget event, all tagged op:<fp>", async () => {
164
191
  const r = await authedFetch("/api/users", {
165
192
  method: "POST",
166
193
  body: JSON.stringify({
167
194
  name: "Tester",
168
- email: "tester@dev",
195
+ email: "tester@dev.example",
169
196
  dailyBudgetUsd: 5,
170
197
  identities: [
171
198
  { kind: "slack", externalId: "U_QA1" },
@@ -197,6 +224,40 @@ describe("POST /api/users", () => {
197
224
  });
198
225
 
199
226
  describe("PATCH /api/users/:id", () => {
227
+ test("validates email when present and permits profile updates without email", async () => {
228
+ const user = await createUser({ name: "Email validation", email: "old@example.com" });
229
+ for (const email of ["", "invalid", "tester@dev"]) {
230
+ const response = await authedFetch(`/api/users/${user.id}`, {
231
+ method: "PATCH",
232
+ body: JSON.stringify({ email }),
233
+ });
234
+ expect(response.status).toBe(400);
235
+ expect(await response.text()).toContain("email");
236
+ }
237
+ const unchanged = await authedFetch(`/api/users/${user.id}`);
238
+ expect((await unchanged.json()).user.email).toBe("old@example.com");
239
+ for (const patch of [{ role: "founder" }, { email: "new+swarm@example.com" }]) {
240
+ const response = await authedFetch(`/api/users/${user.id}`, {
241
+ method: "PATCH",
242
+ body: JSON.stringify(patch),
243
+ });
244
+ expect(response.status).toBe(200);
245
+ expect((await response.json()).user).toMatchObject(patch);
246
+ }
247
+ });
248
+
249
+ test("null clears the primary email", async () => {
250
+ const user = await createUser({ name: "Clear email", email: "clear@example.com" });
251
+ const response = await authedFetch(`/api/users/${user.id}`, {
252
+ method: "PATCH",
253
+ body: JSON.stringify({ email: null }),
254
+ });
255
+ expect(response.status).toBe(200);
256
+ expect((await response.json()).user.email ?? null).toBeNull();
257
+ const reloaded = await authedFetch(`/api/users/${user.id}`);
258
+ expect((await reloaded.json()).user.email ?? null).toBeNull();
259
+ });
260
+
200
261
  test("dailyBudgetUsd mirrors into user-scoped budgets and null removes the mirror", async () => {
201
262
  const create = await authedFetch("/api/users", {
202
263
  method: "POST",
@@ -1,5 +1,6 @@
1
1
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
+ import { reasoningLevelsForModel } from "@desplega/model-catalog";
3
4
  import { closeDb, getActivePricingRow, getDbClient, initDb } from "../be/db";
4
5
  import {
5
6
  listModelCatalogOverlay,
@@ -10,6 +11,7 @@ import {
10
11
  import { getModelsCatalog, resetModelsCatalogForTests } from "../be/models-catalog";
11
12
  import type { ModelsDevCache } from "../be/modelsdev-cache";
12
13
  import { MODEL_CATALOG_FRESH_MS, refreshModelCatalog } from "../be/pricing-refresh";
14
+ import { getContextWindowSize } from "../utils/context-window";
13
15
 
14
16
  const TEST_DB_PATH = "./test-model-catalog-table.sqlite";
15
17
 
@@ -212,6 +214,67 @@ describe("persistent model catalog", () => {
212
214
  expect(model?.name).toBe("GPT Base");
213
215
  });
214
216
 
217
+ test("opencode-go lands in the catalog, context windows and effort gating", async () => {
218
+ const cache: ModelsDevCache = {
219
+ "opencode-go": {
220
+ id: "opencode-go",
221
+ name: "OpenCode Go",
222
+ models: {
223
+ "deepseek-v4.1-flash": {
224
+ id: "deepseek-v4.1-flash",
225
+ name: "DeepSeek V4.1 Flash",
226
+ limit: { context: 1_000_000 },
227
+ reasoning: true,
228
+ reasoning_options: [{ type: "effort", values: ["low", "high", "max"] }],
229
+ },
230
+ },
231
+ },
232
+ };
233
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(cache, '"og-v1"') });
234
+
235
+ const catalog = await loadModelsCatalog();
236
+ expect(catalog.providers["opencode-go"]?.models["deepseek-v4.1-flash"]?.limit?.context).toBe(
237
+ 1_000_000,
238
+ );
239
+ // Before the projection covered opencode-go this fell back to 200k.
240
+ expect(getContextWindowSize("opencode-go/deepseek-v4.1-flash")).toBe(1_000_000);
241
+ // opencode keeps the model's own `max`; the catalog now carries the facts.
242
+ expect(
243
+ reasoningLevelsForModel("opencode", "opencode-go/deepseek-v4.1-flash", catalog.providers),
244
+ ).toEqual(["low", "high", "max"]);
245
+ });
246
+
247
+ test("an opencode-go overlay expires once upstream carries the projected facts", async () => {
248
+ const cache: ModelsDevCache = {
249
+ "opencode-go": {
250
+ id: "opencode-go",
251
+ name: "OpenCode Go",
252
+ models: {
253
+ "deepseek-v4.1-flash": {
254
+ id: "deepseek-v4.1-flash",
255
+ limit: { context: 1_000_000 },
256
+ reasoning: true,
257
+ },
258
+ },
259
+ },
260
+ };
261
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(cache, '"og-v1"') });
262
+ await upsertModelCatalogOverlay({
263
+ provider: "opencode-go",
264
+ modelId: "deepseek-v4.1-flash",
265
+ contextWindow: 1_000_000,
266
+ reason: "hand-verified until models.dev caught up",
267
+ });
268
+
269
+ await refreshModelCatalog({ force: true, now: 2_000, fetchImpl: ok(cache, '"og-v2"') });
270
+
271
+ expect((await listModelCatalogOverlay()).map((o) => `${o.provider}/${o.modelId}`)).toEqual([]);
272
+ const model = (await loadModelsCatalog()).providers["opencode-go"]?.models[
273
+ "deepseek-v4.1-flash"
274
+ ];
275
+ expect(model?.limit?.context).toBe(1_000_000);
276
+ });
277
+
215
278
  test("overlay auto-expires once upstream matches", async () => {
216
279
  await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
217
280
  await upsertModelCatalogOverlay({
@@ -228,14 +228,26 @@ describe("profile CRUD", () => {
228
228
  (await call("PATCH", "/api/model-profiles/oc-off", { description: "kept" })).status,
229
229
  ).toBe(200);
230
230
 
231
+ // opencode passes the model's own declared efforts through, `max` included.
232
+ expect(
233
+ (await call("POST", "/api/model-profiles", { ...body, name: "oc-max", effort: "max" }))
234
+ .status,
235
+ ).toBe(201);
236
+
231
237
  // Control: a level this model does not list is still refused, on both verbs.
232
- const max = await call("POST", "/api/model-profiles", {
233
- ...body,
234
- name: "oc-max",
235
- effort: "max",
236
- });
237
- expect(max.status).toBe(400);
238
- expect((await call("PATCH", "/api/model-profiles/oc-low", { effort: "max" })).status).toBe(400);
238
+ // `openai/gpt-5.1-codex` declares low|medium|high, so it accepts no `max`.
239
+ const noMax = { harness: "opencode", model: "openai/gpt-5.1-codex" };
240
+ expect(
241
+ (await call("POST", "/api/model-profiles", { ...noMax, name: "oc-nomax", effort: "max" }))
242
+ .status,
243
+ ).toBe(400);
244
+ expect(
245
+ (await call("POST", "/api/model-profiles", { ...noMax, name: "oc-capped", effort: "low" }))
246
+ .status,
247
+ ).toBe(201);
248
+ expect((await call("PATCH", "/api/model-profiles/oc-capped", { effort: "max" })).status).toBe(
249
+ 400,
250
+ );
239
251
  });
240
252
 
241
253
  test("a tier seed and a profile in use cannot be deleted", async () => {
@@ -74,6 +74,33 @@ describe("buildModelsCatalog", () => {
74
74
  });
75
75
  expect(catalog.openai?.models["gpt-keyed"]?.id).toBe("gpt-keyed");
76
76
  });
77
+
78
+ test("projects opencode-go (OpenCode Go) like its sibling opencode", () => {
79
+ const catalog = buildModelsCatalog({
80
+ requesty: { models: { ignored: { id: "ignored" } } },
81
+ "opencode-go": {
82
+ id: "opencode-go",
83
+ name: "OpenCode Go",
84
+ models: {
85
+ "deepseek-v4.1-flash": {
86
+ id: "deepseek-v4.1-flash",
87
+ name: "DeepSeek V4.1 Flash",
88
+ limit: { context: 1_000_000 },
89
+ reasoning: true,
90
+ reasoning_options: [{ type: "effort", values: ["low", "high", "max"] }],
91
+ },
92
+ },
93
+ },
94
+ });
95
+ // A non-picker provider is dropped; opencode-go is kept.
96
+ expect(Object.keys(catalog)).toEqual(["opencode-go"]);
97
+ expect(catalog["opencode-go"]?.models["deepseek-v4.1-flash"]).toMatchObject({
98
+ id: "deepseek-v4.1-flash",
99
+ limit: { context: 1_000_000 },
100
+ reasoning: true,
101
+ reasoning_options: [{ type: "effort", values: ["low", "high", "max"] }],
102
+ });
103
+ });
77
104
  });
78
105
 
79
106
  describe("getModelsCatalog", () => {
@@ -85,6 +112,11 @@ describe("getModelsCatalog", () => {
85
112
  expect(Object.keys(result.providers.openrouter?.models ?? {}).length).toBeGreaterThan(0);
86
113
  expect(Object.keys(result.providers.anthropic?.models ?? {}).length).toBeGreaterThan(0);
87
114
  expect(Object.keys(result.providers.opencode?.models ?? {}).length).toBeGreaterThan(0);
115
+ // OpenCode Go is a catalog provider too (its sibling `opencode` is Zen).
116
+ expect(result.providers["opencode-go"]?.name).toBe("OpenCode Go");
117
+ expect(result.providers["opencode-go"]?.models["deepseek-v4-flash"]?.limit?.context).toBe(
118
+ 1_000_000,
119
+ );
88
120
  expect(result.providers.nan?.models["glm5.3-flash"]).toBeDefined();
89
121
  expect(Object.keys(result.providers.helmcode?.models ?? {}).sort()).toEqual([
90
122
  "deepseek-v4-flash",
@@ -159,3 +191,21 @@ describe("nan and helmcode sections", () => {
159
191
  expect(getContextWindowSize("nan/qwen3.6")).toBe(262_144);
160
192
  });
161
193
  });
194
+
195
+ describe("opencode-go section", () => {
196
+ test("the snapshot gives opencode-go ids their context window, not the 200k fallback", () => {
197
+ expect(getContextWindowSize("opencode-go/deepseek-v4-flash")).toBe(1_000_000);
198
+ });
199
+
200
+ test("opencode and pi keep the model's own max for an opencode-go id", () => {
201
+ const catalog = getModelsCatalog().providers;
202
+ // `deepseek-v4-flash` declares low|high|max and both harnesses pass it through.
203
+ for (const harness of ["opencode", "pi"] as const) {
204
+ expect(reasoningLevelsForModel(harness, "opencode-go/deepseek-v4-flash", catalog)).toEqual([
205
+ "low",
206
+ "high",
207
+ "max",
208
+ ]);
209
+ }
210
+ });
211
+ });
@@ -184,6 +184,27 @@ describe("onboarding state", () => {
184
184
  expect(body.state.version).toBe(1);
185
185
  });
186
186
 
187
+ test("a stored org name completes the name step, the old default does not", async () => {
188
+ const nameStep = async () =>
189
+ ((await (await request("/api/onboarding")).json()) as { state: OnboardingState }).state.steps
190
+ .name;
191
+ const previous = process.env.SWARM_ORG_NAME;
192
+ try {
193
+ delete process.env.SWARM_ORG_NAME;
194
+ expect((await nameStep()).status).toBe("todo");
195
+ process.env.SWARM_ORG_NAME = " Your Swarm ";
196
+ expect((await nameStep()).status).toBe("todo");
197
+ process.env.SWARM_ORG_NAME = "Acme";
198
+ expect(await nameStep()).toMatchObject({ status: "done", method: "custom_name" });
199
+ // Clearing the name later never undoes a done step.
200
+ delete process.env.SWARM_ORG_NAME;
201
+ expect((await nameStep()).status).toBe("done");
202
+ } finally {
203
+ if (previous === undefined) delete process.env.SWARM_ORG_NAME;
204
+ else process.env.SWARM_ORG_NAME = previous;
205
+ }
206
+ });
207
+
187
208
  test("concurrent first GETs create state and started telemetry once", async () => {
188
209
  const responses = await Promise.all(
189
210
  Array.from({ length: 8 }, () => request("/api/onboarding")),