@desplega.ai/agent-swarm 1.152.2 → 1.153.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 +1 -1
- package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-ahmjxhek.js} +4 -4
- package/dist/{actions-0sttc7f5.js → actions-jzrfpkmn.js} +30 -26
- package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
- package/dist/{app-p8teva95.js → app-03na2vwb.js} +9 -6
- package/dist/{assistant-r45jnsrt.js → assistant-hfkxvhgt.js} +31 -27
- package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
- package/dist/{boot-reembed-je26h5y7.js → boot-reembed-c98xrad3.js} +8 -6
- package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-zt3qm9xn.js} +9 -7
- package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-jcqd8ksj.js} +6 -4
- package/dist/{claude-adapter-csc1mzka.js → claude-adapter-mgryg8g3.js} +17 -15
- package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
- package/dist/{claude-sdk-session-fcd6f078.js → claude-sdk-session-jjf4zss1.js} +17 -15
- package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
- package/dist/{cli-fm4ytsdk.js → cli-0d9nbqxf.js} +1 -1
- package/dist/cli-0x5qzvkg.js +4279 -0
- package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
- package/dist/{cli-wbyk465z.js → cli-1hwnb4hc.js} +1 -1
- package/dist/{cli-xw7vak96.js → cli-3bcmp015.js} +5 -5
- package/dist/cli-3er6gcsn.js +1849 -0
- package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
- package/dist/{cli-4ahpr4sw.js → cli-5mbmvxnn.js} +5 -5
- package/dist/{cli-xy1cchaf.js → cli-5taqtpze.js} +235 -8
- package/dist/{cli-fmy65jdn.js → cli-6ks9xf1n.js} +7 -5
- package/dist/cli-7g9s78jf.js +2827 -0
- package/dist/{cli-jew3jda5.js → cli-7pnz2v6s.js} +2 -2
- package/dist/{cli-r84ctdxn.js → cli-83wkg402.js} +2 -2
- package/dist/{cli-jvy9qgjs.js → cli-a9scg8sv.js} +4 -4
- package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
- package/dist/{cli-ht5rs63n.js → cli-b2g6mzjc.js} +15 -6
- package/dist/{cli-jbmc0r7r.js → cli-bw0dfwkr.js} +1 -1
- package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
- package/dist/{cli-wpv5rgg2.js → cli-cdq08v3r.js} +2 -2
- package/dist/cli-cgj4hc12.js +18902 -0
- package/dist/cli-d6hk10xx.js +14075 -0
- package/dist/{cli-zvgj5zg8.js → cli-djvxhnc4.js} +31 -3324
- package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
- package/dist/{cli-3bpzj8me.js → cli-eazstkad.js} +2 -2
- package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
- package/dist/cli-h38x0qhg.js +6912 -0
- package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
- package/dist/{pi-mono-adapter-jcqt71cg.js → cli-ja7mrygn.js} +67343 -101083
- package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
- package/dist/{cli-gphgnvpf.js → cli-m04pt18h.js} +2 -2
- package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
- package/dist/{cli-j4nv2wqy.js → cli-mzgyhxbe.js} +10 -4
- package/dist/{cli-qn042eh7.js → cli-n0q9b7j4.js} +46 -1870
- package/dist/cli-n7h1kqpf.js +29 -0
- package/dist/{cli-ckz5t2hb.js → cli-nq0kgyxe.js} +5 -5
- package/dist/{cli-z3wkng35.js → cli-qrqhc31j.js} +151 -125
- package/dist/{cli-56h18ng2.js → cli-r8tdds6s.js} +1 -1
- package/dist/{cli-ch38q5z4.js → cli-rj7de4b2.js} +6788 -2541
- package/dist/{cli-qd65dvr6.js → cli-s5s1f2yn.js} +56 -40
- package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
- package/dist/{cli-2nnd56zt.js → cli-tdh4hjr2.js} +3 -2
- package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
- package/dist/{cli-b9dd0h3k.js → cli-vw03hg5t.js} +3 -2
- package/dist/{cli-zvkq5csn.js → cli-ybpp1srm.js} +6 -6
- package/dist/cli-yn0a417a.js +4069 -0
- package/dist/{cli-gpqcs935.js → cli-ysc1kbet.js} +2 -2
- package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
- package/dist/cli-zcsyfw2z.js +3335 -0
- package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
- package/dist/cli-zy5w9gpt.js +230 -0
- package/dist/cli.js +26 -34
- package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-8548daap.js} +12 -11
- package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-2dad8p4x.js} +12 -11
- package/dist/{commands-0f5rxz51.js → commands-355dbpa8.js} +6 -4
- package/dist/{db-r3jxf3wy.js → db-4jjvwz1s.js} +6 -4
- package/dist/{dispatcher-06vph6q4.js → dispatcher-ca590nkz.js} +29 -25
- package/dist/dsh-adapter-rgvj6ykp.js +19 -0
- package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
- package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-xtw7tggr.js} +33 -73
- package/dist/{google-vertex-fr665tqs.js → google-vertex-zdqpms6n.js} +38 -68
- package/dist/{handlers-0hgtc910.js → handlers-2y6dqpvf.js} +32 -28
- package/dist/{hook-9kvrv1er.js → hook-0s8knepr.js} +11 -9
- package/dist/{hook-fbfhkat3.js → hook-k0gec254.js} +12 -10
- package/dist/{http-xbyx8k5j.js → http-jp2nb4zw.js} +300 -237
- package/dist/{index-tfsdxyv6.js → index-0wzg3sp3.js} +39 -24
- package/dist/{index-rnyahfn5.js → index-3mq6ce93.js} +29 -25
- package/dist/{index-jsw3aynb.js → index-42s7eezq.js} +29 -25
- package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
- package/dist/{index-gpfn1741.js → index-b56xkbc7.js} +8 -6
- package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
- package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
- package/dist/{index-sgk50mbg.js → index-s37dbevy.js} +30 -26
- package/dist/index-w097eg0x.js +5 -0
- package/dist/jiti-loader-b4837mtm.js +34 -0
- package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
- package/dist/{keepalive-s3xpr7sw.js → keepalive-9pfw7t02.js} +8 -6
- package/dist/{lead-rq4n5r16.js → lead-7vcmcdq4.js} +44 -39
- package/dist/{maintenance-f0atj1ab.js → maintenance-x277jbpd.js} +9 -7
- package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
- package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
- package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-5fwa7th2.js} +7 -5
- package/dist/{onboard-br5j1kch.js → onboard-17sv0f51.js} +103 -35
- package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
- package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
- package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
- package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
- package/dist/{otel-impl-vdqge2nt.js → otel-impl-srjy4be4.js} +4 -4
- package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
- package/dist/pi-mono-adapter-946a0sdd.js +1316 -0
- package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-vetcm563.js} +8 -6
- package/dist/{rbac-roles-jh280k9g.js → rbac-roles-cfzgr6r1.js} +8 -6
- package/dist/{rbac-roles-t78xt39b.js → rbac-roles-cmxaddmy.js} +7 -5
- package/dist/{render-v2-7p8r2ymg.js → render-v2-f9hzff72.js} +9 -6
- package/dist/{seed-pricing-hf889enh.js → seed-pricing-jggk6rsy.js} +7 -5
- package/dist/{setup-w6mb03jv.js → setup-8mfceb9t.js} +4 -5
- package/dist/virtual-modules-ax661xp0.js +54 -0
- package/dist/{work-objects-nt8gy1kj.js → work-objects-vdsdcczj.js} +32 -28
- package/dist/{worker-dwy617t0.js → worker-cd0nhyv1.js} +44 -39
- package/openapi.json +36 -14
- package/package.json +6 -6
- package/src/be/automation-preflight.ts +6 -4
- package/src/be/extensions/db.ts +35 -4
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +213 -0
- package/src/be/rbac-audit.ts +2 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/onboard/compose-generator.ts +6 -1
- package/src/commands/onboard/env-generator.ts +6 -1
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/integration-slack.tsx +56 -16
- package/src/commands/onboard/steps/review.tsx +5 -0
- package/src/commands/onboard/types.ts +6 -0
- package/src/commands/onboard.tsx +5 -1
- package/src/commands/provider-credentials.ts +14 -2
- package/src/extensions/lifecycle.ts +15 -21
- package/src/http/agents.ts +2 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +5 -3
- package/src/http/extensions.ts +108 -34
- package/src/http/status.ts +22 -10
- package/src/prompts/base-prompt.ts +15 -7
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/codex-models.ts +15 -1
- package/src/providers/dsh-adapter.ts +248 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/modelsdev-reasoning.json +50 -0
- package/src/rbac/legacy-policy.ts +15 -1
- package/src/rbac/types.ts +2 -0
- package/src/server.ts +7 -1
- package/src/slack/app.ts +32 -10
- package/src/slack/config.ts +29 -0
- package/src/telemetry.ts +2 -3
- package/src/tests/agents-harness-provider.test.ts +19 -0
- package/src/tests/agents-list-model-display.test.ts +60 -1
- package/src/tests/base-prompt.test.ts +25 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-gpt6-release.test.ts +97 -0
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/dsh-adapter.test.ts +276 -0
- package/src/tests/e2e-nightly-report.test.ts +3 -3
- package/src/tests/e2e-redact.test.ts +2 -0
- package/src/tests/env-flag.test.ts +6 -0
- package/src/tests/extensions-db.test.ts +41 -0
- package/src/tests/extensions-http.test.ts +148 -4
- package/src/tests/extensions-lifecycle.test.ts +18 -0
- package/src/tests/extensions-mcp-tools.test.ts +9 -5
- package/src/tests/internal-ai/complete-structured.test.ts +32 -9
- package/src/tests/internal-ai/credentials.test.ts +4 -4
- package/src/tests/internal-ai/models.test.ts +20 -0
- package/src/tests/migration-046-budgets.test.ts +2 -0
- package/src/tests/model-control.test.ts +2 -2
- package/src/tests/onboard-compose.test.ts +14 -0
- package/src/tests/onboard-env.test.ts +17 -0
- package/src/tests/onboard-manifest.test.ts +12 -0
- package/src/tests/opus-5-5-registry.test.ts +48 -0
- package/src/tests/pricing-refresh.test.ts +1 -1
- package/src/tests/pricing-routes.test.ts +15 -3
- package/src/tests/provider-registration.test.ts +5 -2
- package/src/tests/rbac-admission-e2e.test.ts +54 -0
- package/src/tests/rbac-e2e-helpers.ts +2 -0
- package/src/tests/rbac-engine.test.ts +32 -0
- package/src/tests/rbac-wire-e2e.test.ts +33 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/reload-config.test.ts +33 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/slack-config.test.ts +136 -0
- package/src/tests/slack-mode-lifecycle.test.ts +69 -0
- package/src/tests/status.test.ts +22 -2
- package/src/tests/task-tool-preload-mcp.test.ts +3 -1
- package/src/tests/telemetry-init.test.ts +18 -0
- package/src/tests/workflow-llm.test.ts +3 -3
- package/src/tools/extension-common.ts +4 -0
- package/src/tools/extension-install.ts +1 -1
- package/src/tools/extension-list.ts +4 -3
- package/src/types.ts +17 -8
- package/src/utils/context-window.ts +1 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/internal-ai/complete-structured.ts +8 -0
- package/src/utils/internal-ai/models.ts +5 -5
- package/templates/schedules/daily-status-report/content.md +25 -12
- package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
- package/templates/skills/swarm-extensions/content.md +9 -5
- package/dist/cli-4wppfkcm.js +0 -16384
- package/dist/cli-55qrx3q8.js +0 -6041
- package/dist/cli-cg4vnyh4.js +0 -305
- package/dist/cli-jyetr64j.js +0 -52
- package/dist/cli-pp9ax9pa.js +0 -520
- package/dist/cli-qa66qaf0.js +0 -6076
- package/dist/cli-qghpvca2.js +0 -590
- package/dist/cli-rembwgt8.js +0 -861
- package/dist/cli-tcvj7j2r.js +0 -1971
- package/dist/cli-vfcth11k.js +0 -36
- package/dist/cli-w1d1bgkk.js +0 -1422
- package/dist/cli-y6x0gqtg.js +0 -5977
- package/dist/es5-tj3tn4jj.js +0 -844
- package/dist/index-1kq5hw2p.js +0 -467
- package/dist/index-1p8wdstt.js +0 -7
- package/dist/index-8fdjzb2f.js +0 -100
- package/dist/index-bxmwzbgw.js +0 -365
- package/dist/index-nzerxd5z.js +0 -7
- package/dist/index-pyymnzat.js +0 -1168
- package/dist/index-qv6atbab.js +0 -619
- package/dist/index-rja32qa7.js +0 -801
- package/dist/index-tzg5srfp.js +0 -211
- package/dist/index-vhncw983.js +0 -90
- package/dist/index-xk036zsk.js +0 -859
- package/dist/index-zyazbzcb.js +0 -6
- package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
|
@@ -898,6 +898,12 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
|
|
|
898
898
|
cacheReadPerMillion: 0.25,
|
|
899
899
|
cacheWritePerMillion: 12.5,
|
|
900
900
|
});
|
|
901
|
+
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-5-5"]).toEqual({
|
|
902
|
+
inputPerMillion: 4.0,
|
|
903
|
+
outputPerMillion: 20.0,
|
|
904
|
+
cacheReadPerMillion: 0.2,
|
|
905
|
+
cacheWritePerMillion: 5.0,
|
|
906
|
+
});
|
|
901
907
|
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-5"]).toEqual({
|
|
902
908
|
inputPerMillion: 5.0,
|
|
903
909
|
outputPerMillion: 25.0,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { modelGroupsForHarness } from "../../apps/ui/src/lib/agent-runtime-models";
|
|
3
|
+
import { buildModelsCatalog } from "../be/models-catalog";
|
|
4
|
+
import cache from "../be/modelsdev-cache.json";
|
|
5
|
+
import { buildModelsDevSeedRows } from "../be/seed-pricing";
|
|
6
|
+
import {
|
|
7
|
+
CODEX_DEFAULT_MODEL,
|
|
8
|
+
CODEX_MODELS,
|
|
9
|
+
computeCodexCostUsd,
|
|
10
|
+
FALLBACK_CODEX_MODEL_PRICING,
|
|
11
|
+
getCodexContextWindow,
|
|
12
|
+
resolveCodexModel,
|
|
13
|
+
} from "../providers/codex-models";
|
|
14
|
+
import { applyReasoningEffort, reasoningCapability } from "../providers/reasoning-effort";
|
|
15
|
+
|
|
16
|
+
const releases = [
|
|
17
|
+
{ id: "gpt-6-sol", label: "GPT-6 Sol", input: 2, output: 10, read: 0.2, write: 2.5 },
|
|
18
|
+
{ id: "gpt-6-luna", label: "GPT-6 Luna", input: 0.1, output: 0.5, read: 0.01, write: 0.125 },
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
for (const release of releases) {
|
|
22
|
+
describe(release.id, () => {
|
|
23
|
+
test("uses the exact bare CLI ID with verified context and base costs", () => {
|
|
24
|
+
expect(CODEX_MODELS).toContain(release.id);
|
|
25
|
+
expect(resolveCodexModel(release.id.toUpperCase())).toBe(release.id);
|
|
26
|
+
expect(getCodexContextWindow(release.id)).toBe(1_050_000);
|
|
27
|
+
expect(cache.openai.models[release.id].limit.output).toBe(128_000);
|
|
28
|
+
expect(FALLBACK_CODEX_MODEL_PRICING[release.id]).toEqual({
|
|
29
|
+
inputPerMillion: release.input,
|
|
30
|
+
cachedInputPerMillion: release.read,
|
|
31
|
+
outputPerMillion: release.output,
|
|
32
|
+
});
|
|
33
|
+
expect(computeCodexCostUsd(release.id, 1_000_000, 250_000, 100_000)).toBeCloseTo(
|
|
34
|
+
release.input * 0.75 + release.read * 0.25 + release.output * 0.1,
|
|
35
|
+
8,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("exposes the full reasoning range and transports none/max correctly", () => {
|
|
40
|
+
expect(reasoningCapability("codex", release.id)).toEqual({
|
|
41
|
+
supported: true,
|
|
42
|
+
levels: ["off", "low", "medium", "high", "xhigh", "max"],
|
|
43
|
+
default: "medium",
|
|
44
|
+
});
|
|
45
|
+
for (const [effort, value] of [
|
|
46
|
+
["off", "none"],
|
|
47
|
+
["max", "max"],
|
|
48
|
+
] as const) {
|
|
49
|
+
expect(applyReasoningEffort("codex", release.id, effort)).toEqual({
|
|
50
|
+
kind: "codex-config",
|
|
51
|
+
config: { model_reasoning_effort: value },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("seeds all four token classes under canonical Codex IDs", () => {
|
|
57
|
+
const rows = buildModelsDevSeedRows(cache).filter(
|
|
58
|
+
(row) => row.provider === "codex" && row.model === release.id,
|
|
59
|
+
);
|
|
60
|
+
expect(
|
|
61
|
+
Object.fromEntries(rows.map((row) => [row.tokenClass, row.pricePerMillionUsd])),
|
|
62
|
+
).toEqual({
|
|
63
|
+
input: release.input,
|
|
64
|
+
output: release.output,
|
|
65
|
+
cached_input: release.read,
|
|
66
|
+
cache_write: release.write,
|
|
67
|
+
});
|
|
68
|
+
expect(cache.openai.models[release.id].cost.tiers).toEqual([
|
|
69
|
+
{
|
|
70
|
+
input: release.input * 2,
|
|
71
|
+
output: release.output * 1.5,
|
|
72
|
+
cache_read: release.read * 2,
|
|
73
|
+
cache_write: release.write * 2,
|
|
74
|
+
tier: { type: "context", size: 272000 },
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("is available through the catalog and direct UI selector", () => {
|
|
80
|
+
const model = buildModelsCatalog(cache).openai?.models[release.id];
|
|
81
|
+
expect(model?.limit?.context).toBe(1_050_000);
|
|
82
|
+
expect(model?.cost).toEqual({ input: release.input, output: release.output });
|
|
83
|
+
const option = modelGroupsForHarness("codex", undefined, undefined)
|
|
84
|
+
.flatMap((group) => group.models)
|
|
85
|
+
.find((entry) => entry.id === release.id);
|
|
86
|
+
expect(option?.label).toBe(release.label);
|
|
87
|
+
expect(option?.reasoningLevels).toEqual(["off", "low", "medium", "high", "xhigh", "max"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test("release additions preserve rollout-safe defaults and aliases", () => {
|
|
93
|
+
expect(CODEX_DEFAULT_MODEL).toBe("gpt-5.6-terra");
|
|
94
|
+
expect(resolveCodexModel("sonnet")).toBe("gpt-5.6-terra");
|
|
95
|
+
expect(resolveCodexModel("haiku")).toBe("gpt-5.6-luna");
|
|
96
|
+
expect(resolveCodexModel("opus")).toBe("gpt-5.6-sol");
|
|
97
|
+
});
|
|
@@ -50,6 +50,12 @@ beforeAll(async () => {
|
|
|
50
50
|
value: BOOTSTRAP_VALUE,
|
|
51
51
|
isSecret: true,
|
|
52
52
|
});
|
|
53
|
+
await upsertSwarmConfig({
|
|
54
|
+
scope: "global",
|
|
55
|
+
key: "SLACK_SIGNING_SECRET",
|
|
56
|
+
value: "synthetic-api-only-signing-secret",
|
|
57
|
+
isSecret: true,
|
|
58
|
+
});
|
|
53
59
|
await upsertSwarmConfig({
|
|
54
60
|
scope: "global",
|
|
55
61
|
key: "SOME_WORKER_SECRET",
|
|
@@ -86,6 +92,7 @@ describe("API-only config keys are never served over HTTP", () => {
|
|
|
86
92
|
test("GET /api/config/resolved WITHOUT agentId strips the bootstrap key", async () => {
|
|
87
93
|
const { configs } = await getJson("/api/config/resolved?includeSecrets=true");
|
|
88
94
|
expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
|
|
95
|
+
expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
|
|
89
96
|
// A legitimate worker secret still comes through.
|
|
90
97
|
expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
|
|
91
98
|
});
|
|
@@ -95,11 +102,13 @@ describe("API-only config keys are never served over HTTP", () => {
|
|
|
95
102
|
`/api/config/resolved?includeSecrets=true&agentId=${agentId}`,
|
|
96
103
|
);
|
|
97
104
|
expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
|
|
105
|
+
expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
test("GET /api/config?scope=global strips the bootstrap key", async () => {
|
|
101
109
|
const { configs } = await getJson("/api/config?scope=global&includeSecrets=true");
|
|
102
110
|
expect(configs.some((c) => c.key === BOOTSTRAP_KEY)).toBe(false);
|
|
111
|
+
expect(configs.some((c) => c.key === "SLACK_SIGNING_SECRET")).toBe(false);
|
|
103
112
|
expect(configs.some((c) => c.key === "SOME_WORKER_SECRET")).toBe(true);
|
|
104
113
|
});
|
|
105
114
|
});
|
|
@@ -18,6 +18,7 @@ describe("getContextWindowSize", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test("returns 1M for opus models", () => {
|
|
21
|
+
expect(getContextWindowSize("claude-opus-5-5")).toBe(1_000_000);
|
|
21
22
|
expect(getContextWindowSize("claude-opus-5")).toBe(1_000_000);
|
|
22
23
|
expect(getContextWindowSize("claude-opus-4-8")).toBe(1_000_000);
|
|
23
24
|
expect(getContextWindowSize("claude-opus-4-7")).toBe(1_000_000);
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmod, mkdtemp, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { checkProviderCredentials } from "../commands/provider-credentials";
|
|
6
|
+
import { createProviderAdapter } from "../providers";
|
|
7
|
+
import { checkDshCredentials, DshAdapter } from "../providers/dsh-adapter";
|
|
8
|
+
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
9
|
+
import { DEFAULT_MODEL_TIER_MAP } from "../types";
|
|
10
|
+
import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
11
|
+
import { resolveHarnessProvider } from "../utils/harness-provider";
|
|
12
|
+
import { DEFAULT_OPENROUTER_BASE_URL } from "../utils/openrouter-base-url";
|
|
13
|
+
|
|
14
|
+
const directories: string[] = [];
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await Promise.all(
|
|
17
|
+
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function fixture(mode = "success"): Promise<ProviderSessionConfig> {
|
|
22
|
+
const cwd = await mkdtemp(join(tmpdir(), "dsh-adapter-test-"));
|
|
23
|
+
directories.push(cwd);
|
|
24
|
+
const binary = join(cwd, "dsh");
|
|
25
|
+
await Bun.write(
|
|
26
|
+
binary,
|
|
27
|
+
`#!${process.execPath}
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const patchPath = args[args.indexOf("--patch") + 1];
|
|
30
|
+
const patch = await Bun.file(patchPath).json();
|
|
31
|
+
const prompt = await Bun.stdin.text();
|
|
32
|
+
await Bun.write("invocation.json", JSON.stringify({ args, patchPath, patch, prompt, cwd: process.cwd() }));
|
|
33
|
+
const emit = (event) => console.log(JSON.stringify(event));
|
|
34
|
+
emit({ type: "session", sessionId: "dsh-test-session" });
|
|
35
|
+
const mode = process.env.DSH_TEST_MODE;
|
|
36
|
+
if (mode === "abort") { setInterval(() => {}, 1000); }
|
|
37
|
+
else {
|
|
38
|
+
if (mode === "malformed") console.log("bad json");
|
|
39
|
+
emit({ type: "status", phase: "turn_end", reason: { kind: mode === "failure" ? "error" : "completed" } });
|
|
40
|
+
if (mode !== "missing-final") {
|
|
41
|
+
// A split UTF-8 character plus a final line without a newline exercises both decoders.
|
|
42
|
+
const line = Buffer.from(JSON.stringify({ type: "final", text: "Done ✓" }));
|
|
43
|
+
const split = line.indexOf(Buffer.from("✓")) + 1;
|
|
44
|
+
process.stdout.write(line.subarray(0, split));
|
|
45
|
+
await Bun.sleep(10);
|
|
46
|
+
process.stdout.write(line.subarray(split));
|
|
47
|
+
}
|
|
48
|
+
if (mode === "failure") { console.error(process.env.DEEPSEEK_API_KEY); process.exitCode = 7; }
|
|
49
|
+
}
|
|
50
|
+
`,
|
|
51
|
+
);
|
|
52
|
+
await chmod(binary, 0o700);
|
|
53
|
+
return {
|
|
54
|
+
prompt: "--not-a-flag\nKeep $() and `quotes` literal.",
|
|
55
|
+
systemPrompt: "System instructions:\n!!js must remain text",
|
|
56
|
+
model: "deepseek-v4-pro",
|
|
57
|
+
role: "worker",
|
|
58
|
+
agentId: "agent-test",
|
|
59
|
+
taskId: "task-test",
|
|
60
|
+
apiUrl: "http://unused.invalid",
|
|
61
|
+
apiKey: "unused",
|
|
62
|
+
cwd,
|
|
63
|
+
logFile: join(cwd, "log.jsonl"),
|
|
64
|
+
env: {
|
|
65
|
+
DSH_BINARY: binary,
|
|
66
|
+
DEEPSEEK_API_KEY: "dsh-test-credential-value",
|
|
67
|
+
OPENROUTER_API_KEY: "",
|
|
68
|
+
OPENROUTER_BASE_URL: "",
|
|
69
|
+
DSH_TEST_MODE: mode,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("dsh harness", () => {
|
|
75
|
+
test("registry, model defaults, and credential dispatch select dsh", async () => {
|
|
76
|
+
expect(await createProviderAdapter("dsh")).toBeInstanceOf(DshAdapter);
|
|
77
|
+
expect(resolveHarnessProvider({ HARNESS_PROVIDER: "dsh" }, {})).toBe("dsh");
|
|
78
|
+
expect(DEFAULT_MODEL_TIER_MAP.dsh).toEqual(DEFAULT_MODEL_TIER_MAP.pi);
|
|
79
|
+
expect(checkDshCredentials({ OPENAI_API_KEY: "unrelated" }).ready).toBe(false);
|
|
80
|
+
expect(checkDshCredentials({ DEEPSEEK_API_KEY: " " }).ready).toBe(false);
|
|
81
|
+
expect(checkDshCredentials({}).missing).toEqual(["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY"]);
|
|
82
|
+
expect((await checkProviderCredentials("dsh", { OPENROUTER_API_KEY: "present" })).ready).toBe(
|
|
83
|
+
true,
|
|
84
|
+
);
|
|
85
|
+
expect(getModelAwareCredentialVars("dsh", DEFAULT_MODEL_TIER_MAP.dsh.ultra)).toEqual([
|
|
86
|
+
"OPENROUTER_API_KEY",
|
|
87
|
+
]);
|
|
88
|
+
expect(getModelAwareCredentialVars("dsh", "deepseek-v4-pro")).toEqual(["DEEPSEEK_API_KEY"]);
|
|
89
|
+
expect((await checkProviderCredentials("dsh", { DEEPSEEK_API_KEY: "present" })).ready).toBe(
|
|
90
|
+
true,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("passes prompt on stdin, patches model/system, preserves cwd and cleans up", async () => {
|
|
95
|
+
const config = await fixture();
|
|
96
|
+
const session = await new DshAdapter().createSession(config);
|
|
97
|
+
const events: ProviderEvent[] = [];
|
|
98
|
+
session.onEvent((event) => events.push(event));
|
|
99
|
+
expect(await session.waitForCompletion()).toMatchObject({
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
isError: false,
|
|
102
|
+
output: "Done ✓",
|
|
103
|
+
sessionId: "dsh-test-session",
|
|
104
|
+
});
|
|
105
|
+
const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
106
|
+
expect(invocation.prompt).toBe(config.prompt);
|
|
107
|
+
expect(invocation.cwd).toBe(config.cwd);
|
|
108
|
+
expect(invocation.args).toEqual([
|
|
109
|
+
"--profile",
|
|
110
|
+
"headless",
|
|
111
|
+
"--patch",
|
|
112
|
+
invocation.patchPath,
|
|
113
|
+
"--json",
|
|
114
|
+
"-",
|
|
115
|
+
]);
|
|
116
|
+
expect(invocation.patch).toContainEqual({
|
|
117
|
+
id: "agent-default-model",
|
|
118
|
+
config: { provider: "deepseek-official", model: config.model },
|
|
119
|
+
});
|
|
120
|
+
expect(invocation.patch).toContainEqual({
|
|
121
|
+
id: "system-prompt",
|
|
122
|
+
config: { personaPrefix: config.systemPrompt },
|
|
123
|
+
});
|
|
124
|
+
expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
|
|
125
|
+
expect(events).toContainEqual({
|
|
126
|
+
type: "session_init",
|
|
127
|
+
sessionId: "dsh-test-session",
|
|
128
|
+
provider: "dsh",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const model of [
|
|
133
|
+
DEFAULT_MODEL_TIER_MAP.dsh.regular,
|
|
134
|
+
DEFAULT_MODEL_TIER_MAP.dsh.ultra,
|
|
135
|
+
"openrouter/vendor/new-model",
|
|
136
|
+
]) {
|
|
137
|
+
test(`routes ${model} through OpenRouter with only its key`, async () => {
|
|
138
|
+
const config = await fixture();
|
|
139
|
+
config.model = model;
|
|
140
|
+
config.env = {
|
|
141
|
+
...config.env,
|
|
142
|
+
DEEPSEEK_API_KEY: "",
|
|
143
|
+
OPENROUTER_API_KEY: "openrouter-test-key",
|
|
144
|
+
};
|
|
145
|
+
const session = await new DshAdapter().createSession(config);
|
|
146
|
+
expect((await session.waitForCompletion()).isError).toBe(false);
|
|
147
|
+
const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
148
|
+
const modelId = model.slice("openrouter/".length);
|
|
149
|
+
expect(patch).toContainEqual({
|
|
150
|
+
id: "agent-default-model",
|
|
151
|
+
config: { provider: "openrouter", model: modelId },
|
|
152
|
+
});
|
|
153
|
+
expect(patch).toContainEqual({
|
|
154
|
+
id: "llm-pi-ai",
|
|
155
|
+
config: {
|
|
156
|
+
providers: {
|
|
157
|
+
openrouter: {
|
|
158
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
159
|
+
baseURL: DEFAULT_OPENROUTER_BASE_URL,
|
|
160
|
+
api: "openai-completions",
|
|
161
|
+
models: [{ id: modelId }],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
expect(JSON.stringify(patch)).not.toContain("openrouter-test-key");
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test("defaults to OpenRouter and honors its base URL when both keys exist", async () => {
|
|
171
|
+
const config = await fixture();
|
|
172
|
+
config.model = undefined;
|
|
173
|
+
config.env = {
|
|
174
|
+
...config.env,
|
|
175
|
+
OPENROUTER_API_KEY: "openrouter-test-key",
|
|
176
|
+
OPENROUTER_BASE_URL: " https://gateway.example/proxy/v1/// ",
|
|
177
|
+
};
|
|
178
|
+
const session = await new DshAdapter().createSession(config);
|
|
179
|
+
expect((await session.waitForCompletion()).isError).toBe(false);
|
|
180
|
+
const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
181
|
+
expect(patch[0].config).toEqual({
|
|
182
|
+
provider: "openrouter",
|
|
183
|
+
model: "deepseek/deepseek-v4.1-flash",
|
|
184
|
+
});
|
|
185
|
+
expect(patch[2].config.providers.openrouter.baseURL).toBe("https://gateway.example/proxy/v1");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("bare models keep direct DeepSeek when both keys exist", async () => {
|
|
189
|
+
const config = await fixture();
|
|
190
|
+
config.env = { ...config.env, OPENROUTER_API_KEY: "openrouter-test-key" };
|
|
191
|
+
const session = await new DshAdapter().createSession(config);
|
|
192
|
+
expect((await session.waitForCompletion()).isError).toBe(false);
|
|
193
|
+
const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
194
|
+
expect(patch[0].config.provider).toBe("deepseek-official");
|
|
195
|
+
expect(patch[2]).toEqual({ id: "llm-deepseek", config: { apiKeyEnv: "DEEPSEEK_API_KEY" } });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("fails closed for absent or mismatched credentials and empty model IDs", async () => {
|
|
199
|
+
const config = await fixture();
|
|
200
|
+
config.env = { ...config.env, DEEPSEEK_API_KEY: "", OPENROUTER_API_KEY: "" };
|
|
201
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
202
|
+
"DEEPSEEK_API_KEY or OPENROUTER_API_KEY",
|
|
203
|
+
);
|
|
204
|
+
config.env.DEEPSEEK_API_KEY = "direct-test-key";
|
|
205
|
+
config.model = DEFAULT_MODEL_TIER_MAP.dsh.regular;
|
|
206
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
207
|
+
"requires OPENROUTER_API_KEY",
|
|
208
|
+
);
|
|
209
|
+
config.env = { ...config.env, DEEPSEEK_API_KEY: "", OPENROUTER_API_KEY: "openrouter-test-key" };
|
|
210
|
+
config.model = "deepseek-v4-pro";
|
|
211
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
212
|
+
"requires DEEPSEEK_API_KEY",
|
|
213
|
+
);
|
|
214
|
+
config.model = "openrouter/";
|
|
215
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow("requires a model ID");
|
|
216
|
+
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("fails closed when only npx is installed", async () => {
|
|
220
|
+
const config = await fixture();
|
|
221
|
+
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
222
|
+
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
223
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
224
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.1.7-alpha.2",
|
|
225
|
+
);
|
|
226
|
+
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("uses a preinstalled dsh from PATH without DSH_BINARY", async () => {
|
|
230
|
+
const config = await fixture();
|
|
231
|
+
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
232
|
+
const session = await new DshAdapter().createSession(config);
|
|
233
|
+
expect((await session.waitForCompletion()).isError).toBe(false);
|
|
234
|
+
const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
235
|
+
expect(invocation.args[0]).toBe("--profile");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("does not fall back when an explicit DSH_BINARY is missing", async () => {
|
|
239
|
+
const config = await fixture();
|
|
240
|
+
config.env = { ...config.env, DSH_BINARY: join(config.cwd, "missing"), PATH: config.cwd };
|
|
241
|
+
await expect(new DshAdapter().createSession(config)).rejects.toThrow();
|
|
242
|
+
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
for (const mode of ["failure", "missing-final", "malformed"]) {
|
|
246
|
+
test(`reports ${mode} as failure even with output or exit zero`, async () => {
|
|
247
|
+
const config = await fixture(mode);
|
|
248
|
+
const session = await new DshAdapter().createSession(config);
|
|
249
|
+
const events: ProviderEvent[] = [];
|
|
250
|
+
session.onEvent((event) => events.push(event));
|
|
251
|
+
const result = await session.waitForCompletion();
|
|
252
|
+
expect(result.isError).toBe(true);
|
|
253
|
+
expect(result.exitCode).toBe(mode === "failure" ? 7 : 1);
|
|
254
|
+
expect(JSON.stringify({ result, events })).not.toContain(config.env!.DEEPSEEK_API_KEY);
|
|
255
|
+
const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
256
|
+
expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test("abort terminates a running process and removes its patch", async () => {
|
|
261
|
+
const config = await fixture("abort");
|
|
262
|
+
const session = await new DshAdapter().createSession(config);
|
|
263
|
+
await new Promise<void>((resolve) =>
|
|
264
|
+
session.onEvent((event) => {
|
|
265
|
+
if (event.type === "session_init") resolve();
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
await session.abort();
|
|
269
|
+
expect(await session.waitForCompletion()).toMatchObject({
|
|
270
|
+
isError: true,
|
|
271
|
+
failureReason: "dsh session aborted",
|
|
272
|
+
});
|
|
273
|
+
const invocation = await Bun.file(join(config.cwd, "invocation.json")).json();
|
|
274
|
+
expect(await Bun.file(invocation.patchPath).exists()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -85,11 +85,11 @@ describe("nightly report", () => {
|
|
|
85
85
|
});
|
|
86
86
|
expect(report.ok).toBe(true);
|
|
87
87
|
expect(report.missingLegs).toEqual([]);
|
|
88
|
-
expect(report.totalCostUsd).toBeCloseTo(0.
|
|
88
|
+
expect(report.totalCostUsd).toBeCloseTo(0.0738, 6);
|
|
89
89
|
expect(report.warnings).toEqual([]);
|
|
90
90
|
const markdown = nightlyMarkdown(report, [], DEFAULT_PROVIDERS);
|
|
91
91
|
expect(markdown).toContain("## Nightly E2E: PASS");
|
|
92
|
-
expect(markdown).toContain("
|
|
92
|
+
expect(markdown).toContain("6/6 harness legs passed");
|
|
93
93
|
expect(markdown).toContain("| claude | claude-model | PASS | 1 |");
|
|
94
94
|
expect(markdown).not.toContain("### Warnings");
|
|
95
95
|
});
|
|
@@ -159,7 +159,7 @@ describe("nightly report", () => {
|
|
|
159
159
|
results: [contractResult(), harnessResult([leg("claude")]), harnessResult([failed])],
|
|
160
160
|
});
|
|
161
161
|
expect(report.ok).toBe(false);
|
|
162
|
-
expect(report.missingLegs).toEqual(["claude-sdk", "pi", "opencode"]);
|
|
162
|
+
expect(report.missingLegs).toEqual(["claude-sdk", "pi", "opencode", "dsh"]);
|
|
163
163
|
expect(report.warnings).toContain("pi: no result file from the leg.");
|
|
164
164
|
expect(report.warnings).toContain("codex: needed 2 attempts.");
|
|
165
165
|
const markdown = nightlyMarkdown(report, [], DEFAULT_PROVIDERS);
|
|
@@ -43,6 +43,7 @@ describe("e2e redaction", () => {
|
|
|
43
43
|
const saved = { ...process.env };
|
|
44
44
|
try {
|
|
45
45
|
process.env.OPENROUTER_API_KEY = "example-sk-or-v1-known-value-1234";
|
|
46
|
+
process.env.DEEPSEEK_API_KEY = "deepseek-known-value-1234";
|
|
46
47
|
process.env.CODEX_OAUTH = JSON.stringify({
|
|
47
48
|
access: "access-token-value-1234",
|
|
48
49
|
refresh: "refresh-token-value-1234",
|
|
@@ -51,6 +52,7 @@ describe("e2e redaction", () => {
|
|
|
51
52
|
});
|
|
52
53
|
const known = knownSecrets(["extra-secret-value"]);
|
|
53
54
|
expect(known).toContain("example-sk-or-v1-known-value-1234");
|
|
55
|
+
expect(known).toContain("deepseek-known-value-1234");
|
|
54
56
|
expect(known).toContain("access-token-value-1234");
|
|
55
57
|
expect(known).toContain("refresh-token-value-1234");
|
|
56
58
|
expect(known).toContain("extra-secret-value");
|
|
@@ -62,6 +62,12 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
|
|
|
62
62
|
expect(validateConfigValue("SLACK_THREAD_STEERING_MODE", "now")).toContain("must be one of");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test("Slack transport is constrained", () => {
|
|
66
|
+
expect(validateConfigValue("SLACK_MODE", "socket")).toBeNull();
|
|
67
|
+
expect(validateConfigValue("SLACK_MODE", "http")).toBeNull();
|
|
68
|
+
expect(validateConfigValue("SLACK_MODE", "webhook")).toContain("must be one of");
|
|
69
|
+
});
|
|
70
|
+
|
|
65
71
|
test("Slack reaction shortcode keys accept bare and colon-wrapped names", () => {
|
|
66
72
|
const keys = [
|
|
67
73
|
"SLACK_REACTION_ACCEPTED",
|
|
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
2
2
|
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
3
3
|
import {
|
|
4
4
|
deleteExtension,
|
|
5
|
+
ExtensionOwnershipError,
|
|
5
6
|
getExtensionById,
|
|
6
7
|
getExtensionFiles,
|
|
7
8
|
getExtensionVersion,
|
|
@@ -67,6 +68,46 @@ describe("extension DB helpers", () => {
|
|
|
67
68
|
).toBe(1);
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
test("concurrent worker installs cannot take ownership of the same name", async () => {
|
|
72
|
+
const bundle = await loadBundleFixture("minimal");
|
|
73
|
+
const results = await Promise.allSettled(
|
|
74
|
+
["worker-a", "worker-b"].map((agentId) =>
|
|
75
|
+
installExtension({ ...bundle, agentId, ownerOnly: agentId }),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
|
|
79
|
+
const rejected = results.find((result) => result.status === "rejected");
|
|
80
|
+
expect(rejected?.status === "rejected" && rejected.reason).toBeInstanceOf(
|
|
81
|
+
ExtensionOwnershipError,
|
|
82
|
+
);
|
|
83
|
+
const winner = results.find((result) => result.status === "fulfilled");
|
|
84
|
+
if (winner?.status !== "fulfilled") throw new Error("missing winning install");
|
|
85
|
+
expect(await listExtensionVersions(winner.value.extension.id)).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("worker metadata writes and deletion recheck disabled ownership at the database write", async () => {
|
|
89
|
+
const installed = await installExtension({
|
|
90
|
+
...(await loadBundleFixture("minimal")),
|
|
91
|
+
agentId: "owner",
|
|
92
|
+
});
|
|
93
|
+
const id = installed.extension.id;
|
|
94
|
+
await expect(
|
|
95
|
+
updateExtensionMeta(id, { priority: 7, ownerOnly: "other" }),
|
|
96
|
+
).rejects.toBeInstanceOf(ExtensionOwnershipError);
|
|
97
|
+
await expect(deleteExtension(id, "other")).rejects.toBeInstanceOf(ExtensionOwnershipError);
|
|
98
|
+
await setExtensionState(id, { enabled: true, status: "enabled" });
|
|
99
|
+
await expect(
|
|
100
|
+
updateExtensionMeta(id, { priority: 7, ownerOnly: "owner" }),
|
|
101
|
+
).rejects.toBeInstanceOf(ExtensionOwnershipError);
|
|
102
|
+
await expect(deleteExtension(id, "owner")).rejects.toBeInstanceOf(ExtensionOwnershipError);
|
|
103
|
+
expect(await getExtensionById(id)).toMatchObject({ enabled: true, priority: 100 });
|
|
104
|
+
await setExtensionState(id, { enabled: false, status: "disabled" });
|
|
105
|
+
expect(await updateExtensionMeta(id, { priority: 7, ownerOnly: "owner" })).toMatchObject({
|
|
106
|
+
priority: 7,
|
|
107
|
+
});
|
|
108
|
+
expect(await deleteExtension(id, "owner")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
70
111
|
test("matching bundles dedupe and changed hooks create a complete snapshot", async () => {
|
|
71
112
|
const bundle = await loadBundleFixture("minimal");
|
|
72
113
|
const first = await installExtension(bundle);
|