@desplega.ai/agent-swarm 1.165.0 → 1.167.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 (296) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
  6. package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
  9. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
  10. package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
  11. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
  12. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
  13. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
  14. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
  15. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
  19. package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
  20. package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
  23. package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
  24. package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
  25. package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
  32. package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
  33. package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
  34. package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  39. package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
  40. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  41. package/dist/cli-kw0jcfje.js +516 -0
  42. package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
  43. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  44. package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
  45. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  46. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  47. package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
  48. package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
  49. package/dist/cli-nkqjj42q.js +104 -0
  50. package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
  51. package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
  52. package/dist/cli-q1vnz8wv.js +96 -0
  53. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  54. package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
  55. package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
  56. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  57. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  58. package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
  59. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  60. package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
  61. package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
  62. package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
  63. package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/cli-wt7ccy4r.js +2654 -0
  67. package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
  68. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  69. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  70. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  71. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  72. package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
  73. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  74. package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
  75. package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
  76. package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
  77. package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
  78. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  79. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  80. package/dist/cli.js +36 -32
  81. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  82. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
  83. package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
  84. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
  85. package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
  86. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
  87. package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
  88. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  89. package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
  90. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
  91. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  92. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  93. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  94. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  95. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  96. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  97. package/dist/grok-adapter-jdt2dz2s.js +39 -0
  98. package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
  99. package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
  100. package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
  101. package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
  102. package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
  103. package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
  104. package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
  105. package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
  106. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  107. package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
  108. package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
  109. package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
  110. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  111. package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
  112. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  113. package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
  114. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
  115. package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
  116. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  117. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  118. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  119. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  120. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
  121. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  122. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
  123. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  124. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
  125. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
  126. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
  127. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
  128. package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
  129. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  130. package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
  131. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  132. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  133. package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
  134. package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
  135. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  136. package/openapi.json +5 -2
  137. package/package.json +15 -11
  138. package/src/be/automation-preflight.ts +9 -4
  139. package/src/be/boot-scrub-sweep.ts +302 -0
  140. package/src/be/crypto/key-bootstrap.ts +1 -1
  141. package/src/be/db/approvals.ts +15 -1
  142. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  143. package/src/be/db/config-secrets.ts +31 -0
  144. package/src/be/db/runtime.ts +4 -0
  145. package/src/be/db/script-runs.ts +15 -0
  146. package/src/be/db/tasks/write.ts +1 -1
  147. package/src/be/db-queries/mcp-oauth.ts +5 -0
  148. package/src/be/db-queries/oauth.ts +77 -0
  149. package/src/be/db.ts +162 -59
  150. package/src/be/events.ts +17 -1
  151. package/src/be/memory/index-content.ts +2 -2
  152. package/src/be/memory/providers/openai-embedding.ts +7 -2
  153. package/src/be/memory/providers/sqlite-store.ts +63 -11
  154. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  155. package/src/be/memory/types.ts +14 -0
  156. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  157. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  158. package/src/be/modelsdev-cache.json +61 -0
  159. package/src/be/scripts/db.ts +28 -4
  160. package/src/be/scripts/source-secrets.ts +37 -0
  161. package/src/be/scrub-json.ts +32 -0
  162. package/src/be/sealed-json.ts +59 -0
  163. package/src/be/secret-registry.ts +131 -0
  164. package/src/be/seed-pricing.ts +84 -53
  165. package/src/be/swarm-config-guard.ts +13 -0
  166. package/src/be/workflow-replay.ts +77 -0
  167. package/src/commands/provider-credentials.ts +28 -5
  168. package/src/commands/runner.ts +7 -12
  169. package/src/heartbeat/heartbeat.ts +91 -19
  170. package/src/hooks/hook.ts +72 -27
  171. package/src/http/codex-oauth-device.ts +8 -10
  172. package/src/http/db-query-bounded.ts +3 -0
  173. package/src/http/db-query-shared.ts +107 -15
  174. package/src/http/db-query.ts +10 -2
  175. package/src/http/index.ts +23 -6
  176. package/src/http/mcp.ts +5 -0
  177. package/src/http/onboarding.ts +2 -0
  178. package/src/http/script-runs.ts +9 -1
  179. package/src/http/scripts.ts +15 -0
  180. package/src/http/workflows.ts +3 -2
  181. package/src/oauth/mcp-wrapper.ts +1 -1
  182. package/src/providers/acp-adapter.ts +25 -11
  183. package/src/providers/acp-target-catalog.ts +35 -1
  184. package/src/providers/acp-targets.ts +78 -7
  185. package/src/providers/amp-adapter.ts +1 -1
  186. package/src/providers/claude-adapter.ts +26 -0
  187. package/src/providers/codex-adapter.ts +30 -3
  188. package/src/providers/codex-oauth/auth-json.ts +21 -4
  189. package/src/providers/codex-oauth/types.ts +2 -0
  190. package/src/providers/cursor-adapter.ts +1 -1
  191. package/src/providers/dsh-adapter.ts +10 -1
  192. package/src/providers/grok-adapter.ts +18 -2
  193. package/src/providers/opencode-adapter.ts +21 -5
  194. package/src/providers/pi-mono-extension.ts +56 -25
  195. package/src/providers/pricing-sources.md +2 -0
  196. package/src/script-workflows/supervisor.ts +11 -2
  197. package/src/scripts-runtime/eval-harness.ts +2 -12
  198. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  199. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  200. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  201. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  202. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  203. package/src/scripts-runtime/executors/registry.ts +2 -0
  204. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  205. package/src/scripts-runtime/script-error-hints.ts +14 -0
  206. package/src/server.ts +66 -4
  207. package/src/tests/acp-adapter.test.ts +277 -12
  208. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  209. package/src/tests/check-text-columns.test.ts +145 -0
  210. package/src/tests/ci-unit-test-report.test.ts +73 -0
  211. package/src/tests/claude-default-route.test.ts +75 -0
  212. package/src/tests/claude-hook-output.test.ts +233 -0
  213. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  214. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  215. package/src/tests/codex-adapter.test.ts +166 -26
  216. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  217. package/src/tests/codex-oauth-device.test.ts +23 -37
  218. package/src/tests/codex-oauth.test.ts +4 -1
  219. package/src/tests/context-window.test.ts +2 -2
  220. package/src/tests/credential-check.test.ts +51 -1
  221. package/src/tests/db-query-denylist.test.ts +202 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/db-task-write-contract.test.ts +1 -0
  224. package/src/tests/dsh-adapter.test.ts +23 -1
  225. package/src/tests/enabled-tools.test.ts +269 -0
  226. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  227. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  228. package/src/tests/events-scrub.test.ts +88 -0
  229. package/src/tests/fixtures/secret-corpus.ts +320 -0
  230. package/src/tests/grok-adapter.test.ts +13 -0
  231. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  232. package/src/tests/heartbeat-checklist.test.ts +158 -1
  233. package/src/tests/onboarding.test.ts +44 -0
  234. package/src/tests/opencode-adapter.test.ts +66 -3
  235. package/src/tests/opencode-plugin.test.ts +37 -0
  236. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  237. package/src/tests/openrouter-base-url.test.ts +30 -39
  238. package/src/tests/pi-mono-extension.test.ts +161 -2
  239. package/src/tests/pricing-refresh.test.ts +70 -1
  240. package/src/tests/reasoning-effort.test.ts +3 -1
  241. package/src/tests/script-connections.test.ts +5 -0
  242. package/src/tests/script-executor-conformance.test.ts +3 -0
  243. package/src/tests/script-executor-quickjs.test.ts +442 -0
  244. package/src/tests/script-executor-registry.test.ts +6 -1
  245. package/src/tests/script-runs-http.test.ts +88 -1
  246. package/src/tests/script-runs-scrub.test.ts +275 -0
  247. package/src/tests/scripts-http.test.ts +82 -1
  248. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  249. package/src/tests/sealed-key-guard.test.ts +166 -0
  250. package/src/tests/secret-canary-sweep.test.ts +958 -0
  251. package/src/tests/secret-registry.test.ts +355 -0
  252. package/src/tests/secret-rules.test.ts +230 -0
  253. package/src/tests/secret-scrubber.test.ts +3 -2
  254. package/src/tests/telemetry-trigger.test.ts +12 -1
  255. package/src/tests/workflow-llm.test.ts +14 -0
  256. package/src/tests/workflow-replay.test.ts +518 -0
  257. package/src/tests/workflow-scrub.test.ts +242 -0
  258. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  259. package/src/tools/db-query.ts +1 -1
  260. package/src/tools/inject-learning.ts +1 -1
  261. package/src/tools/request-human-input.ts +7 -2
  262. package/src/tools/workflows/get-workflow-run.ts +3 -2
  263. package/src/types.ts +1 -1
  264. package/src/utils/enabled-tools.ts +27 -0
  265. package/src/utils/internal-ai/complete-structured.ts +11 -3
  266. package/src/utils/openrouter-base-url.ts +23 -0
  267. package/src/utils/secret-rules.generated.ts +2055 -0
  268. package/src/utils/secret-scrubber.ts +363 -29
  269. package/src/workflows/engine.ts +39 -4
  270. package/src/workflows/executors/raw-llm.ts +1 -0
  271. package/src/workflows/executors/system-one-decision.ts +2 -0
  272. package/src/workflows/executors/validate.ts +1 -0
  273. package/src/workflows/executors/workflow-llm.ts +9 -2
  274. package/src/workflows/recovery.ts +10 -1
  275. package/src/workflows/resume.ts +62 -3
  276. package/src/workflows/retry-poller.ts +3 -0
  277. package/templates/skills/agent-browser/SKILL.md +2 -0
  278. package/templates/skills/agent-browser/content.md +2 -0
  279. package/templates/skills/code-quality/content.md +1 -0
  280. package/templates/skills/heartbeat-runbook/content.md +1 -1
  281. package/templates/skills/work-on-task/SKILL.md +4 -0
  282. package/templates/skills/work-on-task/content.md +4 -0
  283. package/templates/skills/workflow-iterate/content.md +9 -0
  284. package/dist/acp-adapter-w6zws6me.js +0 -23
  285. package/dist/boot-scrub-logs-246yn076.js +0 -99
  286. package/dist/cli-3a4m1aws.js +0 -572
  287. package/dist/cli-3dpj7atn.js +0 -11
  288. package/dist/cli-a87b04k8.js +0 -951
  289. package/dist/cli-cvk6yzw5.js +0 -40
  290. package/dist/cli-gvcgmx69.js +0 -183
  291. package/dist/cli-h1spv8v4.js +0 -427
  292. package/dist/cli-jqt9hrnz.js +0 -168
  293. package/dist/cli-yhmwmv0g.js +0 -9281
  294. package/dist/grok-adapter-yt9pab3k.js +0 -29
  295. package/src/be/boot-scrub-logs.ts +0 -132
  296. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -0,0 +1,390 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ BOOT_SCRUB_TARGETS,
5
+ bootScrubCursorKey,
6
+ bootScrubDoneKey,
7
+ runBootScrubSweep,
8
+ } from "../be/boot-scrub-sweep";
9
+ import { closeDb, getDb, getDbClient, initDb, isSqliteVecAvailable } from "../be/db";
10
+ import { getMemoryStore } from "../be/memory";
11
+ import { contentSha256 } from "../commands/profile-sync";
12
+ import {
13
+ refreshSecretScrubberCache,
14
+ registerVolatileSecret,
15
+ SCRUBBER_RULES_VERSION,
16
+ } from "../utils/secret-scrubber";
17
+ import { randomToken, type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
18
+
19
+ const TEST_DB_PATH = "./test-boot-scrub-sweep.sqlite";
20
+ const AGENT_ID = "5c2b0000-0000-4000-8000-000000000001";
21
+ // Distinctive non-secret words: FTS must still find the row by them after redaction.
22
+ const MARKER_WORD = "quokkazebra";
23
+
24
+ let known: SyntheticSecret;
25
+ let envName: string;
26
+ let envValue: string;
27
+ let password: string;
28
+
29
+ /** Text carrying all three secret forms: known value, env value and a `*_PASSWORD=` assignment. */
30
+ function secretText(label: string): string {
31
+ return `${label} ${MARKER_WORD} token=${known.value} env=${envValue} X_DEMO_PASSWORD=${password} end`;
32
+ }
33
+
34
+ function expectRedacted(value: string | null | undefined, where: string): void {
35
+ expect(value, where).toBeString();
36
+ for (const secret of [known.value, envValue, password]) {
37
+ expect(value!.includes(secret), `${where} still holds a secret`).toBe(false);
38
+ }
39
+ expect(value!, where).toContain("[REDACTED:");
40
+ }
41
+
42
+ async function writeAudit(): Promise<{ tbl: string; id: string }[]> {
43
+ return getDbClient().query<{ tbl: string; id: string }>("SELECT tbl, id FROM scrub_write_audit");
44
+ }
45
+
46
+ async function ftsHits(term: string): Promise<string[]> {
47
+ const rows = await getDbClient().query<{ memory_id: string }>(
48
+ "SELECT memory_id FROM memory_fts WHERE memory_fts MATCH ?",
49
+ [`"${term}"`],
50
+ );
51
+ return rows.map((r) => r.memory_id);
52
+ }
53
+
54
+ const ids = {
55
+ log: crypto.randomUUID(),
56
+ logControl: crypto.randomUUID(),
57
+ task: crypto.randomUUID(),
58
+ taskControl: crypto.randomUUID(),
59
+ event: crypto.randomUUID(),
60
+ run: crypto.randomUUID(),
61
+ step: crypto.randomUUID(),
62
+ memory: "",
63
+ };
64
+
65
+ describe("boot retro-sweep", () => {
66
+ beforeAll(async () => {
67
+ for (const suffix of ["", "-wal", "-shm"]) {
68
+ try {
69
+ await unlink(TEST_DB_PATH + suffix);
70
+ } catch {}
71
+ }
72
+ initDb(TEST_DB_PATH);
73
+ // workflow_run_steps.runId references workflow_runs; the sweep never
74
+ // touches FKs, so seed the step without building a whole workflow.
75
+ getDb().run("PRAGMA foreign_keys = OFF");
76
+
77
+ known = syntheticSecret("sweep");
78
+ envName = "X_DEMO_SWEEP_API_KEY";
79
+ envValue = randomToken(32);
80
+ process.env[envName] = envValue;
81
+ refreshSecretScrubberCache();
82
+ password = randomToken(24);
83
+
84
+ const db = getDbClient();
85
+ // Every UPDATE on a swept table lands here: proves untouched rows saw no write.
86
+ await db.run("CREATE TABLE scrub_write_audit (tbl TEXT NOT NULL, id TEXT NOT NULL)");
87
+ for (const { table } of BOOT_SCRUB_TARGETS) {
88
+ await db.run(
89
+ `CREATE TRIGGER audit_${table} AFTER UPDATE ON ${table}
90
+ BEGIN INSERT INTO scrub_write_audit (tbl, id) VALUES ('${table}', NEW.id); END`,
91
+ );
92
+ }
93
+
94
+ const now = new Date().toISOString();
95
+ await db.run(
96
+ `INSERT INTO session_logs (id, sessionId, iteration, content, lineNumber, createdAt)
97
+ VALUES (?, 's1', 1, ?, 1, ?), (?, 's1', 1, ?, 2, ?)`,
98
+ [ids.log, secretText("log"), now, ids.logControl, "plain log line, nothing to hide", now],
99
+ );
100
+ await db.run(
101
+ `INSERT INTO agent_tasks (id, task, output, failureReason, progress, createdAt, lastUpdatedAt)
102
+ VALUES (?, ?, ?, ?, ?, ?, ?), (?, 'control task', 'control output', NULL, NULL, ?, ?)`,
103
+ [
104
+ ids.task,
105
+ secretText("task"),
106
+ secretText("output"),
107
+ secretText("failure"),
108
+ secretText("progress"),
109
+ now,
110
+ now,
111
+ ids.taskControl,
112
+ now,
113
+ now,
114
+ ],
115
+ );
116
+ await db.run(
117
+ `INSERT INTO events (id, category, event, source, data, createdAt)
118
+ VALUES (?, 'test', 'test.event', 'test', ?, ?)`,
119
+ [ids.event, JSON.stringify({ note: secretText("event"), nested: { n: 1 } }), now],
120
+ );
121
+ await db.run(
122
+ `INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, input, output, error, diagnostics)
123
+ VALUES (?, ?, 'n1', 'agent-task', ?, ?, ?, ?)`,
124
+ [
125
+ ids.step,
126
+ ids.run,
127
+ JSON.stringify({ prompt: secretText("input") }),
128
+ JSON.stringify({ result: secretText("step-output") }),
129
+ secretText("error"),
130
+ JSON.stringify({ trace: secretText("diagnostics") }),
131
+ ],
132
+ );
133
+
134
+ const store = getMemoryStore();
135
+ const legacy = {
136
+ name: `note ${known.value}`,
137
+ content: secretText("memory"),
138
+ summary: secretText("summary"),
139
+ };
140
+ const memory = await store.store({
141
+ agentId: AGENT_ID,
142
+ scope: "agent",
143
+ source: "manual",
144
+ ...legacy,
145
+ });
146
+ ids.memory = memory.id;
147
+ // store() now scrubs at write; put the raw text back so the row looks like
148
+ // one written before that, which is what the sweep exists for.
149
+ await db.run(
150
+ "UPDATE agent_memory SET name = ?, content = ?, summary = ?, contentHash = ? WHERE id = ?",
151
+ [legacy.name, legacy.content, legacy.summary, contentSha256(legacy.content), memory.id],
152
+ );
153
+ await db.run(
154
+ "UPDATE agent_memory_version SET content = ?, contentHash = ? WHERE memory_id = ?",
155
+ [legacy.content, contentSha256(legacy.content), memory.id],
156
+ );
157
+ await db.run("DELETE FROM memory_fts WHERE memory_id = ?", [memory.id]);
158
+ await db.run("INSERT INTO memory_fts(memory_id, name, content) VALUES (?, ?, ?)", [
159
+ memory.id,
160
+ legacy.name,
161
+ legacy.content,
162
+ ]);
163
+ const embedding = new Float32Array(512);
164
+ embedding[0] = 1;
165
+ await store.updateEmbedding(memory.id, embedding, "test");
166
+ });
167
+
168
+ // The test preload clears volatile secrets after every test.
169
+ beforeEach(() => registerVolatileSecret(known.value, known.name));
170
+
171
+ afterAll(async () => {
172
+ delete process.env[envName];
173
+ known.cleanup();
174
+ refreshSecretScrubberCache();
175
+ closeDb();
176
+ for (const suffix of ["", "-wal", "-shm"]) {
177
+ try {
178
+ await unlink(TEST_DB_PATH + suffix);
179
+ } catch {}
180
+ }
181
+ });
182
+
183
+ test("redacts every target column, keeps context, JSON and FTS, drops the stale vector", async () => {
184
+ const db = getDbClient();
185
+ // Positive controls: the secret is really in the index and the vector exists.
186
+ expect(await ftsHits(password)).toContain(ids.memory);
187
+ const before = await db.get<{ embedding: Uint8Array | null }>(
188
+ "SELECT embedding FROM agent_memory WHERE id = ?",
189
+ [ids.memory],
190
+ );
191
+ expect(before?.embedding).not.toBeNull();
192
+ if (isSqliteVecAvailable()) {
193
+ expect(
194
+ await db.get("SELECT memory_id FROM memory_vec WHERE memory_id = ?", [ids.memory]),
195
+ ).not.toBeNull();
196
+ }
197
+
198
+ const stats = await runBootScrubSweep({ version: SCRUBBER_RULES_VERSION, reembed: false });
199
+ expect(stats).not.toBeNull();
200
+ const byTable = Object.fromEntries(stats!.map((s) => [s.table, s]));
201
+ expect(byTable.session_logs!.changed).toBe(1);
202
+ expect(byTable.agent_tasks!.changed).toBe(1);
203
+ expect(byTable.agent_memory!.changed).toBe(1);
204
+ expect(byTable.agent_memory_version!.changed).toBe(1);
205
+ expect(byTable.events!.changed).toBe(1);
206
+ expect(byTable.workflow_run_steps!.changed).toBe(1);
207
+
208
+ const log = await db.get<{ content: string }>("SELECT content FROM session_logs WHERE id = ?", [
209
+ ids.log,
210
+ ]);
211
+ expectRedacted(log?.content, "session_logs.content");
212
+ expect(log!.content).toContain(MARKER_WORD);
213
+
214
+ const task = await db.get<Record<string, string>>(
215
+ "SELECT task, output, failureReason, progress FROM agent_tasks WHERE id = ?",
216
+ [ids.task],
217
+ );
218
+ for (const col of ["task", "output", "failureReason", "progress"]) {
219
+ expectRedacted(task?.[col], `agent_tasks.${col}`);
220
+ }
221
+
222
+ const mem = await db.get<{
223
+ name: string;
224
+ content: string;
225
+ summary: string;
226
+ contentHash: string;
227
+ embedding: Uint8Array | null;
228
+ }>("SELECT name, content, summary, contentHash, embedding FROM agent_memory WHERE id = ?", [
229
+ ids.memory,
230
+ ]);
231
+ expectRedacted(mem?.name, "agent_memory.name");
232
+ expectRedacted(mem?.content, "agent_memory.content");
233
+ expectRedacted(mem?.summary, "agent_memory.summary");
234
+ expect(mem!.contentHash).toBe(contentSha256(mem!.content));
235
+ expect(mem!.embedding).toBeNull();
236
+ if (isSqliteVecAvailable()) {
237
+ expect(
238
+ await db.get("SELECT memory_id FROM memory_vec WHERE memory_id = ?", [ids.memory]),
239
+ ).toBeNull();
240
+ }
241
+ // FTS was rewritten from the scrubbed text, not left stale.
242
+ expect(await ftsHits(MARKER_WORD)).toContain(ids.memory);
243
+ expect(await ftsHits(password)).not.toContain(ids.memory);
244
+
245
+ const version = await db.get<{ content: string; contentHash: string }>(
246
+ "SELECT content, contentHash FROM agent_memory_version WHERE memory_id = ?",
247
+ [ids.memory],
248
+ );
249
+ expectRedacted(version?.content, "agent_memory_version.content");
250
+ expect(version!.contentHash).toBe(contentSha256(version!.content));
251
+
252
+ const event = await db.get<{ data: string }>("SELECT data FROM events WHERE id = ?", [
253
+ ids.event,
254
+ ]);
255
+ expectRedacted(event?.data, "events.data");
256
+ const parsed = JSON.parse(event!.data) as { note: string; nested: { n: number } };
257
+ expect(parsed.nested.n).toBe(1);
258
+ expect(parsed.note).toContain(MARKER_WORD);
259
+
260
+ const step = await db.get<Record<string, string>>(
261
+ "SELECT input, output, error, diagnostics FROM workflow_run_steps WHERE id = ?",
262
+ [ids.step],
263
+ );
264
+ for (const col of ["input", "output", "error", "diagnostics"]) {
265
+ expectRedacted(step?.[col], `workflow_run_steps.${col}`);
266
+ }
267
+ for (const col of ["input", "output", "diagnostics"]) {
268
+ expect(() => JSON.parse(step![col]!)).not.toThrow();
269
+ }
270
+ });
271
+
272
+ test("control rows without a secret see no write", async () => {
273
+ const audit = await writeAudit();
274
+ // Positive control: the trigger fired for the rows that held a secret.
275
+ expect(audit).toContainEqual({ tbl: "session_logs", id: ids.log });
276
+ expect(audit).toContainEqual({ tbl: "agent_tasks", id: ids.task });
277
+ // The controls were never written.
278
+ expect(audit.map((a) => a.id)).not.toContain(ids.logControl);
279
+ expect(audit.map((a) => a.id)).not.toContain(ids.taskControl);
280
+ const control = await getDbClient().get<{ content: string }>(
281
+ "SELECT content FROM session_logs WHERE id = ?",
282
+ [ids.logControl],
283
+ );
284
+ expect(control?.content).toBe("plain log line, nothing to hide");
285
+ });
286
+
287
+ test("marks the version done and is a no-op on rerun; a new version runs again", async () => {
288
+ const db = getDbClient();
289
+ expect(
290
+ await db.get("SELECT key FROM seed_state WHERE kind = 'maintenance' AND key = ?", [
291
+ bootScrubDoneKey(SCRUBBER_RULES_VERSION),
292
+ ]),
293
+ ).not.toBeNull();
294
+ const cursors = await db.query("SELECT key FROM seed_state WHERE key LIKE 'boot-scrub-v%:%'");
295
+ expect(cursors).toHaveLength(0);
296
+
297
+ const auditBefore = (await writeAudit()).length;
298
+ expect(await runBootScrubSweep({ version: SCRUBBER_RULES_VERSION, reembed: false })).toBeNull();
299
+
300
+ const bumped = SCRUBBER_RULES_VERSION + 100;
301
+ const stats = await runBootScrubSweep({ version: bumped, reembed: false });
302
+ expect(stats).not.toBeNull();
303
+ expect(stats!.every((s) => s.changed === 0)).toBe(true);
304
+ expect(stats!.find((s) => s.table === "session_logs")!.scanned).toBeGreaterThanOrEqual(2);
305
+ expect((await writeAudit()).length).toBe(auditBefore);
306
+ expect(
307
+ await db.get("SELECT key FROM seed_state WHERE kind = 'maintenance' AND key = ?", [
308
+ bootScrubDoneKey(bumped),
309
+ ]),
310
+ ).not.toBeNull();
311
+ });
312
+
313
+ test("an interrupted run resumes from the cursor without rescanning batch 1", async () => {
314
+ const db = getDbClient();
315
+ const now = new Date().toISOString();
316
+ for (let i = 0; i < 5; i++) {
317
+ await db.run(
318
+ `INSERT INTO session_logs (id, sessionId, iteration, content, lineNumber, createdAt)
319
+ VALUES (?, 's2', 1, ?, ?, ?)`,
320
+ [crypto.randomUUID(), secretText(`resume-${i}`), 10 + i, now],
321
+ );
322
+ }
323
+ const version = SCRUBBER_RULES_VERSION + 200;
324
+ let firstBatch: string[] = [];
325
+ await expect(
326
+ runBootScrubSweep({
327
+ version,
328
+ batchSize: 2,
329
+ reembed: false,
330
+ afterBatch: ({ table, ids: batchIds }) => {
331
+ if (table !== "session_logs") return;
332
+ firstBatch = batchIds;
333
+ throw new Error("interrupted after batch 1");
334
+ },
335
+ }),
336
+ ).rejects.toThrow("interrupted after batch 1");
337
+ expect(firstBatch).toHaveLength(2);
338
+
339
+ const cursor = await db.get<{ seededHash: string }>(
340
+ "SELECT seededHash FROM seed_state WHERE kind = 'maintenance' AND key = ?",
341
+ [bootScrubCursorKey(version, "session_logs")],
342
+ );
343
+ expect(cursor?.seededHash).toBe(firstBatch[1]);
344
+ expect(
345
+ await db.get("SELECT key FROM seed_state WHERE key = ?", [bootScrubDoneKey(version)]),
346
+ ).toBeNull();
347
+
348
+ const resumed: string[] = [];
349
+ const stats = await runBootScrubSweep({
350
+ version,
351
+ batchSize: 2,
352
+ reembed: false,
353
+ afterBatch: ({ table, ids: batchIds }) => {
354
+ if (table === "session_logs") resumed.push(...batchIds);
355
+ },
356
+ });
357
+ expect(resumed.length).toBeGreaterThan(0);
358
+ for (const id of firstBatch) expect(resumed).not.toContain(id);
359
+ const total = await db.get<{ n: number }>("SELECT COUNT(*) AS n FROM session_logs");
360
+ expect(stats!.find((s) => s.table === "session_logs")!.scanned).toBe(total!.n - 2);
361
+
362
+ // Batch 1 was committed before the interrupt, the rest by the resumed run.
363
+ const remaining = await db.query<{ content: string }>(
364
+ "SELECT content FROM session_logs WHERE sessionId = 's2'",
365
+ );
366
+ for (const row of remaining) expectRedacted(row.content, "resumed session_logs.content");
367
+ });
368
+
369
+ test("skips a row whose scrub would break valid JSON", async () => {
370
+ const db = getDbClient();
371
+ const id = crypto.randomUUID();
372
+ const data = JSON.stringify({ msg: "BREAKJSON here" });
373
+ await db.run(
374
+ `INSERT INTO events (id, category, event, source, data, createdAt)
375
+ VALUES (?, 'test', 'test.event', 'test', ?, ?)`,
376
+ [id, data, new Date().toISOString()],
377
+ );
378
+ // Force a scrub result that is not valid JSON (an unbalanced quote).
379
+ const stats = await runBootScrubSweep({
380
+ version: SCRUBBER_RULES_VERSION + 300,
381
+ reembed: false,
382
+ scrub: (text) => text.replace("BREAKJSON", '"'),
383
+ });
384
+ const events = stats!.find((s) => s.table === "events")!;
385
+ expect(events.skippedInvalidJson).toBe(1);
386
+ expect(events.changed).toBe(0);
387
+ const row = await db.get<{ data: string }>("SELECT data FROM events WHERE id = ?", [id]);
388
+ expect(row?.data).toBe(data);
389
+ });
390
+ });
@@ -0,0 +1,145 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3
+ import { unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import {
6
+ checkTextColumns,
7
+ listTextColumns,
8
+ TEXT_COLUMNS_PATH,
9
+ type TextColumnClassification,
10
+ } from "../../scripts/check-text-columns";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTask,
15
+ getDbClient,
16
+ initDb,
17
+ updateTaskProgress,
18
+ } from "../be/db";
19
+ import { runMigrations } from "../be/migrations/runner";
20
+ import { type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
21
+
22
+ const repoRoot = join(import.meta.dir, "..", "..");
23
+
24
+ async function loadCommitted(): Promise<TextColumnClassification> {
25
+ return (await Bun.file(join(repoRoot, TEXT_COLUMNS_PATH)).json()) as TextColumnClassification;
26
+ }
27
+
28
+ describe("TEXT-column classification check", () => {
29
+ let db: Database;
30
+
31
+ beforeAll(() => {
32
+ db = new Database(":memory:");
33
+ const originalLog = console.log;
34
+ console.log = () => {};
35
+ try {
36
+ runMigrations(db);
37
+ } finally {
38
+ console.log = originalLog;
39
+ }
40
+ });
41
+
42
+ afterAll(() => {
43
+ db.close();
44
+ });
45
+
46
+ test("the committed classification covers the migrated schema exactly", async () => {
47
+ const classification = await loadCommitted();
48
+ expect(listTextColumns(db).length).toBeGreaterThan(100);
49
+ expect(checkTextColumns(db, classification)).toEqual([]);
50
+ });
51
+
52
+ test("a new TEXT column from a later migration fails until it is classified", async () => {
53
+ const classification = await loadCommitted();
54
+ const scratch = new Database(":memory:");
55
+ try {
56
+ const originalLog = console.log;
57
+ console.log = () => {};
58
+ try {
59
+ runMigrations(scratch);
60
+ } finally {
61
+ console.log = originalLog;
62
+ }
63
+ // Stand-in for a future migration that adds a free-text sink.
64
+ scratch.run("CREATE TABLE canary_notes (id TEXT PRIMARY KEY, body TEXT NOT NULL, n INTEGER)");
65
+
66
+ const violations = checkTextColumns(scratch, classification);
67
+ expect(violations).toEqual([
68
+ "canary_notes.id: unclassified TEXT column",
69
+ "canary_notes.body: unclassified TEXT column",
70
+ ]);
71
+
72
+ const classified: TextColumnClassification = {
73
+ ...classification,
74
+ canary_notes: { id: { exempt: "identifier" }, body: { pending: "test batch" } },
75
+ };
76
+ expect(checkTextColumns(scratch, classified)).toEqual([]);
77
+ } finally {
78
+ scratch.close();
79
+ }
80
+ });
81
+
82
+ test("stale and malformed entries are rejected", async () => {
83
+ const classification = await loadCommitted();
84
+ const firstTable = Object.keys(classification)[0] as string;
85
+ const firstColumn = Object.keys(classification[firstTable] ?? {})[0] as string;
86
+ const bad = {
87
+ ...classification,
88
+ [firstTable]: { ...classification[firstTable], [firstColumn]: { exempt: "" } },
89
+ gone_table: { note: "scrubbed" },
90
+ } as TextColumnClassification;
91
+
92
+ const violations = checkTextColumns(db, bad);
93
+ expect(violations).toContain("gone_table.note: stale entry, no such TEXT column");
94
+ expect(
95
+ violations.some((v) => v.startsWith(`${firstTable}.${firstColumn}: invalid entry`)),
96
+ ).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe("ScrubbedText at a production writer", () => {
101
+ const dbPath = "./test-check-text-columns-writer.sqlite";
102
+ let secret: SyntheticSecret;
103
+
104
+ async function removeDbFiles(): Promise<void> {
105
+ for (const suffix of ["", "-wal", "-shm"]) {
106
+ try {
107
+ await unlink(dbPath + suffix);
108
+ } catch {}
109
+ }
110
+ }
111
+
112
+ beforeAll(async () => {
113
+ await removeDbFiles();
114
+ initDb(dbPath);
115
+ secret = syntheticSecret("textcol");
116
+ });
117
+
118
+ afterAll(async () => {
119
+ secret.cleanup();
120
+ closeDb();
121
+ await removeDbFiles();
122
+ });
123
+
124
+ test("a column classified scrubbed holds no secret after its real writer runs", async () => {
125
+ const committed = await loadCommitted();
126
+ expect(committed.agent_tasks?.progress).toBe("scrubbed");
127
+
128
+ const agent = await createAgent({
129
+ name: "text-columns-writer",
130
+ isLead: false,
131
+ status: "idle",
132
+ capabilities: [],
133
+ });
134
+ const task = await createTask(agent.id, "classification writer check");
135
+ await updateTaskProgress(task.id, `step 2 of 3 token=${secret.value} done`);
136
+
137
+ const row = await getDbClient().get<{ progress: string }>(
138
+ "SELECT progress FROM agent_tasks WHERE id = ?",
139
+ [task.id],
140
+ );
141
+ expect(row?.progress).toContain("step 2 of 3");
142
+ expect(row?.progress).toContain(`[REDACTED:${secret.name}]`);
143
+ expect(row?.progress.includes(secret.value)).toBe(false);
144
+ });
145
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildPayload, parseJunit } from "../../scripts/ci-unit-test-report";
3
+
4
+ // Shape emitted by `bun test --reporter=junit` (bun 1.4): one testsuite per file,
5
+ // nested testsuites per describe, and attribute values XML-escaped.
6
+ const JUNIT = `<?xml version="1.0" encoding="UTF-8"?>
7
+ <testsuites name="bun test" tests="6" assertions="5" failures="1" skipped="2" time="0.3">
8
+ <testsuite name="src/a.test.ts" file="src/a.test.ts" tests="4" failures="1" skipped="1" time="0.2">
9
+ <testsuite name="outer" file="src/a.test.ts" line="3" tests="4" time="0">
10
+ <testsuite name="inner &amp; &quot;nested&quot;" file="src/a.test.ts" line="4" tests="2" time="0">
11
+ <testcase name="slow one" classname="inner" time="0.120" file="src/a.test.ts" line="5" assertions="1" />
12
+ <testcase name="breaks &lt;sometimes&gt;" classname="inner" time="0.004" file="src/a.test.ts" line="6" assertions="1">
13
+ <failure type="AssertionError" message="expect(received).toBe(expected)&#10;">AssertionError&#10;</failure>
14
+ </testcase>
15
+ </testsuite>
16
+ <testcase name="fast" classname="outer" time="0.000010" file="src/a.test.ts" line="8" assertions="1" />
17
+ <testcase name="later" classname="outer" time="0" file="src/a.test.ts" line="9" assertions="0">
18
+ <skipped message="TODO" />
19
+ </testcase>
20
+ </testsuite>
21
+ </testsuite>
22
+ <testsuite name="src/b.test.ts" file="src/b.test.ts" tests="2" failures="0" skipped="1" time="0.1">
23
+ <testcase name="top level" classname="" time="0.06" file="src/b.test.ts" line="2" assertions="2" />
24
+ <testcase name="skipped" classname="" time="0" file="src/b.test.ts" line="3" assertions="0">
25
+ <skipped />
26
+ </testcase>
27
+ </testsuite>
28
+ </testsuites>`;
29
+
30
+ describe("ci-unit-test-report", () => {
31
+ test("parses describe chains, statuses, durations and XML entities", () => {
32
+ expect(parseJunit(JUNIT)).toEqual([
33
+ {
34
+ file: "src/a.test.ts",
35
+ name: 'outer > inner & "nested" > slow one',
36
+ ms: 120,
37
+ status: "pass",
38
+ },
39
+ {
40
+ file: "src/a.test.ts",
41
+ name: 'outer > inner & "nested" > breaks <sometimes>',
42
+ ms: 4,
43
+ status: "fail",
44
+ },
45
+ { file: "src/a.test.ts", name: "outer > fast", ms: 0, status: "pass" },
46
+ { file: "src/a.test.ts", name: "outer > later", ms: 0, status: "skip" },
47
+ { file: "src/b.test.ts", name: "top level", ms: 60, status: "pass" },
48
+ { file: "src/b.test.ts", name: "skipped", ms: 0, status: "skip" },
49
+ ]);
50
+ });
51
+
52
+ test("keeps every non-pass and every test at or above minMs, with per-file totals", () => {
53
+ const payload = buildPayload(parseJunit(JUNIT), new Map([["src/a.test.ts", "abc123"]]), 50);
54
+ expect(payload.totals).toEqual({ tests: 6, pass: 3, fail: 1, skip: 2, testMs: 184 });
55
+ expect(payload.files).toEqual([
56
+ ["src/a.test.ts", "abc123", 4, 1, 124],
57
+ ["src/b.test.ts", null, 2, 0, 60],
58
+ ]);
59
+ expect(payload.tests).toEqual([
60
+ [0, 'outer > inner & "nested" > slow one', 120, "p"],
61
+ [0, 'outer > inner & "nested" > breaks <sometimes>', 4, "f"],
62
+ [0, "outer > later", 0, "s"],
63
+ [1, "top level", 60, "p"],
64
+ [1, "skipped", 0, "s"],
65
+ ]);
66
+ });
67
+
68
+ test("an empty report parses to no tests", () => {
69
+ expect(
70
+ parseJunit('<?xml version="1.0"?><testsuites name="bun test" tests="0"></testsuites>'),
71
+ ).toEqual([]);
72
+ });
73
+ });
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  buildClaudeSessionEnvironment,
14
14
  checkClaudeCredentials,
15
+ claudeOpenRouterAttributionEnv,
15
16
  resolveClaudeBinaryArgv,
16
17
  withClaudeRouteEnv,
17
18
  } from "../providers/claude-adapter";
@@ -258,3 +259,77 @@ describe("internal-ai credential", () => {
258
259
  expect(cred?.kind).toBe("anthropic");
259
260
  });
260
261
  });
262
+
263
+ describe("claude OpenRouter attribution", () => {
264
+ const OPENROUTER = {
265
+ ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
266
+ ANTHROPIC_AUTH_TOKEN: "sk-or",
267
+ };
268
+ const ATTRIBUTION = [
269
+ "HTTP-Referer: https://agent-swarm.dev",
270
+ "X-OpenRouter-Title: Agent Swarm",
271
+ "X-OpenRouter-Categories: personal-agent,cloud-agent",
272
+ ];
273
+ const spawnEnv = (env: Record<string, string>) =>
274
+ buildClaudeSessionEnvironment(
275
+ {
276
+ taskId: crypto.randomUUID(),
277
+ agentId: crypto.randomUUID(),
278
+ env,
279
+ apiUrl: "http://fixture.invalid",
280
+ apiKey: "example-fixture-swarm-key",
281
+ } as ProviderSessionConfig,
282
+ "sonnet",
283
+ "/tmp/fixture-task",
284
+ ).env;
285
+
286
+ test("an openrouter.ai base URL appends the attribution headers", () => {
287
+ expect(spawnEnv(OPENROUTER).ANTHROPIC_CUSTOM_HEADERS).toBe(ATTRIBUTION.join("\n"));
288
+ });
289
+
290
+ test("the operator's header lines are kept in order; ours replace same-name lines", () => {
291
+ const env = spawnEnv({
292
+ ...OPENROUTER,
293
+ ANTHROPIC_CUSTOM_HEADERS:
294
+ "X-Gateway-Key: secret\nhttp-referer: https://mine.example\nX-Team: a",
295
+ });
296
+ expect(env.ANTHROPIC_CUSTOM_HEADERS).toBe(
297
+ ["X-Gateway-Key: secret", "X-Team: a", ...ATTRIBUTION].join("\n"),
298
+ );
299
+ });
300
+
301
+ test("unset, Anthropic, gateway and lookalike base URLs leave ANTHROPIC_CUSTOM_HEADERS byte-identical", () => {
302
+ const operator = "X-Gateway-Key: secret\n\nX-Team: a ";
303
+ for (const base of [
304
+ {},
305
+ { ANTHROPIC_BASE_URL: "https://api.anthropic.com" },
306
+ { ANTHROPIC_BASE_URL: GATEWAY_URL },
307
+ { ANTHROPIC_BASE_URL: "https://openrouter.ai.evil.example/api" },
308
+ { ANTHROPIC_BASE_URL: "not a url" },
309
+ ]) {
310
+ expect(claudeOpenRouterAttributionEnv(base)).toEqual({});
311
+ expect(
312
+ spawnEnv({ ...base, ANTHROPIC_API_KEY: "sk-ant" }).ANTHROPIC_CUSTOM_HEADERS,
313
+ ).toBeUndefined();
314
+ expect(
315
+ spawnEnv({ ...base, ANTHROPIC_API_KEY: "sk-ant", ANTHROPIC_CUSTOM_HEADERS: operator })
316
+ .ANTHROPIC_CUSTOM_HEADERS,
317
+ ).toBe(operator);
318
+ }
319
+ });
320
+
321
+ test("OPENROUTER_APP_ATTRIBUTION=false leaves the operator's value untouched", () => {
322
+ for (const off of ["false", "0", "FALSE"]) {
323
+ expect(
324
+ spawnEnv({
325
+ ...OPENROUTER,
326
+ OPENROUTER_APP_ATTRIBUTION: off,
327
+ ANTHROPIC_CUSTOM_HEADERS: "X-A: b",
328
+ }).ANTHROPIC_CUSTOM_HEADERS,
329
+ ).toBe("X-A: b");
330
+ expect(
331
+ spawnEnv({ ...OPENROUTER, OPENROUTER_APP_ATTRIBUTION: off }).ANTHROPIC_CUSTOM_HEADERS,
332
+ ).toBe(undefined);
333
+ }
334
+ });
335
+ });