@desplega.ai/agent-swarm 2.4.4 → 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-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
- package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.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-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
- package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
- package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
- package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
- package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
- package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
- package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
- package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
- package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
- package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
- package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
- package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
- package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
- package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
- package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
- package/openapi.json +9 -2
- package/package.json +7 -7
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- 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/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +24 -1
- package/src/be/task-attachment-store.ts +30 -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/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/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/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/cursor-adapter.ts +1 -1
- 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/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/config-api-only-keys.test.ts +9 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- 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 +242 -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/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/realtime-transport.test.ts +13 -7
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- 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/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- 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 +6 -0
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- 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/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- 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 +5 -5
- 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/handlers-0pvee9nw.js +0 -39
- package/dist/index-thm0y6a8.js +0 -41
|
@@ -7,7 +7,11 @@ import { closeDb, createAgent, getSwarmConfigs, initDb, upsertSwarmConfig } from
|
|
|
7
7
|
import { handleConfig } from "../http/config";
|
|
8
8
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
9
9
|
import { registerSetConfigTool } from "../tools/swarm-config/set-config";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ENV_EXPORT_RESTRICTED_KEYS,
|
|
12
|
+
effectiveExportToEnv,
|
|
13
|
+
envExportRestrictedError,
|
|
14
|
+
} from "../utils/config-env-export";
|
|
11
15
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
12
16
|
import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
|
|
13
17
|
import { listenOnFreePort } from "./test-net";
|
|
@@ -153,6 +157,16 @@ describe("swarm_config exportToEnv", () => {
|
|
|
153
157
|
expect(row?.exportToEnv).toBe(false);
|
|
154
158
|
});
|
|
155
159
|
|
|
160
|
+
test("the speech-to-text key never exports, even from an older stored true", async () => {
|
|
161
|
+
const key = "TRANSCRIPTION_API_KEY";
|
|
162
|
+
const stored = await upsertSwarmConfig({ scope: "global", key, value: "t", isSecret: true });
|
|
163
|
+
expect(stored.exportToEnv).toBe(false);
|
|
164
|
+
await expect(
|
|
165
|
+
upsertSwarmConfig({ scope: "global", key, value: "t", isSecret: true, exportToEnv: true }),
|
|
166
|
+
).rejects.toThrow(envExportRestrictedError(key));
|
|
167
|
+
expect(effectiveExportToEnv(key, true)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
156
170
|
test("the config API and /api/config/resolved carry the flag", async () => {
|
|
157
171
|
const put = await fetch(`${baseUrl}/api/config`, {
|
|
158
172
|
method: "PUT",
|
|
@@ -231,6 +245,11 @@ describe("migration 215", () => {
|
|
|
231
245
|
"X_CLIENT_SECRET",
|
|
232
246
|
"connection.example.secret",
|
|
233
247
|
];
|
|
248
|
+
// Restricted after migration 215 shipped. Row readers force the flag off
|
|
249
|
+
// (effectiveExportToEnv), so an older stored true never reaches an agent.
|
|
250
|
+
const RESTRICTED_AT_215 = [...ENV_EXPORT_RESTRICTED_KEYS].filter(
|
|
251
|
+
(key) => key !== "TRANSCRIPTION_API_KEY",
|
|
252
|
+
);
|
|
234
253
|
|
|
235
254
|
test("backfills true and turns off only the deny list", () => {
|
|
236
255
|
const db = new Database(":memory:");
|
|
@@ -243,7 +262,7 @@ describe("migration 215", () => {
|
|
|
243
262
|
const insert = db.prepare(
|
|
244
263
|
"INSERT INTO swarm_config (id, scope, key, value, isSecret, createdAt, lastUpdatedAt) VALUES (?, 'global', ?, 'v', 1, 'now', 'now')",
|
|
245
264
|
);
|
|
246
|
-
const keys = [...MUST_STAY_EXPORTED, ...DENY_ONLY, ...
|
|
265
|
+
const keys = [...MUST_STAY_EXPORTED, ...DENY_ONLY, ...RESTRICTED_AT_215];
|
|
247
266
|
for (const key of keys) insert.run(crypto.randomUUID(), key);
|
|
248
267
|
|
|
249
268
|
db.exec(readFileSync(MIGRATION, "utf8"));
|
|
@@ -257,7 +276,7 @@ describe("migration 215", () => {
|
|
|
257
276
|
.map((row) => [row.key, row.exportToEnv]),
|
|
258
277
|
);
|
|
259
278
|
for (const key of MUST_STAY_EXPORTED) expect([key, flags[key]]).toEqual([key, 1]);
|
|
260
|
-
for (const key of [...DENY_ONLY, ...
|
|
279
|
+
for (const key of [...DENY_ONLY, ...RESTRICTED_AT_215]) {
|
|
261
280
|
expect([key, flags[key]]).toEqual([key, 0]);
|
|
262
281
|
}
|
|
263
282
|
} finally {
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio a user sends from the sessions UI or Slack gets its transcript in the
|
|
3
|
+
* task text, through the same `transcribeAudio` helper as the Chat SDK path.
|
|
4
|
+
* No provider configured, or a failed call: one line says so and the task is
|
|
5
|
+
* still created.
|
|
6
|
+
*/
|
|
7
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
8
|
+
import { mkdtemp, rm, unlink } from "node:fs/promises";
|
|
9
|
+
import {
|
|
10
|
+
createServer as createHttpServer,
|
|
11
|
+
type IncomingMessage,
|
|
12
|
+
type Server,
|
|
13
|
+
type ServerResponse,
|
|
14
|
+
} from "node:http";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
closeDb,
|
|
19
|
+
createAgent,
|
|
20
|
+
createTaskExtended,
|
|
21
|
+
getTaskAttachments,
|
|
22
|
+
getTaskById,
|
|
23
|
+
initDb,
|
|
24
|
+
} from "../be/db";
|
|
25
|
+
import { resetFileStorageProviderForTests } from "../fs/registry";
|
|
26
|
+
import { handleCore } from "../http/core";
|
|
27
|
+
import { handleFs } from "../http/fs";
|
|
28
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
29
|
+
import type { SlackFile } from "../slack/files";
|
|
30
|
+
import {
|
|
31
|
+
buildEffectiveText,
|
|
32
|
+
createSlackTaskWithFiles,
|
|
33
|
+
fetchSlackFiles,
|
|
34
|
+
} from "../slack/inbound-files";
|
|
35
|
+
import { NOT_TRANSCRIBED_NOTE, TRANSCRIPTION_ENV_KEYS } from "../utils/transcription";
|
|
36
|
+
import { listenOnFreePort } from "./test-net";
|
|
37
|
+
|
|
38
|
+
const TEST_DB_PATH = "./test-task-audio-transcription.sqlite";
|
|
39
|
+
const API_KEY = "example-task-audio-key";
|
|
40
|
+
const BOT_TOKEN = "example-task-audio-slack-token";
|
|
41
|
+
const AUDIO = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 1, 2, 3, 4, 5]);
|
|
42
|
+
const NO_PROVIDER = NOT_TRANSCRIBED_NOTE;
|
|
43
|
+
const savedEnv = new Map(TRANSCRIPTION_ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
44
|
+
|
|
45
|
+
let api: Server;
|
|
46
|
+
let apiPort: number;
|
|
47
|
+
let slackHost: ReturnType<typeof Bun.serve>;
|
|
48
|
+
let stt: ReturnType<typeof Bun.serve>;
|
|
49
|
+
let sttMode: "ok" | "fail" = "ok";
|
|
50
|
+
let sttFilenames: string[] = [];
|
|
51
|
+
let fsDir: string;
|
|
52
|
+
let agentId: string;
|
|
53
|
+
|
|
54
|
+
async function removeDbFiles(): Promise<void> {
|
|
55
|
+
for (const suffix of ["", "-wal", "-shm"]) await unlink(TEST_DB_PATH + suffix).catch(() => {});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function useTranscription(): void {
|
|
59
|
+
process.env.TRANSCRIPTION_API_KEY = "stt-task-audio-key";
|
|
60
|
+
process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
await removeDbFiles();
|
|
65
|
+
fsDir = await mkdtemp(join(tmpdir(), "task-audio-"));
|
|
66
|
+
process.env.AGENT_FS_LOCAL_DIR = fsDir;
|
|
67
|
+
delete process.env.AGENT_FS_API_URL;
|
|
68
|
+
resetFileStorageProviderForTests();
|
|
69
|
+
initDb(TEST_DB_PATH);
|
|
70
|
+
|
|
71
|
+
stt = Bun.serve({
|
|
72
|
+
port: 0,
|
|
73
|
+
hostname: "127.0.0.1",
|
|
74
|
+
async fetch(req) {
|
|
75
|
+
const file = (await req.formData()).get("file") as File;
|
|
76
|
+
sttFilenames.push(file.name);
|
|
77
|
+
if (sttMode === "fail") {
|
|
78
|
+
return Response.json({ error: { message: "upstream overloaded" } }, { status: 503 });
|
|
79
|
+
}
|
|
80
|
+
return Response.json({ text: " Ship the release notes today. " });
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
slackHost = Bun.serve({
|
|
84
|
+
port: 0,
|
|
85
|
+
fetch: (req) =>
|
|
86
|
+
req.headers.get("authorization") === `Bearer ${BOT_TOKEN}`
|
|
87
|
+
? new Response(AUDIO, { headers: { "content-type": "audio/webm" } })
|
|
88
|
+
: new Response("forbidden", { status: 403 }),
|
|
89
|
+
});
|
|
90
|
+
api = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
91
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
92
|
+
if (await handleCore(req, res, myAgentId, API_KEY)) return;
|
|
93
|
+
const ok = await handleFs(
|
|
94
|
+
req,
|
|
95
|
+
res,
|
|
96
|
+
getPathSegments(req.url || ""),
|
|
97
|
+
parseQueryParams(req.url || ""),
|
|
98
|
+
myAgentId,
|
|
99
|
+
);
|
|
100
|
+
if (!ok) {
|
|
101
|
+
res.writeHead(404);
|
|
102
|
+
res.end("Not Found");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
apiPort = await listenOnFreePort(api);
|
|
106
|
+
agentId = (await createAgent({ name: "task-audio-lead", isLead: true, status: "idle" })).id;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterAll(async () => {
|
|
110
|
+
await new Promise<void>((resolve) => api.close(() => resolve()));
|
|
111
|
+
stt.stop(true);
|
|
112
|
+
slackHost.stop(true);
|
|
113
|
+
closeDb();
|
|
114
|
+
await removeDbFiles();
|
|
115
|
+
await rm(fsDir, { recursive: true, force: true });
|
|
116
|
+
delete process.env.AGENT_FS_LOCAL_DIR;
|
|
117
|
+
resetFileStorageProviderForTests();
|
|
118
|
+
for (const [key, value] of savedEnv) {
|
|
119
|
+
if (value === undefined) delete process.env[key];
|
|
120
|
+
else process.env[key] = value;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
sttMode = "ok";
|
|
126
|
+
sttFilenames = [];
|
|
127
|
+
// A provider key in the shell (OPENAI_API_KEY, ...) would switch transcription on.
|
|
128
|
+
for (const key of TRANSCRIPTION_ENV_KEYS) delete process.env[key];
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** The composer's upload: raw body, `name` query, the file's own content type. */
|
|
132
|
+
function upload(taskId: string, name: string, contentType: string): Promise<Response> {
|
|
133
|
+
return fetch(
|
|
134
|
+
`http://localhost:${apiPort}/api/fs/tasks/${taskId}/files?name=${encodeURIComponent(name)}&intent=user-upload`,
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: AUDIO,
|
|
138
|
+
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": contentType },
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function draftTask(text: string): Promise<string> {
|
|
144
|
+
return (await createTaskExtended(text, { agentId, source: "ui", status: "draft" })).id;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("sessions UI upload", () => {
|
|
148
|
+
test("a recorded webm clip uploaded to a draft puts its transcript in the task text", async () => {
|
|
149
|
+
useTranscription();
|
|
150
|
+
const taskId = await draftTask("what did I say?");
|
|
151
|
+
|
|
152
|
+
// What the composer's mic button uploads in Chrome and Firefox.
|
|
153
|
+
const name = "recording-2026-10-21-143005.webm";
|
|
154
|
+
const res = await upload(taskId, name, "audio/webm;codecs=opus");
|
|
155
|
+
|
|
156
|
+
expect(res.status).toBe(201);
|
|
157
|
+
expect(sttFilenames).toEqual([name]);
|
|
158
|
+
expect((await getTaskById(taskId))!.task).toBe(
|
|
159
|
+
`what did I say?\n\n[File: ${name} (audio/webm)]\n[Transcript of ${name}]\nShip the release notes today.`,
|
|
160
|
+
);
|
|
161
|
+
expect(await getTaskAttachments(taskId)).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("parallel audio uploads each keep their block", async () => {
|
|
165
|
+
useTranscription();
|
|
166
|
+
const taskId = await draftTask("two notes");
|
|
167
|
+
|
|
168
|
+
const responses = await Promise.all([
|
|
169
|
+
upload(taskId, "a.m4a", "audio/mp4"),
|
|
170
|
+
upload(taskId, "b.m4a", "audio/mp4"),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
expect(responses.map((r) => r.status)).toEqual([201, 201]);
|
|
174
|
+
const text = (await getTaskById(taskId))!.task;
|
|
175
|
+
expect(text).toContain("[Transcript of a.m4a]");
|
|
176
|
+
expect(text).toContain("[Transcript of b.m4a]");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("without a transcription provider the task gets one line saying so", async () => {
|
|
180
|
+
const taskId = await draftTask("listen to this");
|
|
181
|
+
|
|
182
|
+
const res = await upload(taskId, "memo.ogg", "audio/ogg");
|
|
183
|
+
|
|
184
|
+
expect(res.status).toBe(201);
|
|
185
|
+
expect(sttFilenames).toEqual([]);
|
|
186
|
+
expect((await getTaskById(taskId))!.task).toBe(
|
|
187
|
+
`listen to this\n\n[File: memo.ogg (audio/ogg)]\n${NO_PROVIDER}`,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a failed transcription still stores the upload and says why", async () => {
|
|
192
|
+
useTranscription();
|
|
193
|
+
sttMode = "fail";
|
|
194
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
195
|
+
const taskId = await draftTask("listen to this");
|
|
196
|
+
try {
|
|
197
|
+
const res = await upload(taskId, "memo.ogg", "audio/ogg");
|
|
198
|
+
expect(res.status).toBe(201);
|
|
199
|
+
} finally {
|
|
200
|
+
warn.mockRestore();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
expect((await getTaskById(taskId))!.task).toContain(
|
|
204
|
+
"[File: memo.ogg (audio/ogg)]\n(transcription failed: upstream overloaded)",
|
|
205
|
+
);
|
|
206
|
+
expect(await getTaskAttachments(taskId)).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a task out of draft and a non-audio file are left as they are", async () => {
|
|
210
|
+
useTranscription();
|
|
211
|
+
const running = (await createTaskExtended("already dispatched", { agentId, source: "ui" })).id;
|
|
212
|
+
const draft = await draftTask("a picture");
|
|
213
|
+
|
|
214
|
+
expect((await upload(running, "late.ogg", "audio/ogg")).status).toBe(201);
|
|
215
|
+
expect((await upload(draft, "shot.png", "image/png")).status).toBe(201);
|
|
216
|
+
|
|
217
|
+
expect(sttFilenames).toEqual([]);
|
|
218
|
+
expect((await getTaskById(running))!.task).toBe("already dispatched");
|
|
219
|
+
expect((await getTaskById(draft))!.task).toBe("a picture");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Slack inbound files", () => {
|
|
224
|
+
function slackFile(id: string, name: string, extra: Partial<SlackFile> = {}): SlackFile {
|
|
225
|
+
return {
|
|
226
|
+
id,
|
|
227
|
+
name,
|
|
228
|
+
mimetype: "audio/webm",
|
|
229
|
+
filetype: "webm",
|
|
230
|
+
size: AUDIO.byteLength,
|
|
231
|
+
url_private: `${slackHost.url}files-pri/T0TEAM-${id}/${name}`,
|
|
232
|
+
url_private_download: `${slackHost.url}files-pri/T0TEAM-${id}/download/${name}`,
|
|
233
|
+
...extra,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const client = { token: BOT_TOKEN } as never;
|
|
237
|
+
|
|
238
|
+
/** What the Slack handlers do: fetch, build the task text, create the task. */
|
|
239
|
+
async function slackTask(text: string, files: SlackFile[]) {
|
|
240
|
+
await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
|
|
241
|
+
const description = buildEffectiveText(text, inbound.files, inbound.failed, inbound.notes);
|
|
242
|
+
return (await createSlackTaskWithFiles(description, { agentId, source: "slack" }, inbound))
|
|
243
|
+
.task;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
test("a voice clip gets its transcript under its file line and stays attached", async () => {
|
|
247
|
+
useTranscription();
|
|
248
|
+
// Slack voice clips are `slack_audio`, whatever their MIME type says.
|
|
249
|
+
const clip = slackFile("F0CLIP0001", "audio_message.mp4", {
|
|
250
|
+
mimetype: "video/mp4",
|
|
251
|
+
subtype: "slack_audio",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const task = await slackTask("please do this", [clip]);
|
|
255
|
+
|
|
256
|
+
expect(task.task).toBe(
|
|
257
|
+
`please do this\n\n[File: audio_message.mp4 (video/mp4, 9 B) id=F0CLIP0001]\n[Transcript of audio_message.mp4]\nShip the release notes today.`,
|
|
258
|
+
);
|
|
259
|
+
expect((await getTaskById(task.id))!.status).toBe("pending");
|
|
260
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("an audio file without a provider gets one line saying so", async () => {
|
|
264
|
+
const task = await slackTask("", [slackFile("F0AUDIO002", "memo.webm")]);
|
|
265
|
+
|
|
266
|
+
expect(task.task).toBe(`[File: memo.webm (audio/webm, 9 B) id=F0AUDIO002]\n${NO_PROVIDER}`);
|
|
267
|
+
expect(sttFilenames).toEqual([]);
|
|
268
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("a failed transcription never blocks the task", async () => {
|
|
272
|
+
useTranscription();
|
|
273
|
+
sttMode = "fail";
|
|
274
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
275
|
+
let task: Awaited<ReturnType<typeof slackTask>>;
|
|
276
|
+
try {
|
|
277
|
+
task = await slackTask("hi", [slackFile("F0AUDIO003", "memo.webm")]);
|
|
278
|
+
} finally {
|
|
279
|
+
warn.mockRestore();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
expect(task.task).toContain(
|
|
283
|
+
"[File: memo.webm (audio/webm, 9 B) id=F0AUDIO003]\n(transcription failed: upstream overloaded)",
|
|
284
|
+
);
|
|
285
|
+
expect((await getTaskById(task.id))!.status).toBe("pending");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("an agent's own file fetch and non-audio files are not transcribed", async () => {
|
|
289
|
+
useTranscription();
|
|
290
|
+
await using fetched = await fetchSlackFiles(client, [slackFile("F0AUDIO004", "memo.webm")]);
|
|
291
|
+
expect(fetched.notes).toBeUndefined();
|
|
292
|
+
|
|
293
|
+
const task = await slackTask("see image", [
|
|
294
|
+
slackFile("F0IMAGE005", "shot.png", { mimetype: "image/png", filetype: "png" }),
|
|
295
|
+
]);
|
|
296
|
+
expect(task.task).not.toContain("Transcript");
|
|
297
|
+
expect(sttFilenames).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|