@desplega.ai/agent-swarm 2.5.3 → 2.6.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 +2 -2
- package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
- package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
- package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
- package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
- package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/cli-1tem6ns9.js +894 -0
- package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
- package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
- package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
- package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
- package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
- package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
- package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
- package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
- package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
- package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
- package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
- package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
- package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
- package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
- package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
- package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
- package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
- package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
- package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
- package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
- package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
- package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
- package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
- package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
- package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
- package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
- package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
- package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
- package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
- package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
- package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
- package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
- package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
- package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
- package/dist/cli-nq677csy.js +140 -0
- package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
- package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
- package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
- package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
- package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
- package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
- package/dist/handlers-eqsnmv95.js +41 -0
- package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
- package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
- package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
- package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
- package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
- package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
- package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
- package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
- package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
- package/dist/index-yqgbfgaj.js +43 -0
- package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
- package/dist/lead-23e7z59t.js +79 -0
- package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
- package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
- package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
- package/dist/render-v2-tjay5hhf.js +16 -0
- package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
- package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
- package/dist/session-source-backfill-qgtc86z1.js +110 -0
- package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
- package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
- package/dist/worker-e3w7vye1.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1323 -16
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/assets/source-key.ts +95 -0
- package/src/be/db/api-keys.ts +80 -0
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +21 -6
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +500 -7
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +126 -131
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/typecheck.ts +1 -1
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/users.ts +9 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/controller.ts +33 -0
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/provider-credentials.ts +210 -170
- package/src/commands/runner.ts +31 -6
- package/src/extensions/apply-task-create.ts +64 -11
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +30 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +53 -0
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +49 -17
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/index.ts +11 -0
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pool-keys.ts +354 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +207 -33
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +34 -5
- package/src/providers/claude-managed-adapter.ts +42 -9
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +83 -11
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scheduler/scheduler.ts +3 -2
- package/src/scripts-runtime/swarm-sdk.ts +7 -1
- package/src/scripts-runtime/types/stdlib.d.ts +1 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +321 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/credential-check.test.ts +290 -2
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-pre-task.test.ts +325 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +336 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/types.ts +24 -0
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/context-window.ts +8 -6
- package/src/utils/credentials.ts +5 -2
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-19xr6dnz.js +0 -41
- package/dist/index-kw2mmat7.js +0 -43
- package/dist/lead-zyerk1x2.js +0 -79
- package/dist/render-v2-hgytz8nn.js +0 -12
- package/dist/worker-42t8emjt.js +0 -79
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
claimContextBoundNote,
|
|
8
|
+
contextBoundNoteThreshold,
|
|
9
|
+
contextStatePath,
|
|
10
|
+
createContextBoundNote,
|
|
11
|
+
recordContextState,
|
|
12
|
+
} from "../hooks/context-bound-note";
|
|
13
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
14
|
+
import { normalizeClaudeMessage } from "../providers/claude-session-events";
|
|
15
|
+
|
|
16
|
+
let dir: string;
|
|
17
|
+
let n = 0;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
dir = await mkdtemp(join(tmpdir(), "context-bound-note-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await rm(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** A fresh task file path with `contextTokens` recorded next to it. */
|
|
28
|
+
function taskFileWith(contextTokens?: number): string {
|
|
29
|
+
const taskFile = join(dir, `task-${++n}.json`);
|
|
30
|
+
if (contextTokens !== undefined) recordContextState(taskFile, contextTokens);
|
|
31
|
+
return taskFile;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("contextBoundNoteThreshold", () => {
|
|
35
|
+
test("off when unset, blank, invalid or 0", () => {
|
|
36
|
+
expect(contextBoundNoteThreshold({})).toBe(0);
|
|
37
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "" })).toBe(0);
|
|
38
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: " " })).toBe(0);
|
|
39
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "-5" })).toBe(0);
|
|
40
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "lots" })).toBe(0);
|
|
41
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "1e5" })).toBe(0);
|
|
42
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "99999999999999999999" })).toBe(
|
|
43
|
+
0,
|
|
44
|
+
);
|
|
45
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "0" })).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a positive integer turns the note on at that threshold", () => {
|
|
49
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "150000" })).toBe(150_000);
|
|
50
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: " 200000 " })).toBe(200_000);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("claimContextBoundNote", () => {
|
|
55
|
+
test("fires once at the threshold, not again on later calls", async () => {
|
|
56
|
+
const taskFile = taskFileWith(200_000);
|
|
57
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({ contextTokens: 200_000 });
|
|
58
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
59
|
+
recordContextState(taskFile, 400_000);
|
|
60
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("concurrent hooks fire the note once", async () => {
|
|
64
|
+
const taskFile = taskFileWith(300_000);
|
|
65
|
+
const results = await Promise.all(
|
|
66
|
+
Array.from({ length: 8 }, () => claimContextBoundNote(taskFile, 200_000)),
|
|
67
|
+
);
|
|
68
|
+
expect(results.filter((r) => r !== null)).toEqual([{ contextTokens: 300_000 }]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no-op below the threshold, then fires once it is crossed", async () => {
|
|
72
|
+
const taskFile = taskFileWith(199_999);
|
|
73
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
74
|
+
recordContextState(taskFile, 210_000);
|
|
75
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({ contextTokens: 210_000 });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns the model the session recorded", async () => {
|
|
79
|
+
const taskFile = join(dir, `task-${++n}.json`);
|
|
80
|
+
recordContextState(taskFile, 250_000, "claude-opus-5-5");
|
|
81
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({
|
|
82
|
+
contextTokens: 250_000,
|
|
83
|
+
model: "claude-opus-5-5",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("disabled at 0", async () => {
|
|
88
|
+
expect(await claimContextBoundNote(taskFileWith(900_000), 0)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("fails open on missing task file, missing state or corrupt state", async () => {
|
|
92
|
+
expect(await claimContextBoundNote(undefined, 200_000)).toBeNull();
|
|
93
|
+
expect(await claimContextBoundNote(taskFileWith(), 200_000)).toBeNull();
|
|
94
|
+
|
|
95
|
+
const corrupt = taskFileWith();
|
|
96
|
+
await Bun.write(contextStatePath(corrupt), "{not json");
|
|
97
|
+
expect(await claimContextBoundNote(corrupt, 200_000)).toBeNull();
|
|
98
|
+
|
|
99
|
+
const wrongShape = taskFileWith();
|
|
100
|
+
await Bun.write(contextStatePath(wrongShape), JSON.stringify({ contextTokens: "300000" }));
|
|
101
|
+
expect(await claimContextBoundNote(wrongShape, 200_000)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("fails open when the marker cannot be written", async () => {
|
|
105
|
+
const taskFile = taskFileWith(300_000);
|
|
106
|
+
const writeFile = spyOn(fsPromises, "writeFile").mockRejectedValue(
|
|
107
|
+
Object.assign(new Error("EIO: i/o error"), { code: "EIO" }),
|
|
108
|
+
);
|
|
109
|
+
try {
|
|
110
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
111
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
112
|
+
} finally {
|
|
113
|
+
writeFile.mockRestore();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("createContextBoundNote", () => {
|
|
119
|
+
function observer(
|
|
120
|
+
threshold: number,
|
|
121
|
+
inject: (note: string) => unknown = () => {},
|
|
122
|
+
canDeliver: () => boolean = () => true,
|
|
123
|
+
model?: () => string | undefined,
|
|
124
|
+
) {
|
|
125
|
+
const notes: string[] = [];
|
|
126
|
+
const observe = createContextBoundNote({
|
|
127
|
+
threshold,
|
|
128
|
+
canDeliver,
|
|
129
|
+
model,
|
|
130
|
+
inject: (note) => {
|
|
131
|
+
notes.push(note);
|
|
132
|
+
return inject(note);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return { observe, notes };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("fires once at the threshold, not again on later requests", async () => {
|
|
139
|
+
const { observe, notes } = observer(200_000);
|
|
140
|
+
await observe(200_000);
|
|
141
|
+
await observe(400_000);
|
|
142
|
+
expect(notes).toEqual([await renderContextBoundNote(200_000)]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("no-op below the threshold, then fires once it is crossed", async () => {
|
|
146
|
+
const { observe, notes } = observer(200_000);
|
|
147
|
+
await observe(199_999);
|
|
148
|
+
expect(notes).toEqual([]);
|
|
149
|
+
await observe(210_000);
|
|
150
|
+
expect(notes).toEqual([await renderContextBoundNote(210_000)]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("disabled at 0", async () => {
|
|
154
|
+
const { observe, notes } = observer(0);
|
|
155
|
+
await observe(900_000);
|
|
156
|
+
expect(notes).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("fails open on missing or corrupt sizes", async () => {
|
|
160
|
+
const { observe, notes } = observer(200_000);
|
|
161
|
+
await observe(undefined);
|
|
162
|
+
await observe(Number.NaN);
|
|
163
|
+
await observe(Number.POSITIVE_INFINITY);
|
|
164
|
+
await observe("300000" as unknown as number);
|
|
165
|
+
expect(notes).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("never rejects when injection fails, and does not retry", async () => {
|
|
169
|
+
const { observe, notes } = observer(200_000, () => Promise.reject(new Error("closed")));
|
|
170
|
+
await expect(observe(300_000)).resolves.toBeUndefined();
|
|
171
|
+
await observe(300_000);
|
|
172
|
+
expect(notes).toHaveLength(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("holds a note rendered after its window closed, then delivers it on the next call", async () => {
|
|
176
|
+
let open = false;
|
|
177
|
+
const { observe, notes } = observer(200_000, undefined, () => open);
|
|
178
|
+
await observe(300_000);
|
|
179
|
+
expect(notes).toEqual([]);
|
|
180
|
+
open = true;
|
|
181
|
+
await observe(500_000);
|
|
182
|
+
// The held note, not a re-render at the later size.
|
|
183
|
+
expect(notes).toEqual([await renderContextBoundNote(300_000)]);
|
|
184
|
+
await observe(600_000);
|
|
185
|
+
expect(notes).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("never delivers when the window stays closed", async () => {
|
|
189
|
+
const { observe, notes } = observer(200_000, undefined, () => false);
|
|
190
|
+
await observe(300_000);
|
|
191
|
+
await observe(400_000);
|
|
192
|
+
expect(notes).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("renders with the session's model at render time", async () => {
|
|
196
|
+
let model = "made-up-model";
|
|
197
|
+
const { observe, notes } = observer(200_000, undefined, undefined, () => model);
|
|
198
|
+
model = "claude-opus-5-5";
|
|
199
|
+
await observe(210_000);
|
|
200
|
+
expect(notes).toEqual([
|
|
201
|
+
"Context is 210,000 tokens (21% of the 1,000,000-token window). Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.",
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("checks the window after rendering, not before", async () => {
|
|
206
|
+
let open = true;
|
|
207
|
+
const { observe, notes } = observer(200_000, undefined, () => open);
|
|
208
|
+
const pending = observe(300_000);
|
|
209
|
+
open = false;
|
|
210
|
+
await pending;
|
|
211
|
+
expect(notes).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("renderContextBoundNote", () => {
|
|
216
|
+
const rest =
|
|
217
|
+
"Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
|
|
218
|
+
|
|
219
|
+
test("renders the code default with a formatted token count and no model", async () => {
|
|
220
|
+
expect(await renderContextBoundNote(213_456)).toBe(`Context is 213,456 tokens. ${rest}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("adds the window share for a known model", async () => {
|
|
224
|
+
expect(await renderContextBoundNote(210_000, "claude-opus-5-5")).toBe(
|
|
225
|
+
`Context is 210,000 tokens (21% of the 1,000,000-token window). ${rest}`,
|
|
226
|
+
);
|
|
227
|
+
expect(await renderContextBoundNote(200_000, "gpt-5")).toBe(
|
|
228
|
+
`Context is 200,000 tokens (50% of the 400,000-token window). ${rest}`,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("omits the share for an unknown model instead of using the 200k fallback", async () => {
|
|
233
|
+
expect(await renderContextBoundNote(210_000, "made-up-model")).toBe(
|
|
234
|
+
`Context is 210,000 tokens. ${rest}`,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("normalizeClaudeMessage requestContextTokens", () => {
|
|
240
|
+
const state = {
|
|
241
|
+
taskId: "task-fixture",
|
|
242
|
+
agentId: "agent-fixture",
|
|
243
|
+
model: "claude-opus-5-5",
|
|
244
|
+
contextWindowSize: 1_000_000,
|
|
245
|
+
transport: "cli" as const,
|
|
246
|
+
};
|
|
247
|
+
const usage = {
|
|
248
|
+
input_tokens: 10,
|
|
249
|
+
cache_read_input_tokens: 180_000,
|
|
250
|
+
cache_creation_input_tokens: 25_000,
|
|
251
|
+
output_tokens: 4_000,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
test("is input + cache read + cache write of a main-thread request", () => {
|
|
255
|
+
const normalized = normalizeClaudeMessage(
|
|
256
|
+
{ type: "assistant", message: { content: [], usage } },
|
|
257
|
+
state,
|
|
258
|
+
);
|
|
259
|
+
expect(normalized.requestContextTokens).toBe(205_010);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("is unset for subagent messages", () => {
|
|
263
|
+
const normalized = normalizeClaudeMessage(
|
|
264
|
+
{ type: "assistant", parent_tool_use_id: "toolu_1", message: { content: [], usage } },
|
|
265
|
+
state,
|
|
266
|
+
);
|
|
267
|
+
expect(normalized.requestContextTokens).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { OPENAI_COMPATIBLE_PROVIDERS } from "@desplega/model-catalog";
|
|
3
3
|
import {
|
|
4
4
|
buildCredStatusReport,
|
|
5
5
|
CREDENTIAL_PROVIDER_CHECKERS,
|
|
6
|
+
checkEachKey,
|
|
6
7
|
checkOpenAICompatibleProvider,
|
|
7
8
|
checkProviderCredentials,
|
|
8
9
|
isBedrockMode,
|
|
@@ -27,6 +28,241 @@ function fsWith(present: Set<string>): { existsSync(p: string): boolean } {
|
|
|
27
28
|
|
|
28
29
|
const noFiles = fsWith(new Set());
|
|
29
30
|
|
|
31
|
+
describe("pool live test", () => {
|
|
32
|
+
const realFetch = globalThis.fetch;
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
globalThis.fetch = realFetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("all members pass in parallel with the maximum latency", async () => {
|
|
38
|
+
let release!: () => void;
|
|
39
|
+
const gate = new Promise<void>((resolve) => {
|
|
40
|
+
release = resolve;
|
|
41
|
+
});
|
|
42
|
+
const check = mock(async (key: string) => {
|
|
43
|
+
await gate;
|
|
44
|
+
return { ok: true, latency_ms: key === "fake-abcde" ? 30 : 10 };
|
|
45
|
+
});
|
|
46
|
+
const pending = checkEachKey("OPENAI_API_KEY", " fake-abcde, fake-fghij ", check);
|
|
47
|
+
expect(check).toHaveBeenCalledTimes(2);
|
|
48
|
+
release();
|
|
49
|
+
expect(await pending).toEqual({
|
|
50
|
+
ok: true,
|
|
51
|
+
latency_ms: 30,
|
|
52
|
+
keys: [
|
|
53
|
+
{ keyType: "OPENAI_API_KEY", index: 0, suffix: "abcde", ok: true, latency_ms: 30 },
|
|
54
|
+
{ keyType: "OPENAI_API_KEY", index: 1, suffix: "fghij", ok: true, latency_ms: 10 },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("one failed member preserves pool order and reports a warning", async () => {
|
|
60
|
+
const result = await checkEachKey(
|
|
61
|
+
"ANTHROPIC_API_KEY",
|
|
62
|
+
"fake-first,fake-abcde,fake-third",
|
|
63
|
+
async (key) =>
|
|
64
|
+
key === "fake-abcde"
|
|
65
|
+
? { ok: false, error: "Unauthorized", httpStatus: 401, latency_ms: 20 }
|
|
66
|
+
: { ok: true, latency_ms: 10 },
|
|
67
|
+
);
|
|
68
|
+
expect(result).toMatchObject({
|
|
69
|
+
ok: true,
|
|
70
|
+
latency_ms: 20,
|
|
71
|
+
warning: "1 of 3 keys failed: key ending abcde (HTTP 401).",
|
|
72
|
+
});
|
|
73
|
+
expect(result.keys).toEqual([
|
|
74
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "first", ok: true, latency_ms: 10 },
|
|
75
|
+
{
|
|
76
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
77
|
+
index: 1,
|
|
78
|
+
suffix: "abcde",
|
|
79
|
+
ok: false,
|
|
80
|
+
error: "Unauthorized",
|
|
81
|
+
httpStatus: 401,
|
|
82
|
+
latency_ms: 20,
|
|
83
|
+
},
|
|
84
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 2, suffix: "third", ok: true, latency_ms: 10 },
|
|
85
|
+
]);
|
|
86
|
+
expect(result.error).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("several failed members use HTTP status or the member error", async () => {
|
|
90
|
+
const result = await checkEachKey(
|
|
91
|
+
"OPENAI_API_KEY",
|
|
92
|
+
"fake-abcde,fake-fghij,fake-third",
|
|
93
|
+
async (key) => {
|
|
94
|
+
if (key === "fake-abcde") {
|
|
95
|
+
return { ok: false, error: "Unauthorized", httpStatus: 401, latency_ms: 10 };
|
|
96
|
+
}
|
|
97
|
+
if (key === "fake-fghij") return { ok: false, error: "timeout", latency_ms: 30 };
|
|
98
|
+
return { ok: true, latency_ms: 20 };
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
expect(result.ok).toBe(true);
|
|
102
|
+
expect(result.warning).toBe(
|
|
103
|
+
"2 of 3 keys failed: key ending abcde (HTTP 401), key ending fghij (timeout).",
|
|
104
|
+
);
|
|
105
|
+
expect(result.latency_ms).toBe(30);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("all failed members produce an error", async () => {
|
|
109
|
+
const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde,fake-fghij", async () => ({
|
|
110
|
+
ok: false,
|
|
111
|
+
error: "Unauthorized",
|
|
112
|
+
httpStatus: 401,
|
|
113
|
+
latency_ms: 10,
|
|
114
|
+
}));
|
|
115
|
+
expect(result).toMatchObject({
|
|
116
|
+
ok: false,
|
|
117
|
+
error: "All 2 keys failed: key ending abcde (HTTP 401), key ending fghij (HTTP 401).",
|
|
118
|
+
latency_ms: 10,
|
|
119
|
+
});
|
|
120
|
+
expect(result.keys).toHaveLength(2);
|
|
121
|
+
expect(result.warning).toBeUndefined();
|
|
122
|
+
expect(result.httpStatus).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("one failed key preserves the member error and HTTP status", async () => {
|
|
126
|
+
const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde", async () => ({
|
|
127
|
+
ok: false,
|
|
128
|
+
error: "HTTP 401: rejected",
|
|
129
|
+
httpStatus: 401,
|
|
130
|
+
latency_ms: 10,
|
|
131
|
+
}));
|
|
132
|
+
expect(result).toMatchObject({
|
|
133
|
+
ok: false,
|
|
134
|
+
error: "HTTP 401: rejected",
|
|
135
|
+
httpStatus: 401,
|
|
136
|
+
latency_ms: 10,
|
|
137
|
+
});
|
|
138
|
+
expect(result.keys).toHaveLength(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a thrown check becomes a scrubbed member failure", async () => {
|
|
142
|
+
const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde,fake-fghij", async (key) => {
|
|
143
|
+
if (key === "fake-abcde") throw new Error("timeout");
|
|
144
|
+
return { ok: true, latency_ms: 0 };
|
|
145
|
+
});
|
|
146
|
+
expect(result.ok).toBe(true);
|
|
147
|
+
expect(result.warning).toBe("1 of 2 keys failed: key ending abcde (timeout).");
|
|
148
|
+
expect(result.keys?.[0]).toMatchObject({ ok: false, error: "timeout" });
|
|
149
|
+
expect(result.keys?.[0]?.latency_ms).toBeGreaterThanOrEqual(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a CODEX_OAUTH JSON blob remains one member", async () => {
|
|
153
|
+
const blob = '{"access":"fake-access","refresh":"fake-refresh","expires":123}';
|
|
154
|
+
const check = mock(async () => ({ ok: true, latency_ms: 0 }));
|
|
155
|
+
const result = await checkEachKey("CODEX_OAUTH", ` ${blob} `, check);
|
|
156
|
+
expect(check).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(check).toHaveBeenCalledWith(blob);
|
|
158
|
+
expect(result.keys).toHaveLength(1);
|
|
159
|
+
expect(result.keys?.[0]?.suffix).toBe(blob.slice(-5));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("an empty split checks the trimmed input once", async () => {
|
|
163
|
+
const check = mock(async () => ({ ok: false, error: "Missing key", latency_ms: 0 }));
|
|
164
|
+
const result = await checkEachKey("OPENAI_API_KEY", " , , ", check);
|
|
165
|
+
expect(check).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(check).toHaveBeenCalledWith(", ,");
|
|
167
|
+
expect(result.error).toBe("Missing key");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("all configured members skip the live test", async () => {
|
|
171
|
+
const result = await checkEachKey("ANTHROPIC_API_KEY", "fake-abcde,fake-fghij", async () => ({
|
|
172
|
+
ok: true,
|
|
173
|
+
skipped: true,
|
|
174
|
+
latency_ms: 10,
|
|
175
|
+
}));
|
|
176
|
+
expect(result).toEqual({ ok: true, skipped: true, latency_ms: 10 });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("Claude API-key pools send a separate request for each member", async () => {
|
|
180
|
+
const fetchMock = mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
181
|
+
const key = new Headers(init?.headers).get("x-api-key");
|
|
182
|
+
return new Response("{}", { status: key === "fake-good1" ? 200 : 401 });
|
|
183
|
+
});
|
|
184
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
185
|
+
const result = await validateProviderCredentials("claude", {
|
|
186
|
+
ANTHROPIC_API_KEY: "fake-good1,fake-bad01",
|
|
187
|
+
});
|
|
188
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
189
|
+
expect(
|
|
190
|
+
fetchMock.mock.calls.map(([, init]) => new Headers(init?.headers).get("x-api-key")),
|
|
191
|
+
).toEqual(["fake-good1", "fake-bad01"]);
|
|
192
|
+
expect(result.ok).toBe(true);
|
|
193
|
+
expect(result.warning).toBe("1 of 2 keys failed: key ending bad01 (HTTP 401).");
|
|
194
|
+
expect(result.keys?.[1]?.httpStatus).toBe(401);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("Claude subscription pools check the OAuth profile for each member", async () => {
|
|
198
|
+
const fetchMock = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
|
|
199
|
+
return new Response("{}", { status: 200 });
|
|
200
|
+
});
|
|
201
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
202
|
+
const result = await validateProviderCredentials("claude", {
|
|
203
|
+
CLAUDE_CODE_OAUTH_TOKEN: "fake-token-a,fake-token-b",
|
|
204
|
+
});
|
|
205
|
+
expect(result.ok).toBe(true);
|
|
206
|
+
expect(result.keys).toHaveLength(2);
|
|
207
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
208
|
+
for (const [url, init] of fetchMock.mock.calls) {
|
|
209
|
+
expect(String(url)).toBe("https://api.anthropic.com/api/oauth/profile");
|
|
210
|
+
expect(init?.method).toBe("GET");
|
|
211
|
+
}
|
|
212
|
+
expect(
|
|
213
|
+
fetchMock.mock.calls.map(([, init]) => new Headers(init?.headers).get("Authorization")),
|
|
214
|
+
).toEqual(["Bearer fake-token-a", "Bearer fake-token-b"]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("buildCredStatusReport copies the warning and per-key results", async () => {
|
|
218
|
+
globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
|
|
219
|
+
const key = new Headers(init?.headers).get("x-api-key");
|
|
220
|
+
return new Response("{}", { status: key === "fake-good1" ? 200 : 401 });
|
|
221
|
+
}) as typeof fetch;
|
|
222
|
+
const report = await buildCredStatusReport(
|
|
223
|
+
"claude",
|
|
224
|
+
{ ANTHROPIC_API_KEY: "fake-good1,fake-bad01" },
|
|
225
|
+
{},
|
|
226
|
+
"boot",
|
|
227
|
+
);
|
|
228
|
+
expect(report.liveTest?.warning).toBe("1 of 2 keys failed: key ending bad01 (HTTP 401).");
|
|
229
|
+
expect(report.liveTest?.keys).toEqual([
|
|
230
|
+
{
|
|
231
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
232
|
+
index: 0,
|
|
233
|
+
suffix: "good1",
|
|
234
|
+
ok: true,
|
|
235
|
+
latency_ms: expect.any(Number),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
239
|
+
index: 1,
|
|
240
|
+
suffix: "bad01",
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "HTTP 401: {}",
|
|
243
|
+
httpStatus: 401,
|
|
244
|
+
latency_ms: expect.any(Number),
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
expect(AgentCredStatusSchema.parse(report).liveTest).toEqual(report.liveTest);
|
|
248
|
+
const success = await buildCredStatusReport(
|
|
249
|
+
"claude",
|
|
250
|
+
{ ANTHROPIC_API_KEY: "fake-good1" },
|
|
251
|
+
{},
|
|
252
|
+
"boot",
|
|
253
|
+
);
|
|
254
|
+
expect(success.liveTest).not.toHaveProperty("warning");
|
|
255
|
+
const presenceOnly = await buildCredStatusReport(
|
|
256
|
+
"acp",
|
|
257
|
+
{ ACP_COMMAND: "fake-command" },
|
|
258
|
+
{},
|
|
259
|
+
"boot",
|
|
260
|
+
);
|
|
261
|
+
expect(presenceOnly.liveTest).not.toHaveProperty("warning");
|
|
262
|
+
expect(presenceOnly.liveTest).not.toHaveProperty("keys");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
30
266
|
// ─── claude ──────────────────────────────────────────────────────────────────
|
|
31
267
|
|
|
32
268
|
describe("checkClaudeCredentials", () => {
|
|
@@ -1174,7 +1410,10 @@ describe("buildCredStatusReport", () => {
|
|
|
1174
1410
|
expect(failure.liveTest?.ok).toBe(false);
|
|
1175
1411
|
expect(failure.liveTest?.error).toContain("HTTP 401");
|
|
1176
1412
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1177
|
-
|
|
1413
|
+
// OpenRouter's /models is public, so its key is checked on /key.
|
|
1414
|
+
expect(String(fetchMock.mock.calls[0]?.[0])).toMatch(
|
|
1415
|
+
key === "OPENROUTER_API_KEY" ? /\/key$/ : /\/models$/,
|
|
1416
|
+
);
|
|
1178
1417
|
} finally {
|
|
1179
1418
|
globalThis.fetch = realFetch;
|
|
1180
1419
|
}
|
|
@@ -1437,3 +1676,52 @@ describe("runBedrockSdkProbeAndEnumerate — intersection logic", () => {
|
|
|
1437
1676
|
mock.restore();
|
|
1438
1677
|
});
|
|
1439
1678
|
});
|
|
1679
|
+
|
|
1680
|
+
describe("OpenRouter and DeepSeek live checks", () => {
|
|
1681
|
+
const realFetch = globalThis.fetch;
|
|
1682
|
+
afterEach(() => {
|
|
1683
|
+
globalThis.fetch = realFetch;
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
/** Answers by path; records every URL. */
|
|
1687
|
+
function stubByPath(statusFor: (url: string) => number) {
|
|
1688
|
+
const urls: string[] = [];
|
|
1689
|
+
globalThis.fetch = (async (input: string | URL | Request) => {
|
|
1690
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
1691
|
+
urls.push(url);
|
|
1692
|
+
return new Response("{}", { status: statusFor(url) });
|
|
1693
|
+
}) as typeof fetch;
|
|
1694
|
+
return urls;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
test("OpenRouter checks the key on /key, not the public /models", async () => {
|
|
1698
|
+
const urls = stubByPath((url) => (url.endsWith("/key") ? 401 : 200));
|
|
1699
|
+
const result = await validateProviderCredentials("pi", { OPENROUTER_API_KEY: "or-bad" });
|
|
1700
|
+
expect(result).toMatchObject({ ok: false, httpStatus: 401 });
|
|
1701
|
+
expect(urls).toEqual(["https://openrouter.ai/api/v1/key"]);
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
test("a base URL without /key falls back to /models", async () => {
|
|
1705
|
+
const urls = stubByPath((url) => (url.endsWith("/key") ? 404 : 200));
|
|
1706
|
+
const result = await validateProviderCredentials("pi", { OPENROUTER_API_KEY: "or-key" });
|
|
1707
|
+
expect(result.ok).toBe(true);
|
|
1708
|
+
expect(urls).toEqual([
|
|
1709
|
+
"https://openrouter.ai/api/v1/key",
|
|
1710
|
+
"https://openrouter.ai/api/v1/models",
|
|
1711
|
+
]);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
test("dsh checks DEEPSEEK_API_KEY on DeepSeek /models, else OpenRouter", async () => {
|
|
1715
|
+
const urls = stubByPath(() => 401);
|
|
1716
|
+
const deepseek = await validateProviderCredentials("dsh", {
|
|
1717
|
+
DEEPSEEK_API_KEY: "ds-bad",
|
|
1718
|
+
OPENROUTER_API_KEY: "or-key",
|
|
1719
|
+
});
|
|
1720
|
+
expect(deepseek).toMatchObject({ ok: false, httpStatus: 401 });
|
|
1721
|
+
expect(urls).toEqual(["https://api.deepseek.com/models"]);
|
|
1722
|
+
|
|
1723
|
+
const openrouter = await validateProviderCredentials("dsh", { OPENROUTER_API_KEY: "or-key" });
|
|
1724
|
+
expect(openrouter).toMatchObject({ ok: false, httpStatus: 401 });
|
|
1725
|
+
expect(urls.at(-1)).toBe("https://openrouter.ai/api/v1/key");
|
|
1726
|
+
});
|
|
1727
|
+
});
|