@desplega.ai/agent-swarm 2.4.3 → 2.4.5
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-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
- package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
- package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
- package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
- package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
- package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
- package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
- package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
- package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
- package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
- package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
- package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +45 -20
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
- package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
- package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +303 -13
- package/package.json +8 -8
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +209 -4
- package/src/apps/page-validator.ts +21 -3
- package/src/be/chunking.ts +42 -9
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +14 -1
- package/src/be/db.ts +5 -1
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +40 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +7 -0
- package/src/fs/provider.ts +13 -1
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/fs.ts +54 -34
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/http/users.ts +12 -1
- package/src/integrations/chat-sdk/adapters.ts +7 -3
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +120 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +234 -4
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.ts +4 -4
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +558 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +265 -1
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/swarm-config-export-to-env.test.ts +22 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/types.ts +8 -1
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/request-auth-context.ts +2 -1
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +84 -262
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +316 -114
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +31 -7
- package/templates/skills/apps/content.md +31 -7
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +8 -8
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
- package/dist/index-d0g18bax.js +0 -41
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isTranscriptionConfigured,
|
|
4
|
+
NOT_TRANSCRIBED_NOTE,
|
|
5
|
+
resolveTranscriptionConfig,
|
|
6
|
+
TRANSCRIPTION_ENV_KEYS,
|
|
7
|
+
TranscriptionError,
|
|
8
|
+
transcribeAudio,
|
|
9
|
+
transcribeAudioNotes,
|
|
10
|
+
} from "../utils/transcription";
|
|
11
|
+
|
|
12
|
+
// Includes the provider keys the fallback reads: a worker shell often has
|
|
13
|
+
// OPENAI_API_KEY or ELEVENLABS_API_KEY set, which would leak into every test.
|
|
14
|
+
const ENV_KEYS = TRANSCRIPTION_ENV_KEYS;
|
|
15
|
+
const savedEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
16
|
+
|
|
17
|
+
type Captured = {
|
|
18
|
+
path: string;
|
|
19
|
+
authorization: string | null;
|
|
20
|
+
xiApiKey: string | null;
|
|
21
|
+
fields: string[];
|
|
22
|
+
model: FormDataEntryValue | null;
|
|
23
|
+
modelId: FormDataEntryValue | null;
|
|
24
|
+
filename: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const captured: Captured[] = [];
|
|
28
|
+
// The file part's Content-Type as sent. Bun's `formData()` re-derives a File's
|
|
29
|
+
// type from its extension (`.webm` reads back as video/webm), so read it raw.
|
|
30
|
+
const sentFileTypes: string[] = [];
|
|
31
|
+
let reply: () => Response = () => Response.json({ text: " hello " });
|
|
32
|
+
let stt: ReturnType<typeof Bun.serve>;
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
36
|
+
stt = Bun.serve({
|
|
37
|
+
port: 0,
|
|
38
|
+
hostname: "127.0.0.1",
|
|
39
|
+
async fetch(req) {
|
|
40
|
+
const raw = await req.clone().text();
|
|
41
|
+
sentFileTypes.push(
|
|
42
|
+
/name="file"; filename="[^"]*"\r\nContent-Type: ([^\r\n]+)/.exec(raw)?.[1] ?? "",
|
|
43
|
+
);
|
|
44
|
+
const form = await req.formData();
|
|
45
|
+
captured.push({
|
|
46
|
+
path: new URL(req.url).pathname,
|
|
47
|
+
authorization: req.headers.get("authorization"),
|
|
48
|
+
xiApiKey: req.headers.get("xi-api-key"),
|
|
49
|
+
fields: [...form.keys()].sort(),
|
|
50
|
+
model: form.get("model"),
|
|
51
|
+
modelId: form.get("model_id"),
|
|
52
|
+
filename: (form.get("file") as File).name,
|
|
53
|
+
});
|
|
54
|
+
return reply();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
stt.stop(true);
|
|
61
|
+
for (const [key, value] of savedEnv) {
|
|
62
|
+
if (value === undefined) delete process.env[key];
|
|
63
|
+
else process.env[key] = value;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
captured.length = 0;
|
|
69
|
+
sentFileTypes.length = 0;
|
|
70
|
+
reply = () => Response.json({ text: " hello " });
|
|
71
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function configure(provider?: string) {
|
|
75
|
+
process.env.TRANSCRIPTION_API_KEY = "stt-test-key-123456";
|
|
76
|
+
process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
|
|
77
|
+
if (provider) process.env.TRANSCRIPTION_PROVIDER = provider;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const audio = {
|
|
81
|
+
data: new Blob([new Uint8Array([1, 2, 3])]),
|
|
82
|
+
filename: "note.ogg",
|
|
83
|
+
mimeType: "audio/ogg",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe("transcribeAudio request shape", () => {
|
|
87
|
+
test("elevenlabs posts to /speech-to-text with xi-api-key and model_id", async () => {
|
|
88
|
+
configure("elevenlabs");
|
|
89
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
90
|
+
expect(captured).toEqual([
|
|
91
|
+
{
|
|
92
|
+
path: "/v1/speech-to-text",
|
|
93
|
+
authorization: null,
|
|
94
|
+
xiApiKey: "stt-test-key-123456",
|
|
95
|
+
fields: ["file", "model_id"],
|
|
96
|
+
model: null,
|
|
97
|
+
modelId: "scribe_v2",
|
|
98
|
+
filename: "note.ogg",
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("elevenlabs honours TRANSCRIPTION_MODEL in model_id", async () => {
|
|
104
|
+
configure("ElevenLabs");
|
|
105
|
+
process.env.TRANSCRIPTION_MODEL = "scribe_v1";
|
|
106
|
+
await transcribeAudio(audio);
|
|
107
|
+
expect(captured[0]).toMatchObject({ fields: ["file", "model_id"], modelId: "scribe_v1" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("negative control: the default provider keeps the OpenAI shape", async () => {
|
|
111
|
+
configure();
|
|
112
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
113
|
+
expect(captured).toEqual([
|
|
114
|
+
{
|
|
115
|
+
path: "/v1/audio/transcriptions",
|
|
116
|
+
authorization: "Bearer stt-test-key-123456",
|
|
117
|
+
xiApiKey: null,
|
|
118
|
+
fields: ["file", "model"],
|
|
119
|
+
model: "whisper-1",
|
|
120
|
+
modelId: null,
|
|
121
|
+
filename: "note.ogg",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("an unknown provider fails before any request", async () => {
|
|
127
|
+
configure("deepgram");
|
|
128
|
+
await expect(transcribeAudio(audio)).rejects.toThrow(
|
|
129
|
+
new TranscriptionError(
|
|
130
|
+
'TRANSCRIPTION_PROVIDER "deepgram" is not supported (use openai, groq, elevenlabs)',
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
expect(captured).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an elevenlabs error detail is surfaced with the key scrubbed", async () => {
|
|
137
|
+
configure("elevenlabs");
|
|
138
|
+
reply = () =>
|
|
139
|
+
Response.json(
|
|
140
|
+
{ detail: { status: "invalid_api_key", message: "Invalid key stt-test-key-123456" } },
|
|
141
|
+
{ status: 401 },
|
|
142
|
+
);
|
|
143
|
+
await expect(transcribeAudio(audio)).rejects.toThrow(
|
|
144
|
+
"Invalid key [REDACTED:TRANSCRIPTION_API_KEY]",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("provider key fallback", () => {
|
|
150
|
+
// Built at runtime so the secret scanner does not read them as keys.
|
|
151
|
+
const fake = (provider: string) => [provider, "fixture", "value"].join("-");
|
|
152
|
+
const ELEVEN_FAKE = fake("eleven");
|
|
153
|
+
const OPENAI_FAKE = fake("openai");
|
|
154
|
+
const GROQ_FAKE = fake("groq");
|
|
155
|
+
|
|
156
|
+
// Auto-detected keys always go to the provider's public endpoint, so record
|
|
157
|
+
// that URL and forward the request to the local stub on the same path.
|
|
158
|
+
const realFetch = globalThis.fetch;
|
|
159
|
+
const calledUrls: string[] = [];
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
calledUrls.length = 0;
|
|
162
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
163
|
+
const url = new URL(String(input));
|
|
164
|
+
calledUrls.push(url.href);
|
|
165
|
+
return realFetch(`http://127.0.0.1:${stt.port}${url.pathname}`, init);
|
|
166
|
+
}) as typeof fetch;
|
|
167
|
+
});
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
globalThis.fetch = realFetch;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("no key at all means not configured", async () => {
|
|
173
|
+
expect(resolveTranscriptionConfig()).toBeNull();
|
|
174
|
+
expect(isTranscriptionConfigured()).toBe(false);
|
|
175
|
+
const notes = await transcribeAudioNotes([{ key: "a", name: "a.ogg", data: audio.data }], "t");
|
|
176
|
+
expect(notes.get("a")).toBe(NOT_TRANSCRIBED_NOTE);
|
|
177
|
+
expect(NOT_TRANSCRIBED_NOTE).toBe(
|
|
178
|
+
"(not transcribed: no transcription provider configured, set TRANSCRIPTION_API_KEY or one of ELEVENLABS_API_KEY, OPENAI_API_KEY, GROQ_API_KEY)",
|
|
179
|
+
);
|
|
180
|
+
expect(calledUrls).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("ELEVENLABS_API_KEY alone uses the elevenlabs shape", async () => {
|
|
184
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
185
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
186
|
+
provider: "elevenlabs",
|
|
187
|
+
keyEnv: "ELEVENLABS_API_KEY",
|
|
188
|
+
source: "auto",
|
|
189
|
+
});
|
|
190
|
+
expect(await transcribeAudio(audio)).toBe("hello");
|
|
191
|
+
expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
|
|
192
|
+
expect(captured[0]).toMatchObject({
|
|
193
|
+
xiApiKey: ELEVEN_FAKE,
|
|
194
|
+
authorization: null,
|
|
195
|
+
modelId: "scribe_v2",
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("OPENAI_API_KEY alone uses the openai shape", async () => {
|
|
200
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
201
|
+
expect(resolveTranscriptionConfig()).toMatchObject({ provider: "openai", source: "auto" });
|
|
202
|
+
await transcribeAudio(audio);
|
|
203
|
+
expect(calledUrls).toEqual(["https://api.openai.com/v1/audio/transcriptions"]);
|
|
204
|
+
expect(captured[0]).toMatchObject({
|
|
205
|
+
authorization: `Bearer ${OPENAI_FAKE}`,
|
|
206
|
+
model: "whisper-1",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("GROQ_API_KEY alone uses Groq's OpenAI-compatible endpoint and whisper model", async () => {
|
|
211
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
212
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
213
|
+
provider: "groq",
|
|
214
|
+
keyEnv: "GROQ_API_KEY",
|
|
215
|
+
});
|
|
216
|
+
await transcribeAudio(audio);
|
|
217
|
+
expect(calledUrls).toEqual(["https://api.groq.com/openai/v1/audio/transcriptions"]);
|
|
218
|
+
expect(captured[0]).toMatchObject({
|
|
219
|
+
authorization: `Bearer ${GROQ_FAKE}`,
|
|
220
|
+
model: "whisper-large-v3-turbo",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("ElevenLabs wins over OpenAI and Groq when all are set", async () => {
|
|
225
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
226
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
227
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
228
|
+
await transcribeAudio(audio);
|
|
229
|
+
expect(captured[0]).toMatchObject({ xiApiKey: ELEVEN_FAKE, authorization: null });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("OpenAI wins over Groq", () => {
|
|
233
|
+
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
234
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
235
|
+
expect(resolveTranscriptionConfig()?.provider).toBe("openai");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("an explicit TRANSCRIPTION_API_KEY overrides both detected keys", async () => {
|
|
239
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
240
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
241
|
+
configure();
|
|
242
|
+
expect(resolveTranscriptionConfig()).toMatchObject({
|
|
243
|
+
provider: "openai",
|
|
244
|
+
keyEnv: "TRANSCRIPTION_API_KEY",
|
|
245
|
+
source: "explicit",
|
|
246
|
+
});
|
|
247
|
+
await transcribeAudio(audio);
|
|
248
|
+
// The explicit key honours TRANSCRIPTION_BASE_URL.
|
|
249
|
+
expect(calledUrls).toEqual([`http://127.0.0.1:${stt.port}/v1/audio/transcriptions`]);
|
|
250
|
+
expect(captured[0]).toMatchObject({
|
|
251
|
+
authorization: "Bearer stt-test-key-123456",
|
|
252
|
+
xiApiKey: null,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("an auto-detected key never goes to TRANSCRIPTION_BASE_URL", async () => {
|
|
257
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
258
|
+
process.env.TRANSCRIPTION_BASE_URL = "https://collector.example/v1";
|
|
259
|
+
await transcribeAudio(audio);
|
|
260
|
+
expect(calledUrls).toEqual(["https://api.elevenlabs.io/v1/speech-to-text"]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("a pinned provider uses only its own key", async () => {
|
|
264
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
265
|
+
process.env.OPENAI_API_KEY = OPENAI_FAKE;
|
|
266
|
+
process.env.TRANSCRIPTION_PROVIDER = "openai";
|
|
267
|
+
await transcribeAudio(audio);
|
|
268
|
+
expect(captured[0]).toMatchObject({ authorization: `Bearer ${OPENAI_FAKE}` });
|
|
269
|
+
|
|
270
|
+
delete process.env.OPENAI_API_KEY;
|
|
271
|
+
expect(isTranscriptionConfigured()).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a detected key is scrubbed from errors under its own name", async () => {
|
|
275
|
+
process.env.ELEVENLABS_API_KEY = ELEVEN_FAKE;
|
|
276
|
+
reply = () => Response.json({ detail: `bad key ${ELEVEN_FAKE}` }, { status: 401 });
|
|
277
|
+
await expect(transcribeAudio(audio)).rejects.toThrow("bad key [REDACTED:ELEVENLABS_API_KEY]");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// The composer's mic button records webm in Chrome and Firefox; the upload
|
|
281
|
+
// route drops `;codecs=` before it gets here. Each provider reads the format
|
|
282
|
+
// from the file name, and all three list webm as supported.
|
|
283
|
+
test.each([
|
|
284
|
+
["elevenlabs", "ELEVENLABS_API_KEY", "https://api.elevenlabs.io/v1/speech-to-text"],
|
|
285
|
+
["openai", "OPENAI_API_KEY", "https://api.openai.com/v1/audio/transcriptions"],
|
|
286
|
+
["groq", "GROQ_API_KEY", "https://api.groq.com/openai/v1/audio/transcriptions"],
|
|
287
|
+
])("a recorded webm clip reaches %s as a .webm audio/webm file", async (name, keyEnv, url) => {
|
|
288
|
+
process.env[keyEnv] = fake(name);
|
|
289
|
+
const clip = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], { type: "audio/webm" });
|
|
290
|
+
const notes = await transcribeAudioNotes(
|
|
291
|
+
[{ key: "c", name: "recording-2026-10-21-143005.webm", data: clip, mimeType: "audio/webm" }],
|
|
292
|
+
"t",
|
|
293
|
+
);
|
|
294
|
+
expect(notes.get("c")).toBe("[Transcript of recording-2026-10-21-143005.webm]\nhello");
|
|
295
|
+
expect(calledUrls).toEqual([url]);
|
|
296
|
+
expect(captured[0]).toMatchObject({ filename: "recording-2026-10-21-143005.webm" });
|
|
297
|
+
expect(sentFileTypes).toEqual(["audio/webm"]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { parseWorkerTokenArgs, runWorkerToken } from "../commands/worker-token";
|
|
3
|
+
|
|
4
|
+
const env = { MCP_BASE_URL: "http://swarm.test", AGENT_SWARM_API_KEY: "operator-key-for-test" };
|
|
5
|
+
|
|
6
|
+
function fakeFetch(agents: { id: string; name: string }[]) {
|
|
7
|
+
const calls: { url: string; method: string; body?: unknown }[] = [];
|
|
8
|
+
const impl = (async (url: string, init?: RequestInit) => {
|
|
9
|
+
calls.push({
|
|
10
|
+
url,
|
|
11
|
+
method: init?.method ?? "GET",
|
|
12
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
13
|
+
});
|
|
14
|
+
if (url.endsWith("/api/agents")) return Response.json({ agents });
|
|
15
|
+
if (init?.method === "DELETE") return Response.json({ revoked: 1 });
|
|
16
|
+
return Response.json({ plaintext: "minted-value", expiresAt: null, agentCreated: true });
|
|
17
|
+
}) as unknown as typeof fetch;
|
|
18
|
+
return { calls, impl };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function capture() {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
const err: string[] = [];
|
|
24
|
+
return { out, err, stdout: (l: string) => out.push(l), stderr: (l: string) => err.push(l) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("argument parsing requires one action and exactly one agent selector", () => {
|
|
28
|
+
expect(parseWorkerTokenArgs(["create", "--name", "w"]).error).toBeUndefined();
|
|
29
|
+
expect(parseWorkerTokenArgs(["create"]).error).toContain("exactly one");
|
|
30
|
+
expect(parseWorkerTokenArgs(["create", "--name", "w", "--agent-id", "a"]).error).toContain(
|
|
31
|
+
"exactly one",
|
|
32
|
+
);
|
|
33
|
+
expect(parseWorkerTokenArgs(["--name", "w"]).error).toContain("Missing action");
|
|
34
|
+
expect(parseWorkerTokenArgs(["create", "--name", "w", "--ttl-days", "0"]).error).toContain(
|
|
35
|
+
"--ttl-days",
|
|
36
|
+
);
|
|
37
|
+
expect(parseWorkerTokenArgs(["create", "--bogus"]).error).toContain("Unknown argument");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("create by an unused name makes the agent and prints only the token to stdout", async () => {
|
|
41
|
+
const { calls, impl } = fakeFetch([]);
|
|
42
|
+
const io = capture();
|
|
43
|
+
const code = await runWorkerToken(["create", "--name", "w1", "--ttl-days", "2"], {
|
|
44
|
+
env,
|
|
45
|
+
fetch: impl,
|
|
46
|
+
...io,
|
|
47
|
+
});
|
|
48
|
+
expect(code).toBe(0);
|
|
49
|
+
expect(io.out).toEqual(["minted-value"]);
|
|
50
|
+
expect(io.err.join("\n")).not.toContain("minted-value");
|
|
51
|
+
const mint = calls[1]!;
|
|
52
|
+
expect(mint.method).toBe("POST");
|
|
53
|
+
expect(mint.url).toMatch(/^http:\/\/swarm\.test\/api\/agents\/[0-9a-f-]{36}\/token$/);
|
|
54
|
+
expect(mint.body).toEqual({ name: "w1", ttlMs: 2 * 86_400_000 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("rotate and revoke resolve an existing name; an ambiguous name fails", async () => {
|
|
58
|
+
const agents = [{ id: "agent-1", name: "w1" }];
|
|
59
|
+
for (const [action, method, suffix] of [
|
|
60
|
+
["rotate", "POST", "/rotate"],
|
|
61
|
+
["revoke", "DELETE", ""],
|
|
62
|
+
] as const) {
|
|
63
|
+
const { calls, impl } = fakeFetch(agents);
|
|
64
|
+
expect(await runWorkerToken([action, "--name", "w1"], { env, fetch: impl, ...capture() })).toBe(
|
|
65
|
+
0,
|
|
66
|
+
);
|
|
67
|
+
expect(calls[1]).toMatchObject({
|
|
68
|
+
method,
|
|
69
|
+
url: `http://swarm.test/api/agents/agent-1/token${suffix}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const { impl } = fakeFetch([...agents, { id: "agent-2", name: "w1" }]);
|
|
73
|
+
const io = capture();
|
|
74
|
+
expect(await runWorkerToken(["rotate", "--name", "w1"], { env, fetch: impl, ...io })).toBe(1);
|
|
75
|
+
expect(io.err.join("\n")).toContain("Several agents");
|
|
76
|
+
});
|
|
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
|
|
|
984
984
|
// ─── Registry Wiring ─────────────────────────────────────────
|
|
985
985
|
|
|
986
986
|
describe("createExecutorRegistry", () => {
|
|
987
|
-
test("registers all
|
|
987
|
+
test("registers all 15 executors (10 instant + 5 async)", () => {
|
|
988
988
|
const registry = createExecutorRegistry(mockDeps);
|
|
989
989
|
const types = registry.types();
|
|
990
990
|
|
|
@@ -993,6 +993,7 @@ describe("createExecutorRegistry", () => {
|
|
|
993
993
|
expect(types).toContain("notify");
|
|
994
994
|
expect(types).toContain("raw-llm");
|
|
995
995
|
expect(types).toContain("system-one-decision");
|
|
996
|
+
expect(types).toContain("system-one-extraction");
|
|
996
997
|
expect(types).toContain("script");
|
|
997
998
|
expect(types).toContain("swarm-script");
|
|
998
999
|
expect(types).toContain("vcs");
|
|
@@ -1002,7 +1003,7 @@ describe("createExecutorRegistry", () => {
|
|
|
1002
1003
|
expect(types).toContain("human-in-the-loop");
|
|
1003
1004
|
expect(types).toContain("wait");
|
|
1004
1005
|
expect(types).toContain("sub-workflow");
|
|
1005
|
-
expect(types).toHaveLength(
|
|
1006
|
+
expect(types).toHaveLength(15);
|
|
1006
1007
|
});
|
|
1007
1008
|
|
|
1008
1009
|
test("instant executors have mode instant, async executors have mode async", () => {
|
|
@@ -1012,6 +1013,7 @@ describe("createExecutorRegistry", () => {
|
|
|
1012
1013
|
"code-match",
|
|
1013
1014
|
"notify",
|
|
1014
1015
|
"raw-llm",
|
|
1016
|
+
"system-one-extraction",
|
|
1015
1017
|
"script",
|
|
1016
1018
|
"swarm-script",
|
|
1017
1019
|
"vcs",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS, validateConfigValue } from "../be/swarm-config-guard";
|
|
4
5
|
import type { WorkflowNode } from "../types";
|
|
5
6
|
import { validateDefinition } from "../workflows/definition";
|
|
6
7
|
import { InProcessEventBus } from "../workflows/event-bus";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
SystemOneDecisionExecutor,
|
|
12
13
|
type SystemOneDecisionExecutorOptions,
|
|
13
14
|
} from "../workflows/executors/system-one-decision";
|
|
15
|
+
import { SYSTEM_ONE_PROVIDER_IDS } from "../workflows/executors/system-one-providers";
|
|
14
16
|
import { interpolate } from "../workflows/template";
|
|
15
17
|
|
|
16
18
|
const TEST_DB_PATH = "./test-workflow-system-one-decision-providers.sqlite";
|
|
@@ -50,7 +52,7 @@ afterAll(async () => {
|
|
|
50
52
|
|
|
51
53
|
beforeEach(async () => {
|
|
52
54
|
await getDbClient().run(
|
|
53
|
-
"DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN')",
|
|
55
|
+
"DELETE FROM swarm_config WHERE key IN ('OPENAI_API_KEY', 'OPENAI_DECISIONS_API_KEY', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN', 'SYSTEM_ONE_DEFAULT_PROVIDER')",
|
|
54
56
|
);
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -708,6 +710,101 @@ describe("system-one-decision openai provider", () => {
|
|
|
708
710
|
});
|
|
709
711
|
});
|
|
710
712
|
|
|
713
|
+
// ─── Default provider ───────────────────────────────────────
|
|
714
|
+
|
|
715
|
+
describe("system-one-decision default provider", () => {
|
|
716
|
+
const TYPESAFE_KEY = "typesafe-example-test-key-0123456789";
|
|
717
|
+
const jevBody = () =>
|
|
718
|
+
jsonResponse({
|
|
719
|
+
model: "jev",
|
|
720
|
+
answers: systemOneAnswers,
|
|
721
|
+
usage: { input_tokens: 1, output_tokens: 0 },
|
|
722
|
+
});
|
|
723
|
+
const setDefault = (value: string) =>
|
|
724
|
+
upsertSwarmConfig({ scope: "global", key: "SYSTEM_ONE_DEFAULT_PROVIDER", value });
|
|
725
|
+
const withTypesafeKey = {
|
|
726
|
+
getApiKey: async (id: string) =>
|
|
727
|
+
id === "typesafe" ? TYPESAFE_KEY : id === "cloudflare" ? CF_TOKEN : null,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
test("unset, a node with no provider still calls typesafe", async () => {
|
|
731
|
+
const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
|
|
732
|
+
const result = await run(executor, config());
|
|
733
|
+
expect(result.status).toBe("success");
|
|
734
|
+
expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
735
|
+
expect(calls[0]?.body.model).toBe("jev-latest");
|
|
736
|
+
// The schema leaves the provider unset; the executor resolves it per run.
|
|
737
|
+
expect(SystemOneDecisionConfigSchema.parse(config()).provider).toBeUndefined();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("set to cloudflare, a node with no provider calls Clef with no workflow edit", async () => {
|
|
741
|
+
await setDefault("cloudflare");
|
|
742
|
+
const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())], withTypesafeKey);
|
|
743
|
+
const result = await run(executor, config());
|
|
744
|
+
expect(result.status).toBe("success");
|
|
745
|
+
expect(calls[0]?.url).toBe(`${CF_BASE}/clef`);
|
|
746
|
+
expect((calls[0]?.init.headers as Record<string, string>).authorization).toBe(
|
|
747
|
+
`Bearer ${CF_TOKEN}`,
|
|
748
|
+
);
|
|
749
|
+
expect(result.output).toMatchObject({ model: "clef", answers: systemOneAnswers as never });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("a node that names its provider keeps it", async () => {
|
|
753
|
+
await setDefault("cloudflare");
|
|
754
|
+
const { executor, calls } = makeExecutor([jevBody()], withTypesafeKey);
|
|
755
|
+
expect((await run(executor, config({ provider: "typesafe" }))).status).toBe("success");
|
|
756
|
+
expect(calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("readiness checks the default provider's credentials", async () => {
|
|
760
|
+
await setDefault("cloudflare");
|
|
761
|
+
const { executor } = makeExecutor([], { getApiKey: async () => null });
|
|
762
|
+
const problems = await executor.checkReadiness([{ id: "a", config: config() }]);
|
|
763
|
+
expect(problems).toHaveLength(1);
|
|
764
|
+
expect(problems[0]?.nodeIds).toEqual(["a"]);
|
|
765
|
+
expect(problems[0]?.message).toContain("CLOUDFLARE_API_TOKEN is not configured");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("the environment is read when no row is stored; a stored row wins", async () => {
|
|
769
|
+
const env = { SYSTEM_ONE_DEFAULT_PROVIDER: "cloudflare" };
|
|
770
|
+
const fromEnv = makeExecutor([jsonResponse(clefEnvelope())], { ...withTypesafeKey, env });
|
|
771
|
+
expect((await run(fromEnv.executor, config())).status).toBe("success");
|
|
772
|
+
expect(fromEnv.calls[0]?.url).toBe(`${CF_BASE}/clef`);
|
|
773
|
+
|
|
774
|
+
await setDefault("typesafe");
|
|
775
|
+
const stored = makeExecutor([jevBody()], { ...withTypesafeKey, env });
|
|
776
|
+
expect((await run(stored.executor, config())).status).toBe("success");
|
|
777
|
+
expect(stored.calls[0]?.url).toBe("https://api.typesafe.ai/v1/systemone");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("a defaulted provider's limits are checked at run, and nothing is sent", async () => {
|
|
781
|
+
await setDefault("cloudflare");
|
|
782
|
+
const qs: Record<string, unknown> = {};
|
|
783
|
+
const rs: Record<string, unknown> = {};
|
|
784
|
+
for (let i = 0; i < 65; i++) {
|
|
785
|
+
qs[`q${i}`] = { type: "noul", instructions: `Question ${i}?` };
|
|
786
|
+
rs[`q${i}`] = { type: "noul" };
|
|
787
|
+
}
|
|
788
|
+
const { executor, calls } = makeExecutor([jsonResponse(clefEnvelope())]);
|
|
789
|
+
const result = await run(executor, config({ questions: qs, returns: rs }));
|
|
790
|
+
expect(result.status).toBe("failed");
|
|
791
|
+
expect(result.error).toContain("at most 64 questions per node; this node has 65");
|
|
792
|
+
expect(calls).toHaveLength(0);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("the config guard accepts exactly the decide hosts", () => {
|
|
796
|
+
expect([...SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS].sort()).toEqual(
|
|
797
|
+
[...SYSTEM_ONE_PROVIDER_IDS].sort(),
|
|
798
|
+
);
|
|
799
|
+
for (const id of SYSTEM_ONE_PROVIDER_IDS) {
|
|
800
|
+
expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", id)).toBeNull();
|
|
801
|
+
}
|
|
802
|
+
expect(validateConfigValue("SYSTEM_ONE_DEFAULT_PROVIDER", "jev")).toContain(
|
|
803
|
+
"Invalid SYSTEM_ONE_DEFAULT_PROVIDER value",
|
|
804
|
+
);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
711
808
|
// ─── Existing providers ─────────────────────────────────────
|
|
712
809
|
|
|
713
810
|
describe("system-one-decision SystemOne wire is unchanged", () => {
|
|
@@ -227,7 +227,8 @@ describe("system-one-decision config schema", () => {
|
|
|
227
227
|
questions: { fit: { type: "noul", instructions: "Is it real?" } },
|
|
228
228
|
returns: { fit: { type: "noul" } },
|
|
229
229
|
});
|
|
230
|
-
|
|
230
|
+
// Unset: the executor reads SYSTEM_ONE_DEFAULT_PROVIDER at run time (typesafe when unset).
|
|
231
|
+
expect(parsed.provider).toBeUndefined();
|
|
231
232
|
// The model default belongs to the provider, so the schema leaves it unset.
|
|
232
233
|
expect(parsed.model).toBeUndefined();
|
|
233
234
|
expect(parsed.timeoutMs).toBe(30_000);
|