@desplega.ai/agent-swarm 2.4.4 → 2.5.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 (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -0,0 +1,425 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ createUser,
8
+ deletePromptTemplate,
9
+ getDb,
10
+ getKv,
11
+ getTaskById,
12
+ initDb,
13
+ upsertPromptTemplate,
14
+ } from "../be/db";
15
+ import { linkIdentity } from "../be/users";
16
+ import { resetBitbucketBotCache } from "../bitbucket/api";
17
+ import { getBitbucketBotName, resetBitbucket } from "../bitbucket/auth";
18
+ import {
19
+ detectMention,
20
+ handlePullRequestClosed,
21
+ handlePullRequestCommentCreated,
22
+ handlePullRequestCreated,
23
+ isDuplicateDelivery,
24
+ markDeliveryProcessed,
25
+ resetBitbucketDedup,
26
+ } from "../bitbucket/handlers";
27
+ import type { PullRequestCommentEvent, PullRequestEvent } from "../bitbucket/types";
28
+ import commentSample from "./fixtures/bitbucket/pullrequest-comment-created.json";
29
+ import commentInlineSample from "./fixtures/bitbucket/pullrequest-comment-created-inline.json";
30
+ import mentionSample from "./fixtures/bitbucket/pullrequest-created-mention.json";
31
+ import reviewerSample from "./fixtures/bitbucket/pullrequest-created-reviewer.json";
32
+ import fulfilledSample from "./fixtures/bitbucket/pullrequest-fulfilled.json";
33
+ import rejectedSample from "./fixtures/bitbucket/pullrequest-rejected.json";
34
+
35
+ // Fixtures follow the documented Bitbucket Cloud event payloads
36
+ // (https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/),
37
+ // with placeholder workspace, repository and account ids.
38
+ const TEST_DB_PATH = "./test-bitbucket-handlers.sqlite";
39
+ const BOT_ACCOUNT_ID = "557058:00000000-0000-4000-8000-0000000000b0";
40
+ const HUMAN_ACCOUNT_ID = "557058:11111111-1111-4111-8111-111111111111";
41
+ const REPO = "acme-ws/widgets";
42
+ const REPO_UUID = "0f9b2c4e-7a1d-4e5f-9c3b-2d8e6a1f4b70";
43
+ const LEAD_ID = "lead-bitbucket-001";
44
+
45
+ function withPr<T extends PullRequestEvent>(sample: unknown, id: number): T {
46
+ const event = structuredClone(sample) as T;
47
+ event.pullrequest.id = id;
48
+ if (event.pullrequest.links?.html) {
49
+ event.pullrequest.links.html.href = `https://bitbucket.org/${REPO}/pull-requests/${id}`;
50
+ }
51
+ return event;
52
+ }
53
+
54
+ function commentEvent(sample: unknown, prId: number, raw?: string): PullRequestCommentEvent {
55
+ const event = withPr<PullRequestCommentEvent>(sample, prId);
56
+ event.comment.id = prId * 10;
57
+ if (raw !== undefined) event.comment.content.raw = raw;
58
+ return event;
59
+ }
60
+
61
+ async function seedActiveTask(prId: number) {
62
+ return createTaskExtended(`[Bitbucket PR #${prId}] Existing`, {
63
+ source: "bitbucket",
64
+ vcsProvider: "bitbucket",
65
+ vcsRepo: REPO,
66
+ vcsEventType: "pull_request",
67
+ vcsNumber: prId,
68
+ agentId: LEAD_ID,
69
+ });
70
+ }
71
+
72
+ beforeAll(async () => {
73
+ try {
74
+ await unlink(TEST_DB_PATH);
75
+ } catch {}
76
+ initDb(TEST_DB_PATH);
77
+ await createAgent({ id: LEAD_ID, name: "BitbucketTestLead", status: "idle", isLead: true });
78
+ });
79
+
80
+ afterAll(async () => {
81
+ closeDb();
82
+ for (const suffix of ["", "-wal", "-shm"]) {
83
+ try {
84
+ await unlink(`${TEST_DB_PATH}${suffix}`);
85
+ } catch {}
86
+ }
87
+ });
88
+
89
+ beforeEach(() => {
90
+ process.env.BITBUCKET_BOT_ACCOUNT_ID = BOT_ACCOUNT_ID;
91
+ });
92
+
93
+ afterEach(() => {
94
+ delete process.env.BITBUCKET_BOT_ACCOUNT_ID;
95
+ resetBitbucket();
96
+ resetBitbucketBotCache();
97
+ resetBitbucketDedup();
98
+ });
99
+
100
+ describe("detectMention", () => {
101
+ test("matches a picker mention and a typed @name", () => {
102
+ expect(detectMention(`hi @{${BOT_ACCOUNT_ID}} there`, BOT_ACCOUNT_ID)).toBe(true);
103
+ expect(detectMention(`hi @${getBitbucketBotName()} there`, null)).toBe(true);
104
+ });
105
+
106
+ test("ignores other accounts and lookalike names", () => {
107
+ expect(detectMention(`hi @{${HUMAN_ACCOUNT_ID}}`, BOT_ACCOUNT_ID)).toBe(false);
108
+ expect(detectMention(`hi @${getBitbucketBotName()}-two`, BOT_ACCOUNT_ID)).toBe(false);
109
+ expect(detectMention(null, BOT_ACCOUNT_ID)).toBe(false);
110
+ });
111
+
112
+ test("reads BITBUCKET_BOT_NAME at match time, so a config reload applies", () => {
113
+ process.env.BITBUCKET_BOT_NAME = "renamed-bot";
114
+ try {
115
+ expect(detectMention("hi @renamed-bot there", null)).toBe(true);
116
+ expect(detectMention("hi @agent-swarm-bot there", null)).toBe(false);
117
+ } finally {
118
+ delete process.env.BITBUCKET_BOT_NAME;
119
+ }
120
+ });
121
+ });
122
+
123
+ // ═══════════════════════════════════════════════════════
124
+ // pullrequest:created
125
+ // ═══════════════════════════════════════════════════════
126
+
127
+ describe("handlePullRequestCreated", () => {
128
+ test("creates a task when the description mentions the bot", async () => {
129
+ const result = await handlePullRequestCreated(withPr(mentionSample, 101));
130
+ expect(result.created).toBe(true);
131
+
132
+ const task = await getTaskById(result.taskId!);
133
+ expect(task?.source).toBe("bitbucket");
134
+ expect(task?.vcsProvider).toBe("bitbucket");
135
+ expect(task?.taskType).toBe("bitbucket-pr");
136
+ expect(task?.vcsRepo).toBe(REPO);
137
+ expect(task?.vcsNumber).toBe(101);
138
+ expect(task?.vcsEventType).toBe("pull_request");
139
+ expect(task?.vcsAuthor).toBe(HUMAN_ACCOUNT_ID);
140
+ expect(task?.vcsUrl).toBe(`https://bitbucket.org/${REPO}/pull-requests/101`);
141
+ expect(task?.contextKey).toBe(`task:trackers:bitbucket:${REPO_UUID}:pr:101`);
142
+ expect(task?.task).toContain("[Bitbucket PR #101] Add retry to the widget fetcher");
143
+ expect(task?.task).toContain("Branch: feature/retry → main");
144
+ expect(task?.task).toContain("Context: please add tests for the retry path.");
145
+ expect(task?.task).toContain("api.bitbucket.org/2.0/repositories/acme-ws/widgets");
146
+ expect(task?.task).not.toContain(BOT_ACCOUNT_ID);
147
+ });
148
+
149
+ test("creates a review task when the bot is listed as a reviewer", async () => {
150
+ const result = await handlePullRequestCreated(withPr(reviewerSample, 102));
151
+ expect(result.created).toBe(true);
152
+
153
+ const task = await getTaskById(result.taskId!);
154
+ expect(task?.taskType).toBe("bitbucket-pr");
155
+ expect(task?.task).toContain("Review requested: Add retry to the widget fetcher");
156
+ expect(task?.task).toContain("You were added as a reviewer");
157
+ });
158
+
159
+ test("creates one task when the bot is both mentioned and a reviewer", async () => {
160
+ const event = withPr<PullRequestEvent>(mentionSample, 103);
161
+ event.pullrequest.reviewers = [{ account_id: BOT_ACCOUNT_ID, display_name: "Bot" }];
162
+ const first = await handlePullRequestCreated(event);
163
+ expect(first.created).toBe(true);
164
+ expect((await getTaskById(first.taskId!))?.task).not.toContain("Review requested");
165
+ expect((await handlePullRequestCreated(event)).created).toBe(false);
166
+ });
167
+
168
+ test("skips a PR without a bot mention or review request", async () => {
169
+ const event = withPr<PullRequestEvent>(mentionSample, 104);
170
+ event.pullrequest.description = "Just a regular PR.";
171
+ expect((await handlePullRequestCreated(event)).created).toBe(false);
172
+ });
173
+
174
+ test("skips a PR opened by the bot itself", async () => {
175
+ const event = withPr<PullRequestEvent>(mentionSample, 105);
176
+ event.pullrequest.author = { account_id: BOT_ACCOUNT_ID, display_name: "Bot" };
177
+ expect((await handlePullRequestCreated(event)).created).toBe(false);
178
+ });
179
+
180
+ test("deduplicates a redelivered event", async () => {
181
+ const event = withPr(mentionSample, 106);
182
+ expect((await handlePullRequestCreated(event)).created).toBe(true);
183
+ expect((await handlePullRequestCreated(event)).created).toBe(false);
184
+ });
185
+
186
+ test("ignores a payload missing the pull request", async () => {
187
+ expect((await handlePullRequestCreated({ actor: {} })).created).toBe(false);
188
+ });
189
+
190
+ test("a skip_event template override suppresses the task", async () => {
191
+ const override = upsertPromptTemplate({
192
+ eventType: "bitbucket.pull_request.opened",
193
+ scope: "global",
194
+ state: "skip_event",
195
+ body: "Skipped body",
196
+ });
197
+ try {
198
+ expect((await handlePullRequestCreated(withPr(mentionSample, 107))).created).toBe(false);
199
+ } finally {
200
+ await deletePromptTemplate(override.id);
201
+ }
202
+ });
203
+ });
204
+
205
+ // ═══════════════════════════════════════════════════════
206
+ // pullrequest:comment_created
207
+ // ═══════════════════════════════════════════════════════
208
+
209
+ describe("handlePullRequestCommentCreated", () => {
210
+ test("creates a task for a comment that mentions the bot", async () => {
211
+ const result = await handlePullRequestCommentCreated(commentEvent(commentSample, 201));
212
+ expect(result.created).toBe(true);
213
+
214
+ const task = await getTaskById(result.taskId!);
215
+ expect(task?.source).toBe("bitbucket");
216
+ expect(task?.taskType).toBe("bitbucket-comment");
217
+ expect(task?.vcsEventType).toBe("pull_request_comment");
218
+ expect(task?.vcsNumber).toBe(201);
219
+ expect(task?.vcsCommentId).toBe(2010);
220
+ expect(task?.contextKey).toBe(`task:trackers:bitbucket:${REPO_UUID}:pr:201`);
221
+ expect(task?.task).toContain("[Bitbucket Comment on PR #201]");
222
+ expect(task?.task).toContain("can you fix the flaky test?");
223
+ expect(task?.task).not.toContain("File:");
224
+ expect(task?.parentTaskId).toBeFalsy();
225
+ });
226
+
227
+ test("an inline comment carries its file and line", async () => {
228
+ const result = await handlePullRequestCommentCreated(commentEvent(commentInlineSample, 202));
229
+ expect(result.created).toBe(true);
230
+ const task = await getTaskById(result.taskId!);
231
+ expect(task?.task).toContain("File: src/fetcher.ts:17");
232
+ expect(task?.task).toContain("this off-by-one looks wrong");
233
+ });
234
+
235
+ test("links to the active task for the same PR", async () => {
236
+ const existing = await seedActiveTask(203);
237
+ const result = await handlePullRequestCommentCreated(commentEvent(commentSample, 203));
238
+ expect(result.created).toBe(true);
239
+
240
+ const task = await getTaskById(result.taskId!);
241
+ expect(task?.parentTaskId).toBe(existing.id);
242
+ expect(task?.task).toContain(`active task (${existing.id})`);
243
+ });
244
+
245
+ test("skips a comment without a bot mention", async () => {
246
+ const event = commentEvent(commentSample, 204, "Looks good to me.");
247
+ expect((await handlePullRequestCommentCreated(event)).created).toBe(false);
248
+ });
249
+
250
+ test("skips a comment written by the bot", async () => {
251
+ const event = commentEvent(commentSample, 205);
252
+ event.comment.user = { account_id: BOT_ACCOUNT_ID, display_name: "Bot" };
253
+ expect((await handlePullRequestCommentCreated(event)).created).toBe(false);
254
+ });
255
+
256
+ test("matches a typed @name when no bot account id is known", async () => {
257
+ delete process.env.BITBUCKET_BOT_ACCOUNT_ID;
258
+ const event = commentEvent(commentSample, 206, `@${getBitbucketBotName()} please rebase`);
259
+ expect((await handlePullRequestCommentCreated(event)).created).toBe(true);
260
+ });
261
+
262
+ test("ignores a payload missing the comment", async () => {
263
+ const event = withPr(mentionSample, 207);
264
+ expect((await handlePullRequestCommentCreated(event)).created).toBe(false);
265
+ });
266
+ });
267
+
268
+ // ═══════════════════════════════════════════════════════
269
+ // pullrequest:fulfilled / pullrequest:rejected
270
+ // ═══════════════════════════════════════════════════════
271
+
272
+ describe("handlePullRequestClosed", () => {
273
+ test("a merge cancels the active task", async () => {
274
+ const existing = await seedActiveTask(301);
275
+ const result = await handlePullRequestClosed(withPr(fulfilledSample, 301), "merged");
276
+ expect(result.created).toBe(false);
277
+ const task = await getTaskById(existing.id);
278
+ expect(task?.status).toBe("failed");
279
+ expect(task?.failureReason).toBe("PR was merged");
280
+ });
281
+
282
+ test("a decline cancels the active task", async () => {
283
+ const existing = await seedActiveTask(302);
284
+ await handlePullRequestClosed(withPr(rejectedSample, 302), "declined");
285
+ const task = await getTaskById(existing.id);
286
+ expect(task?.status).toBe("failed");
287
+ expect(task?.failureReason).toBe("PR was declined");
288
+ });
289
+
290
+ test("a merge cancels every active task for the PR, not only the newest", async () => {
291
+ const prTask = await seedActiveTask(304);
292
+ const commentTask = await createTaskExtended("[Bitbucket PR #304] Comment", {
293
+ source: "bitbucket",
294
+ vcsProvider: "bitbucket",
295
+ vcsRepo: REPO,
296
+ vcsEventType: "pull_request_comment",
297
+ vcsNumber: 304,
298
+ agentId: LEAD_ID,
299
+ parentTaskId: prTask.id,
300
+ });
301
+ await handlePullRequestClosed(withPr(fulfilledSample, 304), "merged");
302
+ for (const id of [prTask.id, commentTask.id]) {
303
+ const task = await getTaskById(id);
304
+ expect(task?.status).toBe("failed");
305
+ expect(task?.failureReason).toBe("PR was merged");
306
+ }
307
+ });
308
+
309
+ test("a close with no active task is a no-op", async () => {
310
+ const result = await handlePullRequestClosed(withPr(fulfilledSample, 303), "merged");
311
+ expect(result).toEqual({ created: false });
312
+ });
313
+ });
314
+
315
+ // ═══════════════════════════════════════════════════════
316
+ // Delivery dedup and identity resolution
317
+ // ═══════════════════════════════════════════════════════
318
+
319
+ describe("delivery dedup", () => {
320
+ test("a delivery is a duplicate only after it was acked", () => {
321
+ expect(isDuplicateDelivery("req-1")).toBe(false);
322
+ expect(isDuplicateDelivery("req-1")).toBe(false);
323
+ markDeliveryProcessed("req-1");
324
+ expect(isDuplicateDelivery("req-1")).toBe(true);
325
+ expect(isDuplicateDelivery("req-2")).toBe(false);
326
+ expect(isDuplicateDelivery(undefined)).toBe(false);
327
+ });
328
+ });
329
+
330
+ describe("identity resolution", () => {
331
+ const UNMAPPED_NS = "integration:unmapped:bitbucket";
332
+
333
+ test("a linked identity populates requestedByUserId", async () => {
334
+ const known = await createUser({ name: "Known Reviewer" });
335
+ const accountId = "557058:22222222-2222-4222-8222-222222222222";
336
+ await linkIdentity(known.id, "bitbucket", accountId, { kind: "system", id: "test" });
337
+
338
+ const event = withPr<PullRequestEvent>(mentionSample, 401);
339
+ event.pullrequest.author = { ...event.pullrequest.author, account_id: accountId };
340
+ const result = await handlePullRequestCreated(event);
341
+ const task = await getTaskById(result.taskId!);
342
+ expect(task?.requestedByUserId).toBe(known.id);
343
+ });
344
+
345
+ test("an unknown sender is recorded as unmapped", async () => {
346
+ const accountId = "557058:33333333-3333-4333-8333-333333333333";
347
+ const event = withPr<PullRequestEvent>(mentionSample, 402);
348
+ event.pullrequest.author = { ...event.pullrequest.author, account_id: accountId };
349
+ const result = await handlePullRequestCreated(event);
350
+ const task = await getTaskById(result.taskId!);
351
+ expect(task?.requestedByUserId).toBeFalsy();
352
+
353
+ const meta = await getKv(UNMAPPED_NS, `${accountId}:meta`);
354
+ expect((meta?.value as Record<string, unknown>).sampleEventType).toBe("pull_request");
355
+ const count = await getKv(UNMAPPED_NS, `${accountId}:count`);
356
+ expect(count?.value).toBe(1);
357
+ });
358
+ });
359
+
360
+ // ═══════════════════════════════════════════════════════
361
+ // Provider scoping and retry after a failed write
362
+ // ═══════════════════════════════════════════════════════
363
+
364
+ describe("same owner/repo on another host", () => {
365
+ async function seedGitHubTask(prId: number) {
366
+ return createTaskExtended(`[GitHub PR #${prId}] Same name, other host`, {
367
+ source: "github",
368
+ vcsProvider: "github",
369
+ vcsRepo: REPO,
370
+ vcsEventType: "pull_request",
371
+ vcsNumber: prId,
372
+ agentId: LEAD_ID,
373
+ });
374
+ }
375
+
376
+ test("a Bitbucket merge does not cancel a GitHub task with the same repo and number", async () => {
377
+ const github = await seedGitHubTask(501);
378
+ await handlePullRequestClosed(withPr(fulfilledSample, 501), "merged");
379
+ expect((await getTaskById(github.id))?.status).not.toBe("failed");
380
+ });
381
+
382
+ test("a Bitbucket comment does not parent onto a GitHub task with the same repo and number", async () => {
383
+ const github = await seedGitHubTask(502);
384
+ const result = await handlePullRequestCommentCreated(commentEvent(commentSample, 502));
385
+ expect(result.created).toBe(true);
386
+ const task = await getTaskById(result.taskId!);
387
+ expect(task?.parentTaskId).toBeFalsy();
388
+ expect(task?.task).not.toContain(github.id);
389
+ });
390
+ });
391
+
392
+ describe("retry after a failed task insert", () => {
393
+ // A temp trigger makes the next agent_tasks INSERT fail, as a transient DB
394
+ // error would; Bitbucket then retries the delivery after our 5xx.
395
+ function failTaskInserts() {
396
+ getDb().run(
397
+ "CREATE TEMP TRIGGER bb_fail_insert BEFORE INSERT ON agent_tasks BEGIN SELECT RAISE(ABORT, 'injected fault'); END",
398
+ );
399
+ }
400
+ function restoreTaskInserts() {
401
+ getDb().run("DROP TRIGGER IF EXISTS temp.bb_fail_insert");
402
+ }
403
+
404
+ test("a PR event that failed to write creates its task on retry", async () => {
405
+ const event = withPr(mentionSample, 601);
406
+ failTaskInserts();
407
+ try {
408
+ await expect(handlePullRequestCreated(event)).rejects.toThrow();
409
+ } finally {
410
+ restoreTaskInserts();
411
+ }
412
+ expect((await handlePullRequestCreated(event)).created).toBe(true);
413
+ });
414
+
415
+ test("a comment event that failed to write creates its task on retry", async () => {
416
+ const event = commentEvent(commentSample, 602);
417
+ failTaskInserts();
418
+ try {
419
+ await expect(handlePullRequestCommentCreated(event)).rejects.toThrow();
420
+ } finally {
421
+ restoreTaskInserts();
422
+ }
423
+ expect((await handlePullRequestCommentCreated(event)).created).toBe(true);
424
+ });
425
+ });
@@ -0,0 +1,220 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { createHmac } from "node:crypto";
3
+ import { unlink } from "node:fs/promises";
4
+ import type { IncomingMessage, ServerResponse } from "node:http";
5
+ import { closeDb, createAgent, getTaskById, initDb } from "../be/db";
6
+ import { resetBitbucketBotCache } from "../bitbucket/api";
7
+ import { initBitbucket, resetBitbucket } from "../bitbucket/auth";
8
+ import { resetBitbucketDedup } from "../bitbucket/handlers";
9
+ import { handleWebhooks } from "../http/webhooks";
10
+ import { workflowEventBus } from "../workflows/event-bus";
11
+ import commentSample from "./fixtures/bitbucket/pullrequest-comment-created.json";
12
+
13
+ // End-to-end through the HTTP dispatch block: signature, delivery dedup,
14
+ // event routing and workflow events, with the real handlers and a test DB.
15
+ const TEST_DB_PATH = "./test-bitbucket-webhook.sqlite";
16
+ const SECRET = "example-webhook-secret";
17
+ const PATH = ["api", "bitbucket", "webhook"];
18
+
19
+ function sign(body: string, secret = SECRET): string {
20
+ return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
21
+ }
22
+
23
+ function fakeReqRes(rawBody: string, headers: Record<string, string>) {
24
+ const req = {
25
+ method: "POST",
26
+ headers,
27
+ async *[Symbol.asyncIterator]() {
28
+ yield Buffer.from(rawBody);
29
+ },
30
+ } as unknown as IncomingMessage;
31
+ const captured = { status: 0, body: "" };
32
+ const res = {
33
+ setHeader() {},
34
+ writeHead(status: number) {
35
+ captured.status = status;
36
+ return this;
37
+ },
38
+ end(chunk?: string) {
39
+ if (chunk) captured.body = chunk;
40
+ return this;
41
+ },
42
+ } as unknown as ServerResponse;
43
+ return { req, res, captured };
44
+ }
45
+
46
+ async function deliver(rawBody: string, headers: Record<string, string>) {
47
+ const { req, res, captured } = fakeReqRes(rawBody, headers);
48
+ expect(await handleWebhooks(req, res, PATH)).toBe(true);
49
+ return { status: captured.status, body: captured.body ? JSON.parse(captured.body) : null };
50
+ }
51
+
52
+ function commentBody(prId: number): string {
53
+ const event = structuredClone(commentSample);
54
+ event.pullrequest.id = prId;
55
+ event.comment.id = prId * 10;
56
+ return JSON.stringify(event);
57
+ }
58
+
59
+ let emitted: Array<{ name: string; payload: unknown }> = [];
60
+ let originalEmit: typeof workflowEventBus.emit;
61
+
62
+ beforeAll(async () => {
63
+ try {
64
+ await unlink(TEST_DB_PATH);
65
+ } catch {}
66
+ initDb(TEST_DB_PATH);
67
+ await createAgent({ id: "lead-bb-webhook", name: "BBWebhookLead", status: "idle", isLead: true });
68
+ });
69
+
70
+ afterAll(async () => {
71
+ closeDb();
72
+ for (const suffix of ["", "-wal", "-shm"]) {
73
+ try {
74
+ await unlink(`${TEST_DB_PATH}${suffix}`);
75
+ } catch {}
76
+ }
77
+ });
78
+
79
+ beforeEach(() => {
80
+ process.env.BITBUCKET_WEBHOOK_SECRET = SECRET;
81
+ process.env.BITBUCKET_BOT_ACCOUNT_ID = "557058:00000000-0000-4000-8000-0000000000b0";
82
+ initBitbucket();
83
+ emitted = [];
84
+ originalEmit = workflowEventBus.emit.bind(workflowEventBus);
85
+ workflowEventBus.emit = ((name: string, payload: unknown) => {
86
+ // task.created and other lifecycle events fire too; keep the webhook's own.
87
+ if (name.startsWith("bitbucket.")) emitted.push({ name, payload });
88
+ }) as typeof workflowEventBus.emit;
89
+ });
90
+
91
+ afterEach(() => {
92
+ workflowEventBus.emit = originalEmit;
93
+ delete process.env.BITBUCKET_WEBHOOK_SECRET;
94
+ delete process.env.BITBUCKET_BOT_ACCOUNT_ID;
95
+ delete process.env.BITBUCKET_DISABLE;
96
+ resetBitbucket();
97
+ resetBitbucketBotCache();
98
+ resetBitbucketDedup();
99
+ });
100
+
101
+ describe("POST /api/bitbucket/webhook", () => {
102
+ test("503 when the integration is disabled", async () => {
103
+ process.env.BITBUCKET_DISABLE = "true";
104
+ const body = commentBody(1);
105
+ const res = await deliver(body, { "x-hub-signature": sign(body) });
106
+ expect(res.status).toBe(503);
107
+ });
108
+
109
+ test("401 on a bad signature, 200 on the correct one", async () => {
110
+ const body = commentBody(2);
111
+ const headers = { "x-event-key": "pullrequest:comment_created", "x-request-uuid": "r-2" };
112
+ const bad = await deliver(body, { ...headers, "x-hub-signature": sign(body, "wrong") });
113
+ expect(bad.status).toBe(401);
114
+ const missing = await deliver(body, headers);
115
+ expect(missing.status).toBe(401);
116
+ const good = await deliver(body, { ...headers, "x-hub-signature": sign(body) });
117
+ expect(good.status).toBe(200);
118
+ expect(good.body.created).toBe(true);
119
+ });
120
+
121
+ test("a signed comment creates a task and emits the workflow event", async () => {
122
+ const body = commentBody(3);
123
+ const res = await deliver(body, {
124
+ "x-event-key": "pullrequest:comment_created",
125
+ "x-request-uuid": "r-3",
126
+ "x-hub-signature": sign(body),
127
+ });
128
+ expect(res.status).toBe(200);
129
+ expect(res.body.created).toBe(true);
130
+ const task = await getTaskById(res.body.taskId);
131
+ expect(task?.source).toBe("bitbucket");
132
+ expect(task?.contextKey).toStartWith("task:trackers:bitbucket:");
133
+
134
+ expect(emitted).toEqual([
135
+ {
136
+ name: "bitbucket.pull_request.commented",
137
+ payload: {
138
+ repo: "acme-ws/widgets",
139
+ number: 3,
140
+ body: "@{557058:00000000-0000-4000-8000-0000000000b0} can you fix the flaky test?",
141
+ action: "commented",
142
+ user_login: "557058:11111111-1111-4111-8111-111111111111",
143
+ },
144
+ },
145
+ ]);
146
+ });
147
+
148
+ test("a replayed X-Request-UUID is acked without a second task or event", async () => {
149
+ const body = commentBody(4);
150
+ const headers = {
151
+ "x-event-key": "pullrequest:comment_created",
152
+ "x-request-uuid": "r-4",
153
+ "x-hub-signature": sign(body),
154
+ };
155
+ expect((await deliver(body, headers)).body.created).toBe(true);
156
+ emitted = [];
157
+ const replay = await deliver(body, headers);
158
+ expect(replay.status).toBe(200);
159
+ expect(replay.body).toEqual({ created: false });
160
+ expect(emitted).toEqual([]);
161
+ });
162
+
163
+ test("an unsupported event key is acked and ignored", async () => {
164
+ const body = commentBody(5);
165
+ const res = await deliver(body, {
166
+ "x-event-key": "repo:push",
167
+ "x-request-uuid": "r-5",
168
+ "x-hub-signature": sign(body),
169
+ });
170
+ expect(res.status).toBe(200);
171
+ expect(res.body).toEqual({ created: false });
172
+ expect(emitted).toEqual([]);
173
+ });
174
+
175
+ test("a payload missing the pull request acks with created:false and no event", async () => {
176
+ const body = JSON.stringify({ actor: {}, repository: {} });
177
+ const res = await deliver(body, {
178
+ "x-event-key": "pullrequest:fulfilled",
179
+ "x-request-uuid": "r-6",
180
+ "x-hub-signature": sign(body),
181
+ });
182
+ expect(res.status).toBe(200);
183
+ expect(res.body).toEqual({ created: false });
184
+ expect(emitted).toEqual([]);
185
+ });
186
+
187
+ test("413 on a declared Content-Length over 1 MiB, before any signature check", async () => {
188
+ const body = commentBody(7);
189
+ const res = await deliver(body, {
190
+ "content-length": String(1024 * 1024 + 1),
191
+ "x-event-key": "pullrequest:comment_created",
192
+ "x-hub-signature": sign(body),
193
+ });
194
+ expect(res.status).toBe(413);
195
+ expect(emitted).toEqual([]);
196
+ });
197
+
198
+ test("413 on a streamed body over 1 MiB with no Content-Length", async () => {
199
+ const chunk = Buffer.alloc(512 * 1024, "a");
200
+ const req = {
201
+ method: "POST",
202
+ headers: { "x-event-key": "pullrequest:comment_created", "x-hub-signature": "sha256=00" },
203
+ async *[Symbol.asyncIterator]() {
204
+ yield chunk;
205
+ yield chunk;
206
+ yield chunk;
207
+ },
208
+ } as unknown as IncomingMessage;
209
+ const { res, captured } = fakeReqRes("", {});
210
+ expect(await handleWebhooks(req, res, PATH)).toBe(true);
211
+ expect(captured.status).toBe(413);
212
+ expect(emitted).toEqual([]);
213
+ });
214
+
215
+ test("400 on a signed but non-JSON body", async () => {
216
+ const body = "not json";
217
+ const res = await deliver(body, { "x-hub-signature": sign(body) });
218
+ expect(res.status).toBe(400);
219
+ });
220
+ });