@desplega.ai/agent-swarm 2.7.0 → 2.7.1
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/dist/{acp-adapter-ma1js5n7.js → acp-adapter-8dn5tq56.js} +6 -6
- package/dist/{actions-1vhmtvg9.js → actions-3r6gw7dy.js} +18 -18
- package/dist/{amp-adapter-sxqqvmdk.js → amp-adapter-wjtq413p.js} +3 -3
- package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
- package/dist/{app-mwx1d5a9.js → app-d53yhykk.js} +3 -3
- package/dist/{assistant-yx1p6g82.js → assistant-ksrawj3k.js} +20 -20
- package/dist/{boot-reembed-7h78vmvv.js → boot-reembed-fxfw17m2.js} +10 -3
- package/dist/{boot-reembed-5h37a2c2.js → boot-reembed-k5y5xbj0.js} +15 -8
- package/dist/{boot-scrub-sweep-8mawd4gw.js → boot-scrub-sweep-se41j9fd.js} +3 -3
- package/dist/{boot-seeds-ca84dhnn.js → boot-seeds-r3kwwv8b.js} +4 -4
- package/dist/{claude-adapter-wqt4hfkq.js → claude-adapter-ahmyf231.js} +7 -7
- package/dist/{claude-managed-adapter-6fjqtr2z.js → claude-managed-adapter-5knamegc.js} +3 -3
- package/dist/{claude-managed-setup-rqv98sea.js → claude-managed-setup-mdc7dy6b.js} +2 -2
- package/dist/{claude-sdk-session-f29f3b2x.js → claude-sdk-session-n5wen9zk.js} +8728 -8599
- package/dist/{cli-m5e802r7.js → cli-0dqtv2jg.js} +2 -2
- package/dist/{cli-e1595bgx.js → cli-0tww7dnz.js} +2 -2
- package/dist/{cli-2y7v133v.js → cli-0w4e4v58.js} +7 -7
- package/dist/{cli-d20qvpr2.js → cli-2sjm3t4r.js} +1 -1
- package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
- package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
- package/dist/{cli-twctnkng.js → cli-3v2cyqb8.js} +3 -3
- package/dist/{cli-he9gbdx0.js → cli-5qt9vzmq.js} +39 -20
- package/dist/{cli-a6ds2epe.js → cli-5rp1w4cr.js} +1 -1
- package/dist/{cli-6srv653c.js → cli-6vwjsetx.js} +4 -4
- package/dist/{cli-1hhbzkbb.js → cli-6w2yrrc0.js} +8 -8
- package/dist/{cli-f12mr20z.js → cli-75tdydgv.js} +1 -1
- package/dist/{cli-j3y0x2n4.js → cli-8feswf2v.js} +43 -33
- package/dist/{cli-07q03xm2.js → cli-8wd3mbyj.js} +1 -1
- package/dist/{cli-ymjpkxgn.js → cli-9csfthbw.js} +8 -6
- package/dist/{cli-f1e0ptjj.js → cli-b41p764a.js} +21 -2
- package/dist/{cli-kv7sbc2c.js → cli-c1t533hh.js} +3 -3
- package/dist/{cli-54sj64fc.js → cli-c28jxpcv.js} +1 -1
- package/dist/{cli-8mbc9za9.js → cli-cb50g9cd.js} +1 -1
- package/dist/{cli-c668vvcf.js → cli-d8gaphtr.js} +1 -1
- package/dist/{cli-86ehn4s5.js → cli-dgtfysne.js} +2 -2
- package/dist/{cli-sjctjr48.js → cli-dscnhn18.js} +1 -1
- package/dist/{cli-j0ppywdr.js → cli-dv11pgpv.js} +12 -12
- package/dist/{cli-1m9x8dr5.js → cli-ekptgk2m.js} +5 -5
- package/dist/{cli-9gsm2rzp.js → cli-f3mzy662.js} +17 -17
- package/dist/{cli-s0mgx214.js → cli-fk7k8s9x.js} +2 -2
- package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
- package/dist/{cli-yd2nreqw.js → cli-hkxqdmnb.js} +2 -2
- package/dist/{cli-8wmj18cz.js → cli-hpkvgdad.js} +7 -4
- package/dist/{cli-wqr7cea4.js → cli-hq2v4rr5.js} +37 -9
- package/dist/{cli-sgqm2582.js → cli-k1zb7raw.js} +5 -5
- package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
- package/dist/{cli-ay40dnj3.js → cli-k7d3hvya.js} +2 -2
- package/dist/{cli-j9bpmykh.js → cli-kbtdftbw.js} +5 -5
- package/dist/{cli-x37r7qwf.js → cli-kj69b9tw.js} +25 -5
- package/dist/{cli-gsfk58nw.js → cli-kk9jy4nm.js} +1 -1
- package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
- package/dist/{cli-h5jeg46p.js → cli-mdadewqp.js} +5 -5
- package/dist/{cli-a89zdqz9.js → cli-nf6k60sn.js} +1 -1
- package/dist/{cli-v6shg7na.js → cli-nzdf09js.js} +1 -1
- package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
- package/dist/{cli-6a9qfa8b.js → cli-pzafyqtk.js} +118 -48
- package/dist/{cli-48gxjmcz.js → cli-qvaa1gjp.js} +87 -24
- package/dist/{cli-qj442fws.js → cli-rg4b2g6j.js} +1 -1
- package/dist/{cli-aypd09nf.js → cli-rm66vqdz.js} +1 -1
- package/dist/{cli-9pshx6rd.js → cli-s504c3w7.js} +1 -1
- package/dist/{cli-ptfq202x.js → cli-swkx9jm2.js} +3 -3
- package/dist/{cli-m30k9777.js → cli-thsaq4pd.js} +16 -16
- package/dist/{cli-60sty27p.js → cli-twjnyvs8.js} +2 -2
- package/dist/{cli-rd5m70gj.js → cli-typdpe8g.js} +4 -4
- package/dist/{cli-yxbsmx56.js → cli-vg8pwmsd.js} +6 -6
- package/dist/{cli-s09j9vp5.js → cli-vhpckzvp.js} +1 -1
- package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
- package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
- package/dist/{cli-b3yzyc57.js → cli-x3cngb1h.js} +44 -12
- package/dist/{cli-6mgdba42.js → cli-xbz6mcg4.js} +2 -2
- package/dist/{cli-2pvzmas1.js → cli-y4tdxsqb.js} +3 -3
- package/dist/{cli-f4dbh7a5.js → cli-yqrsp3ny.js} +7 -7
- package/dist/cli.js +27 -19
- package/dist/{codex-adapter-zk59x2zh.js → codex-adapter-7wepzq15.js} +5 -5
- package/dist/{codex-hook-xyjhvxwd.js → codex-hook-pbdh4xhx.js} +2 -2
- package/dist/{codex-session-runner-kh6s4j3n.js → codex-session-runner-02sb49e4.js} +5 -5
- package/dist/{commands-68et1qv2.js → commands-3rcspxct.js} +2 -2
- package/dist/{controller-fyrbnzg4.js → controller-8ar4avd6.js} +3 -3
- package/dist/{controllers-connect-nep9h2xv.js → controllers-connect-ajvh9h0v.js} +19 -19
- package/dist/{cursor-adapter-yss3pg7t.js → cursor-adapter-gd0sa7bh.js} +1 -1
- package/dist/{db-28gjyvxj.js → db-dv9j7ts5.js} +2 -2
- package/dist/{db-8ftwvzba.js → db-z26vgdvj.js} +3 -3
- package/dist/{dispatcher-hayexmkr.js → dispatcher-8m0htjq8.js} +17 -17
- package/dist/{dsh-adapter-46wawff9.js → dsh-adapter-y8gyk130.js} +1 -1
- package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
- package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
- package/dist/{easter-egg-3d-8d1e0h4m.js → easter-egg-3d-28f880xx.js} +1 -1
- package/dist/{execute-cjrfxfnm.js → execute-9seb79vh.js} +1 -1
- package/dist/{external-channel-xbmxcg09.js → external-channel-smz7jyth.js} +2 -2
- package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
- package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
- package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
- package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
- package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
- package/dist/{grok-adapter-ag4wks2a.js → grok-adapter-z0nnvtbn.js} +7 -7
- package/dist/handlers-pspryh6g.js +41 -0
- package/dist/{heartbeat-psfvcgwp.js → heartbeat-f928aqyp.js} +20 -20
- package/dist/{hook-cyq4mryn.js → hook-d7c605r4.js} +7 -7
- package/dist/{hook-8pz04697.js → hook-q9hv5xf9.js} +5 -5
- package/dist/{http-haj1nd6k.js → http-q6j4k989.js} +934 -180
- package/dist/{index-z5h22pes.js → index-5nb266jr.js} +17 -17
- package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
- package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
- package/dist/{index-zyq8ct52.js → index-ch0c1xff.js} +17 -17
- package/dist/{index-syvhnvtw.js → index-ebnjhxh1.js} +1 -1
- package/dist/{index-y1ajx04a.js → index-ed8dy8rn.js} +20 -20
- package/dist/{index-jwt2bj6f.js → index-ejqdkkqx.js} +11 -10
- package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
- package/dist/{index-q52f47pe.js → index-yn1yxg85.js} +1 -1
- package/dist/{keepalive-xdvsamvj.js → keepalive-zzc3hks1.js} +3 -3
- package/dist/{lead-hqg57kdp.js → lead-c0pjcaw4.js} +36 -36
- package/dist/{maintenance-4y2xzh3a.js → maintenance-kqsxzpzx.js} +1 -1
- package/dist/{oauth-refresh-sweep-jd4r7zbs.js → oauth-refresh-sweep-7qt0g55z.js} +2 -2
- package/dist/{onboard-18447q9q.js → onboard-k7x13fqg.js} +118 -5
- package/dist/{opencode-adapter-26b2frdf.js → opencode-adapter-tqvrtter.js} +5 -5
- package/dist/{otel-impl-f9tgr8yw.js → otel-impl-sqkf6k3b.js} +97 -97
- package/dist/{pi-mono-adapter-qn6qbga1.js → pi-mono-adapter-m2qnafxm.js} +63 -9
- package/dist/{pricing-refresh-dw2tgb53.js → pricing-refresh-wkpwhgvs.js} +3 -3
- package/dist/{rbac-roles-s4nwj2m3.js → rbac-roles-dpn0c0wj.js} +7 -4
- package/dist/{rbac-roles-aygm2760.js → rbac-roles-wk7xd6a6.js} +2 -2
- package/dist/{render-v2-yfvdfr0x.js → render-v2-ex2m4g2k.js} +3 -3
- package/dist/{resume-1bmvjbf3.js → resume-725g9b0b.js} +7 -7
- package/dist/{runtime-f7tpzedy.js → runtime-kajas15g.js} +1 -1
- package/dist/{session-access-r5garej6.js → session-access-0j2ezjje.js} +8 -4
- package/dist/{session-source-backfill-424wzv0r.js → session-source-backfill-62202awe.js} +2 -2
- package/dist/{setup-8tygvxkq.js → setup-9sj11ax6.js} +1 -1
- package/dist/{virtual-modules-xsw323b3.js → virtual-modules-c6fcq6zr.js} +2 -2
- package/dist/{work-objects-ggnghpgf.js → work-objects-0y07495y.js} +20 -20
- package/dist/{worker-sqjd1nqf.js → worker-d4q9sqbc.js} +36 -36
- package/openapi.json +1070 -9
- package/package.json +3 -3
- package/src/be/connector-codes.ts +65 -22
- package/src/be/memory/boot-reembed.ts +4 -1
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/embedding-recovery.ts +48 -0
- package/src/be/memory/index-content.ts +5 -2
- package/src/be/memory/providers/openai-embedding.ts +65 -14
- package/src/be/memory/task-learning.ts +110 -8
- package/src/be/memory/types.ts +13 -2
- package/src/be/migrations/246_connector_code_kind.sql +11 -0
- package/src/be/onboarding.ts +125 -15
- package/src/be/rbac-roles.ts +4 -1
- package/src/be/scripts/embeddings.ts +2 -2
- package/src/be/seed/agent-fs-provision.ts +75 -14
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/cli.tsx +9 -0
- package/src/comb/config.ts +6 -1
- package/src/commands/onboard/agent-fs-version.ts +5 -0
- package/src/commands/onboard/compose-generator.ts +112 -1
- package/src/commands/onboard/env-generator.ts +9 -0
- package/src/commands/onboard/tei-image.ts +12 -0
- package/src/commands/onboard/types.ts +7 -0
- package/src/commands/onboard.tsx +11 -1
- package/src/commands/runner.ts +14 -1
- package/src/http/agentfs-proxy.ts +369 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/codex-oauth-device.ts +8 -1
- package/src/http/fs.ts +64 -10
- package/src/http/inbox-state.ts +27 -1
- package/src/http/index.ts +4 -1
- package/src/http/memory.ts +7 -4
- package/src/http/onboarding.ts +139 -11
- package/src/http/session-access.ts +24 -0
- package/src/http/sessions.ts +10 -6
- package/src/http/tasks.ts +85 -11
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/user-roles.ts +4 -1
- package/src/http/users.ts +272 -3
- package/src/http/utils.ts +22 -3
- package/src/prompts/base-prompt.ts +24 -9
- package/src/prompts/session-templates.ts +26 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +6 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +58 -3
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
- package/src/tests/agentfs-proxy.test.ts +1106 -0
- package/src/tests/base-prompt.test.ts +69 -2
- package/src/tests/codex-adapter.test.ts +46 -8
- package/src/tests/codex-oauth-device.test.ts +12 -1
- package/src/tests/comb-config-guard.test.ts +20 -0
- package/src/tests/comb-links.test.ts +79 -1
- package/src/tests/comb-status.test.ts +44 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
- package/src/tests/fixtures/onboard/content.env.txt +29 -0
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/dev.env.txt +28 -0
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
- package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
- package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
- package/src/tests/fixtures/onboard/full.env.txt +36 -0
- package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/research.env.txt +28 -0
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
- package/src/tests/fixtures/onboard/solo.env.txt +26 -0
- package/src/tests/http-log-scrubbing.test.ts +34 -1
- package/src/tests/http-task-write-ownership.test.ts +5 -1
- package/src/tests/invites-http.test.ts +464 -0
- package/src/tests/memory-embedding-input-cap.test.ts +122 -1
- package/src/tests/memory-embedding-recovery.test.ts +137 -0
- package/src/tests/onboard-compose.test.ts +142 -0
- package/src/tests/onboard-env.test.ts +24 -0
- package/src/tests/onboard-non-interactive.test.ts +29 -0
- package/src/tests/onboard-tei-image.test.ts +36 -0
- package/src/tests/onboarding.test.ts +451 -4
- package/src/tests/openai-embedding-provider.test.ts +105 -2
- package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
- package/src/tests/preload-env.test.ts +4 -0
- package/src/tests/preload.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +19 -3
- package/src/tests/rbac-admission-e2e.test.ts +22 -6
- package/src/tests/rbac-admission.test.ts +342 -4
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/rbac-roles.test.ts +9 -1
- package/src/tests/send-task-requester-gate.test.ts +64 -3
- package/src/tests/sessions-move.test.ts +39 -1
- package/src/tests/store-progress-learning.test.ts +95 -2
- package/src/tests/whoami-route.test.ts +41 -0
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-search.ts +1 -1
- package/src/tools/store-progress.ts +5 -1
- package/src/tools/utils.ts +12 -1
- package/src/utils/constants.ts +39 -0
- package/src/utils/task-attachment-links.ts +15 -3
- package/templates/skills/swarm-docs/content.md +1 -1
- package/dist/handlers-tzmevqb5.js +0 -41
- package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
- package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
- package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
- package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
- /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
OnboardingStateSchema,
|
|
18
18
|
updateOnboardingAiFromCodexDevice,
|
|
19
19
|
} from "../be/onboarding";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
__resetInjectedEnvTracking,
|
|
22
|
+
_resetAutoReloadForTests,
|
|
23
|
+
handleCore,
|
|
24
|
+
loadGlobalConfigsIntoEnv,
|
|
25
|
+
} from "../http/core";
|
|
21
26
|
import { handleOnboarding } from "../http/onboarding";
|
|
22
27
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
23
28
|
import { telemetry } from "../telemetry";
|
|
@@ -32,12 +37,16 @@ const ENV_KEYS = [
|
|
|
32
37
|
"EMBEDDING_API_KEY",
|
|
33
38
|
"EMBEDDING_API_BASE_URL",
|
|
34
39
|
"EMBEDDING_MODEL",
|
|
40
|
+
"EMBEDDING_QUERY_PREFIX",
|
|
41
|
+
"EMBEDDING_DOCUMENT_PREFIX",
|
|
35
42
|
"GITHUB_TOKEN",
|
|
36
43
|
"GITLAB_TOKEN",
|
|
37
44
|
"BITBUCKET_TOKEN",
|
|
38
45
|
"SLACK_BOT_TOKEN",
|
|
39
46
|
"SLACK_APP_TOKEN",
|
|
40
47
|
"SLACK_MODE",
|
|
48
|
+
"COMB_ENABLED",
|
|
49
|
+
"AGENT_FS_API_URL",
|
|
41
50
|
];
|
|
42
51
|
|
|
43
52
|
let server: Server;
|
|
@@ -176,9 +185,22 @@ function embeddingResponse(dimensions: number): Response {
|
|
|
176
185
|
|
|
177
186
|
describe("onboarding state", () => {
|
|
178
187
|
test("fresh install creates active onboarding state", async () => {
|
|
188
|
+
process.env.COMB_ENABLED = "true";
|
|
189
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
|
|
179
190
|
const response = await request("/api/onboarding");
|
|
180
191
|
expect(response.status).toBe(200);
|
|
181
|
-
const body = (await response.json()) as {
|
|
192
|
+
const body = (await response.json()) as {
|
|
193
|
+
state: OnboardingState;
|
|
194
|
+
signals: { files: { enabled: boolean } };
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
expect(body.signals.files).toEqual({ enabled: true });
|
|
198
|
+
expect(body.state.steps.files).toEqual({
|
|
199
|
+
status: "todo",
|
|
200
|
+
at: null,
|
|
201
|
+
method: null,
|
|
202
|
+
errorClass: null,
|
|
203
|
+
});
|
|
182
204
|
|
|
183
205
|
expect(body.state.autoCompleted).toBe(false);
|
|
184
206
|
expect(body.state.completedAt).toBeNull();
|
|
@@ -235,6 +257,7 @@ describe("onboarding state", () => {
|
|
|
235
257
|
null,
|
|
236
258
|
null,
|
|
237
259
|
null,
|
|
260
|
+
null,
|
|
238
261
|
]);
|
|
239
262
|
});
|
|
240
263
|
|
|
@@ -273,6 +296,39 @@ describe("onboarding state", () => {
|
|
|
273
296
|
expect(capturedTelemetry.filter((entry) => entry.event === "started")).toHaveLength(0);
|
|
274
297
|
});
|
|
275
298
|
|
|
299
|
+
test("a row stored before the files step back-fills it as todo", async () => {
|
|
300
|
+
process.env.COMB_ENABLED = "true";
|
|
301
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
|
|
302
|
+
const state = (await (await request("/api/onboarding")).json()) as {
|
|
303
|
+
state: OnboardingState;
|
|
304
|
+
};
|
|
305
|
+
const stored = structuredClone(state.state) as unknown as {
|
|
306
|
+
startedAt: string;
|
|
307
|
+
steps: Record<string, unknown>;
|
|
308
|
+
};
|
|
309
|
+
stored.steps.integrations = {
|
|
310
|
+
status: "skipped",
|
|
311
|
+
at: stored.startedAt,
|
|
312
|
+
method: null,
|
|
313
|
+
errorClass: null,
|
|
314
|
+
};
|
|
315
|
+
delete stored.steps.files;
|
|
316
|
+
await upsertSwarmConfig({
|
|
317
|
+
scope: "global",
|
|
318
|
+
key: "onboarding_state",
|
|
319
|
+
value: JSON.stringify(stored),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const body = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
|
|
323
|
+
expect(body.state.steps.integrations.status).toBe("skipped");
|
|
324
|
+
expect(body.state.steps.files).toEqual({
|
|
325
|
+
status: "todo",
|
|
326
|
+
at: null,
|
|
327
|
+
method: null,
|
|
328
|
+
errorClass: null,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
276
332
|
test("a user row marks the install as existing", async () => {
|
|
277
333
|
await createUser({ name: "Existing operator" });
|
|
278
334
|
await expectExistingInstall();
|
|
@@ -321,15 +377,69 @@ describe("onboarding state", () => {
|
|
|
321
377
|
test("GET derives the authenticated connection", async () => {
|
|
322
378
|
const body = (await (await request("/api/onboarding")).json()) as {
|
|
323
379
|
state: OnboardingState;
|
|
324
|
-
signals: { providers: unknown[]; embeddings:
|
|
380
|
+
signals: { providers: unknown[]; embeddings: Record<string, unknown> };
|
|
325
381
|
};
|
|
326
382
|
|
|
327
383
|
expect(body.state.steps.connect).toMatchObject({ status: "done", method: "api_key" });
|
|
328
384
|
expect(body.signals.providers).toEqual([]);
|
|
329
|
-
expect(body.signals.embeddings).toEqual({
|
|
385
|
+
expect(body.signals.embeddings).toEqual({
|
|
386
|
+
configured: false,
|
|
387
|
+
dimensions: 512,
|
|
388
|
+
model: null,
|
|
389
|
+
local: false,
|
|
390
|
+
provider: null,
|
|
391
|
+
localDefault: null,
|
|
392
|
+
});
|
|
330
393
|
expect((await getState()).steps.connect.status).toBe("done");
|
|
331
394
|
});
|
|
332
395
|
|
|
396
|
+
test("embedding signals name the model and say whether the endpoint is local", async () => {
|
|
397
|
+
const cases: Array<[string | undefined, boolean, string]> = [
|
|
398
|
+
["http://tei:80/v1", true, "local"],
|
|
399
|
+
["http://localhost:8080/v1", true, "local"],
|
|
400
|
+
["http://127.0.0.1:8091/v1", true, "local"],
|
|
401
|
+
["http://10.0.4.2/v1", true, "local"],
|
|
402
|
+
["http://[::1]:8080/v1", true, "local"],
|
|
403
|
+
["https://api.openai.com/v1", false, "openai"],
|
|
404
|
+
["https://openrouter.ai/api/v1", false, "openrouter"],
|
|
405
|
+
["https://ai-gateway.vercel.sh/v1", false, "vercel"],
|
|
406
|
+
["https://my-resource.services.ai.azure.com/openai/v1", false, "azure"],
|
|
407
|
+
["http://[2001:db8::1]/v1", false, "custom"],
|
|
408
|
+
[undefined, false, "openai"],
|
|
409
|
+
];
|
|
410
|
+
process.env.EMBEDDING_API_KEY = "local-tei";
|
|
411
|
+
process.env.EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
412
|
+
for (const [baseUrl, local, provider] of cases) {
|
|
413
|
+
if (baseUrl === undefined) delete process.env.EMBEDDING_API_BASE_URL;
|
|
414
|
+
else process.env.EMBEDDING_API_BASE_URL = baseUrl;
|
|
415
|
+
const body = (await (await request("/api/onboarding")).json()) as {
|
|
416
|
+
signals: { embeddings: Record<string, unknown> };
|
|
417
|
+
};
|
|
418
|
+
expect({ baseUrl, embeddings: body.signals.embeddings }).toEqual({
|
|
419
|
+
baseUrl,
|
|
420
|
+
embeddings: {
|
|
421
|
+
configured: true,
|
|
422
|
+
dimensions: 512,
|
|
423
|
+
model: "nomic-ai/nomic-embed-text-v1.5",
|
|
424
|
+
local,
|
|
425
|
+
provider,
|
|
426
|
+
// The environment is the deployment's: a local endpoint is its local default.
|
|
427
|
+
localDefault: local ? { model: "nomic-ai/nomic-embed-text-v1.5" } : null,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
// The signal never carries the key or the URL.
|
|
431
|
+
expect(JSON.stringify(body.signals.embeddings)).not.toContain("local-tei");
|
|
432
|
+
if (baseUrl) expect(JSON.stringify(body.signals.embeddings)).not.toContain(baseUrl);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// No model set: the provider's default model.
|
|
436
|
+
delete process.env.EMBEDDING_MODEL;
|
|
437
|
+
const fallback = (await (await request("/api/onboarding")).json()) as {
|
|
438
|
+
signals: { embeddings: { model: string | null } };
|
|
439
|
+
};
|
|
440
|
+
expect(fallback.signals.embeddings.model).toBe("text-embedding-3-small");
|
|
441
|
+
});
|
|
442
|
+
|
|
333
443
|
test("agent signals count idle or busy agents as ready", async () => {
|
|
334
444
|
await createAgent({ name: "ready-lead", isLead: true, status: "idle" });
|
|
335
445
|
await createAgent({ name: "busy-worker", isLead: false, status: "busy" });
|
|
@@ -580,7 +690,67 @@ describe("onboarding state", () => {
|
|
|
580
690
|
).toBe(404);
|
|
581
691
|
});
|
|
582
692
|
|
|
693
|
+
test("without Comb the files step reads as skipped, unpersisted and with no event", async () => {
|
|
694
|
+
const first = (await (await request("/api/onboarding")).json()) as {
|
|
695
|
+
state: OnboardingState;
|
|
696
|
+
signals: { files: { enabled: boolean } };
|
|
697
|
+
};
|
|
698
|
+
expect(first.signals.files).toEqual({ enabled: false });
|
|
699
|
+
expect(first.state.steps.files.status).toBe("skipped");
|
|
700
|
+
// A write for another step persists the row: files must stay todo in storage.
|
|
701
|
+
expect((await put({ action: "skip", step: "name" })).status).toBe(200);
|
|
702
|
+
expect((await getState()).steps.files.status).toBe("todo");
|
|
703
|
+
await Bun.sleep(10);
|
|
704
|
+
expect(
|
|
705
|
+
capturedTelemetry.filter(
|
|
706
|
+
(entry) => entry.event === "step_skipped" && entry.properties.step === "files",
|
|
707
|
+
),
|
|
708
|
+
).toHaveLength(0);
|
|
709
|
+
|
|
710
|
+
// Turning Comb on brings the step back.
|
|
711
|
+
process.env.COMB_ENABLED = "true";
|
|
712
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
|
|
713
|
+
const after = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
|
|
714
|
+
expect(after.state.steps.files.status).toBe("todo");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("PUT completes the files step with created or connected", async () => {
|
|
718
|
+
process.env.COMB_ENABLED = "true";
|
|
719
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
|
|
720
|
+
await request("/api/onboarding");
|
|
721
|
+
capturedTelemetry.length = 0;
|
|
722
|
+
|
|
723
|
+
const created = await put({ action: "complete", step: "files", method: "created" });
|
|
724
|
+
expect(created.status).toBe(200);
|
|
725
|
+
expect(((await created.json()) as { state: OnboardingState }).state.steps.files).toMatchObject({
|
|
726
|
+
status: "done",
|
|
727
|
+
method: "created",
|
|
728
|
+
});
|
|
729
|
+
await Bun.sleep(10);
|
|
730
|
+
expect(capturedTelemetry).toContainEqual(
|
|
731
|
+
expect.objectContaining({
|
|
732
|
+
event: "step_completed",
|
|
733
|
+
properties: expect.objectContaining({ step: "files", method: "created" }),
|
|
734
|
+
}),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const connected = await put({ action: "complete", step: "files", method: "connected" });
|
|
738
|
+
expect(connected.status).toBe(200);
|
|
739
|
+
expect((await getState()).steps.files).toMatchObject({ status: "done", method: "connected" });
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("a completed files step stays done when Comb is off", async () => {
|
|
743
|
+
process.env.COMB_ENABLED = "true";
|
|
744
|
+
process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
|
|
745
|
+
await put({ action: "complete", step: "files", method: "connected" });
|
|
746
|
+
delete process.env.COMB_ENABLED;
|
|
747
|
+
const body = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
|
|
748
|
+
expect(body.state.steps.files).toMatchObject({ status: "done", method: "connected" });
|
|
749
|
+
});
|
|
750
|
+
|
|
583
751
|
test("PUT rejects a bad completion method and connect skip", async () => {
|
|
752
|
+
expect((await put({ action: "complete", step: "files", method: "pasted" })).status).toBe(400);
|
|
753
|
+
expect((await put({ action: "complete", step: "files", method: "api_key" })).status).toBe(400);
|
|
584
754
|
expect((await put({ action: "complete", step: "ai", method: "not-a-provider" })).status).toBe(
|
|
585
755
|
400,
|
|
586
756
|
);
|
|
@@ -1044,6 +1214,68 @@ describe("onboarding memory probe", () => {
|
|
|
1044
1214
|
}
|
|
1045
1215
|
});
|
|
1046
1216
|
|
|
1217
|
+
test("existing preset probes a private configured endpoint outside local development", async () => {
|
|
1218
|
+
let hits = 0;
|
|
1219
|
+
const fake = startEmbeddingServer(() => {
|
|
1220
|
+
hits += 1;
|
|
1221
|
+
return embeddingResponse(512);
|
|
1222
|
+
});
|
|
1223
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
1224
|
+
process.env.NODE_ENV = "production";
|
|
1225
|
+
process.env.EMBEDDING_API_BASE_URL = fake.baseUrl;
|
|
1226
|
+
process.env.EMBEDDING_MODEL = "test-embedding-model";
|
|
1227
|
+
process.env.EMBEDDING_API_KEY = "local-tei";
|
|
1228
|
+
try {
|
|
1229
|
+
const existing = await request("/api/onboarding/memory", {
|
|
1230
|
+
method: "POST",
|
|
1231
|
+
body: { preset: "existing" },
|
|
1232
|
+
});
|
|
1233
|
+
expect(await existing.json()).toMatchObject({ ok: true, dimensions: 512 });
|
|
1234
|
+
expect(hits).toBe(1);
|
|
1235
|
+
|
|
1236
|
+
// The same private URL from the request body is still blocked.
|
|
1237
|
+
const custom = await request("/api/onboarding/memory", {
|
|
1238
|
+
method: "POST",
|
|
1239
|
+
body: {
|
|
1240
|
+
preset: "custom",
|
|
1241
|
+
baseUrl: fake.baseUrl,
|
|
1242
|
+
model: "test-embedding-model",
|
|
1243
|
+
apiKey: "local-tei",
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
expect(await custom.json()).toMatchObject({
|
|
1247
|
+
ok: false,
|
|
1248
|
+
errorClass: "network",
|
|
1249
|
+
error: "The endpoint URL is not allowed.",
|
|
1250
|
+
});
|
|
1251
|
+
expect(hits).toBe(1);
|
|
1252
|
+
} finally {
|
|
1253
|
+
fake.server.stop(true);
|
|
1254
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
1255
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
test("existing preset with no configured endpoint keeps the SSRF check", async () => {
|
|
1260
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
1261
|
+
process.env.NODE_ENV = "production";
|
|
1262
|
+
process.env.EMBEDDING_API_KEY = "local-tei";
|
|
1263
|
+
try {
|
|
1264
|
+
const response = await request("/api/onboarding/memory", {
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
body: { preset: "existing", baseUrl: "http://127.0.0.1:9/v1" },
|
|
1267
|
+
});
|
|
1268
|
+
expect(await response.json()).toMatchObject({
|
|
1269
|
+
ok: false,
|
|
1270
|
+
errorClass: "network",
|
|
1271
|
+
error: "The endpoint URL is not allowed.",
|
|
1272
|
+
});
|
|
1273
|
+
} finally {
|
|
1274
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
1275
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1047
1279
|
test("successful memory retry records the successful preset after a failure", async () => {
|
|
1048
1280
|
let dimensions = 512;
|
|
1049
1281
|
const fake = startEmbeddingServer(() => embeddingResponse(dimensions));
|
|
@@ -1090,3 +1322,218 @@ describe("onboarding memory probe", () => {
|
|
|
1090
1322
|
});
|
|
1091
1323
|
});
|
|
1092
1324
|
});
|
|
1325
|
+
|
|
1326
|
+
describe("onboarding memory: the deployment's local setup", () => {
|
|
1327
|
+
const NOMIC = "nomic-ai/nomic-embed-text-v1.5";
|
|
1328
|
+
|
|
1329
|
+
/** The bundled install: a local endpoint, its model, a key, and nomic's task prefixes. */
|
|
1330
|
+
function setDeploymentEnv(localUrl: string) {
|
|
1331
|
+
process.env.EMBEDDING_API_BASE_URL = localUrl;
|
|
1332
|
+
process.env.EMBEDDING_MODEL = NOMIC;
|
|
1333
|
+
process.env.EMBEDDING_API_KEY = "local-tei";
|
|
1334
|
+
process.env.EMBEDDING_QUERY_PREFIX = "search_query: ";
|
|
1335
|
+
process.env.EMBEDDING_DOCUMENT_PREFIX = "search_document: ";
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async function storedEmbeddingRows(): Promise<Record<string, string>> {
|
|
1339
|
+
const rows = await getSwarmConfigs({ scope: "global" });
|
|
1340
|
+
return Object.fromEntries(
|
|
1341
|
+
rows.filter((row) => row.key.startsWith("EMBEDDING_")).map((row) => [row.key, row.value]),
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
async function embeddingSignals(): Promise<Record<string, unknown>> {
|
|
1346
|
+
const body = (await (await request("/api/onboarding")).json()) as {
|
|
1347
|
+
signals: { embeddings: Record<string, unknown> };
|
|
1348
|
+
};
|
|
1349
|
+
return body.signals.embeddings;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
test("a hosted save stores empty prefixes over the local model's prefixes", async () => {
|
|
1353
|
+
const hosted = startEmbeddingServer(() => embeddingResponse(512));
|
|
1354
|
+
setDeploymentEnv("http://tei:80/v1");
|
|
1355
|
+
try {
|
|
1356
|
+
const response = await request("/api/onboarding/memory", {
|
|
1357
|
+
method: "POST",
|
|
1358
|
+
body: {
|
|
1359
|
+
preset: "custom",
|
|
1360
|
+
baseUrl: hosted.baseUrl,
|
|
1361
|
+
model: "test-embedding-model",
|
|
1362
|
+
apiKey: "example-hosted-key",
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
expect(await response.json()).toMatchObject({ ok: true });
|
|
1366
|
+
expect(await storedEmbeddingRows()).toEqual({
|
|
1367
|
+
EMBEDDING_API_BASE_URL: hosted.baseUrl,
|
|
1368
|
+
EMBEDDING_MODEL: "test-embedding-model",
|
|
1369
|
+
EMBEDDING_API_KEY: "example-hosted-key",
|
|
1370
|
+
EMBEDDING_QUERY_PREFIX: "",
|
|
1371
|
+
EMBEDDING_DOCUMENT_PREFIX: "",
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
// The deployment's own model keeps its prefixes: the empty rows go.
|
|
1375
|
+
const same = await request("/api/onboarding/memory", {
|
|
1376
|
+
method: "POST",
|
|
1377
|
+
body: { preset: "custom", baseUrl: hosted.baseUrl, model: NOMIC, apiKey: "example-key" },
|
|
1378
|
+
});
|
|
1379
|
+
expect(await same.json()).toMatchObject({ ok: true });
|
|
1380
|
+
const rows = await storedEmbeddingRows();
|
|
1381
|
+
expect(rows.EMBEDDING_QUERY_PREFIX).toBeUndefined();
|
|
1382
|
+
expect(rows.EMBEDDING_DOCUMENT_PREFIX).toBeUndefined();
|
|
1383
|
+
} finally {
|
|
1384
|
+
hosted.server.stop(true);
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
test("without deployment prefixes a hosted save stores none", async () => {
|
|
1389
|
+
const hosted = startEmbeddingServer(() => embeddingResponse(512));
|
|
1390
|
+
try {
|
|
1391
|
+
const response = await request("/api/onboarding/memory", {
|
|
1392
|
+
method: "POST",
|
|
1393
|
+
body: {
|
|
1394
|
+
preset: "custom",
|
|
1395
|
+
baseUrl: hosted.baseUrl,
|
|
1396
|
+
model: "test-embedding-model",
|
|
1397
|
+
apiKey: "example-hosted-key",
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
expect(await response.json()).toMatchObject({ ok: true });
|
|
1401
|
+
expect(Object.keys(await storedEmbeddingRows()).sort()).toEqual([
|
|
1402
|
+
"EMBEDDING_API_BASE_URL",
|
|
1403
|
+
"EMBEDDING_API_KEY",
|
|
1404
|
+
"EMBEDDING_MODEL",
|
|
1405
|
+
]);
|
|
1406
|
+
} finally {
|
|
1407
|
+
hosted.server.stop(true);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
test("a stored hosted setup keeps the local default, and restore brings it back", async () => {
|
|
1412
|
+
let localHits = 0;
|
|
1413
|
+
const local = startEmbeddingServer(() => {
|
|
1414
|
+
localHits += 1;
|
|
1415
|
+
return embeddingResponse(512);
|
|
1416
|
+
});
|
|
1417
|
+
setDeploymentEnv(local.baseUrl);
|
|
1418
|
+
try {
|
|
1419
|
+
// A hosted setup stored earlier, applied as the boot or a reload does.
|
|
1420
|
+
await upsertSwarmConfig({
|
|
1421
|
+
scope: "global",
|
|
1422
|
+
key: "EMBEDDING_API_BASE_URL",
|
|
1423
|
+
value: "https://openrouter.ai/api/v1",
|
|
1424
|
+
});
|
|
1425
|
+
await upsertSwarmConfig({
|
|
1426
|
+
scope: "global",
|
|
1427
|
+
key: "EMBEDDING_MODEL",
|
|
1428
|
+
value: "google/gemini-embedding-2",
|
|
1429
|
+
});
|
|
1430
|
+
await upsertSwarmConfig({
|
|
1431
|
+
scope: "global",
|
|
1432
|
+
key: "EMBEDDING_API_KEY",
|
|
1433
|
+
value: "example-openrouter-key",
|
|
1434
|
+
isSecret: true,
|
|
1435
|
+
});
|
|
1436
|
+
await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_QUERY_PREFIX", value: "" });
|
|
1437
|
+
await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_DOCUMENT_PREFIX", value: "" });
|
|
1438
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
1439
|
+
resetEmbeddingProvider();
|
|
1440
|
+
|
|
1441
|
+
expect(await embeddingSignals()).toEqual({
|
|
1442
|
+
configured: true,
|
|
1443
|
+
dimensions: 512,
|
|
1444
|
+
model: "google/gemini-embedding-2",
|
|
1445
|
+
local: false,
|
|
1446
|
+
provider: "openrouter",
|
|
1447
|
+
localDefault: { model: NOMIC },
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
const response = await request("/api/onboarding/memory", {
|
|
1451
|
+
method: "POST",
|
|
1452
|
+
body: { preset: "existing", restoreDefault: true },
|
|
1453
|
+
});
|
|
1454
|
+
expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
|
|
1455
|
+
// It tested the deployment's endpoint, not the stored one.
|
|
1456
|
+
expect(localHits).toBe(1);
|
|
1457
|
+
expect(await storedEmbeddingRows()).toEqual({});
|
|
1458
|
+
expect(process.env.EMBEDDING_API_BASE_URL).toBe(local.baseUrl);
|
|
1459
|
+
expect(process.env.EMBEDDING_QUERY_PREFIX).toBe("search_query: ");
|
|
1460
|
+
expect(await embeddingSignals()).toMatchObject({
|
|
1461
|
+
model: NOMIC,
|
|
1462
|
+
local: true,
|
|
1463
|
+
provider: "local",
|
|
1464
|
+
localDefault: { model: NOMIC },
|
|
1465
|
+
});
|
|
1466
|
+
expect((await getState()).steps.memory).toMatchObject({ status: "done", method: "existing" });
|
|
1467
|
+
} finally {
|
|
1468
|
+
local.server.stop(true);
|
|
1469
|
+
__resetInjectedEnvTracking();
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
test("a stored private endpoint keeps the SSRF check", async () => {
|
|
1474
|
+
let hits = 0;
|
|
1475
|
+
const stored = startEmbeddingServer(() => {
|
|
1476
|
+
hits += 1;
|
|
1477
|
+
return embeddingResponse(512);
|
|
1478
|
+
});
|
|
1479
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
1480
|
+
process.env.NODE_ENV = "production";
|
|
1481
|
+
setDeploymentEnv("https://embeddings.example.com/v1");
|
|
1482
|
+
try {
|
|
1483
|
+
// A config writer stores a loopback URL over the deployment's endpoint.
|
|
1484
|
+
await upsertSwarmConfig({
|
|
1485
|
+
scope: "global",
|
|
1486
|
+
key: "EMBEDDING_API_BASE_URL",
|
|
1487
|
+
value: stored.baseUrl,
|
|
1488
|
+
});
|
|
1489
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
1490
|
+
expect(process.env.EMBEDDING_API_BASE_URL).toBe(stored.baseUrl);
|
|
1491
|
+
const response = await request("/api/onboarding/memory", {
|
|
1492
|
+
method: "POST",
|
|
1493
|
+
body: { preset: "existing" },
|
|
1494
|
+
});
|
|
1495
|
+
expect(await response.json()).toMatchObject({
|
|
1496
|
+
ok: false,
|
|
1497
|
+
errorClass: "network",
|
|
1498
|
+
error: "The endpoint URL is not allowed.",
|
|
1499
|
+
});
|
|
1500
|
+
expect(hits).toBe(0);
|
|
1501
|
+
} finally {
|
|
1502
|
+
stored.server.stop(true);
|
|
1503
|
+
__resetInjectedEnvTracking();
|
|
1504
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
1505
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
test("a failed restore test keeps the stored setup", async () => {
|
|
1510
|
+
const local = startEmbeddingServer(() => new Response("model loading", { status: 503 }));
|
|
1511
|
+
setDeploymentEnv(local.baseUrl);
|
|
1512
|
+
try {
|
|
1513
|
+
await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_MODEL", value: "stored-model" });
|
|
1514
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
1515
|
+
const response = await request("/api/onboarding/memory", {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
body: { preset: "existing", restoreDefault: true },
|
|
1518
|
+
});
|
|
1519
|
+
expect(await response.json()).toMatchObject({ ok: false });
|
|
1520
|
+
expect(await storedEmbeddingRows()).toEqual({ EMBEDDING_MODEL: "stored-model" });
|
|
1521
|
+
expect(process.env.EMBEDDING_MODEL).toBe("stored-model");
|
|
1522
|
+
} finally {
|
|
1523
|
+
local.server.stop(true);
|
|
1524
|
+
__resetInjectedEnvTracking();
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
test("restoreDefault needs the existing preset", async () => {
|
|
1529
|
+
const response = await request("/api/onboarding/memory", {
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
body: {
|
|
1532
|
+
preset: "custom",
|
|
1533
|
+
baseUrl: "https://embeddings.example.com/v1",
|
|
1534
|
+
restoreDefault: true,
|
|
1535
|
+
},
|
|
1536
|
+
});
|
|
1537
|
+
expect(response.status).toBe(400);
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
MAX_EMBEDDING_INPUT_CHARS,
|
|
4
|
+
OpenAIEmbeddingProvider,
|
|
5
|
+
} from "../be/memory/providers/openai-embedding";
|
|
3
6
|
|
|
4
7
|
// A gateway that ignores `encoding_format` and always answers with plain floats.
|
|
5
8
|
let server: ReturnType<typeof Bun.serve>;
|
|
6
9
|
const bodies: Array<Record<string, unknown>> = [];
|
|
7
10
|
const previousBaseUrl = process.env.EMBEDDING_API_BASE_URL;
|
|
11
|
+
const PREFIX_ENVS = ["EMBEDDING_QUERY_PREFIX", "EMBEDDING_DOCUMENT_PREFIX"] as const;
|
|
12
|
+
const previousPrefixes = new Map(PREFIX_ENVS.map((key) => [key, process.env[key]]));
|
|
8
13
|
|
|
9
14
|
beforeAll(() => {
|
|
10
15
|
server = Bun.serve({
|
|
@@ -35,6 +40,36 @@ afterAll(() => {
|
|
|
35
40
|
else process.env.EMBEDDING_API_BASE_URL = previousBaseUrl;
|
|
36
41
|
});
|
|
37
42
|
|
|
43
|
+
function clearPrefixes(): void {
|
|
44
|
+
for (const key of PREFIX_ENVS) delete process.env[key];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
bodies.length = 0;
|
|
49
|
+
clearPrefixes();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
clearPrefixes();
|
|
54
|
+
for (const [key, value] of previousPrefixes) {
|
|
55
|
+
if (value !== undefined) process.env[key] = value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function makeProvider(): OpenAIEmbeddingProvider {
|
|
60
|
+
return new OpenAIEmbeddingProvider({
|
|
61
|
+
apiKey: "test-key",
|
|
62
|
+
model: "test-embedding-model",
|
|
63
|
+
dimensions: 4,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sentInputs(): string[] {
|
|
68
|
+
return bodies.flatMap((body) =>
|
|
69
|
+
Array.isArray(body.input) ? body.input : [body.input],
|
|
70
|
+
) as string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
describe("OpenAIEmbeddingProvider", () => {
|
|
39
74
|
test("requests float vectors, so a plain-float reply decodes correctly", async () => {
|
|
40
75
|
const provider = new OpenAIEmbeddingProvider({
|
|
@@ -53,4 +88,72 @@ describe("OpenAIEmbeddingProvider", () => {
|
|
|
53
88
|
[0.25, 0.5, 0.75, 1],
|
|
54
89
|
]);
|
|
55
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("task prefixes", () => {
|
|
93
|
+
function setNomicPrefixes(): void {
|
|
94
|
+
process.env.EMBEDDING_QUERY_PREFIX = "search_query: ";
|
|
95
|
+
process.env.EMBEDDING_DOCUMENT_PREFIX = "search_document: ";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("kind query gets the query prefix", async () => {
|
|
99
|
+
setNomicPrefixes();
|
|
100
|
+
const provider = makeProvider();
|
|
101
|
+
await provider.embed("who owns billing?", { kind: "query" });
|
|
102
|
+
await provider.embedBatch(["a", "b"], { kind: "query" });
|
|
103
|
+
|
|
104
|
+
expect(sentInputs()).toEqual([
|
|
105
|
+
"search_query: who owns billing?",
|
|
106
|
+
"search_query: a",
|
|
107
|
+
"search_query: b",
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("kind document gets the document prefix", async () => {
|
|
112
|
+
setNomicPrefixes();
|
|
113
|
+
const provider = makeProvider();
|
|
114
|
+
await provider.embed("billing is owned by Ada", { kind: "document" });
|
|
115
|
+
await provider.embedBatch(["a", "", "b"], { kind: "document" });
|
|
116
|
+
|
|
117
|
+
// An empty input is skipped, never embedded as a bare prefix.
|
|
118
|
+
expect(sentInputs()).toEqual([
|
|
119
|
+
"search_document: billing is owned by Ada",
|
|
120
|
+
"search_document: a",
|
|
121
|
+
"search_document: b",
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("no prefix when the envs are unset", async () => {
|
|
126
|
+
const provider = makeProvider();
|
|
127
|
+
await provider.embed("hello", { kind: "query" });
|
|
128
|
+
await provider.embedBatch(["a"], { kind: "document" });
|
|
129
|
+
|
|
130
|
+
expect(sentInputs()).toEqual(["hello", "a"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("no prefix without a kind", async () => {
|
|
134
|
+
setNomicPrefixes();
|
|
135
|
+
const provider = makeProvider();
|
|
136
|
+
await provider.embed("hello");
|
|
137
|
+
await provider.embedBatch(["a"]);
|
|
138
|
+
|
|
139
|
+
expect(sentInputs()).toEqual(["hello", "a"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("the prefixes are read when the provider is constructed", async () => {
|
|
143
|
+
const provider = makeProvider();
|
|
144
|
+
setNomicPrefixes();
|
|
145
|
+
await provider.embed("hello", { kind: "query" });
|
|
146
|
+
|
|
147
|
+
expect(sentInputs()).toEqual(["hello"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("prefix plus input stays within the char cap", async () => {
|
|
151
|
+
setNomicPrefixes();
|
|
152
|
+
await makeProvider().embed("x".repeat(MAX_EMBEDDING_INPUT_CHARS + 500), { kind: "document" });
|
|
153
|
+
|
|
154
|
+
const [sent] = sentInputs();
|
|
155
|
+
expect(sent!.startsWith("search_document: x")).toBe(true);
|
|
156
|
+
expect(sent!.length).toBe(MAX_EMBEDDING_INPUT_CHARS);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
56
159
|
});
|