@desplega.ai/agent-swarm 1.161.0 → 1.162.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 +3 -2
- package/dist/{acp-adapter-xx489vvb.js → acp-adapter-74m73ae9.js} +1 -1
- package/dist/{actions-ykjkn0m9.js → actions-4sr14gnt.js} +34 -31
- package/dist/{anthropic-messages-5qx80j9k.js → anthropic-messages-8xxrzky9.js} +22 -26
- package/dist/{app-0hdqa4k1.js → app-pt68fbwp.js} +13 -11
- package/dist/{artifact-3v5t9jtg.js → artifact-fey7dvj9.js} +3 -2
- package/dist/{assistant-am2g5g8a.js → assistant-cv3jytks.js} +37 -34
- package/dist/{azure-openai-responses-fszbkp0e.js → azure-openai-responses-f5cqsyj8.js} +15 -10
- package/dist/{boot-reembed-xy9t7avg.js → boot-reembed-4ht4gqth.js} +13 -11
- package/dist/{boot-reembed-gt5gjkt4.js → boot-reembed-nxpcvpj9.js} +12 -10
- package/dist/{boot-scrub-logs-9x0985y5.js → boot-scrub-logs-k93kyw8k.js} +9 -7
- package/dist/{boot-seeds-trv0d4vg.js → boot-seeds-1skcrs0e.js} +13 -10
- package/dist/{claude-adapter-jb6e1e54.js → claude-adapter-r5g0tzge.js} +23 -20
- package/dist/{claude-managed-setup-d1q9hv2b.js → claude-managed-setup-pkyrgshb.js} +3 -2
- package/dist/{claude-sdk-session-zga700s2.js → claude-sdk-session-jxyxnrwd.js} +5565 -5512
- package/dist/{cli-we0q7t0e.js → cli-2zy95vhm.js} +137 -64
- package/dist/{cli-35pj2yva.js → cli-4cwxn7gp.js} +38 -11
- package/dist/{cli-ad4qxm6j.js → cli-4dcpaefp.js} +2 -2
- package/dist/{cli-82c1pacn.js → cli-4x0csdjq.js} +71 -37
- package/dist/{cli-sqm3r5ps.js → cli-5bcd7sfy.js} +14 -18
- package/dist/{cli-d56tgd45.js → cli-5k6k8ta3.js} +2 -2
- package/dist/{cli-1er0rhz8.js → cli-5xhpb3kv.js} +15 -15
- package/dist/{cli-yrg7c73k.js → cli-7af8yhav.js} +10 -183
- package/dist/{cli-r6fsxc76.js → cli-7tre70zp.js} +513 -961
- package/dist/{cli-bh7jed3k.js → cli-8ev9xqnx.js} +34 -18
- package/dist/cli-bsky8g7w.js +449 -0
- package/dist/{cli-hn04t9db.js → cli-cebmgwsc.js} +14 -10
- package/dist/{cli-cfkrztza.js → cli-cvatsam7.js} +2 -2
- package/dist/{cli-2658f6at.js → cli-cvk6yzw5.js} +6 -8
- package/dist/{cli-ptr8h7kp.js → cli-eg0b3p0k.js} +5 -218
- package/dist/{cli-zb6xbk61.js → cli-g739zqzt.js} +4 -4
- package/dist/{cli-dv6cdjs2.js → cli-gd4favx9.js} +28 -7
- package/dist/{cli-5p3x4zna.js → cli-gpqpmns7.js} +2 -2
- package/dist/{cli-5nbmpy8w.js → cli-hb5c2x5y.js} +1 -1
- package/dist/{cli-jpf7e1zq.js → cli-hmsw1gpy.js} +3 -3
- package/dist/{cli-1cz0c18q.js → cli-jf7wmv1r.js} +2521 -733
- package/dist/{cli-qwsnmjdd.js → cli-jxs2ygaw.js} +168 -61
- package/dist/cli-kdkwd3yr.js +615 -0
- package/dist/{cli-hgmnf38m.js → cli-kh04eqwe.js} +1 -1
- package/dist/{cli-0383bp7f.js → cli-kp42yek0.js} +425 -90
- package/dist/{cli-ybgv4kw2.js → cli-mbssg1xv.js} +9 -2
- package/dist/{cli-jsp14b6c.js → cli-mzfth0bp.js} +38 -1
- package/dist/{cli-8g6mc14k.js → cli-nq7yqw7b.js} +16 -5
- package/dist/{cli-ee8bc46j.js → cli-nt8hecg9.js} +33 -32
- package/dist/{cli-9s68w752.js → cli-p0r8sxj5.js} +1 -1
- package/dist/{cli-58e8yr0d.js → cli-pkd6s2wy.js} +4 -2
- package/dist/{cli-17xpf7cm.js → cli-q24re23n.js} +6 -4
- package/dist/cli-qjjvzwzd.js +222 -0
- package/dist/cli-qrgt5bqf.js +189 -0
- package/dist/{cli-et8m9xsv.js → cli-rfjw9jw2.js} +22 -10
- package/dist/{cli-798vkbks.js → cli-rj0gqwwp.js} +2 -2
- package/dist/{cli-121ph74b.js → cli-rnyzn44p.js} +1 -1
- package/dist/{cli-vnnz5ncg.js → cli-s3hwpt9v.js} +95 -16
- package/dist/{cli-7119ja3w.js → cli-sptrj5f5.js} +2 -2
- package/dist/{cli-a8c82p2t.js → cli-sqvfa090.js} +13 -6
- package/dist/{cli-phfg8vy8.js → cli-swfmp1fx.js} +26 -26
- package/dist/{cli-nsdxtcbg.js → cli-tcen9p5w.js} +8 -5
- package/dist/{cli-9m03pzga.js → cli-vc9bp061.js} +5 -5
- package/dist/{cli-rtx9dygp.js → cli-vxe6qa4h.js} +1 -1
- package/dist/{cli-ecfxwnyb.js → cli-vy3ed88x.js} +1 -1
- package/dist/{cli-0c7ata9h.js → cli-vyqs9ya8.js} +6 -6
- package/dist/cli-wbvnn6tk.js +861 -0
- package/dist/{cli-kgd3p0bm.js → cli-wjgs5wxe.js} +6 -6
- package/dist/cli-wxkkd0e0.js +33 -0
- package/dist/{cli-akfj32vj.js → cli-xqpk3rp7.js} +5 -4
- package/dist/cli-zp38xvtm.js +9487 -0
- package/dist/{cli-3zc55ctx.js → cli-zvhd3f64.js} +2 -2
- package/dist/{cli-ze1d3wzv.js → cli-zxqx25nz.js} +5 -5
- package/dist/cli.js +32 -25
- package/dist/{cloudflare-workers-ai-system-one-k6fbxw9d.js → cloudflare-workers-ai-system-one-k8ddqjkq.js} +8 -6
- package/dist/{codex-adapter-04v97vep.js → codex-adapter-esvmnhn7.js} +13 -13
- package/dist/{codex-hook-20me6byw.js → codex-hook-m73jmqgx.js} +19 -3
- package/dist/{codex-session-runner-ax69mqq3.js → codex-session-runner-ban9excr.js} +13 -13
- package/dist/{commands-18gy08n1.js → commands-75413p2n.js} +9 -7
- package/dist/{db-d2pvjpyt.js → db-7f5pecwg.js} +11 -7
- package/dist/{dispatcher-8wbwyzjg.js → dispatcher-fh9at8nz.js} +33 -30
- package/dist/{dsh-adapter-ysqk9snv.js → dsh-adapter-wpze65d4.js} +3 -3
- package/dist/{e2b-esmt1r16.js → e2b-gjgdjp91.js} +10 -9
- package/dist/{pi-logo-animation-e9a0t68z.js → easter-egg-3d-3w5c2n4c.js} +122 -68
- package/dist/{execute-cmagrbrz.js → execute-f50wg9nh.js} +29 -6
- package/dist/{google-generative-ai-k6n5nbpp.js → google-generative-ai-jxxpnegy.js} +2 -2
- package/dist/{google-vertex-44xxtcvp.js → google-vertex-x7q1angm.js} +5 -5
- package/dist/{handlers-6b69m5kp.js → handlers-w13787bb.js} +36 -33
- package/dist/{hook-srnkt0hb.js → hook-46g1jrvx.js} +28 -15
- package/dist/{hook-k3mydf2n.js → hook-4tpts2ry.js} +20 -17
- package/dist/{http-hrf80ca1.js → http-19pzbfrt.js} +964 -176
- package/dist/{index-9rxdvj6w.js → index-8fd4dm4h.js} +34 -31
- package/dist/{index-1asbaj62.js → index-dqmp15bq.js} +35 -32
- package/dist/{index-45evq4ee.js → index-qb4q1y3e.js} +33 -30
- package/dist/{index-k4nj18qs.js → index-s75wc4wd.js} +12 -10
- package/dist/{index-xmevk2b9.js → index-scptvdxg.js} +27 -25
- package/dist/{keepalive-sfdz6ksp.js → keepalive-xa5ts9cd.js} +11 -9
- package/dist/{lead-ej91e9hw.js → lead-qp4xkvjq.js} +45 -41
- package/dist/{maintenance-80dnd7ke.js → maintenance-rm5885hm.js} +13 -11
- package/dist/{mistral-conversations-pc3ntnxt.js → mistral-conversations-rw7fbt22.js} +4 -4
- package/dist/{node-a5e00qhp.js → node-8zkdq42h.js} +834 -833
- package/dist/{oauth-refresh-sweep-m1etk2mx.js → oauth-refresh-sweep-1csdfebf.js} +10 -8
- package/dist/{onboard-0q8sdfqc.js → onboard-r6b78cs4.js} +10 -10
- package/dist/{openai-codex-responses-n7n683p2.js → openai-codex-responses-efvrgrq3.js} +4 -4
- package/dist/{openai-completions-jpaj5g2b.js → openai-completions-bbbrrz4z.js} +12 -8
- package/dist/{openai-responses-9a3nme9j.js → openai-responses-9jjyrcm1.js} +15 -10
- package/dist/{opencode-adapter-4wcgmvgf.js → opencode-adapter-kb3g6pkf.js} +2 -2
- package/dist/{otel-impl-hnqm9mxq.js → otel-impl-0y9knpky.js} +5 -5
- package/dist/{pi-mono-adapter-d6sqm9wr.js → pi-mono-adapter-z6s1bme2.js} +100 -19
- package/dist/{pricing-refresh-y60z64pm.js → pricing-refresh-mth26tgp.js} +11 -9
- package/dist/{rbac-roles-8pa0754b.js → rbac-roles-kwntb23q.js} +13 -9
- package/dist/rbac-roles-vpk4ebm8.js +45 -0
- package/dist/{registry-v5wytg0s.js → registry-km6t1jd2.js} +1 -1
- package/dist/{render-v2-418s1vj7.js → render-v2-hcm7h00y.js} +13 -11
- package/dist/{runtime-3pknc168.js → runtime-vfjsfsc5.js} +4 -4
- package/dist/{setup-yghdkj4x.js → setup-hbyzd65h.js} +2 -2
- package/dist/{virtual-modules-jcceevt5.js → virtual-modules-9bv7ry91.js} +9 -8
- package/dist/{work-objects-j1sz20v9.js → work-objects-m09sxnhe.js} +36 -33
- package/dist/{worker-d4gcvkjt.js → worker-0h35rvk4.js} +45 -41
- package/openapi.json +491 -12
- package/package.json +13 -6
- package/src/be/api-drain.ts +184 -0
- package/src/be/db/tasks/read.ts +31 -6
- package/src/be/db/tasks/write.ts +5 -4
- package/src/be/db.ts +42 -18
- package/src/be/memory/link-resolver.ts +29 -3
- package/src/be/migrations/192_favorites_agent_fs_path.sql +36 -0
- package/src/be/onboarding.ts +7 -1
- package/src/be/seed-prompt-templates.ts +1 -0
- package/src/be/swarm-config-guard.ts +35 -16
- package/src/be/task-citations.ts +11 -5
- package/src/be/users.ts +3 -0
- package/src/cli.tsx +3 -2
- package/src/comb/agent-fs.ts +39 -0
- package/src/comb/config.ts +37 -0
- package/src/comb/markers.ts +37 -0
- package/src/comb/review-batch.ts +486 -0
- package/src/comb/templates.ts +88 -0
- package/src/commands/context-preamble.ts +4 -1
- package/src/commands/hook.ts +2 -1
- package/src/commands/runner.ts +262 -68
- package/src/commands/templates.ts +10 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/fs/agent-fs-provider.ts +106 -2
- package/src/heartbeat/heartbeat.ts +4 -0
- package/src/hooks/codex-hook.ts +25 -1
- package/src/hooks/hook.ts +13 -0
- package/src/hooks/pr-body-guard.ts +983 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/assets.ts +5 -17
- package/src/http/comb.ts +131 -0
- package/src/http/core.ts +2 -1
- package/src/http/fs.ts +9 -17
- package/src/http/index.ts +23 -9
- package/src/http/onboarding.ts +21 -2
- package/src/http/poll.ts +30 -3
- package/src/http/realtime.ts +62 -0
- package/src/http/request-principal.ts +22 -0
- package/src/http/status.ts +34 -2
- package/src/kv-reserved-namespaces.ts +10 -5
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +123 -0
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/auth.ts +63 -1
- package/src/realtime/tickets.ts +52 -0
- package/src/realtime/transport.ts +51 -22
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -1
- package/src/telemetry-context.ts +175 -0
- package/src/telemetry-contract/EVENTS.md +81 -0
- package/src/telemetry-contract/SOURCE.json +9 -0
- package/src/telemetry-contract/catalog.json +1335 -0
- package/src/telemetry-contract/index.ts +60 -0
- package/src/telemetry-contract/types.gen.ts +1971 -0
- package/src/telemetry-identity.ts +129 -0
- package/src/telemetry-snapshot.ts +211 -0
- package/src/telemetry-trigger.ts +125 -0
- package/src/telemetry-types.typetest.ts +40 -0
- package/src/telemetry.ts +271 -67
- package/src/tests/api-drain-e2e.test.ts +249 -0
- package/src/tests/api-drain-worker-e2e.test.ts +216 -0
- package/src/tests/api-drain.test.ts +351 -0
- package/src/tests/comb-config-guard.test.ts +38 -0
- package/src/tests/comb-links.test.ts +162 -0
- package/src/tests/comb-markers.test.ts +68 -0
- package/src/tests/comb-review-batch.test.ts +770 -0
- package/src/tests/comb-status.test.ts +152 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/favorites-agent-fs-path.test.ts +226 -0
- package/src/tests/memory-link-resolver.test.ts +34 -0
- package/src/tests/onboarding.test.ts +108 -0
- package/src/tests/pi-mono-adapter.test.ts +265 -2
- package/src/tests/pr-body-leaks.test.ts +789 -0
- package/src/tests/preload.ts +8 -0
- package/src/tests/rbac-engine.test.ts +19 -0
- package/src/tests/realtime-room-auth.test.ts +104 -1
- package/src/tests/realtime-tickets.test.ts +74 -0
- package/src/tests/realtime-transport.test.ts +131 -4
- package/src/tests/runner-api-drain.test.ts +301 -0
- package/src/tests/runner-fallback-output.test.ts +61 -0
- package/src/tests/runner-fenced-json-output.test.ts +178 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/send-task-requested-by.test.ts +133 -15
- package/src/tests/task-lifecycle-telemetry.test.ts +8 -3
- package/src/tests/telemetry-context.test.ts +314 -0
- package/src/tests/telemetry-init.test.ts +51 -19
- package/src/tests/telemetry-snapshot.test.ts +229 -0
- package/src/tests/telemetry-trigger.test.ts +200 -0
- package/src/tests/template-registry-helpers.ts +1 -0
- package/src/tests/workflow-agent-task-retry.test.ts +363 -0
- package/src/tests/workflow-definition-telemetry.test.ts +4 -4
- package/src/tools/poll-task.ts +20 -1
- package/src/tools/send-task.ts +9 -1
- package/src/tools/task-action.ts +11 -0
- package/src/types.ts +2 -1
- package/src/utils/api-drain.ts +24 -0
- package/src/utils/constants.ts +57 -16
- package/src/utils/pr-body-leaks.ts +172 -0
- package/src/utils/task-attachment-links.ts +14 -8
- package/src/workflows/recovery.ts +26 -1
- package/src/workflows/resume.ts +31 -4
- package/src/workflows/task-step-routing.ts +75 -2
- package/tsconfig.json +3 -0
- package/dist/cli-tjf4cn8z.js +0 -9119
- package/dist/cli-tsk3nz8a.js +0 -582
- package/dist/{cli-t6v2ah3r.js → cli-twrh04ap.js} +3 -3
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, initDb } from "../be/db";
|
|
4
|
+
import { resetFileStorageProvider } from "../fs/registry";
|
|
5
|
+
import { buildStatusPayload } from "../http/status";
|
|
6
|
+
|
|
7
|
+
const TEST_DB_PATH = "./test-comb-status.sqlite";
|
|
8
|
+
|
|
9
|
+
const ENV_KEYS = [
|
|
10
|
+
"COMB_ENABLED",
|
|
11
|
+
"AGENT_FS_API_URL",
|
|
12
|
+
"AGENT_FS_PUBLIC_URL",
|
|
13
|
+
"AGENT_FS_LIVE_URL",
|
|
14
|
+
"AGENT_FS_DEFAULT_ORG_ID",
|
|
15
|
+
"AGENT_FS_DEFAULT_DRIVE_ID",
|
|
16
|
+
"API_AGENT_FS_API_KEY",
|
|
17
|
+
"AGENT_FS_API_KEY",
|
|
18
|
+
];
|
|
19
|
+
const savedEnv = new Map<string, string | undefined>();
|
|
20
|
+
|
|
21
|
+
async function removeDbFiles(): Promise<void> {
|
|
22
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
23
|
+
await unlink(TEST_DB_PATH + suffix).catch(() => {});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function restoreEnv(): void {
|
|
28
|
+
for (const key of ENV_KEYS) {
|
|
29
|
+
const value = savedEnv.get(key);
|
|
30
|
+
if (value === undefined) delete process.env[key];
|
|
31
|
+
else process.env[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function comb() {
|
|
36
|
+
return (await buildStatusPayload()).agent_fs.comb;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
for (const key of ENV_KEYS) savedEnv.set(key, process.env[key]);
|
|
41
|
+
await removeDbFiles();
|
|
42
|
+
initDb(TEST_DB_PATH);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(async () => {
|
|
46
|
+
closeDb();
|
|
47
|
+
await removeDbFiles();
|
|
48
|
+
restoreEnv();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
53
|
+
resetFileStorageProvider();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
restoreEnv();
|
|
58
|
+
resetFileStorageProvider();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("/status agent_fs.comb", () => {
|
|
62
|
+
test("is off by default", async () => {
|
|
63
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
|
|
64
|
+
const result = await comb();
|
|
65
|
+
expect(result.enabled).toBe(false);
|
|
66
|
+
expect(result.api_url).toBe("http://agent-fs:7433");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("stays off with the flag on but no AGENT_FS_API_URL", async () => {
|
|
70
|
+
process.env.COMB_ENABLED = "true";
|
|
71
|
+
process.env.AGENT_FS_PUBLIC_URL = "http://localhost:7433";
|
|
72
|
+
expect((await comb()).enabled).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("turns on with the flag and AGENT_FS_API_URL", async () => {
|
|
76
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
|
|
77
|
+
for (const value of ["true", " TRUE ", "1"]) {
|
|
78
|
+
process.env.COMB_ENABLED = value;
|
|
79
|
+
expect((await comb()).enabled).toBe(true);
|
|
80
|
+
}
|
|
81
|
+
for (const value of ["false", "0", "yes", ""]) {
|
|
82
|
+
process.env.COMB_ENABLED = value;
|
|
83
|
+
expect((await comb()).enabled).toBe(false);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("AGENT_FS_PUBLIC_URL wins over AGENT_FS_API_URL, without a trailing slash", async () => {
|
|
88
|
+
process.env.COMB_ENABLED = "true";
|
|
89
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs:7433/";
|
|
90
|
+
expect((await comb()).api_url).toBe("http://agent-fs:7433");
|
|
91
|
+
process.env.AGENT_FS_PUBLIC_URL = "https://files.example.com/";
|
|
92
|
+
expect((await comb()).api_url).toBe("https://files.example.com");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("ids come from AGENT_FS_DEFAULT_*, and live_url from AGENT_FS_LIVE_URL", async () => {
|
|
96
|
+
process.env.COMB_ENABLED = "true";
|
|
97
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
|
|
98
|
+
let result = await comb();
|
|
99
|
+
expect(result.org_id).toBeNull();
|
|
100
|
+
expect(result.drive_id).toBeNull();
|
|
101
|
+
expect(result.live_url).toBe("https://live.agent-fs.dev");
|
|
102
|
+
|
|
103
|
+
process.env.AGENT_FS_DEFAULT_ORG_ID = "org-1";
|
|
104
|
+
process.env.AGENT_FS_DEFAULT_DRIVE_ID = "drive-1";
|
|
105
|
+
process.env.AGENT_FS_LIVE_URL = "https://live.example.com/";
|
|
106
|
+
result = await comb();
|
|
107
|
+
expect(result).toEqual({
|
|
108
|
+
enabled: true,
|
|
109
|
+
api_url: "http://agent-fs:7433",
|
|
110
|
+
live_url: "https://live.example.com",
|
|
111
|
+
org_id: "org-1",
|
|
112
|
+
drive_id: "drive-1",
|
|
113
|
+
service_user_id: null,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("service_user_id is the bootstrap key's agent-fs user, asked once", async () => {
|
|
118
|
+
let meCalls = 0;
|
|
119
|
+
const agentFs = Bun.serve({
|
|
120
|
+
port: 0,
|
|
121
|
+
fetch: (request) => {
|
|
122
|
+
if (new URL(request.url).pathname !== "/auth/me") return new Response("", { status: 404 });
|
|
123
|
+
meCalls++;
|
|
124
|
+
return request.headers.get("authorization") === "Bearer af_boot"
|
|
125
|
+
? Response.json({ userId: "svc-user", email: "swarm-admin@agent-fs.local" })
|
|
126
|
+
: Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
process.env.AGENT_FS_API_URL = `http://localhost:${agentFs.port}`;
|
|
131
|
+
process.env.API_AGENT_FS_API_KEY = "af_boot";
|
|
132
|
+
process.env.AGENT_FS_DEFAULT_ORG_ID = "org-1";
|
|
133
|
+
process.env.AGENT_FS_DEFAULT_DRIVE_ID = "drive-1";
|
|
134
|
+
|
|
135
|
+
// Comb off: no lookup.
|
|
136
|
+
expect((await comb()).service_user_id).toBeNull();
|
|
137
|
+
expect(meCalls).toBe(0);
|
|
138
|
+
|
|
139
|
+
process.env.COMB_ENABLED = "true";
|
|
140
|
+
expect((await comb()).service_user_id).toBe("svc-user");
|
|
141
|
+
expect((await comb()).service_user_id).toBe("svc-user");
|
|
142
|
+
expect(meCalls).toBe(1);
|
|
143
|
+
|
|
144
|
+
// A rejected key reports null instead of failing /status.
|
|
145
|
+
process.env.API_AGENT_FS_API_KEY = "af_other";
|
|
146
|
+
resetFileStorageProvider();
|
|
147
|
+
expect((await comb()).service_user_id).toBeNull();
|
|
148
|
+
} finally {
|
|
149
|
+
agentFs.stop(true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -441,6 +441,7 @@ value:installSkill
|
|
|
441
441
|
value:installSystemDefaultSkillsForAgent
|
|
442
442
|
value:isAgentEligibleForTask
|
|
443
443
|
value:isExtensionAgent
|
|
444
|
+
value:isLinearTrackerContextKey
|
|
444
445
|
value:isPendingSlackMessage
|
|
445
446
|
value:isPoolAffinityEnforcementEnabled
|
|
446
447
|
value:isSettledSlackMessage
|
|
@@ -621,6 +622,7 @@ value:getTasksByStatus
|
|
|
621
622
|
value:getTasksCount
|
|
622
623
|
value:hasNonTerminalRerouteDecisionChild
|
|
623
624
|
value:hasNonTerminalResumeChild
|
|
625
|
+
value:isLinearTrackerContextKey
|
|
624
626
|
value:markFinalizedSlackRelaysDelivered
|
|
625
627
|
value:markSlackRelayAttempted
|
|
626
628
|
value:markSlackRelayDelivered
|
|
@@ -464,6 +464,7 @@ value:installSkill
|
|
|
464
464
|
value:installSystemDefaultSkillsForAgent
|
|
465
465
|
value:isAgentEligibleForTask
|
|
466
466
|
value:isExtensionAgent
|
|
467
|
+
value:isLinearTrackerContextKey
|
|
467
468
|
value:isPendingSlackMessage
|
|
468
469
|
value:isPoolAffinityEnforcementEnabled
|
|
469
470
|
value:isSettledSlackMessage
|
|
@@ -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 394 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(394);
|
|
107
|
+
expect(generatedKeys).toHaveLength(394);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -354,7 +354,7 @@ describe("dsh harness", () => {
|
|
|
354
354
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
355
355
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
356
356
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
357
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.
|
|
357
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.1",
|
|
358
358
|
);
|
|
359
359
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
360
360
|
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { closeDb, getDb, initDb } from "../be/db";
|
|
7
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
8
|
+
import { handleFavorites } from "../http/favorites";
|
|
9
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
10
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
11
|
+
|
|
12
|
+
// Migration 192 adds the Comb pin item type `agent-fs-path` to user_favorites.
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-favorites-agent-fs-path.sqlite";
|
|
15
|
+
const MIGRATIONS_DIR = join(import.meta.dir, "../be/migrations");
|
|
16
|
+
|
|
17
|
+
async function removeDb(path: string): Promise<void> {
|
|
18
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
19
|
+
try {
|
|
20
|
+
await unlink(`${path}${suffix}`);
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function jsonReq(method: string, url: string, body?: unknown) {
|
|
26
|
+
const raw = body === undefined ? "" : JSON.stringify(body);
|
|
27
|
+
const req = Readable.from(raw ? [Buffer.from(raw)] : []) as Readable & {
|
|
28
|
+
method: string;
|
|
29
|
+
url: string;
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
req.method = method;
|
|
33
|
+
req.url = url;
|
|
34
|
+
req.headers = { "content-type": "application/json" };
|
|
35
|
+
setRequestAuth(req, { kind: "operator", fingerprint: "op:test-comb-pins" });
|
|
36
|
+
return req;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function call(method: string, url: string, body?: unknown) {
|
|
40
|
+
const req = jsonReq(method, url, body);
|
|
41
|
+
let statusCode = 200;
|
|
42
|
+
const chunks: string[] = [];
|
|
43
|
+
const res = {
|
|
44
|
+
setHeader: () => {},
|
|
45
|
+
writeHead: (code: number) => {
|
|
46
|
+
statusCode = code;
|
|
47
|
+
},
|
|
48
|
+
end: (chunk?: string) => {
|
|
49
|
+
if (chunk) chunks.push(chunk);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
await handleFavorites(
|
|
53
|
+
req,
|
|
54
|
+
res as never,
|
|
55
|
+
getPathSegments(req.url),
|
|
56
|
+
parseQueryParams(req.url),
|
|
57
|
+
undefined,
|
|
58
|
+
);
|
|
59
|
+
return { statusCode, body: chunks.length > 0 ? JSON.parse(chunks.join("")) : null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("favorites: agent-fs-path item type (fresh DB)", () => {
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
await removeDb(TEST_DB_PATH);
|
|
65
|
+
initDb(TEST_DB_PATH);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
closeDb();
|
|
70
|
+
await removeDb(TEST_DB_PATH);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("PUT pins a folder and a file, GET lists them back", async () => {
|
|
74
|
+
for (const itemId of ["org/drive/comb-qa/", "org/drive/comb-qa/my notes 100%.md"]) {
|
|
75
|
+
const put = await call("PUT", "/api/favorites", {
|
|
76
|
+
itemType: "agent-fs-path",
|
|
77
|
+
itemId,
|
|
78
|
+
favorite: true,
|
|
79
|
+
});
|
|
80
|
+
expect(put).toMatchObject({
|
|
81
|
+
statusCode: 200,
|
|
82
|
+
body: { favorite: true, itemType: "agent-fs-path", itemId },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
|
|
87
|
+
expect(list.statusCode).toBe(200);
|
|
88
|
+
expect([...list.body.favoriteIds].sort()).toEqual([
|
|
89
|
+
"org/drive/comb-qa/",
|
|
90
|
+
"org/drive/comb-qa/my notes 100%.md",
|
|
91
|
+
]);
|
|
92
|
+
for (const row of list.body.favorites) expect(row.itemType).toBe("agent-fs-path");
|
|
93
|
+
|
|
94
|
+
// Other item types do not see the pins.
|
|
95
|
+
expect((await call("GET", "/api/favorites?itemType=page")).body.favoriteIds).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("PUT with favorite=false unpins", async () => {
|
|
99
|
+
await call("PUT", "/api/favorites", {
|
|
100
|
+
itemType: "agent-fs-path",
|
|
101
|
+
itemId: "org/drive/comb-qa/",
|
|
102
|
+
favorite: false,
|
|
103
|
+
});
|
|
104
|
+
const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
|
|
105
|
+
expect(list.body.favoriteIds).toEqual(["org/drive/comb-qa/my notes 100%.md"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("GET lists pins newest first", async () => {
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const ids = ["org/drive/order/a.md", "org/drive/order/b.md", "org/drive/order/c.md"];
|
|
111
|
+
for (const [i, itemId] of ids.entries()) {
|
|
112
|
+
await call("PUT", "/api/favorites", { itemType: "agent-fs-path", itemId, favorite: true });
|
|
113
|
+
db.run(
|
|
114
|
+
"UPDATE user_favorites SET lastUpdatedAt = ? WHERE itemType = 'agent-fs-path' AND itemId = ?",
|
|
115
|
+
[`2030-01-0${i + 1}T00:00:00.000Z`, itemId],
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
|
|
119
|
+
const listed = list.body.favoriteIds.filter((id: string) => id.startsWith("org/drive/order/"));
|
|
120
|
+
expect(listed).toEqual([...ids].reverse());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("an unknown item type is still rejected", async () => {
|
|
124
|
+
const put = await call("PUT", "/api/favorites", {
|
|
125
|
+
itemType: "agent-fs-file",
|
|
126
|
+
itemId: "org/drive/a.md",
|
|
127
|
+
favorite: true,
|
|
128
|
+
});
|
|
129
|
+
expect(put.statusCode).toBe(400);
|
|
130
|
+
const list = await call("GET", "/api/favorites?itemType=agent-fs-file");
|
|
131
|
+
expect(list.statusCode).toBe(400);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("migration 192 (existing DB)", () => {
|
|
136
|
+
// In memory: the full migration chain on a file DB passed the 10 s test timeout on a loaded CI shard.
|
|
137
|
+
test("keeps every favorite row and the indexes, and widens the CHECK", async () => {
|
|
138
|
+
const db = new Database(":memory:");
|
|
139
|
+
try {
|
|
140
|
+
runMigrations(db);
|
|
141
|
+
|
|
142
|
+
// Roll back to the pre-192 shape: re-run 116 on the empty table (it
|
|
143
|
+
// rebuilds user_favorites with the old CHECK and the same indexes).
|
|
144
|
+
db.exec(await Bun.file(join(MIGRATIONS_DIR, "116_favorite_principal_scope.sql")).text());
|
|
145
|
+
db.run("DELETE FROM _migrations WHERE version = 192");
|
|
146
|
+
expect(() =>
|
|
147
|
+
db.run(
|
|
148
|
+
"INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator:x', 'agent-fs-path', 'o/d/a.md')",
|
|
149
|
+
),
|
|
150
|
+
).toThrow(/CHECK constraint failed/);
|
|
151
|
+
|
|
152
|
+
db.run("INSERT INTO users (id, name) VALUES ('user-1', 'Pin User')");
|
|
153
|
+
const insert = db.prepare(
|
|
154
|
+
`INSERT INTO user_favorites
|
|
155
|
+
(id, favoriteScope, userId, itemType, itemId, createdAt, lastUpdatedAt, created_by, updated_by)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
157
|
+
);
|
|
158
|
+
// Page, workflow, and schedule favorites for a user scope and the
|
|
159
|
+
// operator scope, with distinct timestamps and audit values per row.
|
|
160
|
+
let n = 0;
|
|
161
|
+
for (const [scope, userId] of [
|
|
162
|
+
["user:user-1", "user-1"],
|
|
163
|
+
["operator", null],
|
|
164
|
+
] as const) {
|
|
165
|
+
for (const itemType of ["page", "workflow", "schedule"]) {
|
|
166
|
+
n++;
|
|
167
|
+
insert.run(
|
|
168
|
+
`fav-${n}`,
|
|
169
|
+
scope,
|
|
170
|
+
userId,
|
|
171
|
+
itemType,
|
|
172
|
+
`${itemType}-${n}`,
|
|
173
|
+
`2026-09-0${n}T00:00:00.000Z`,
|
|
174
|
+
`2026-09-1${n}T00:00:00.000Z`,
|
|
175
|
+
n % 2 ? "op:abc" : null,
|
|
176
|
+
n % 3 ? userId : null,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const before = db.query("SELECT * FROM user_favorites ORDER BY id").all();
|
|
181
|
+
expect(before).toHaveLength(6);
|
|
182
|
+
|
|
183
|
+
runMigrations(db);
|
|
184
|
+
|
|
185
|
+
expect(db.query("SELECT version FROM _migrations WHERE version = 192").get()).not.toBeNull();
|
|
186
|
+
expect(db.query("SELECT * FROM user_favorites ORDER BY id").all()).toEqual(before);
|
|
187
|
+
|
|
188
|
+
const indexes = db
|
|
189
|
+
.query<{ name: string; unique: number }, []>("PRAGMA index_list(user_favorites)")
|
|
190
|
+
.all();
|
|
191
|
+
expect(indexes.map((index) => index.name)).toEqual(
|
|
192
|
+
expect.arrayContaining([
|
|
193
|
+
"idx_user_favorites_scope_type",
|
|
194
|
+
"idx_user_favorites_user_type",
|
|
195
|
+
"idx_user_favorites_item",
|
|
196
|
+
]),
|
|
197
|
+
);
|
|
198
|
+
// The UNIQUE (favoriteScope, itemType, itemId) constraint survives.
|
|
199
|
+
const unique = indexes
|
|
200
|
+
.filter((index) => index.unique === 1)
|
|
201
|
+
.map((index) =>
|
|
202
|
+
db
|
|
203
|
+
.query<{ name: string }, []>(`PRAGMA index_info("${index.name}")`)
|
|
204
|
+
.all()
|
|
205
|
+
.map((column) => column.name),
|
|
206
|
+
);
|
|
207
|
+
expect(unique).toContainEqual(["favoriteScope", "itemType", "itemId"]);
|
|
208
|
+
|
|
209
|
+
// The new type is accepted, an unknown type is still rejected.
|
|
210
|
+
db.run(
|
|
211
|
+
"INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator', 'agent-fs-path', 'o/d/comb-qa/')",
|
|
212
|
+
);
|
|
213
|
+
expect(() =>
|
|
214
|
+
db.run(
|
|
215
|
+
"INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator', 'bogus', 'x')",
|
|
216
|
+
),
|
|
217
|
+
).toThrow(/CHECK constraint failed/);
|
|
218
|
+
|
|
219
|
+
// The user FK still cascades.
|
|
220
|
+
db.run("DELETE FROM users WHERE id = 'user-1'");
|
|
221
|
+
expect(db.query("SELECT id FROM user_favorites WHERE userId = 'user-1'").all()).toEqual([]);
|
|
222
|
+
} finally {
|
|
223
|
+
db.close();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -61,6 +61,40 @@ describe("resolveLinks", () => {
|
|
|
61
61
|
expect(fsLinks[0]!.resolver).toBe("agent-fs-path");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("a dashboard (Comb) file link resolves to the same target as the live URL", () => {
|
|
65
|
+
const route =
|
|
66
|
+
"/file/~/648a5f3c-35c8-4f11-8673-b89de52cd6bd/2faf73ba-4eee-4472-8b3b-359c4ed6bfbb/thoughts/final%20plan.md";
|
|
67
|
+
const savedAppUrl = process.env.APP_URL;
|
|
68
|
+
const savedDashboardUrl = process.env.DASHBOARD_URL;
|
|
69
|
+
process.env.APP_URL = "https://swarm.example.com/dash/";
|
|
70
|
+
delete process.env.DASHBOARD_URL;
|
|
71
|
+
try {
|
|
72
|
+
const fsTargets = (content: string) =>
|
|
73
|
+
resolveLinks(content)
|
|
74
|
+
.filter((l) => l.linkType === "agent-fs-file")
|
|
75
|
+
.map((l) => ({ targetId: l.targetId, metadata: l.metadata }));
|
|
76
|
+
const live = fsTargets(`Plan at https://live.agent-fs.dev${route}`);
|
|
77
|
+
expect(live).toHaveLength(1);
|
|
78
|
+
expect(fsTargets(`Plan at https://swarm.example.com/dash${route}`)).toEqual(live);
|
|
79
|
+
expect(fsTargets(`Plan at http://localhost:5274${route}`)).toEqual(live);
|
|
80
|
+
// Another host, or the app host without its path prefix, is not a file link.
|
|
81
|
+
expect(fsTargets(`Plan at https://other.example.com${route}`)).toEqual([]);
|
|
82
|
+
expect(fsTargets(`Plan at https://swarm.example.com${route}`)).toEqual([]);
|
|
83
|
+
// A known host must start the host name.
|
|
84
|
+
expect(fsTargets(`Plan at https://evil-swarm.example.com/dash${route}`)).toEqual([]);
|
|
85
|
+
expect(fsTargets(`Plan at https://notlocalhost:9${route}`)).toEqual([]);
|
|
86
|
+
expect(fsTargets(`Plan at https://myagent-fs${route}`)).toEqual([]);
|
|
87
|
+
// Hosts are case-insensitive.
|
|
88
|
+
expect(fsTargets(`Plan at https://SWARM.Example.com/dash${route}`)).toEqual(live);
|
|
89
|
+
expect(fsTargets(`Plan at https://LIVE.AGENT-FS.DEV${route}`)).toEqual(live);
|
|
90
|
+
} finally {
|
|
91
|
+
if (savedAppUrl === undefined) delete process.env.APP_URL;
|
|
92
|
+
else process.env.APP_URL = savedAppUrl;
|
|
93
|
+
if (savedDashboardUrl === undefined) delete process.env.DASHBOARD_URL;
|
|
94
|
+
else process.env.DASHBOARD_URL = savedDashboardUrl;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
64
98
|
test("extracts agent-ui page links", () => {
|
|
65
99
|
const links = resolveLinks(
|
|
66
100
|
"See app.agent-swarm.dev/pages/abc12345-1234-1234-1234-123456789abc",
|
|
@@ -613,6 +613,114 @@ describe("onboarding memory probe", () => {
|
|
|
613
613
|
});
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
test("splits a 404 into a missing path and a missing model or deployment", async () => {
|
|
617
|
+
const cases = [
|
|
618
|
+
{
|
|
619
|
+
// Azure without `/openai/v1`.
|
|
620
|
+
body: { error: { code: "404", message: "Resource not found" } },
|
|
621
|
+
errorClass: "endpoint",
|
|
622
|
+
error: "The endpoint path was not found (404). Check the base URL.",
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
// A proxy or server page with no JSON body.
|
|
626
|
+
body: "404 page not found",
|
|
627
|
+
errorClass: "endpoint",
|
|
628
|
+
error: "The endpoint path was not found (404). Check the base URL.",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
body: {
|
|
632
|
+
error: {
|
|
633
|
+
message: "The model `nope` does not exist or you do not have access to it.",
|
|
634
|
+
type: "invalid_request_error",
|
|
635
|
+
code: "model_not_found",
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
errorClass: "model",
|
|
639
|
+
error: "The endpoint could not find the embedding model or deployment (404).",
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
body: {
|
|
643
|
+
error: {
|
|
644
|
+
code: "DeploymentNotFound",
|
|
645
|
+
message: "The API deployment for this resource does not exist.",
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
errorClass: "model",
|
|
649
|
+
error: "The endpoint could not find the embedding model or deployment (404).",
|
|
650
|
+
},
|
|
651
|
+
] as const;
|
|
652
|
+
|
|
653
|
+
for (const testCase of cases) {
|
|
654
|
+
const fake = startEmbeddingServer(() =>
|
|
655
|
+
typeof testCase.body === "string"
|
|
656
|
+
? new Response(testCase.body, { status: 404 })
|
|
657
|
+
: Response.json(testCase.body, { status: 404 }),
|
|
658
|
+
);
|
|
659
|
+
try {
|
|
660
|
+
const response = await request("/api/onboarding/memory", {
|
|
661
|
+
method: "POST",
|
|
662
|
+
body: {
|
|
663
|
+
preset: "azure",
|
|
664
|
+
baseUrl: fake.baseUrl,
|
|
665
|
+
model: "my-embedding-deployment",
|
|
666
|
+
apiKey: "azure-resource-key",
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
expect(response.status).toBe(200);
|
|
670
|
+
expect(await response.json()).toMatchObject({
|
|
671
|
+
ok: false,
|
|
672
|
+
errorClass: testCase.errorClass,
|
|
673
|
+
error: testCase.error,
|
|
674
|
+
});
|
|
675
|
+
} finally {
|
|
676
|
+
fake.server.stop(true);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// A failed step keeps its first error class.
|
|
680
|
+
expect((await getState()).steps.memory).toMatchObject({
|
|
681
|
+
status: "failed",
|
|
682
|
+
errorClass: "endpoint",
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("the azure preset saves its base URL and deployment name", async () => {
|
|
687
|
+
let path = "";
|
|
688
|
+
let model = "";
|
|
689
|
+
const fake = Bun.serve({
|
|
690
|
+
hostname: "127.0.0.1",
|
|
691
|
+
port: 0,
|
|
692
|
+
async fetch(req) {
|
|
693
|
+
path = new URL(req.url).pathname;
|
|
694
|
+
model = ((await req.json()) as { model: string }).model;
|
|
695
|
+
return embeddingResponse(512);
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
try {
|
|
699
|
+
const baseUrl = `http://127.0.0.1:${fake.port}/openai/v1`;
|
|
700
|
+
const response = await request("/api/onboarding/memory", {
|
|
701
|
+
method: "POST",
|
|
702
|
+
body: {
|
|
703
|
+
preset: "azure",
|
|
704
|
+
baseUrl,
|
|
705
|
+
model: "my-embedding-deployment",
|
|
706
|
+
apiKey: "azure-resource-key",
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
|
|
710
|
+
expect(path).toBe("/openai/v1/embeddings");
|
|
711
|
+
expect(model).toBe("my-embedding-deployment");
|
|
712
|
+
const configs = await getSwarmConfigs({ scope: "global" });
|
|
713
|
+
const saved = Object.fromEntries(configs.map((config) => [config.key, config.value]));
|
|
714
|
+
expect(saved).toMatchObject({
|
|
715
|
+
EMBEDDING_API_BASE_URL: baseUrl,
|
|
716
|
+
EMBEDDING_MODEL: "my-embedding-deployment",
|
|
717
|
+
});
|
|
718
|
+
expect((await getState()).steps.memory).toMatchObject({ status: "done", method: "azure" });
|
|
719
|
+
} finally {
|
|
720
|
+
fake.stop(true);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
616
724
|
test("a wrong dimension keeps memory done after a successful probe", async () => {
|
|
617
725
|
let dimensions = 512;
|
|
618
726
|
const fake = startEmbeddingServer(() => embeddingResponse(dimensions));
|