@desplega.ai/agent-swarm 2.5.4 → 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-ek38cwsv.js → acp-adapter-dgfakfpw.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-fbqma8aa.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-en0jy6hs.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-k8a5cgpp.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-a16433gr.js} +29 -25
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-6c1sjfnr.js} +1 -1
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-f4nn561n.js} +1 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-9bmkspyw.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-nys1kjxa.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-55207973.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-3cm35gey.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-nw5yd8cp.js} +11 -11
- package/dist/{cli-c61mbdds.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-jbnn4vxb.js → cli-24j1jeeb.js} +3 -3
- package/dist/{cli-pzatvw94.js → cli-2sjhpsns.js} +2 -2
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-3aqd4jaa.js} +12 -7
- package/dist/{cli-gbfxp48k.js → cli-3fwqy71j.js} +15 -15
- package/dist/{cli-7p4jxgtt.js → cli-3gfvmzht.js} +6 -6
- package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
- package/dist/{cli-asq5ydm4.js → cli-4hxrf0p4.js} +131 -50
- package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
- package/dist/{cli-k4tjsdx9.js → cli-5rj83bam.js} +272 -195
- package/dist/{cli-sbwsxvtd.js → cli-5sckpqcx.js} +2 -2
- package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
- package/dist/{cli-ethgy7ha.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-hh5we352.js → cli-9abdgn5t.js} +7 -7
- package/dist/{cli-ff1tm9n9.js → cli-a2rr0fmg.js} +1 -1
- package/dist/{cli-c952c7ac.js → cli-a34pr3w1.js} +2 -2
- package/dist/{cli-s682zgqz.js → cli-abkm71vr.js} +1 -1
- package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
- package/dist/{cli-bvpgr9ny.js → cli-aj8djyqt.js} +5 -5
- package/dist/{cli-gwhjmh8g.js → cli-art7c1zh.js} +5 -5
- package/dist/{cli-5bnsc0e1.js → cli-az25fn5f.js} +4 -4
- package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-1r5ad7j5.js → cli-c2habvsf.js} +12 -12
- package/dist/{cli-64mh9mf3.js → cli-ca6banwz.js} +69 -17
- package/dist/{cli-25n3mv5d.js → cli-cz37jqss.js} +5 -5
- package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
- package/dist/{cli-gn5af0x5.js → cli-emhjcj2y.js} +11 -11
- package/dist/{cli-6a1xg1p7.js → cli-esakngh8.js} +2 -2
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-4b2z6n0m.js → cli-ffvsj2qw.js} +17 -8
- package/dist/{cli-abs8ndtr.js → cli-g20jjwt4.js} +26 -26
- package/dist/{cli-jb30dzvk.js → cli-g8ka5j4j.js} +446 -164
- package/dist/{cli-19202kew.js → cli-gf95q3bm.js} +5 -5
- package/dist/{cli-eftzyh9y.js → cli-gnsb1zxa.js} +2 -2
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-e42vss7b.js → cli-hfeb68z4.js} +5 -5
- package/dist/{cli-g9vd9ece.js → cli-k21gx5ht.js} +14 -16
- package/dist/{cli-tsma7p8v.js → cli-kf4jree9.js} +2 -2
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-m5a9m0ak.js} +6 -6
- package/dist/{cli-621ce7kw.js → cli-m70bajzr.js} +3 -3
- package/dist/{cli-9j129brq.js → cli-mtnj4fwa.js} +6 -6
- package/dist/{cli-xzfq4at3.js → cli-mvjz5an3.js} +15 -11
- package/dist/{cli-277c31dw.js → cli-n7asmce6.js} +225 -3
- package/dist/{cli-xtrz6bzs.js → cli-nn6vx2mq.js} +1 -1
- package/dist/{cli-2655k16q.js → cli-nq677csy.js} +1 -1
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-wghjkr0e.js → cli-sqma6vw1.js} +10 -7
- 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-m4ecdxvz.js → cli-zqne1qcj.js} +7 -7
- package/dist/cli.js +17 -17
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-d95q4qsn.js} +7 -7
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-gdwd8h0c.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-jqmgk54b.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-z3jq589f.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-r2za39ef.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-4mr4jgkw.js} +2 -2
- package/dist/{db-47vj40dy.js → db-ppv2tze7.js} +3 -3
- package/dist/{db-w759qmx0.js → db-w36wwb5g.js} +4 -2
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-pv5h77vf.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-d4pdpb3r.js} +2 -2
- 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-e2s028da.js → external-channel-y6nkzhnn.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-t08b9ntm.js} +10 -10
- package/dist/handlers-eqsnmv95.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-vn31xwdk.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-6d0zbvvt.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cpjs8q51.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-9c6medda.js} +856 -99
- package/dist/{index-xdaahthb.js → index-38t35h0n.js} +20 -20
- package/dist/{index-3n8m9ea2.js → index-g6k9tctj.js} +1 -1
- package/dist/{index-1ef1myq2.js → index-k0yt1tvq.js} +20 -20
- package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
- package/dist/{index-q7m26kvs.js → index-qhjf2htm.js} +13 -10
- package/dist/index-yqgbfgaj.js +43 -0
- package/dist/{keepalive-v4ta8wgg.js → keepalive-em0117bp.js} +4 -4
- package/dist/lead-23e7z59t.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-kbme3qy9.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
- package/dist/{onboard-sfmpe4np.js → onboard-p6q0vz97.js} +3 -3
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-esv3k9yn.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-54qhqh1z.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-mymbwhj0.js} +10 -10
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-yb75wv33.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-1vp2mw4p.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-dexhc9x8.js} +1 -1
- package/dist/render-v2-tjay5hhf.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-8ayf37g5.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-fdhp987s.js} +4 -4
- package/dist/session-source-backfill-qgtc86z1.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-4rrmqcq3.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
- package/dist/{work-objects-kfahhjsq.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 +1274 -32
- package/package.json +1 -1
- package/src/assets/source-key.ts +95 -0
- package/src/be/db/api-keys.ts +80 -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 +6 -1
- package/src/be/db/workflow-runs.ts +183 -1
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +59 -114
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- 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/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -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/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 +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -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 +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- 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 +78 -9
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- 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 +17 -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/api-keys-validation.test.ts +170 -0
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- 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/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 +53 -1
- 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/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/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- 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/script-connections.test.ts +2 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/sdk-allowlist.test.ts +39 -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 +299 -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 +172 -0
- package/src/tests/workflow-http-v2.test.ts +131 -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/get-workflow-run.ts +14 -4
- package/src/types.ts +24 -0
- package/src/utils/company-profile.ts +51 -1
- 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/executors/sub-workflow.ts +8 -1
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -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
|
+
});
|
|
@@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
|
|
|
2
2
|
import {
|
|
3
3
|
CREDENTIAL_RETRY_INTERVAL_MS,
|
|
4
4
|
type CredentialRefreshState,
|
|
5
|
+
credentialFingerprint,
|
|
6
|
+
KEY_RECHECK_INTERVAL_MS,
|
|
7
|
+
recheckableFailures,
|
|
5
8
|
refreshCredentialStatus,
|
|
9
|
+
reportKeyValidation,
|
|
6
10
|
} from "../commands/credential-refresh";
|
|
7
11
|
import * as credentials from "../commands/provider-credentials";
|
|
12
|
+
import { fetchResolvedEnv } from "../commands/runner";
|
|
8
13
|
import type { AgentCredStatus } from "../types";
|
|
9
14
|
|
|
10
15
|
const api = { apiUrl: "http://unused.test", apiKey: "test", agentId: crypto.randomUUID() };
|
|
@@ -27,7 +32,14 @@ describe("runner credential refresh", () => {
|
|
|
27
32
|
let send: ReturnType<typeof spyOn<typeof credentials, "sendCredStatusReport">>;
|
|
28
33
|
|
|
29
34
|
beforeEach(() => {
|
|
30
|
-
state = {
|
|
35
|
+
state = {
|
|
36
|
+
harnessProvider: "claude",
|
|
37
|
+
ready: true,
|
|
38
|
+
lastRefreshAt: 0,
|
|
39
|
+
inFlight: false,
|
|
40
|
+
fingerprint: credentialFingerprint({}),
|
|
41
|
+
rejectedKeys: 0,
|
|
42
|
+
};
|
|
31
43
|
build = spyOn(credentials, "buildCredStatusReport").mockResolvedValue(ready);
|
|
32
44
|
send = spyOn(credentials, "sendCredStatusReport").mockResolvedValue(undefined);
|
|
33
45
|
});
|
|
@@ -43,6 +55,136 @@ describe("runner credential refresh", () => {
|
|
|
43
55
|
expect(state.ready).toBe(true);
|
|
44
56
|
});
|
|
45
57
|
|
|
58
|
+
test("a changed credential fingerprint refreshes a ready worker immediately", async () => {
|
|
59
|
+
const env = { ANTHROPIC_API_KEY: "test-pool-a,test-pool-b" };
|
|
60
|
+
await refreshCredentialStatus(api, state, "claude", env, 1);
|
|
61
|
+
expect(build).toHaveBeenCalledWith("claude", env, {}, "post_task");
|
|
62
|
+
expect(state.fingerprint).toBe(credentialFingerprint(env));
|
|
63
|
+
await refreshCredentialStatus(api, state, "claude", env, 2);
|
|
64
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
65
|
+
await refreshCredentialStatus(api, state, "claude", {}, 3);
|
|
66
|
+
expect(build).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an unchanged fingerprint does not refresh a ready worker", async () => {
|
|
70
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
|
|
71
|
+
expect(build).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("fingerprints ignore environment order and unrelated values", () => {
|
|
75
|
+
expect(credentialFingerprint({ ANTHROPIC_API_KEY: "test-a", PATH: "a" })).toBe(
|
|
76
|
+
credentialFingerprint({ PATH: "b", ANTHROPIC_API_KEY: "test-a" }),
|
|
77
|
+
);
|
|
78
|
+
for (const key of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "OPENAI_API_KEY"]) {
|
|
79
|
+
expect(credentialFingerprint({ [key]: "changed" })).not.toBe(credentialFingerprint({}));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("resolved credential env preserves the full pool before selection", async () => {
|
|
84
|
+
const originalFetch = globalThis.fetch;
|
|
85
|
+
globalThis.fetch = (async (url) => {
|
|
86
|
+
if (String(url).includes("/api/config/resolved")) {
|
|
87
|
+
return Response.json({
|
|
88
|
+
configs: [{ key: "ANTHROPIC_API_KEY", value: "test-aaa11,test-bbb22" }],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return Response.json({ availableIndices: [1], rejectedIndices: [0] });
|
|
92
|
+
}) as typeof fetch;
|
|
93
|
+
try {
|
|
94
|
+
const result = await fetchResolvedEnv(api.apiUrl, api.apiKey, api.agentId, {
|
|
95
|
+
HARNESS_PROVIDER: "claude",
|
|
96
|
+
});
|
|
97
|
+
expect(result.credentialEnv.ANTHROPIC_API_KEY).toBe("test-aaa11,test-bbb22");
|
|
98
|
+
expect(result.env.ANTHROPIC_API_KEY).toBe("test-bbb22");
|
|
99
|
+
expect(result.credentialEnv).not.toBe(result.env);
|
|
100
|
+
} finally {
|
|
101
|
+
globalThis.fetch = originalFetch;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rejected keys refresh after ten minutes, even when the worker is ready", async () => {
|
|
106
|
+
state.rejectedKeys = 1;
|
|
107
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS - 1);
|
|
108
|
+
expect(build).not.toHaveBeenCalled();
|
|
109
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
|
|
110
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(state.rejectedKeys).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a failed live test without per-key results remains eligible for recheck", async () => {
|
|
115
|
+
build.mockResolvedValueOnce({
|
|
116
|
+
...ready,
|
|
117
|
+
liveTest: { ok: false, error: "timeout", latency_ms: 1 },
|
|
118
|
+
});
|
|
119
|
+
await refreshCredentialStatus(api, state, "pi", {}, 1);
|
|
120
|
+
expect(state.rejectedKeys).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("refresh reports per-key results once per credential type", async () => {
|
|
124
|
+
const keys = [
|
|
125
|
+
{
|
|
126
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
127
|
+
index: 0,
|
|
128
|
+
suffix: "aaa11",
|
|
129
|
+
ok: false,
|
|
130
|
+
httpStatus: 401,
|
|
131
|
+
latency_ms: 1,
|
|
132
|
+
},
|
|
133
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 1, suffix: "bbb22", ok: true, latency_ms: 1 },
|
|
134
|
+
{ keyType: "OPENAI_API_KEY", index: 0, suffix: "ccc33", ok: true, latency_ms: 1 },
|
|
135
|
+
];
|
|
136
|
+
build.mockResolvedValueOnce({ ...ready, liveTest: { ok: false, latency_ms: 1, keys } });
|
|
137
|
+
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
138
|
+
const originalFetch = globalThis.fetch;
|
|
139
|
+
globalThis.fetch = (async (url, init) => {
|
|
140
|
+
requests.push({ url: String(url), init });
|
|
141
|
+
return Response.json({ success: true });
|
|
142
|
+
}) as typeof fetch;
|
|
143
|
+
try {
|
|
144
|
+
await refreshCredentialStatus(api, state, "pi", {}, 1);
|
|
145
|
+
expect(state.rejectedKeys).toBe(1);
|
|
146
|
+
expect(requests).toHaveLength(2);
|
|
147
|
+
expect(requests[0]!.url).toBe(`${api.apiUrl}/api/keys/report-validation`);
|
|
148
|
+
expect(requests[0]!.init?.headers).toMatchObject({
|
|
149
|
+
Authorization: `Bearer ${api.apiKey}`,
|
|
150
|
+
"X-Agent-ID": api.agentId,
|
|
151
|
+
});
|
|
152
|
+
expect(JSON.parse(String(requests[0]!.init?.body))).toEqual({
|
|
153
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
154
|
+
keys: [
|
|
155
|
+
{ keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 },
|
|
156
|
+
{ keyIndex: 1, keySuffix: "bbb22", ok: true },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
expect(JSON.parse(String(requests[1]!.init?.body))).toEqual({
|
|
160
|
+
keyType: "OPENAI_API_KEY",
|
|
161
|
+
keys: [{ keyIndex: 0, keySuffix: "ccc33", ok: true }],
|
|
162
|
+
});
|
|
163
|
+
expect(requests[0]!.init?.signal).toBeInstanceOf(AbortSignal);
|
|
164
|
+
} finally {
|
|
165
|
+
globalThis.fetch = originalFetch;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("validation reporting contains HTTP and network failures", async () => {
|
|
170
|
+
const originalFetch = globalThis.fetch;
|
|
171
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
172
|
+
const fetchMock = mock<typeof fetch>()
|
|
173
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
174
|
+
.mockRejectedValueOnce(new Error("network unavailable"));
|
|
175
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
176
|
+
try {
|
|
177
|
+
await reportKeyValidation(api, [
|
|
178
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "aaa11", ok: true, latency_ms: 1 },
|
|
179
|
+
{ keyType: "OPENAI_API_KEY", index: 0, suffix: "bbb22", ok: true, latency_ms: 1 },
|
|
180
|
+
]);
|
|
181
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
182
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
183
|
+
} finally {
|
|
184
|
+
globalThis.fetch = originalFetch;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
46
188
|
test("a failed readiness write is throttled and retried even when the snapshot is ready", async () => {
|
|
47
189
|
send.mockRejectedValueOnce(new Error("HTTP 503"));
|
|
48
190
|
await expect(refreshCredentialStatus(api, state, "pi", {}, 1)).rejects.toThrow("HTTP 503");
|
|
@@ -113,3 +255,32 @@ describe("runner credential refresh", () => {
|
|
|
113
255
|
expect(build).toHaveBeenCalledTimes(1);
|
|
114
256
|
});
|
|
115
257
|
});
|
|
258
|
+
|
|
259
|
+
describe("recheckableFailures", () => {
|
|
260
|
+
const key = (keyType: string, ok: boolean) => ({
|
|
261
|
+
keyType,
|
|
262
|
+
index: 0,
|
|
263
|
+
suffix: "abcde",
|
|
264
|
+
ok,
|
|
265
|
+
latency_ms: 1,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a Claude subscription token never joins the periodic recheck", () => {
|
|
269
|
+
const liveTest = {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: "rejected",
|
|
272
|
+
latency_ms: 1,
|
|
273
|
+
testedAt: 1,
|
|
274
|
+
keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false), key("ANTHROPIC_API_KEY", false)],
|
|
275
|
+
};
|
|
276
|
+
expect(recheckableFailures(liveTest)).toBe(1);
|
|
277
|
+
expect(
|
|
278
|
+
recheckableFailures({ ...liveTest, keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false)] }),
|
|
279
|
+
).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("a failed test without per-key results counts once", () => {
|
|
283
|
+
expect(recheckableFailures({ ok: false, error: "x", latency_ms: 1, testedAt: 1 })).toBe(1);
|
|
284
|
+
expect(recheckableFailures(null)).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -19,8 +19,13 @@ import {
|
|
|
19
19
|
const TEST_DB_PATH = "./test-db-retention.sqlite";
|
|
20
20
|
const NOW = new Date("2026-08-23T12:00:00.000Z");
|
|
21
21
|
const RETENTION_KEYS = DB_RETENTION_TABLES.map((table) => table.envKey);
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Plain time-horizon tables. Count-based context_versions and status-filtered
|
|
24
|
+
* script_runs have their own suites.
|
|
25
|
+
*/
|
|
26
|
+
const AGE_TABLES = DB_RETENTION_TABLES.filter(
|
|
27
|
+
(table) => table.policy === "age" && !("terminalStatuses" in table),
|
|
28
|
+
);
|
|
24
29
|
type AgeTable = (typeof AGE_TABLES)[number];
|
|
25
30
|
const TUNING_KEYS = [
|
|
26
31
|
"DB_RETENTION_TICK_BUDGET_MS",
|
|
@@ -135,12 +140,13 @@ afterEach(async () => {
|
|
|
135
140
|
});
|
|
136
141
|
|
|
137
142
|
describe("DB retention", () => {
|
|
138
|
-
test("keeps the closed allowlist limited to the
|
|
143
|
+
test("keeps the closed allowlist limited to the five approved tables", () => {
|
|
139
144
|
expect(DB_RETENTION_TABLES.map((table) => table.table)).toEqual([
|
|
140
145
|
"session_logs",
|
|
141
146
|
"agent_log",
|
|
142
147
|
"events",
|
|
143
148
|
"context_versions",
|
|
149
|
+
"script_runs",
|
|
144
150
|
]);
|
|
145
151
|
});
|
|
146
152
|
|
|
@@ -529,6 +529,7 @@ value:recordKeyAuthFailure
|
|
|
529
529
|
value:recordKeyRateLimitWindows
|
|
530
530
|
value:recordKeySeatMismatch
|
|
531
531
|
value:recordKeyUsage
|
|
532
|
+
value:recordKeyValidation
|
|
532
533
|
value:recordSlackMessage
|
|
533
534
|
value:recordTaskProviderIfUnset
|
|
534
535
|
value:recordTaskPullRequestAttachments
|
|
@@ -552,6 +552,7 @@ value:recordKeyAuthFailure
|
|
|
552
552
|
value:recordKeyRateLimitWindows
|
|
553
553
|
value:recordKeySeatMismatch
|
|
554
554
|
value:recordKeyUsage
|
|
555
|
+
value:recordKeyValidation
|
|
555
556
|
value:recordSlackMessage
|
|
556
557
|
value:recordTaskProviderIfUnset
|
|
557
558
|
value:recordTaskPullRequestAttachments
|
|
@@ -684,7 +685,6 @@ STEERING_MESSAGE_SELECT
|
|
|
684
685
|
UNATTRIBUTED_USER_ID
|
|
685
686
|
VERSIONABLE_FIELDS
|
|
686
687
|
WEEK_SECONDS
|
|
687
|
-
WORKFLOW_RUN_SUMMARY_COLUMNS
|
|
688
688
|
abandonSlackOutcomeDelivery
|
|
689
689
|
acceptTask
|
|
690
690
|
assertSkillFileLimits
|
|
@@ -1097,8 +1097,6 @@ rowToUser
|
|
|
1097
1097
|
rowToUserFavorite
|
|
1098
1098
|
rowToWaitState
|
|
1099
1099
|
rowToWorkflow
|
|
1100
|
-
rowToWorkflowRun
|
|
1101
|
-
rowToWorkflowRunStep
|
|
1102
1100
|
rowToWorkflowSummary
|
|
1103
1101
|
rowToWorkflowVersion
|
|
1104
1102
|
scriptRunUpdateSets
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 456 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(456);
|
|
107
|
+
expect(generatedKeys).toHaveLength(456);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkflowRunStep,
|
|
15
15
|
getDbClient,
|
|
16
16
|
getKv,
|
|
17
|
+
getMostRecentTaskInThread,
|
|
17
18
|
getTaskByWorkflowRunStepId,
|
|
18
19
|
initDb,
|
|
19
20
|
startTask,
|
|
@@ -22,7 +23,7 @@ import * as dbClient from "../be/db-client";
|
|
|
22
23
|
import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
|
|
23
24
|
import { installExtension, listExtensionRuns } from "../be/extensions/db";
|
|
24
25
|
import { getCatalogEntry } from "../extensions/catalog";
|
|
25
|
-
import type { TaskBlockedEvent } from "../extensions/contract";
|
|
26
|
+
import type { TaskBlockedEvent, TaskCreateEvent } from "../extensions/contract";
|
|
26
27
|
import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
27
28
|
import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
|
|
28
29
|
import { handleTasks } from "../http/tasks";
|
|
@@ -551,6 +552,57 @@ export default extension;
|
|
|
551
552
|
);
|
|
552
553
|
});
|
|
553
554
|
|
|
555
|
+
test("Slack event metadata retains mixed-sender batches and rejects metadata rewrites", async () => {
|
|
556
|
+
await createAgent({ name: "slack-metadata-lead", isLead: true, status: "idle" });
|
|
557
|
+
await installSource(
|
|
558
|
+
"a-rewrite-slack-metadata",
|
|
559
|
+
`import type { SwarmExtension } from "swarm-extension";
|
|
560
|
+
const extension: SwarmExtension = (api) => {
|
|
561
|
+
api.on("pre.task.create", () => ({ action: "modify", data: {
|
|
562
|
+
priority: 7, slack: { trigger: "mention", messages: [] }
|
|
563
|
+
} as never }));
|
|
564
|
+
};
|
|
565
|
+
export default extension;
|
|
566
|
+
`,
|
|
567
|
+
);
|
|
568
|
+
const recorder = await installAndEnable("record-task-create");
|
|
569
|
+
const channelId = "C_EVENT_METADATA";
|
|
570
|
+
const threadTs = "3000.0001";
|
|
571
|
+
bufferThreadMessage(channelId, threadTs, "first request", "U_FIRST", "3000.0002");
|
|
572
|
+
bufferThreadMessage(channelId, threadTs, "second request", "U_SECOND", "3000.0003");
|
|
573
|
+
|
|
574
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
575
|
+
|
|
576
|
+
const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
577
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
578
|
+
expect(event.slack).toEqual({
|
|
579
|
+
trigger: "thread-buffer",
|
|
580
|
+
messages: [
|
|
581
|
+
{ ts: "3000.0002", userId: "U_FIRST", text: "first request" },
|
|
582
|
+
{ ts: "3000.0003", userId: "U_SECOND", text: "second request" },
|
|
583
|
+
],
|
|
584
|
+
});
|
|
585
|
+
expect(event.origin).toBe("slack");
|
|
586
|
+
expect(event.options).toMatchObject({
|
|
587
|
+
slackTriggerMessageTs: "3000.0003",
|
|
588
|
+
slackUserId: "U_FIRST",
|
|
589
|
+
priority: 7,
|
|
590
|
+
});
|
|
591
|
+
expect(event.options).not.toHaveProperty("slack");
|
|
592
|
+
const task = await getMostRecentTaskInThread(channelId, threadTs);
|
|
593
|
+
expect(task).toMatchObject({
|
|
594
|
+
priority: 7,
|
|
595
|
+
slackTriggerMessageTs: "3000.0003",
|
|
596
|
+
slackUserId: "U_FIRST",
|
|
597
|
+
});
|
|
598
|
+
expect(task).not.toHaveProperty("slack");
|
|
599
|
+
|
|
600
|
+
await createTaskWithSiblingAwareness("ordinary API task", { source: "api" });
|
|
601
|
+
const apiEvent = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
602
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
603
|
+
expect(apiEvent).not.toHaveProperty("slack");
|
|
604
|
+
});
|
|
605
|
+
|
|
554
606
|
test("scheduler and workflow executor provide their task creation origins", async () => {
|
|
555
607
|
const extension = await installSource(
|
|
556
608
|
"record-task-origin",
|