@desplega.ai/agent-swarm 2.4.5 → 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.
- package/README.md +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
|
@@ -100,7 +100,7 @@ describe("findTaskByVcs", () => {
|
|
|
100
100
|
agentId: "vcs-lead-001",
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
const found = await findTaskByVcs("findme/gh-repo", 100);
|
|
103
|
+
const found = await findTaskByVcs("findme/gh-repo", 100, "github");
|
|
104
104
|
expect(found).not.toBeNull();
|
|
105
105
|
expect(found?.vcsProvider).toBe("github");
|
|
106
106
|
});
|
|
@@ -114,7 +114,7 @@ describe("findTaskByVcs", () => {
|
|
|
114
114
|
agentId: "vcs-lead-001",
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
const found = await findTaskByVcs("findme/gl-project", 200);
|
|
117
|
+
const found = await findTaskByVcs("findme/gl-project", 200, "gitlab");
|
|
118
118
|
expect(found).not.toBeNull();
|
|
119
119
|
expect(found?.vcsProvider).toBe("gitlab");
|
|
120
120
|
});
|
|
@@ -133,12 +133,12 @@ describe("findTaskByVcs", () => {
|
|
|
133
133
|
await db.startTask(task.id);
|
|
134
134
|
await db.completeTask(task.id, "Done");
|
|
135
135
|
|
|
136
|
-
const found = await findTaskByVcs("findme/completed", 300);
|
|
136
|
+
const found = await findTaskByVcs("findme/completed", 300, "gitlab");
|
|
137
137
|
expect(found).toBeNull();
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
test("returns null for non-existent repo/number", async () => {
|
|
141
|
-
const found = await findTaskByVcs("nonexistent/repo", 9999);
|
|
141
|
+
const found = await findTaskByVcs("nonexistent/repo", 9999, "github");
|
|
142
142
|
expect(found).toBeNull();
|
|
143
143
|
});
|
|
144
144
|
});
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
14
14
|
import { initAgentMail, isAgentMailEnabled, resetAgentMail } from "../agentmail/app";
|
|
15
|
+
import { validateConfigValue } from "../be/swarm-config-guard";
|
|
16
|
+
import { isBitbucketEnabled, resetBitbucket } from "../bitbucket/auth";
|
|
15
17
|
import { initGitHub, isGitHubEnabled, resetGitHub } from "../github/app";
|
|
16
18
|
import { isJiraEnabled, resetJira } from "../jira/app";
|
|
17
19
|
import { isLinearEnabled, resetLinear } from "../linear/app";
|
|
@@ -28,6 +30,8 @@ const ENV_KEYS = [
|
|
|
28
30
|
"LINEAR_ENABLED",
|
|
29
31
|
"AGENTMAIL_DISABLE",
|
|
30
32
|
"AGENTMAIL_WEBHOOK_SECRET",
|
|
33
|
+
"BITBUCKET_DISABLE",
|
|
34
|
+
"BITBUCKET_WEBHOOK_SECRET",
|
|
31
35
|
] as const;
|
|
32
36
|
const originalEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
33
37
|
|
|
@@ -45,6 +49,7 @@ afterEach(() => {
|
|
|
45
49
|
resetAgentMail();
|
|
46
50
|
resetJira();
|
|
47
51
|
resetLinear();
|
|
52
|
+
resetBitbucket();
|
|
48
53
|
for (const key of ENV_KEYS) {
|
|
49
54
|
const value = originalEnv.get(key);
|
|
50
55
|
if (value === undefined) delete process.env[key];
|
|
@@ -185,6 +190,37 @@ describe("AgentMail AGENTMAIL_DISABLE", () => {
|
|
|
185
190
|
});
|
|
186
191
|
});
|
|
187
192
|
|
|
193
|
+
describe("Bitbucket BITBUCKET_DISABLE", () => {
|
|
194
|
+
test("every truthy spelling disables the integration", () => {
|
|
195
|
+
process.env.BITBUCKET_WEBHOOK_SECRET = "secret";
|
|
196
|
+
for (const spelling of TRUTHY_SPELLINGS) {
|
|
197
|
+
process.env.BITBUCKET_DISABLE = spelling;
|
|
198
|
+
expect(isBitbucketEnabled()).toBe(false);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("non-disabling values keep the integration enabled", () => {
|
|
203
|
+
process.env.BITBUCKET_WEBHOOK_SECRET = "secret";
|
|
204
|
+
for (const value of NON_DISABLING) {
|
|
205
|
+
if (value === undefined) delete process.env.BITBUCKET_DISABLE;
|
|
206
|
+
else process.env.BITBUCKET_DISABLE = value;
|
|
207
|
+
expect(isBitbucketEnabled()).toBe(true);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("stays disabled without a webhook secret", () => {
|
|
212
|
+
expect(isBitbucketEnabled()).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("the config guard validates BITBUCKET_DISABLE as a boolean like GITLAB_DISABLE", () => {
|
|
216
|
+
for (const key of ["BITBUCKET_DISABLE", "GITLAB_DISABLE"]) {
|
|
217
|
+
expect(validateConfigValue(key, "true")).toBeNull();
|
|
218
|
+
expect(validateConfigValue(key, "false")).toBeNull();
|
|
219
|
+
expect(validateConfigValue(key, "maybe")).not.toBeNull();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
188
224
|
describe("Slack SLACK_DISABLE", () => {
|
|
189
225
|
test("every truthy spelling reports disabled", () => {
|
|
190
226
|
for (const spelling of TRUTHY_SPELLINGS) {
|
|
@@ -52,7 +52,7 @@ describe("script MCP tools", () => {
|
|
|
52
52
|
"script-upsert":
|
|
53
53
|
'Typecheck and persist a TypeScript script to the swarm catalog under your agent scope (or global if you\'re a lead). Import `ScriptContext` from "swarm-sdk" for a real context type. Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.',
|
|
54
54
|
"script-delete":
|
|
55
|
-
"
|
|
55
|
+
"Permanently remove a swarm-shared script and all its versions from the catalog. This cannot be undone.",
|
|
56
56
|
"script-query-types":
|
|
57
57
|
// Updated for the frozen SwarmToolResult contract refactor: `name` is
|
|
58
58
|
// now optional — omit it to get the swarm-wide type surface instead
|
|
@@ -34,6 +34,7 @@ const ENV_KEYS = [
|
|
|
34
34
|
"EMBEDDING_MODEL",
|
|
35
35
|
"GITHUB_TOKEN",
|
|
36
36
|
"GITLAB_TOKEN",
|
|
37
|
+
"BITBUCKET_TOKEN",
|
|
37
38
|
"SLACK_BOT_TOKEN",
|
|
38
39
|
"SLACK_APP_TOKEN",
|
|
39
40
|
"SLACK_MODE",
|
|
@@ -380,6 +381,17 @@ describe("onboarding state", () => {
|
|
|
380
381
|
expect(body.state.steps.integrations).toMatchObject({ status: "done", method: "github" });
|
|
381
382
|
});
|
|
382
383
|
|
|
384
|
+
test("GET derives the Bitbucket integration from BITBUCKET_TOKEN", async () => {
|
|
385
|
+
process.env.BITBUCKET_TOKEN = "example-bitbucket-token";
|
|
386
|
+
const body = (await (await request("/api/onboarding")).json()) as {
|
|
387
|
+
state: OnboardingState;
|
|
388
|
+
signals: { integrations: Record<string, boolean> };
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
expect(body.signals.integrations.bitbucket).toBe(true);
|
|
392
|
+
expect(body.state.steps.integrations).toMatchObject({ status: "done", method: "bitbucket" });
|
|
393
|
+
});
|
|
394
|
+
|
|
383
395
|
test("the selected first task completes onboarding after that task completes", async () => {
|
|
384
396
|
// Onboarding starts before the first task exists (the task alone would mark an existing install).
|
|
385
397
|
await request("/api/onboarding");
|
|
@@ -340,8 +340,14 @@ describe("bounded codemode", () => {
|
|
|
340
340
|
undefined,
|
|
341
341
|
fakeCtx().ctx,
|
|
342
342
|
);
|
|
343
|
-
|
|
344
|
-
expect(
|
|
343
|
+
const elapsed = Date.now() - started;
|
|
344
|
+
expect(elapsed).toBeGreaterThanOrEqual(250);
|
|
345
|
+
expect(elapsed).toBeLessThan(5_000);
|
|
346
|
+
// pi 1.4 queues the script for a worker; our deadline ends the wait.
|
|
347
|
+
expect(textOf(result)).toContain(
|
|
348
|
+
"Script aborted: Codemode script exceeded its 300 ms deadline",
|
|
349
|
+
);
|
|
350
|
+
expect(textOf(result)).toContain("The script did not start.");
|
|
345
351
|
} finally {
|
|
346
352
|
holders.abort(new Error("test done"));
|
|
347
353
|
await Promise.all(holding);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
|
|
6
|
+
|
|
7
|
+
test(
|
|
8
|
+
"preload clears inherited worker configuration before test files load",
|
|
9
|
+
async () => {
|
|
10
|
+
const directory = await mkdtemp(join(tmpdir(), "swarm-preload-env-"));
|
|
11
|
+
const fixture = join(directory, "worker-env.test.ts");
|
|
12
|
+
const keys = [
|
|
13
|
+
"SLACK_RENDER_V2",
|
|
14
|
+
"SLACK_STATUS_ICON_COMPLETED",
|
|
15
|
+
"SLACK_FUTURE_SETTING",
|
|
16
|
+
"COMB_ENABLED",
|
|
17
|
+
"APP_URL",
|
|
18
|
+
"AGENT_FS_API_URL",
|
|
19
|
+
"AGENT_FS_LIVE_URL",
|
|
20
|
+
"AGENT_FS_DEFAULT_ORG_ID",
|
|
21
|
+
"AGENT_FS_DEFAULT_DRIVE_ID",
|
|
22
|
+
"SWARM_ORG_NAME",
|
|
23
|
+
"SWARM_ORG_WEBSITE",
|
|
24
|
+
];
|
|
25
|
+
try {
|
|
26
|
+
await Bun.write(
|
|
27
|
+
fixture,
|
|
28
|
+
`import { expect, test } from "bun:test";
|
|
29
|
+
const inherited = ${JSON.stringify(keys)}.filter(key => process.env[key] !== undefined);
|
|
30
|
+
test("worker defaults are cleared before module evaluation", () => {
|
|
31
|
+
expect(inherited).toEqual([]);
|
|
32
|
+
expect(process.env.PRELOAD_ENV_CONTROL).toBe("preserved");
|
|
33
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = "test-icon";
|
|
34
|
+
});
|
|
35
|
+
test("suite configuration survives between tests", () => {
|
|
36
|
+
expect(process.env.SLACK_STATUS_ICON_COMPLETED).toBe("test-icon");
|
|
37
|
+
});
|
|
38
|
+
`,
|
|
39
|
+
);
|
|
40
|
+
const result = expectChildOk(
|
|
41
|
+
await runChild([process.execPath, "test", fixture], {
|
|
42
|
+
cwd: join(import.meta.dir, "../.."),
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
...Object.fromEntries(keys.map((key) => [key, "inherited-worker-setting"])),
|
|
46
|
+
PRELOAD_ENV_CONTROL: "preserved",
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
"test runner with inherited worker env",
|
|
50
|
+
);
|
|
51
|
+
expect(result.stderr).toContain("2 pass");
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(directory, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
57
|
+
);
|
package/src/tests/preload.ts
CHANGED
|
@@ -22,6 +22,27 @@ import { startHangWatchdog } from "./hang-watchdog";
|
|
|
22
22
|
|
|
23
23
|
startHangWatchdog();
|
|
24
24
|
|
|
25
|
+
// Worker deployments configure Slack rendering, links, and organization defaults.
|
|
26
|
+
// Start tests from the same unset defaults as CI; suites can still opt in by
|
|
27
|
+
// setting their own values after the preload runs.
|
|
28
|
+
for (const key of Object.keys(process.env)) {
|
|
29
|
+
if (
|
|
30
|
+
key.startsWith("SLACK_") ||
|
|
31
|
+
[
|
|
32
|
+
"COMB_ENABLED",
|
|
33
|
+
"APP_URL",
|
|
34
|
+
"AGENT_FS_API_URL",
|
|
35
|
+
"AGENT_FS_LIVE_URL",
|
|
36
|
+
"AGENT_FS_DEFAULT_ORG_ID",
|
|
37
|
+
"AGENT_FS_DEFAULT_DRIVE_ID",
|
|
38
|
+
"SWARM_ORG_NAME",
|
|
39
|
+
"SWARM_ORG_WEBSITE",
|
|
40
|
+
].includes(key)
|
|
41
|
+
) {
|
|
42
|
+
delete process.env[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
// The API drain (src/be/api-drain.ts) is off unless API_DRAIN_MAX_MS is set,
|
|
26
47
|
// but a shell can inherit it (a swarm worker container gets the deploy's value).
|
|
27
48
|
// Test servers are stopped with SIGTERM and their fixtures leave in_progress rows
|
|
@@ -6,6 +6,7 @@ import { resolveTemplate } from "../prompts/resolver";
|
|
|
6
6
|
|
|
7
7
|
// Side-effect imports: register all templates from each source
|
|
8
8
|
import "../gitlab/templates";
|
|
9
|
+
import "../bitbucket/templates";
|
|
9
10
|
import "../agentmail/templates";
|
|
10
11
|
import "../linear/templates";
|
|
11
12
|
import "../heartbeat/templates";
|
|
@@ -28,6 +29,7 @@ async function ensureTemplatesRegistered(): Promise<void> {
|
|
|
28
29
|
// module state across parallel test files, a single-template check is racy.
|
|
29
30
|
const ts = Date.now();
|
|
30
31
|
await import(`../gitlab/templates?t=${ts}`);
|
|
32
|
+
await import(`../bitbucket/templates?t=${ts}`);
|
|
31
33
|
await import(`../agentmail/templates?t=${ts}`);
|
|
32
34
|
await import(`../linear/templates?t=${ts}`);
|
|
33
35
|
await import(`../heartbeat/templates?t=${ts}`);
|
|
@@ -94,6 +96,42 @@ describe("template registration — all sources", () => {
|
|
|
94
96
|
expect(eventTypes).toContain("gitlab.pipeline.failed");
|
|
95
97
|
});
|
|
96
98
|
|
|
99
|
+
test("Bitbucket templates are registered (2 common + 3 event)", () => {
|
|
100
|
+
const eventTypes = getAllTemplateDefinitions().map((d) => d.eventType);
|
|
101
|
+
expect(eventTypes).toContain("common.delegation_instruction.bitbucket");
|
|
102
|
+
expect(eventTypes).toContain("common.command_suggestions.bitbucket_pr");
|
|
103
|
+
expect(eventTypes).toContain("bitbucket.pull_request.opened");
|
|
104
|
+
expect(eventTypes).toContain("bitbucket.pull_request.review_requested");
|
|
105
|
+
expect(eventTypes).toContain("bitbucket.comment.mentioned");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("Bitbucket templates resolve with no unresolved variables", () => {
|
|
109
|
+
const shared = {
|
|
110
|
+
pr_id: 42,
|
|
111
|
+
pr_title: "Fix retry",
|
|
112
|
+
repo: "acme-ws/widgets",
|
|
113
|
+
username: "Dana",
|
|
114
|
+
pr_url: "https://bitbucket.org/acme-ws/widgets/pull-requests/42",
|
|
115
|
+
};
|
|
116
|
+
const pr = { ...shared, source_branch: "feat", target_branch: "main", context_section: "" };
|
|
117
|
+
for (const id of ["bitbucket.pull_request.opened", "bitbucket.pull_request.review_requested"]) {
|
|
118
|
+
const result = resolveTemplate(id, pr);
|
|
119
|
+
expect(result.skipped).toBe(false);
|
|
120
|
+
expect(result.unresolved).toEqual([]);
|
|
121
|
+
expect(result.text).toContain("[Bitbucket PR #42]");
|
|
122
|
+
expect(result.text).toContain("api.bitbucket.org/2.0/repositories/acme-ws/widgets");
|
|
123
|
+
}
|
|
124
|
+
const comment = resolveTemplate("bitbucket.comment.mentioned", {
|
|
125
|
+
...shared,
|
|
126
|
+
location_section: "File: src/a.ts:3\n",
|
|
127
|
+
context: "please fix",
|
|
128
|
+
existing_task_note: "",
|
|
129
|
+
});
|
|
130
|
+
expect(comment.unresolved).toEqual([]);
|
|
131
|
+
expect(comment.text).toContain("File: src/a.ts:3");
|
|
132
|
+
expect(comment.text).toContain("please fix");
|
|
133
|
+
});
|
|
134
|
+
|
|
97
135
|
test("AgentMail templates are registered (5 event)", () => {
|
|
98
136
|
const all = getAllTemplateDefinitions();
|
|
99
137
|
const eventTypes = all.map((d) => d.eventType);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mergeLcov } from "../../scripts/run-root-coverage";
|
|
3
|
+
|
|
4
|
+
describe("mergeLcov", () => {
|
|
5
|
+
test("disjoint function hits remain a conservative lower bound without FN/FNDA identities", () => {
|
|
6
|
+
// a.test.ts calls a(); b.test.ts calls b() in the same two-function module.
|
|
7
|
+
const report = "SF:src/example.ts\nFNF:2\nFNH:1\nend_of_record\n";
|
|
8
|
+
const merged = mergeLcov([report, report]);
|
|
9
|
+
|
|
10
|
+
expect(merged).toContain("FNF:2\nFNH:1\n");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("DA hits sum across reports and LH counts lines hit in any report", () => {
|
|
14
|
+
const merged = mergeLcov([
|
|
15
|
+
"SF:src/example.ts\nDA:1,2\nDA:2,0\nDA:3,0\nend_of_record\n",
|
|
16
|
+
"SF:src/example.ts\nDA:1,3\nDA:2,1\nDA:3,0\nend_of_record\n",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
expect(merged).toBe(
|
|
20
|
+
"TN:\nSF:src/example.ts\nFNF:0\nFNH:0\nDA:1,5\nDA:2,1\nDA:3,0\nLF:3\nLH:2\nend_of_record\n",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("FNH never exceeds FNF when merging function totals", () => {
|
|
25
|
+
const reports = [
|
|
26
|
+
"SF:src/example.ts\nFNF:2\nFNH:2\nend_of_record\n",
|
|
27
|
+
"SF:src/example.ts\nFNF:3\nFNH:1\nend_of_record\n",
|
|
28
|
+
];
|
|
29
|
+
for (const inputs of [reports, reports.toReversed(), [...reports, ...reports]]) {
|
|
30
|
+
const merged = mergeLcov(inputs);
|
|
31
|
+
const found = Number(merged.match(/^FNF:(\d+)$/m)![1]);
|
|
32
|
+
const hit = Number(merged.match(/^FNH:(\d+)$/m)![1]);
|
|
33
|
+
|
|
34
|
+
expect(found).toBe(3);
|
|
35
|
+
expect(hit).toBe(2);
|
|
36
|
+
expect(hit).toBeLessThanOrEqual(found);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side Bitbucket PR linking. There is no Bitbucket CLI in the worker
|
|
3
|
+
* image, so `detectVcsForTask` asks the Bitbucket REST API (via `fetch`) for an
|
|
4
|
+
* open PR on the current branch and PATCHes `/api/tasks/:id/vcs` when it finds
|
|
5
|
+
* one. Both calls go through `globalThis.fetch`, which these tests stub, so no
|
|
6
|
+
* server or port is involved. The git side runs against a real temp repo.
|
|
7
|
+
*/
|
|
8
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { detectVcsForTask, findBitbucketOpenPullRequest } from "../commands/runner";
|
|
13
|
+
import { expectChildOk, runChild } from "./test-proc";
|
|
14
|
+
|
|
15
|
+
const API_URL = "http://swarm.invalid";
|
|
16
|
+
const BRANCH = "feature/bb-link";
|
|
17
|
+
const PR_HTML = "https://bitbucket.org/acme-ws/web.app/pull-requests/17";
|
|
18
|
+
|
|
19
|
+
type Call = { url: string; method: string; headers: Headers; body: string };
|
|
20
|
+
type BitbucketReply = { status: number; body: unknown } | "throw";
|
|
21
|
+
|
|
22
|
+
const originalFetch = globalThis.fetch;
|
|
23
|
+
const originalToken = process.env.BITBUCKET_TOKEN;
|
|
24
|
+
let calls: Call[] = [];
|
|
25
|
+
let bitbucketReply: BitbucketReply = { status: 200, body: { values: [] } };
|
|
26
|
+
let repoDir = "";
|
|
27
|
+
let taskSeq = 0;
|
|
28
|
+
|
|
29
|
+
function nextTaskId(): string {
|
|
30
|
+
taskSeq += 1;
|
|
31
|
+
return `00000000-0000-4000-8000-${String(taskSeq).padStart(12, "0")}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function git(args: string[]): Promise<void> {
|
|
35
|
+
expectChildOk(await runChild(["git", "-C", repoDir, ...args]), `git ${args.join(" ")}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stubFetch(): void {
|
|
39
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
40
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
41
|
+
calls.push({
|
|
42
|
+
url,
|
|
43
|
+
method: init?.method ?? "GET",
|
|
44
|
+
headers: new Headers(init?.headers),
|
|
45
|
+
body: typeof init?.body === "string" ? init.body : "",
|
|
46
|
+
});
|
|
47
|
+
if (url.startsWith("https://api.bitbucket.org/")) {
|
|
48
|
+
if (bitbucketReply === "throw") throw new TypeError("network down");
|
|
49
|
+
return new Response(JSON.stringify(bitbucketReply.body), {
|
|
50
|
+
status: bitbucketReply.status,
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
|
|
55
|
+
}) as typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const bitbucketCalls = () => calls.filter((c) => c.url.startsWith("https://api.bitbucket.org/"));
|
|
59
|
+
const patchCalls = () => calls.filter((c) => c.method === "PATCH");
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
repoDir = await mkdtemp(join(tmpdir(), "runner-bb-vcs-"));
|
|
63
|
+
await git(["init", "--quiet"]);
|
|
64
|
+
await git(["checkout", "--quiet", "-b", BRANCH]);
|
|
65
|
+
await git(["remote", "add", "origin", "https://bitbucket.org/acme-ws/web.app.git"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
await rm(repoDir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
calls = [];
|
|
74
|
+
bitbucketReply = { status: 200, body: { values: [] } };
|
|
75
|
+
process.env.BITBUCKET_TOKEN = "example-bb-token";
|
|
76
|
+
stubFetch();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
globalThis.fetch = originalFetch;
|
|
81
|
+
if (originalToken === undefined) delete process.env.BITBUCKET_TOKEN;
|
|
82
|
+
else process.env.BITBUCKET_TOKEN = originalToken;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("detectVcsForTask on a Bitbucket remote", () => {
|
|
86
|
+
test("an open PR for the branch links the task", async () => {
|
|
87
|
+
bitbucketReply = {
|
|
88
|
+
status: 200,
|
|
89
|
+
body: { values: [{ id: 17, links: { html: { href: PR_HTML } } }] },
|
|
90
|
+
};
|
|
91
|
+
const taskId = nextTaskId();
|
|
92
|
+
await detectVcsForTask(API_URL, "swarm-key", taskId, repoDir);
|
|
93
|
+
|
|
94
|
+
const [lookup] = bitbucketCalls();
|
|
95
|
+
expect(lookup).toBeDefined();
|
|
96
|
+
const lookupUrl = new URL(lookup!.url);
|
|
97
|
+
expect(lookupUrl.pathname).toBe("/2.0/repositories/acme-ws/web.app/pullrequests");
|
|
98
|
+
expect(lookupUrl.searchParams.get("q")).toBe(`source.branch.name="${BRANCH}" AND state="OPEN"`);
|
|
99
|
+
expect(lookupUrl.searchParams.get("pagelen")).toBe("1");
|
|
100
|
+
expect(lookup!.headers.get("authorization")).toBe("Bearer example-bb-token");
|
|
101
|
+
|
|
102
|
+
const patches = patchCalls();
|
|
103
|
+
expect(patches).toHaveLength(1);
|
|
104
|
+
expect(patches[0]!.url).toBe(`${API_URL}/api/tasks/${taskId}/vcs`);
|
|
105
|
+
expect(patches[0]!.headers.get("authorization")).toBe("Bearer swarm-key");
|
|
106
|
+
expect(JSON.parse(patches[0]!.body)).toEqual({
|
|
107
|
+
vcsProvider: "bitbucket",
|
|
108
|
+
vcsRepo: "acme-ws/web.app",
|
|
109
|
+
vcsNumber: 17,
|
|
110
|
+
vcsUrl: PR_HTML,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("no open PR sends no PATCH", async () => {
|
|
115
|
+
await detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir);
|
|
116
|
+
expect(bitbucketCalls()).toHaveLength(1);
|
|
117
|
+
expect(patchCalls()).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a non-200 answer is swallowed and sends no PATCH", async () => {
|
|
121
|
+
bitbucketReply = { status: 401, body: { type: "error", error: { message: "Unauthorized" } } };
|
|
122
|
+
await expect(
|
|
123
|
+
detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir),
|
|
124
|
+
).resolves.toBeUndefined();
|
|
125
|
+
expect(bitbucketCalls()).toHaveLength(1);
|
|
126
|
+
expect(patchCalls()).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a network failure is swallowed and sends no PATCH", async () => {
|
|
130
|
+
bitbucketReply = "throw";
|
|
131
|
+
await expect(
|
|
132
|
+
detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir),
|
|
133
|
+
).resolves.toBeUndefined();
|
|
134
|
+
expect(patchCalls()).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("without BITBUCKET_TOKEN it calls nothing", async () => {
|
|
138
|
+
delete process.env.BITBUCKET_TOKEN;
|
|
139
|
+
await detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir);
|
|
140
|
+
expect(calls).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("findBitbucketOpenPullRequest", () => {
|
|
145
|
+
test("escapes quotes in the branch name inside the BBQL query", async () => {
|
|
146
|
+
let seen = "";
|
|
147
|
+
const fetchImpl = (async (input: string | URL | Request) => {
|
|
148
|
+
seen = String(input);
|
|
149
|
+
return new Response(JSON.stringify({ values: [] }), { status: 200 });
|
|
150
|
+
}) as typeof fetch;
|
|
151
|
+
await findBitbucketOpenPullRequest("git@bitbucket.org:ws/repo.git", 'we"ird', {
|
|
152
|
+
token: "t",
|
|
153
|
+
fetchImpl,
|
|
154
|
+
});
|
|
155
|
+
expect(new URL(seen).searchParams.get("q")).toBe(
|
|
156
|
+
'source.branch.name="we\\"ird" AND state="OPEN"',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("rejects a reply without a numeric id or an html link", async () => {
|
|
161
|
+
for (const values of [
|
|
162
|
+
[{ id: "17", links: { html: { href: PR_HTML } } }],
|
|
163
|
+
[{ id: 17, links: {} }],
|
|
164
|
+
[{}],
|
|
165
|
+
]) {
|
|
166
|
+
const fetchImpl = (async () =>
|
|
167
|
+
new Response(JSON.stringify({ values }), { status: 200 })) as unknown as typeof fetch;
|
|
168
|
+
expect(
|
|
169
|
+
await findBitbucketOpenPullRequest("https://bitbucket.org/ws/repo", "b", {
|
|
170
|
+
token: "t",
|
|
171
|
+
fetchImpl,
|
|
172
|
+
}),
|
|
173
|
+
).toBeNull();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("returns null for a non-Bitbucket remote without calling fetch", async () => {
|
|
178
|
+
let called = false;
|
|
179
|
+
const fetchImpl = (async () => {
|
|
180
|
+
called = true;
|
|
181
|
+
return new Response("{}");
|
|
182
|
+
}) as unknown as typeof fetch;
|
|
183
|
+
expect(
|
|
184
|
+
await findBitbucketOpenPullRequest("https://github.com/ws/repo", "b", {
|
|
185
|
+
token: "t",
|
|
186
|
+
fetchImpl,
|
|
187
|
+
}),
|
|
188
|
+
).toBeNull();
|
|
189
|
+
expect(called).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -34,6 +34,9 @@ type StubState = {
|
|
|
34
34
|
};
|
|
35
35
|
calls: { signature: number; list: number; sync: number };
|
|
36
36
|
skillFilesStatus: number;
|
|
37
|
+
/** 404 by default so the legacy list path stays covered; bundle tests opt in. */
|
|
38
|
+
bundleStatus: number;
|
|
39
|
+
bundleCalls: number;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const FAKE_HOME = join(tmpdir(), `runner-refresh-test-${process.pid}`);
|
|
@@ -67,6 +70,8 @@ const state: StubState = {
|
|
|
67
70
|
signature: "hash-v1",
|
|
68
71
|
},
|
|
69
72
|
calls: { signature: 0, list: 0, sync: 0 },
|
|
73
|
+
bundleStatus: 404,
|
|
74
|
+
bundleCalls: 0,
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
@@ -91,6 +96,22 @@ describe("refreshSkillsIfChanged", () => {
|
|
|
91
96
|
generatedAt: new Date().toISOString(),
|
|
92
97
|
});
|
|
93
98
|
}
|
|
99
|
+
if (url.pathname.endsWith("/skills/bundle")) {
|
|
100
|
+
state.bundleCalls++;
|
|
101
|
+
if (state.bundleStatus !== 200) {
|
|
102
|
+
return new Response("not found", { status: state.bundleStatus });
|
|
103
|
+
}
|
|
104
|
+
return Response.json({
|
|
105
|
+
skills: state.skillsBody.skills.map((skill) => ({
|
|
106
|
+
...skill,
|
|
107
|
+
files: skill.isComplex
|
|
108
|
+
? [{ path: "refs/notes.md", content: "bundled note", isBinary: false }]
|
|
109
|
+
: [],
|
|
110
|
+
})),
|
|
111
|
+
total: state.skillsBody.skills.length,
|
|
112
|
+
signature: state.skillsBody.signature,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
94
115
|
if (url.pathname.match(/\/api\/agents\/[^/]+\/skills$/)) {
|
|
95
116
|
state.calls.list++;
|
|
96
117
|
if (state.listStatus !== 200) {
|
|
@@ -337,6 +358,43 @@ describe("refreshSkillsIfChanged", () => {
|
|
|
337
358
|
rmSync(cleanHome, { recursive: true, force: true });
|
|
338
359
|
});
|
|
339
360
|
|
|
361
|
+
test("bundle endpoint replaces the list call and the per-file fan-out", async () => {
|
|
362
|
+
state.calls = { signature: 0, list: 0, sync: 0 };
|
|
363
|
+
state.bundleCalls = 0;
|
|
364
|
+
state.bundleStatus = 200;
|
|
365
|
+
state.signatureHash = "hash-bundle";
|
|
366
|
+
state.skillsBody.signature = "hash-bundle";
|
|
367
|
+
state.skillsBody.skills = [
|
|
368
|
+
{
|
|
369
|
+
id: "skill-complex",
|
|
370
|
+
name: "complex-skill",
|
|
371
|
+
description: "has bundled files",
|
|
372
|
+
content: "---\nname: complex-skill\n---\n\nComplex body.",
|
|
373
|
+
isComplex: true,
|
|
374
|
+
isActive: true,
|
|
375
|
+
isEnabled: true,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const home = join(FAKE_HOME, "bundle-home");
|
|
380
|
+
mkdirSync(home, { recursive: true });
|
|
381
|
+
const lastHash = { current: null as string | null };
|
|
382
|
+
const result = await refreshWithHome(makeCtx(), lastHash, home);
|
|
383
|
+
|
|
384
|
+
expect(result.changed).toBe(true);
|
|
385
|
+
expect(lastHash.current).toBe("hash-bundle");
|
|
386
|
+
// One signature probe + one bundle call. No list call, no manifest and no
|
|
387
|
+
// per-file fetches.
|
|
388
|
+
expect(state.bundleCalls).toBe(1);
|
|
389
|
+
expect(state.calls).toEqual({ signature: 1, list: 0, sync: 0 });
|
|
390
|
+
expect(
|
|
391
|
+
readFileSync(join(home, ".claude", "skills", "complex-skill", "refs", "notes.md"), "utf-8"),
|
|
392
|
+
).toBe("bundled note");
|
|
393
|
+
|
|
394
|
+
state.bundleStatus = 404;
|
|
395
|
+
rmSync(home, { recursive: true, force: true });
|
|
396
|
+
});
|
|
397
|
+
|
|
340
398
|
test("list fetch failure after signature drift leaves pre-existing skills on disk and does not advance hash", async () => {
|
|
341
399
|
// Arrange: fresh isolated home with a pre-existing swarm-managed skill
|
|
342
400
|
const isolatedHome = join(FAKE_HOME, "list-fail-home");
|
|
@@ -74,6 +74,21 @@ describe("scrubSecrets — env-based replacement", () => {
|
|
|
74
74
|
expect(out).toBe("Authorization: Bearer [REDACTED:GITHUB_TOKEN] end");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("redacts the Bitbucket token and webhook secret as exact keys", () => {
|
|
78
|
+
const values = {
|
|
79
|
+
BITBUCKET_TOKEN: `example-bitbucket-${randomToken(24)}`,
|
|
80
|
+
BITBUCKET_WEBHOOK_SECRET: `example-bbhook-${randomToken(24)}`,
|
|
81
|
+
};
|
|
82
|
+
for (const [name, value] of Object.entries(values)) process.env[name] = value;
|
|
83
|
+
refreshSecretScrubberCache();
|
|
84
|
+
for (const [name, value] of Object.entries(values)) {
|
|
85
|
+
expect(isSensitiveKey(name)).toBe(true);
|
|
86
|
+
expect(scrubSecrets(`Authorization: Bearer ${value} end`)).toBe(
|
|
87
|
+
`Authorization: Bearer [REDACTED:${name}] end`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
77
92
|
test("redacts any key with _API_KEY suffix", () => {
|
|
78
93
|
process.env.FOO_SERVICE_API_KEY = "example-supersecretFooApiKey_longerthan12chars";
|
|
79
94
|
refreshSecretScrubberCache();
|