@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
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { validateConfigValue } from "../be/swarm-config-guard";
3
3
  import { buildContext } from "../telemetry-context";
4
+ import {
5
+ COMPANY_ACQUISITION_SOURCES,
6
+ COMPANY_TEAM_SIZES,
7
+ COMPANY_USE_CASES,
8
+ } from "../utils/company-profile";
4
9
  import {
5
10
  isFilledOrgLogoUrl,
6
11
  normalizeOrgWebsite,
@@ -75,6 +80,48 @@ describe("websiteSuggestion", () => {
75
80
  expect(websiteSuggestion("gmail.com")).toBeNull();
76
81
  expect(websiteSuggestion(null)).toBeNull();
77
82
  });
83
+
84
+ test("never suggests a regional or single-domain free-mail provider", () => {
85
+ for (const domain of [
86
+ "hotmail.es",
87
+ "yahoo.co.uk",
88
+ "outlook.fr",
89
+ "web.de",
90
+ "gmx.net",
91
+ "pm.me",
92
+ "mail.ru",
93
+ "live.co.uk",
94
+ "live.nl",
95
+ "yandex.com.tr",
96
+ "yandex.kz",
97
+ "icloud.com",
98
+ "me.com",
99
+ "proton.me",
100
+ "protonmail.ch",
101
+ "aol.de",
102
+ "qq.com",
103
+ "163.com",
104
+ "naver.com",
105
+ "Hotmail.CO.UK",
106
+ "mail.yahoo.com",
107
+ ]) {
108
+ expect(websiteSuggestion(domain)).toBeNull();
109
+ }
110
+ });
111
+
112
+ test("keeps company domains that only look like a provider", () => {
113
+ for (const domain of [
114
+ "acme.co.uk",
115
+ "acme.com.br",
116
+ "mailchimp.com",
117
+ "live.acme.com",
118
+ "yahoo-labs.dev",
119
+ "proton.ai",
120
+ "web.dev",
121
+ ]) {
122
+ expect(websiteSuggestion(domain)).toBe(domain);
123
+ }
124
+ });
78
125
  });
79
126
 
80
127
  describe("SWARM_ORG_WEBSITE config guard", () => {
@@ -95,6 +142,21 @@ describe("SWARM_ORG_WEBSITE config guard", () => {
95
142
  });
96
143
  });
97
144
 
145
+ describe("company profile config guards", () => {
146
+ for (const [key, values] of [
147
+ ["SWARM_ORG_TEAM_SIZE", COMPANY_TEAM_SIZES],
148
+ ["SWARM_ORG_USE_CASE", COMPANY_USE_CASES],
149
+ ["SWARM_ORG_ACQUISITION_SOURCE", COMPANY_ACQUISITION_SOURCES],
150
+ ] as const) {
151
+ test(`${key} accepts its enum or empty, and rejects other values`, () => {
152
+ for (const value of ["", ...values]) expect(validateConfigValue(key, value)).toBeNull();
153
+ for (const value of ["unknown", "OTHER", "free text", null, 1, true]) {
154
+ expect(validateConfigValue(key, value)).toContain(key);
155
+ }
156
+ });
157
+ }
158
+ });
159
+
98
160
  describe("org_website in the telemetry context", () => {
99
161
  const input = {
100
162
  orgId: "org_0123456789abcdef",
@@ -17,6 +17,7 @@ import {
17
17
  checkPiMonoCredentials,
18
18
  createPiRuntimeAuth,
19
19
  extractPiAssistantText,
20
+ listPiOpencodeGoModelIds,
20
21
  PiMonoAdapter,
21
22
  resolveModel,
22
23
  type SessionModelsJson,
@@ -457,6 +458,18 @@ describe("Model name mapping", () => {
457
458
  });
458
459
  });
459
460
 
461
+ describe("listPiOpencodeGoModelIds", () => {
462
+ test("every reported opencode-go id resolves, so a picked id never falls back", () => {
463
+ const ids = listPiOpencodeGoModelIds();
464
+ expect(ids).toContain("deepseek-v4.1-flash");
465
+ for (const id of ids) {
466
+ expect(resolveModel(`opencode-go/${id}`, {})?.id).toBe(id);
467
+ }
468
+ expect(ids).not.toContain("deepseek-v4-flash");
469
+ expect(resolveModel("opencode-go/deepseek-v4-flash", {})).toBeUndefined();
470
+ });
471
+ });
472
+
460
473
  describe("resolveModel — OpenRouter reroute for anthropic shortnames", () => {
461
474
  // Regression coverage for task 37a4a87a: workers spawned with
462
475
  // `provider: pi` + `OPENROUTER_API_KEY` (no ANTHROPIC_API_KEY) and a task
@@ -546,6 +559,16 @@ describe("createPiRuntimeAuth", () => {
546
559
  });
547
560
  });
548
561
 
562
+ test("passes OPENCODE_API_KEY to pi opencode-go runtime auth", async () => {
563
+ const modelRuntime = await createPiRuntimeAuth({
564
+ OPENCODE_API_KEY: "example-opencode-runtime",
565
+ });
566
+
567
+ await expect(modelRuntime.getAuth("opencode-go")).resolves.toMatchObject({
568
+ auth: { apiKey: "example-opencode-runtime" },
569
+ });
570
+ });
571
+
549
572
  test("passes GEMINI_API_KEY to pi google runtime auth", async () => {
550
573
  const modelRuntime = await createPiRuntimeAuth({
551
574
  GEMINI_API_KEY: "example-gemini-runtime",
@@ -339,14 +339,18 @@ describe("parseGhPrCommands", () => {
339
339
  );
340
340
  expect(calls).toEqual([
341
341
  {
342
+ action: "create",
342
343
  cwd: "/tmp/wt",
343
344
  repos: ["o/r"],
345
+ resolvedRepos: ["o/r"],
344
346
  alsoCheckout: false,
345
347
  unknownTarget: false,
346
348
  texts: ["t"],
349
+ bodyTexts: [],
347
350
  bodyFiles: ["body.md"],
348
351
  unresolvedBodyFiles: [],
349
352
  opaqueBody: false,
353
+ externalBody: false,
350
354
  },
351
355
  ]);
352
356
  });
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { codexPreToolUseBlock } from "../hooks/codex-hook";
6
+ import { gitRemoteOrigin, guardGhPrRoom, type PrRoomGuardDeps } from "../hooks/pr-room-guard";
7
+ import { expectChildOk, runChild } from "./test-proc";
8
+
9
+ const ROOM_LINE = "PR room: prr-abcde23457";
10
+ const BLOCK_REASON =
11
+ 'This repo uses PR rooms. Create the room first with the pr-room skill (app action createRoom). Then pass the returned prBody with --body-file. The body must contain the line "PR room: prr-...".';
12
+
13
+ function fakeDeps(overrides: Partial<PrRoomGuardDeps> = {}): PrRoomGuardDeps {
14
+ return {
15
+ env: { PR_ROOM_REPOS: "owner/repo" },
16
+ readFile: async () => "No room marker",
17
+ remoteOrigin: async () => "https://github.com/owner/repo.git",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ const check = (command: string, deps = fakeDeps()) => guardGhPrRoom({ command }, "/w", deps);
23
+
24
+ describe("guardGhPrRoom", () => {
25
+ test.each([undefined, "", " , , "])("allows disabled configuration %p", async (value) => {
26
+ expect(
27
+ await check(
28
+ "gh pr create -R owner/repo -b plain",
29
+ fakeDeps({ env: { PR_ROOM_REPOS: value } }),
30
+ ),
31
+ ).toBeNull();
32
+ });
33
+
34
+ test("blocks a configured repo without a room marker", async () => {
35
+ expect(await check("gh pr create --repo owner/repo --body plain")).toBe(BLOCK_REASON);
36
+ expect(await check("gh pr create -Rowner/repo")).toBe(BLOCK_REASON);
37
+ });
38
+
39
+ test("allows a marker in the inline body", async () => {
40
+ expect(await check(`gh pr create -R owner/repo --body 'Summary\n${ROOM_LINE}'`)).toBeNull();
41
+ });
42
+
43
+ test("resolves body files relative to cd", async () => {
44
+ const paths: string[] = [];
45
+ const deps = fakeDeps({
46
+ readFile: async (path) => {
47
+ paths.push(path);
48
+ return ROOM_LINE;
49
+ },
50
+ });
51
+ expect(
52
+ await check("cd /checkout && gh pr create -R owner/repo --body-file body.md", deps),
53
+ ).toBeNull();
54
+ expect(paths).toEqual(["/checkout/body.md"]);
55
+ });
56
+
57
+ test("allows a heredoc body with a marker", async () => {
58
+ expect(
59
+ await check(`gh pr create -R owner/repo --body-file - <<'EOF'\n${ROOM_LINE}\nEOF`),
60
+ ).toBeNull();
61
+ });
62
+
63
+ test("blocks resolved files and heredocs without a marker", async () => {
64
+ expect(await check("gh pr create -R owner/repo -F body.md")).toBe(BLOCK_REASON);
65
+ expect(await check("gh pr create -R owner/repo -F - <<'EOF'\nplain\nEOF")).toBe(BLOCK_REASON);
66
+ });
67
+
68
+ test("allows another repo without reading its body", async () => {
69
+ expect(await check("gh pr create -R other/repo -b plain")).toBeNull();
70
+ });
71
+
72
+ test.each([
73
+ "GH_REPO=owner/repo gh pr create -b plain",
74
+ "export GH_REPO=owner/repo && gh pr create -b plain",
75
+ ])("checks command targeting: %s", async (command) => {
76
+ expect(await check(command)).toBe(BLOCK_REASON);
77
+ });
78
+
79
+ test("allows an ambiguous target unless every candidate is configured", async () => {
80
+ const otherCheckout = fakeDeps({ remoteOrigin: async () => "https://github.com/other/repo" });
81
+ expect(await check("GH_REPO=owner/repo && gh pr create -b plain", otherCheckout)).toBeNull();
82
+ expect(await check("GH_REPO=owner/repo && gh pr create -b plain")).toBe(BLOCK_REASON);
83
+ expect(await check("gh pr create -R owner/repo -R other/repo -b plain")).toBeNull();
84
+ });
85
+
86
+ test("checks inherited GH_REPO and honors an explicit override", async () => {
87
+ const deps = fakeDeps({ env: { PR_ROOM_REPOS: "owner/repo", GH_REPO: "owner/repo" } });
88
+ expect(await check("gh pr create -b plain", deps)).toBe(BLOCK_REASON);
89
+ expect(await check("gh pr create -R other/repo -b plain", deps)).toBeNull();
90
+ });
91
+
92
+ test("trims configured repos and matches case insensitively", async () => {
93
+ const deps = fakeDeps({ env: { PR_ROOM_REPOS: " other/repo, OWNER/REPO , " } });
94
+ expect(await check("gh -R Owner/Repo pr create -b plain", deps)).toBe(BLOCK_REASON);
95
+ });
96
+
97
+ test("allows edits and unrelated commands", async () => {
98
+ expect(await check("gh pr edit 123 -R owner/repo -b plain")).toBeNull();
99
+ expect(await check("gh pr view 123 -R owner/repo")).toBeNull();
100
+ expect(await guardGhPrRoom(undefined, "/w", fakeDeps())).toBeNull();
101
+ });
102
+
103
+ test.each([
104
+ "https://github.com/Owner/Repo.git",
105
+ "https://github.com/Owner/Repo",
106
+ "git@github.com:Owner/Repo.git",
107
+ "ssh://git@github.com/Owner/Repo.git",
108
+ ])("resolves checkout remote %s", async (remote) => {
109
+ const lookups: string[] = [];
110
+ const deps = fakeDeps({
111
+ remoteOrigin: async (cwd) => {
112
+ lookups.push(cwd);
113
+ return remote;
114
+ },
115
+ });
116
+ expect(await check("cd /checkout && gh pr create -b plain", deps)).toBe(BLOCK_REASON);
117
+ expect(lookups).toEqual(["/checkout"]);
118
+ });
119
+
120
+ test("allows failed remote lookup and unsupported remote forms", async () => {
121
+ expect(
122
+ await check("gh pr create -b plain", fakeDeps({ remoteOrigin: async () => "local/path" })),
123
+ ).toBeNull();
124
+ expect(
125
+ await check(
126
+ "gh pr create -b plain",
127
+ fakeDeps({
128
+ remoteOrigin: async () => {
129
+ throw new Error("no checkout");
130
+ },
131
+ }),
132
+ ),
133
+ ).toBeNull();
134
+ });
135
+
136
+ // git exports GIT_DIR to a pre-push hook in a linked worktree. The lookup then reads that repo.
137
+ test.skipIf(process.env.GIT_DIR !== undefined)(
138
+ "allows a checkout remote lookup that does not finish",
139
+ async () => {
140
+ const dir = await mkdtemp(join(tmpdir(), "pr-room-fifo-"));
141
+ try {
142
+ expectChildOk(await runChild(["git", "init", "-q", dir]), "git init");
143
+ await rm(join(dir, ".git", "config"));
144
+ expectChildOk(await runChild(["mkfifo", join(dir, ".git", "config")]), "mkfifo");
145
+ await expect(gitRemoteOrigin(dir, 200)).rejects.toThrow();
146
+ const deps = fakeDeps({ remoteOrigin: (cwd) => gitRemoteOrigin(cwd, 200) });
147
+ expect(await guardGhPrRoom({ command: "gh pr create -b plain" }, dir, deps)).toBeNull();
148
+ } finally {
149
+ await rm(dir, { recursive: true, force: true });
150
+ }
151
+ },
152
+ );
153
+
154
+ test.each([
155
+ 'gh pr create -R "$UNKNOWN" -b plain',
156
+ 'gh pr create -R owner/repo -b "$UNKNOWN"',
157
+ 'gh pr create -R owner/repo -F "$UNKNOWN"',
158
+ "cd - && gh pr create -R owner/repo -b plain",
159
+ 'cd "$UNKNOWN" && gh pr create -b plain',
160
+ "read GH_REPO && gh pr create -b plain",
161
+ ])("allows unresolved inputs: %s", async (command) => {
162
+ expect(await check(command)).toBeNull();
163
+ });
164
+
165
+ test("allows unreadable body files", async () => {
166
+ const deps = fakeDeps({
167
+ readFile: async () => {
168
+ throw new Error("unreadable");
169
+ },
170
+ });
171
+ expect(await check("gh pr create -R owner/repo -F missing.md", deps)).toBeNull();
172
+ });
173
+
174
+ test("resolves known variables without changing leak guard texts", async () => {
175
+ expect(await check('REPO=owner/repo && gh pr create -R "$REPO" -b plain')).toBe(BLOCK_REASON);
176
+ expect(await check(`BODY='${ROOM_LINE}' && gh pr create -R owner/repo -b "$BODY"`)).toBeNull();
177
+ });
178
+
179
+ test.each([
180
+ "cat room.md | gh pr create -R owner/repo -F -",
181
+ "printf plain | gh pr create -R owner/repo -F -",
182
+ "gh pr create -R owner/repo -b $(cat room.md)",
183
+ "gh pr create -R owner/repo --fill",
184
+ "gh pr create -R owner/repo -f",
185
+ "gh pr create -R owner/repo --fill-first",
186
+ "gh pr create -R owner/repo --fill-verbose",
187
+ "gh pr create -R owner/repo -T template.md -b plain",
188
+ "gh pr create -R owner/repo --template=template.md",
189
+ "gh pr create -R owner/repo -dw -b plain",
190
+ "gh pr create -R owner/repo --web",
191
+ "gh pr create -R owner/repo -e",
192
+ "gh pr create -R owner/repo --editor",
193
+ "gh pr create -R owner/repo --recover input.json",
194
+ ])("allows a body the guard cannot read: %s", async (command) => {
195
+ expect(await check(command)).toBeNull();
196
+ });
197
+
198
+ test.each([
199
+ "PR room: prr-abcde2345",
200
+ "PR room: prr-abcde234578",
201
+ "PR room: prr-ABCDE23457",
202
+ "prefix PR room: prr-abcde23457",
203
+ ])("rejects an invalid marker: %s", async (line) => {
204
+ expect(await check(`gh pr create -R owner/repo -b '${line}'`)).toBe(BLOCK_REASON);
205
+ });
206
+
207
+ test("does not accept a marker only in the title", async () => {
208
+ expect(await check(`gh pr create -R owner/repo -t '${ROOM_LINE}' -b plain`)).toBe(BLOCK_REASON);
209
+ });
210
+
211
+ test("checks every create invocation", async () => {
212
+ expect(await check(`gh pr edit 1 -b plain && gh pr create -R owner/repo -b plain`)).toBe(
213
+ BLOCK_REASON,
214
+ );
215
+ });
216
+
217
+ test("Codex runs the room guard after the leak guard, including app-server sessions", async () => {
218
+ const roomDeps = fakeDeps();
219
+ const leakDeps = {
220
+ env: { SWARM_CODEX_APP_SERVER: "1" },
221
+ readFile: roomDeps.readFile,
222
+ repoVisibility: async () => "PUBLIC",
223
+ };
224
+ const msg = {
225
+ hook_event_name: "PreToolUse",
226
+ cwd: "/w",
227
+ tool_input: { command: "gh pr create -R owner/repo -b plain" },
228
+ };
229
+ expect(await codexPreToolUseBlock(msg, leakDeps, roomDeps)).toBe(BLOCK_REASON);
230
+ const leaky = {
231
+ ...msg,
232
+ tool_input: {
233
+ command:
234
+ "gh pr create -R owner/repo -b 'https://acme.slack.com/archives/C01234567/p1700000000000000'",
235
+ },
236
+ };
237
+ expect(await codexPreToolUseBlock(leaky, leakDeps, roomDeps)).toContain("PR body leak check:");
238
+ expect(
239
+ await codexPreToolUseBlock({ ...msg, hook_event_name: "PostToolUse" }, leakDeps, roomDeps),
240
+ ).toBeNull();
241
+ });
242
+ });
@@ -2,12 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import { readdirSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { isInternalConfigKey, validateConfigValue } from "../be/swarm-config-guard";
5
- import {
6
- buildListing,
7
- mintRankingClaim,
8
- parseTelemetryRanking,
9
- rankingEntryFile,
10
- } from "../ranking/telemetry-ranking";
5
+ import { buildListing, parseTelemetryRanking } from "../ranking/telemetry-ranking";
11
6
  import { RANKING_TAXONOMY } from "../telemetry-contract/ranking-taxonomy.gen";
12
7
  import { orgIdentityStatus } from "../utils/org-identity";
13
8
 
@@ -41,7 +36,6 @@ describe("telemetry_ranking contract fixtures", () => {
41
36
 
42
37
  describe("buildListing", () => {
43
38
  const identity = orgIdentityStatus({ SWARM_ORG_NAME: "Acme", SWARM_ORG_WEBSITE: "acme.dev" });
44
- const claim = mintRankingClaim();
45
39
 
46
40
  test("copies the name and domain from the org identity and blanks to null", () => {
47
41
  const listing = buildListing(
@@ -52,12 +46,10 @@ describe("buildListing", () => {
52
46
  description: " ",
53
47
  sourceCodeUrl: null,
54
48
  },
55
- claim,
56
49
  new Date("2026-10-18T10:00:00Z"),
57
50
  );
58
51
  expect(listing).toEqual({
59
52
  v: 1,
60
- claim,
61
53
  name: "Acme",
62
54
  domain: "acme.dev",
63
55
  category: "engineering",
@@ -66,29 +58,22 @@ describe("buildListing", () => {
66
58
  sourceCodeUrl: null,
67
59
  updatedAt: "2026-10-18T10:00:00.000Z",
68
60
  });
69
- expect(JSON.parse(rankingEntryFile(listing))).toEqual({
70
- $schema: "./schema.json",
71
- domain: "acme.dev",
72
- claim,
73
- });
74
61
  });
75
62
 
76
63
  test("refuses a missing identity and an http source URL", () => {
77
64
  expect(() =>
78
- buildListing(
79
- orgIdentityStatus({ SWARM_ORG_NAME: "Swarm", SWARM_ORG_WEBSITE: "acme.dev" }),
80
- { category: "other" },
81
- claim,
82
- ),
65
+ buildListing(orgIdentityStatus({ SWARM_ORG_NAME: "Swarm", SWARM_ORG_WEBSITE: "acme.dev" }), {
66
+ category: "other",
67
+ }),
83
68
  ).toThrow("SWARM_ORG_NAME");
84
69
  expect(() =>
85
- buildListing(identity, { category: "other", sourceCodeUrl: "http://github.com/acme" }, claim),
70
+ buildListing(identity, { category: "other", sourceCodeUrl: "http://github.com/acme" }),
86
71
  ).toThrow();
87
72
  });
88
73
 
89
- test("mints rk_ + 32 hex claims", () => {
90
- expect(mintRankingClaim()).toMatch(/^rk_[0-9a-f]{32}$/);
91
- expect(mintRankingClaim()).not.toBe(mintRankingClaim());
74
+ test("a v2.4.0 listing with a claim still parses", () => {
75
+ const legacy = { ...JSON.parse(read("valid", "full.json")), claim: `rk_${"0".repeat(32)}` };
76
+ expect(parseTelemetryRanking(JSON.stringify(legacy)).claim).toBe(legacy.claim);
92
77
  });
93
78
 
94
79
  test("the taxonomy comes from the vendored contract", () => {
@@ -163,22 +163,15 @@ describe("ranking routes", () => {
163
163
  test("opt-in stores the listing, turns the setting on and emits it with a matching envelope", async () => {
164
164
  const result = await call("POST", "/api/ranking/opt-in", form);
165
165
  expect(result.statusCode).toBe(200);
166
- const claim = String(result.json.claim);
167
- expect(claim).toMatch(/^rk_[0-9a-f]{32}$/);
166
+ // Opting in is the whole flow: no claim, no entry file, no PR page.
167
+ expect(Object.keys(result.json).sort()).toEqual(["domain", "listing", "verifyUrl"]);
168
168
  expect(result.json.domain).toBe("acme.dev");
169
- expect(String(result.json.prUrl)).toContain("filename=community%2Franking%2Facme.dev.json");
170
- expect(JSON.parse(String(result.json.fileJson))).toEqual({
171
- $schema: "./schema.json",
172
- domain: "acme.dev",
173
- claim,
174
- });
175
169
  expect(String(result.json.verifyUrl)).toBe(
176
- `https://proxy.desplega.sh/v1/ranking/verify?domain=acme.dev&claim=${claim}`,
170
+ "https://proxy.desplega.sh/v1/ranking/verify?domain=acme.dev",
177
171
  );
178
172
 
179
173
  const stored = parseTelemetryRanking(String(await configValue("telemetry_ranking")));
180
174
  expect(stored).toMatchObject({
181
- claim,
182
175
  name: "Acme",
183
176
  domain: "acme.dev",
184
177
  category: "engineering",
@@ -186,6 +179,7 @@ describe("ranking routes", () => {
186
179
  description: "QA",
187
180
  sourceCodeUrl: null,
188
181
  });
182
+ expect(stored).not.toHaveProperty("claim");
189
183
  expect(await configValue("PUBLIC_RANKING_ENABLED")).toBe("true");
190
184
 
191
185
  expect(sent.map((event) => event.event)).toEqual(["ranking.opted_in"]);
@@ -193,10 +187,6 @@ describe("ranking routes", () => {
193
187
  expect(listing).toEqual(stored);
194
188
  expect(sent[0]?.context).toMatchObject({ org_name: "Acme", org_website: "acme.dev" });
195
189
 
196
- // A second opt-in for the same domain keeps the claim.
197
- const again = await call("POST", "/api/ranking/opt-in", { category: "other" });
198
- expect(again.json.claim).toBe(claim);
199
-
200
190
  const state = await call("GET", "/api/ranking");
201
191
  expect(state.json).toMatchObject({
202
192
  eligible: true,
@@ -204,10 +194,12 @@ describe("ranking routes", () => {
204
194
  missing: [],
205
195
  listingMatchesDomain: true,
206
196
  });
197
+ expect(state.json).not.toHaveProperty("prUrl");
198
+ expect(state.json).not.toHaveProperty("fileJson");
207
199
  });
208
200
 
209
- test("only a config admin reads the ranking state and its claim", async () => {
210
- const claim = String((await call("POST", "/api/ranking/opt-in", form)).json.claim);
201
+ test("only a config admin reads the ranking state", async () => {
202
+ await call("POST", "/api/ranking/opt-in", form);
211
203
  const worker: HttpRequestAuth = {
212
204
  kind: "agent",
213
205
  agentId: crypto.randomUUID(),
@@ -220,42 +212,49 @@ describe("ranking routes", () => {
220
212
  for (const auth of [worker, guest]) {
221
213
  const denied = await call("GET", "/api/ranking", undefined, handleRanking, auth);
222
214
  expect(denied.statusCode).toBe(403);
223
- expect(JSON.stringify(denied.json)).not.toContain(claim);
224
215
  }
225
- const state = await call("GET", "/api/ranking");
226
- expect(state.statusCode).toBe(200);
227
- expect((state.json.listing as { claim: string }).claim).toBe(claim);
216
+ expect((await call("GET", "/api/ranking")).statusCode).toBe(200);
228
217
  });
229
218
 
230
- test("concurrent first opt-ins share one claim, and leave then rejoin keeps it", async () => {
231
- const [first, second] = await Promise.all([
232
- call("POST", "/api/ranking/opt-in", form),
233
- call("POST", "/api/ranking/opt-in", form),
234
- ]);
235
- expect(first.statusCode).toBe(200);
236
- expect(second.statusCode).toBe(200);
237
- // Either response may be the one merged into community/ranking.
238
- const merged = String(first.json.claim);
239
- expect(second.json.claim).toBe(merged);
240
- expect(parseTelemetryRanking(String(await configValue("telemetry_ranking")))?.claim).toBe(
241
- merged,
242
- );
219
+ test("a v2.4.0 listing keeps working and its claim is never served or re-sent", async () => {
220
+ const claim = `rk_${"0".repeat(32)}`;
221
+ await upsertSwarmConfig({
222
+ scope: "global",
223
+ key: "telemetry_ranking",
224
+ value: JSON.stringify({
225
+ v: 1,
226
+ claim,
227
+ name: "Acme",
228
+ domain: "acme.dev",
229
+ category: "other",
230
+ subcategory: null,
231
+ description: null,
232
+ sourceCodeUrl: null,
233
+ updatedAt: "2026-10-18T10:00:00.000Z",
234
+ }),
235
+ });
236
+ await upsertSwarmConfig({ scope: "global", key: "PUBLIC_RANKING_ENABLED", value: "true" });
237
+ const state = await call("GET", "/api/ranking");
238
+ expect(state.json).toMatchObject({ enabled: true, listingMatchesDomain: true });
239
+ expect(JSON.stringify(state.json)).not.toContain(claim);
243
240
 
244
- expect((await call("POST", "/api/ranking/opt-out")).statusCode).toBe(200);
245
- const rejoin = await call("POST", "/api/ranking/opt-in", form);
246
- expect(rejoin.json.claim).toBe(merged);
241
+ const put = await call(
242
+ "PUT",
243
+ "/api/config",
244
+ { scope: "global", key: "SWARM_ORG_NAME", value: "Acme Labs" },
245
+ handleConfig,
246
+ );
247
+ expect(put.statusCode).toBe(200);
248
+ expect(sent.map((event) => event.event)).toEqual(["ranking.opted_in"]);
249
+ expect(String(sent[0]?.properties.listing)).not.toContain("claim");
247
250
  });
248
251
 
249
- test("opt-out stores false, emits ranking.opted_out and returns the delete page", async () => {
252
+ test("opt-out stores false and emits ranking.opted_out", async () => {
250
253
  await call("POST", "/api/ranking/opt-in", form);
251
254
  sent = [];
252
255
  const result = await call("POST", "/api/ranking/opt-out");
253
256
  expect(result.statusCode).toBe(200);
254
- expect(result.json).toEqual({
255
- enabled: false,
256
- leaveUrl:
257
- "https://github.com/desplega-ai/agent-swarm/delete/main/community/ranking/acme.dev.json",
258
- });
257
+ expect(result.json).toEqual({ enabled: false });
259
258
  expect(await configValue("PUBLIC_RANKING_ENABLED")).toBe("false");
260
259
  expect(sent.map((event) => [event.event, event.properties.domain])).toEqual([
261
260
  ["ranking.opted_out", "acme.dev"],
@@ -273,8 +272,8 @@ describe("ranking routes", () => {
273
272
  expect(String(result.json.error)).toContain("/api/ranking");
274
273
  });
275
274
 
276
- test("renaming the org re-emits the listing with the same claim", async () => {
277
- const optIn = await call("POST", "/api/ranking/opt-in", form);
275
+ test("renaming the org re-emits the listing", async () => {
276
+ await call("POST", "/api/ranking/opt-in", form);
278
277
  sent = [];
279
278
  const put = await call(
280
279
  "PUT",
@@ -287,7 +286,6 @@ describe("ranking routes", () => {
287
286
  const listing = parseTelemetryRanking(String(sent[0]?.properties.listing));
288
287
  expect(listing).toMatchObject({
289
288
  name: "Acme Labs",
290
- claim: optIn.json.claim,
291
289
  subcategory: "qa-testing",
292
290
  });
293
291
  // The envelope carries the new name before the config reload reaches process.env.
@@ -312,8 +310,12 @@ describe("ranking routes", () => {
312
310
  ["ranking.opted_out", "acme.dev"],
313
311
  ]);
314
312
  }
315
- // After the last case (a new website) the old listing needs a new PR.
313
+ // After the last case (a new website) the old listing needs a new opt-in.
316
314
  const state = await call("GET", "/api/ranking");
317
- expect(state.json).toMatchObject({ enabled: false, listingMatchesDomain: false, prUrl: null });
315
+ expect(state.json).toMatchObject({
316
+ enabled: false,
317
+ listingMatchesDomain: false,
318
+ verifyUrl: null,
319
+ });
318
320
  });
319
321
  });