@desplega.ai/agent-swarm 1.113.0 → 1.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
|
@@ -3,7 +3,7 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { __resetEncryptionKeyForTests } from "../be/crypto";
|
|
6
|
-
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
6
|
+
import { closeDb, createAgent, createMcpServer, getDb, initDb } from "../be/db";
|
|
7
7
|
import {
|
|
8
8
|
createScriptApi,
|
|
9
9
|
getScriptApiById,
|
|
@@ -287,6 +287,54 @@ describe("public execution route", () => {
|
|
|
287
287
|
expect(typeof body.durationMs).toBe("number");
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
test("external endpoint runs receive ctx.mcp connections (parity with /api/scripts/run)", async () => {
|
|
291
|
+
const mcpServer = createMcpServer({
|
|
292
|
+
name: `x-mcp-${crypto.randomUUID()}`,
|
|
293
|
+
transport: "http",
|
|
294
|
+
scope: "global",
|
|
295
|
+
url: "http://mcp.invalid.test/mcp",
|
|
296
|
+
});
|
|
297
|
+
const runtimeDescriptor = {
|
|
298
|
+
slug: "xmcp",
|
|
299
|
+
kind: "mcp",
|
|
300
|
+
connectionId: crypto.randomUUID(),
|
|
301
|
+
tools: [{ name: "ping", inputSchema: {} }],
|
|
302
|
+
};
|
|
303
|
+
getDb()
|
|
304
|
+
.prepare(
|
|
305
|
+
`INSERT INTO script_connections
|
|
306
|
+
(id, slug, kind, scope, allowed_hosts_json, mcp_server_id, generated_runtime_json)
|
|
307
|
+
VALUES (?, ?, 'mcp', 'global', '[]', ?, ?)`,
|
|
308
|
+
)
|
|
309
|
+
.run(runtimeDescriptor.connectionId, "xmcp", mcpServer.id, JSON.stringify(runtimeDescriptor));
|
|
310
|
+
|
|
311
|
+
const script = insertScript({
|
|
312
|
+
name: `mcp-keys-${crypto.randomUUID().slice(0, 8)}`,
|
|
313
|
+
scope: "agent",
|
|
314
|
+
scopeId: workerId,
|
|
315
|
+
source:
|
|
316
|
+
"export default async function run(args, ctx) { return { slugs: Object.keys(ctx.mcp ?? {}) }; }",
|
|
317
|
+
description: "Lists ctx.mcp slugs",
|
|
318
|
+
intent: "test fixture",
|
|
319
|
+
signatureJson: "{}",
|
|
320
|
+
});
|
|
321
|
+
const endpoint = createScriptApi({
|
|
322
|
+
scriptId: script.id,
|
|
323
|
+
agentId: workerId,
|
|
324
|
+
authMode: "none",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const res = await dispatch(`/api/x/script/${endpoint.id}`, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
body: JSON.stringify({}),
|
|
330
|
+
auth: null,
|
|
331
|
+
});
|
|
332
|
+
const body = (await res.json()) as { ok: boolean; result: { slugs: string[] } };
|
|
333
|
+
expect(res.status).toBe(200);
|
|
334
|
+
expect(body.ok).toBe(true);
|
|
335
|
+
expect(body.result.slugs).toContain("xmcp");
|
|
336
|
+
});
|
|
337
|
+
|
|
290
338
|
test("oversized body → 413 before execution, even for authMode 'none'", async () => {
|
|
291
339
|
const script = insertDoubler();
|
|
292
340
|
const endpoint = createScriptApi({
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createApiRegistryClient } from "../scripts-runtime/api-client";
|
|
2
3
|
import { runScript } from "../scripts-runtime/loader";
|
|
3
4
|
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
4
5
|
|
|
5
6
|
const savedEnv = { ...process.env };
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
6
8
|
const resources = { memoryMb: 2048, cpuTimeSec: 20, maxStdoutBytes: 1_048_576 };
|
|
7
9
|
|
|
8
10
|
beforeEach(() => {
|
|
@@ -13,6 +15,7 @@ beforeEach(() => {
|
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
afterEach(() => {
|
|
18
|
+
globalThis.fetch = originalFetch;
|
|
16
19
|
for (const key of Object.keys(process.env)) {
|
|
17
20
|
if (!(key in savedEnv)) delete process.env[key];
|
|
18
21
|
}
|
|
@@ -375,4 +378,292 @@ describe("runScript", () => {
|
|
|
375
378
|
expect(output.result).toEqual({ doubled: 84 });
|
|
376
379
|
expect(output.exitCode).toBe(0);
|
|
377
380
|
});
|
|
381
|
+
|
|
382
|
+
test("ctx.api GraphQL client posts JSON body with credential placeholders", async () => {
|
|
383
|
+
let observed: {
|
|
384
|
+
url: string;
|
|
385
|
+
method: string;
|
|
386
|
+
contentType: string | null;
|
|
387
|
+
authorization: string | null;
|
|
388
|
+
body: unknown;
|
|
389
|
+
} | null = null;
|
|
390
|
+
globalThis.fetch = (async (input, init) => {
|
|
391
|
+
observed = {
|
|
392
|
+
url: String(input),
|
|
393
|
+
method: init?.method ?? "GET",
|
|
394
|
+
contentType: new Headers(init?.headers).get("content-type"),
|
|
395
|
+
authorization: new Headers(init?.headers).get("authorization"),
|
|
396
|
+
body: JSON.parse(String(init?.body)),
|
|
397
|
+
};
|
|
398
|
+
return Response.json({ data: { country: { name: "Ukraine", capital: "Kyiv" } } });
|
|
399
|
+
}) as typeof fetch;
|
|
400
|
+
|
|
401
|
+
const client = createApiRegistryClient([
|
|
402
|
+
{
|
|
403
|
+
slug: "countries",
|
|
404
|
+
kind: "graphql",
|
|
405
|
+
baseUrl: "https://countries.vendor.test/graphql",
|
|
406
|
+
credential: {
|
|
407
|
+
configKey: "COUNTRIES_KEY",
|
|
408
|
+
headerTemplate: "Authorization: Bearer [REDACTED:COUNTRIES_KEY]",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
const result = await client.countries.graphql(
|
|
414
|
+
"query Country($code: ID!) { country(code: $code) { name capital } }",
|
|
415
|
+
{
|
|
416
|
+
code: "UA",
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(result).toEqual({ country: { name: "Ukraine", capital: "Kyiv" } });
|
|
421
|
+
expect(observed).toEqual({
|
|
422
|
+
url: "https://countries.vendor.test/graphql",
|
|
423
|
+
method: "POST",
|
|
424
|
+
contentType: "application/json",
|
|
425
|
+
authorization: "Bearer [REDACTED:COUNTRIES_KEY]",
|
|
426
|
+
body: {
|
|
427
|
+
query: "query Country($code: ID!) { country(code: $code) { name capital } }",
|
|
428
|
+
variables: { code: "UA" },
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("ctx.api OpenAPI client preserves the baseUrl path prefix for absolute spec paths", async () => {
|
|
434
|
+
let observedUrl = "";
|
|
435
|
+
globalThis.fetch = (async (input) => {
|
|
436
|
+
observedUrl = String(input);
|
|
437
|
+
return Response.json({ ok: true });
|
|
438
|
+
}) as typeof fetch;
|
|
439
|
+
|
|
440
|
+
const client = createApiRegistryClient([
|
|
441
|
+
{
|
|
442
|
+
slug: "petstore",
|
|
443
|
+
kind: "openapi",
|
|
444
|
+
baseUrl: "https://petstore.vendor.test/api/v3",
|
|
445
|
+
credential: null,
|
|
446
|
+
operations: [
|
|
447
|
+
{
|
|
448
|
+
name: "getInventory",
|
|
449
|
+
method: "GET",
|
|
450
|
+
path: "/store/inventory",
|
|
451
|
+
parameters: [],
|
|
452
|
+
hasBody: false,
|
|
453
|
+
successStatus: "200",
|
|
454
|
+
requestType: "PetstoreGetInventoryArgs",
|
|
455
|
+
responseType: "PetstoreGetInventoryResponse",
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
await client.petstore.getInventory({});
|
|
462
|
+
expect(observedUrl).toBe("https://petstore.vendor.test/api/v3/store/inventory");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("ctx.api OpenAPI client default path still parses 2xx bodies", async () => {
|
|
466
|
+
const client = createApiRegistryClient(
|
|
467
|
+
[
|
|
468
|
+
{
|
|
469
|
+
slug: "repos",
|
|
470
|
+
kind: "openapi",
|
|
471
|
+
baseUrl: "https://api.vendor.test",
|
|
472
|
+
credential: null,
|
|
473
|
+
operations: [
|
|
474
|
+
{
|
|
475
|
+
name: "getRepo",
|
|
476
|
+
method: "GET",
|
|
477
|
+
path: "/repos/{owner}/{repo}",
|
|
478
|
+
parameters: [
|
|
479
|
+
{ name: "owner", in: "path", required: true, schema: {} },
|
|
480
|
+
{ name: "repo", in: "path", required: true, schema: {} },
|
|
481
|
+
],
|
|
482
|
+
hasBody: false,
|
|
483
|
+
successStatus: "200",
|
|
484
|
+
responseSchema: {},
|
|
485
|
+
requestType: "ReposGetRepoArgs",
|
|
486
|
+
responseType: "ReposGetRepoResponse",
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
{
|
|
492
|
+
fetch: (async () =>
|
|
493
|
+
Response.json({ full_name: "desplega-ai/agent-swarm" })) as typeof fetch,
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
await expect(
|
|
498
|
+
client.repos.getRepo({ path: { owner: "desplega-ai", repo: "agent-swarm" } }),
|
|
499
|
+
).resolves.toEqual({ full_name: "desplega-ai/agent-swarm" });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("ctx.api OpenAPI client enriches default non-2xx errors", async () => {
|
|
503
|
+
const client = createApiRegistryClient(
|
|
504
|
+
[
|
|
505
|
+
{
|
|
506
|
+
slug: "repos",
|
|
507
|
+
kind: "openapi",
|
|
508
|
+
baseUrl: "https://api.vendor.test",
|
|
509
|
+
credential: null,
|
|
510
|
+
operations: [
|
|
511
|
+
{
|
|
512
|
+
name: "getRepo",
|
|
513
|
+
method: "GET",
|
|
514
|
+
path: "/repos/{owner}/{repo}",
|
|
515
|
+
parameters: [
|
|
516
|
+
{ name: "owner", in: "path", required: true, schema: {} },
|
|
517
|
+
{ name: "repo", in: "path", required: true, schema: {} },
|
|
518
|
+
],
|
|
519
|
+
hasBody: false,
|
|
520
|
+
successStatus: "200",
|
|
521
|
+
responseSchema: {},
|
|
522
|
+
requestType: "ReposGetRepoArgs",
|
|
523
|
+
responseType: "ReposGetRepoResponse",
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
{
|
|
529
|
+
fetch: (async () =>
|
|
530
|
+
Response.json(
|
|
531
|
+
{ error: "missing" },
|
|
532
|
+
{ status: 404, statusText: "Not Found" },
|
|
533
|
+
)) as typeof fetch,
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await client.repos.getRepo({ path: { owner: "desplega-ai", repo: "missing" } });
|
|
539
|
+
throw new Error("expected request to throw");
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const err = error as Error & {
|
|
542
|
+
status?: number;
|
|
543
|
+
statusText?: string;
|
|
544
|
+
body?: unknown;
|
|
545
|
+
response?: Response;
|
|
546
|
+
};
|
|
547
|
+
expect(err.message).toBe("ctx.api.repos.getRepo failed with 404");
|
|
548
|
+
expect(err.status).toBe(404);
|
|
549
|
+
expect(err.statusText).toBe("Not Found");
|
|
550
|
+
expect(err.body).toEqual({ error: "missing" });
|
|
551
|
+
expect(err.response?.status).toBe(404);
|
|
552
|
+
expect(await err.response?.json()).toEqual({ error: "missing" });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("ctx.api OpenAPI client raw mode returns binary 2xx responses without parsing", async () => {
|
|
557
|
+
let observedAuthorization: string | null = null;
|
|
558
|
+
const client = createApiRegistryClient(
|
|
559
|
+
[
|
|
560
|
+
{
|
|
561
|
+
slug: "studioApi",
|
|
562
|
+
kind: "openapi",
|
|
563
|
+
baseUrl: "https://studio.vendor.test",
|
|
564
|
+
credential: {
|
|
565
|
+
configKey: "STUDIO_API_KEY",
|
|
566
|
+
headerTemplate: "Authorization: Bearer [REDACTED:STUDIO_API_KEY]",
|
|
567
|
+
},
|
|
568
|
+
operations: [
|
|
569
|
+
{
|
|
570
|
+
name: "downloadRenderFile",
|
|
571
|
+
method: "GET",
|
|
572
|
+
path: "/renders/{id}/file",
|
|
573
|
+
parameters: [{ name: "id", in: "path", required: true, schema: {} }],
|
|
574
|
+
hasBody: false,
|
|
575
|
+
successStatus: "200",
|
|
576
|
+
responseSchema: {},
|
|
577
|
+
requestType: "StudioApiDownloadRenderFileArgs",
|
|
578
|
+
responseType: "StudioApiDownloadRenderFileResponse",
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
{
|
|
584
|
+
fetch: (async (_input, init) => {
|
|
585
|
+
observedAuthorization = new Headers(init?.headers).get("authorization");
|
|
586
|
+
return new Response(new Uint8Array([0, 1, 255]), {
|
|
587
|
+
status: 200,
|
|
588
|
+
headers: { "content-type": "application/octet-stream", "x-render-id": "render-123" },
|
|
589
|
+
});
|
|
590
|
+
}) as typeof fetch,
|
|
591
|
+
},
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const result = (await client.studioApi.downloadRenderFile(
|
|
595
|
+
{ path: { id: "render-123" } },
|
|
596
|
+
{ raw: true },
|
|
597
|
+
)) as { ok: boolean; status: number; headers: Record<string, string>; response: Response };
|
|
598
|
+
|
|
599
|
+
expect(result.ok).toBe(true);
|
|
600
|
+
expect(result.status).toBe(200);
|
|
601
|
+
expect(result.headers).toBeTypeOf("object");
|
|
602
|
+
expect(Array.from(new Uint8Array(await result.response.arrayBuffer()))).toEqual([0, 1, 255]);
|
|
603
|
+
expect(observedAuthorization).toBe("Bearer [REDACTED:STUDIO_API_KEY]");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("ctx.api OpenAPI client raw mode returns non-2xx responses without throwing", async () => {
|
|
607
|
+
const client = createApiRegistryClient(
|
|
608
|
+
[
|
|
609
|
+
{
|
|
610
|
+
slug: "repos",
|
|
611
|
+
kind: "openapi",
|
|
612
|
+
baseUrl: "https://api.vendor.test",
|
|
613
|
+
credential: null,
|
|
614
|
+
operations: [
|
|
615
|
+
{
|
|
616
|
+
name: "getRepo",
|
|
617
|
+
method: "GET",
|
|
618
|
+
path: "/repos/{owner}/{repo}",
|
|
619
|
+
parameters: [
|
|
620
|
+
{ name: "owner", in: "path", required: true, schema: {} },
|
|
621
|
+
{ name: "repo", in: "path", required: true, schema: {} },
|
|
622
|
+
],
|
|
623
|
+
hasBody: false,
|
|
624
|
+
successStatus: "200",
|
|
625
|
+
responseSchema: {},
|
|
626
|
+
requestType: "ReposGetRepoArgs",
|
|
627
|
+
responseType: "ReposGetRepoResponse",
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
{
|
|
633
|
+
fetch: (async () =>
|
|
634
|
+
Response.json(
|
|
635
|
+
{ error: "rate limited" },
|
|
636
|
+
{ status: 429, statusText: "Too Many Requests" },
|
|
637
|
+
)) as typeof fetch,
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const result = (await client.repos.getRepo(
|
|
642
|
+
{ path: { owner: "desplega-ai", repo: "agent-swarm" } },
|
|
643
|
+
{ raw: true },
|
|
644
|
+
)) as { ok: boolean; status: number; statusText: string; response: Response };
|
|
645
|
+
|
|
646
|
+
expect(result.ok).toBe(false);
|
|
647
|
+
expect(result.status).toBe(429);
|
|
648
|
+
expect(result.statusText).toBe("Too Many Requests");
|
|
649
|
+
expect(await result.response.json()).toEqual({ error: "rate limited" });
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("ctx.api GraphQL client throws on errors-only responses", async () => {
|
|
653
|
+
globalThis.fetch = (async () =>
|
|
654
|
+
Response.json({ errors: [{ message: "Cannot query field nope" }] })) as typeof fetch;
|
|
655
|
+
|
|
656
|
+
const client = createApiRegistryClient([
|
|
657
|
+
{
|
|
658
|
+
slug: "countries",
|
|
659
|
+
kind: "graphql",
|
|
660
|
+
baseUrl: "https://countries.vendor.test/graphql",
|
|
661
|
+
credential: null,
|
|
662
|
+
},
|
|
663
|
+
]);
|
|
664
|
+
|
|
665
|
+
await expect(client.countries.graphql("query { nope }")).rejects.toThrow(
|
|
666
|
+
/Cannot query field nope/,
|
|
667
|
+
);
|
|
668
|
+
});
|
|
378
669
|
});
|
|
@@ -9,6 +9,7 @@ import { typecheckScript } from "../be/scripts/typecheck";
|
|
|
9
9
|
import { runSeeder } from "../be/seed";
|
|
10
10
|
import { SEED_SCRIPTS, scriptsSeeder } from "../be/seed-scripts";
|
|
11
11
|
import bootTriage from "../be/seed-scripts/catalog/boot-triage";
|
|
12
|
+
import { renderCatalogReportPage } from "../be/seed-scripts/catalog/catalog-report";
|
|
12
13
|
import compoundInsights from "../be/seed-scripts/catalog/compound-insights";
|
|
13
14
|
import opsCatalogAudit, {
|
|
14
15
|
renderPage as renderOpsCatalogAuditPage,
|
|
@@ -710,6 +711,41 @@ describe("seed-scripts catalog", () => {
|
|
|
710
711
|
expect(html).not.toContain("<ul>");
|
|
711
712
|
});
|
|
712
713
|
|
|
714
|
+
test("catalog report renders complex checks as main-column data panels", () => {
|
|
715
|
+
const html = renderCatalogReportPage({
|
|
716
|
+
title: "Compound Insights Audit",
|
|
717
|
+
slug: "compound-insights",
|
|
718
|
+
description: "Daily ops snapshot.",
|
|
719
|
+
generatedAt: "2026-07-09T12:00:00.000Z",
|
|
720
|
+
lede: "Swarm-wide 3-day snapshot.",
|
|
721
|
+
metrics: [["Tasks", 12]],
|
|
722
|
+
sections: [
|
|
723
|
+
{
|
|
724
|
+
key: "script-usage",
|
|
725
|
+
goal: "Track script execution without dumping JSON into sidebars.",
|
|
726
|
+
findingCount: 0,
|
|
727
|
+
checks: {
|
|
728
|
+
total: 10,
|
|
729
|
+
perScript: [
|
|
730
|
+
{ scriptName: "compound-insights", runs: 3, successRate: 100 },
|
|
731
|
+
{ scriptName: "smart-recall", runs: 2, successRate: 100 },
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
findings: [],
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
appendix: { ok: true },
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
expect(html).toContain('class="report-nav"');
|
|
741
|
+
expect(html).toContain('class="data-panel"');
|
|
742
|
+
expect(html).toContain("<h3>Per Script</h3>");
|
|
743
|
+
expect(html).toContain("<th>Script Name</th>");
|
|
744
|
+
expect(html).toContain("<td>compound-insights</td>");
|
|
745
|
+
expect(html).toContain("<span>Total</span>");
|
|
746
|
+
expect(html).not.toContain("<strong>[{"scriptName"");
|
|
747
|
+
});
|
|
748
|
+
|
|
713
749
|
test("boot-triage returns one read-only post-restart snapshot", async () => {
|
|
714
750
|
const queries: Array<{ sql: string; params?: unknown[] }> = [];
|
|
715
751
|
const result: any = await bootTriage(
|
|
@@ -19,9 +19,16 @@
|
|
|
19
19
|
|
|
20
20
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
21
21
|
import { unlinkSync } from "node:fs";
|
|
22
|
-
import { closeDb, getDb, getKv, initDb } from "../be/db";
|
|
23
|
-
import {
|
|
24
|
-
|
|
22
|
+
import { closeDb, createUser, getDb, getKv, initDb } from "../be/db";
|
|
23
|
+
import {
|
|
24
|
+
findUserByExternalId,
|
|
25
|
+
getUserIdentities,
|
|
26
|
+
type IdentityActor,
|
|
27
|
+
linkIdentity,
|
|
28
|
+
} from "../be/users";
|
|
29
|
+
import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "../slack/enrich";
|
|
30
|
+
|
|
31
|
+
const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
|
|
25
32
|
|
|
26
33
|
const TEST_DB_PATH = "./test-slack-identity-resolution.sqlite";
|
|
27
34
|
|
|
@@ -346,3 +353,30 @@ describe("findUserByExternalId — sanity check after cascade", () => {
|
|
|
346
353
|
expect(looked!.id).toBe(id);
|
|
347
354
|
});
|
|
348
355
|
});
|
|
356
|
+
|
|
357
|
+
describe("rewriteSlackMentions — pure DB, zero Slack API calls", () => {
|
|
358
|
+
test("resolved mention renders '<@id|Name>' — the canonical pair", () => {
|
|
359
|
+
const user = createUser({ name: "Manuel", email: "manuel-rw@example.com" });
|
|
360
|
+
linkIdentity(user.id, "slack", "U3000RESOLVED", SYSTEM_ACTOR);
|
|
361
|
+
|
|
362
|
+
const rewritten = rewriteSlackMentions("hey <@U3000RESOLVED> can you look at this");
|
|
363
|
+
expect(rewritten).toBe("hey <@U3000RESOLVED|Manuel> can you look at this");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("unresolved mention renders '<@id> (unknown user)' — never a guessed name", () => {
|
|
367
|
+
const rewritten = rewriteSlackMentions("hey <@U4000UNKNOWN> can you look at this");
|
|
368
|
+
expect(rewritten).toBe("hey <@U4000UNKNOWN> (unknown user) can you look at this");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("multiple mentions in one string are each rewritten independently", () => {
|
|
372
|
+
const user = createUser({ name: "Tainá", email: "taina-rw@example.com" });
|
|
373
|
+
linkIdentity(user.id, "slack", "U5000RESOLVED", SYSTEM_ACTOR);
|
|
374
|
+
|
|
375
|
+
const rewritten = rewriteSlackMentions("<@U5000RESOLVED> and <@U6000UNKNOWN> both here");
|
|
376
|
+
expect(rewritten).toBe("<@U5000RESOLVED|Tainá> and <@U6000UNKNOWN> (unknown user) both here");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("text with no mentions passes through unchanged", () => {
|
|
380
|
+
expect(rewriteSlackMentions("no mentions here")).toBe("no mentions here");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatInlineCompletionOutputText,
|
|
4
|
+
MAX_INLINE_OUTPUT_MESSAGE_LENGTH,
|
|
5
|
+
shouldPostInlineCompletionOutput,
|
|
6
|
+
} from "../slack/responses";
|
|
7
|
+
import type { AgentTask, TaskAttachment } from "../types";
|
|
8
|
+
|
|
9
|
+
const TASK_ID = "abcdef12-3456-7890-abcd-ef1234567890";
|
|
10
|
+
|
|
11
|
+
function task(overrides: Partial<AgentTask> = {}): AgentTask {
|
|
12
|
+
return {
|
|
13
|
+
id: TASK_ID,
|
|
14
|
+
task: "Answer the question",
|
|
15
|
+
status: "completed",
|
|
16
|
+
output: "Here is the answer with enough substance to post inline.",
|
|
17
|
+
slackChannelId: "C123",
|
|
18
|
+
slackThreadTs: "1700000000.000001",
|
|
19
|
+
slackReplySent: false,
|
|
20
|
+
...overrides,
|
|
21
|
+
} as AgentTask;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function attachment(overrides: Partial<TaskAttachment> = {}): TaskAttachment {
|
|
25
|
+
return {
|
|
26
|
+
id: crypto.randomUUID(),
|
|
27
|
+
taskId: TASK_ID,
|
|
28
|
+
agentId: null,
|
|
29
|
+
name: "report",
|
|
30
|
+
kind: "url",
|
|
31
|
+
url: "https://example.com/report",
|
|
32
|
+
isPrimary: false,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
...overrides,
|
|
35
|
+
} as TaskAttachment;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Slack inline completion output", () => {
|
|
39
|
+
test("posts only completed Slack tasks with substantive output and no primary artifact", () => {
|
|
40
|
+
expect(shouldPostInlineCompletionOutput(task(), [])).toBe(true);
|
|
41
|
+
expect(shouldPostInlineCompletionOutput(task(), [attachment()])).toBe(true);
|
|
42
|
+
expect(shouldPostInlineCompletionOutput(task(), [attachment({ isPrimary: true })])).toBe(false);
|
|
43
|
+
expect(shouldPostInlineCompletionOutput(task({ slackReplySent: true }), [])).toBe(false);
|
|
44
|
+
expect(shouldPostInlineCompletionOutput(task({ output: "done" }), [])).toBe(false);
|
|
45
|
+
expect(shouldPostInlineCompletionOutput(task({ slackChannelId: undefined }), [])).toBe(false);
|
|
46
|
+
expect(shouldPostInlineCompletionOutput(task({ status: "failed" }), [])).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("formats markdown output and keeps the Slack fallback text under 4000 chars", () => {
|
|
50
|
+
const text = formatInlineCompletionOutputText({
|
|
51
|
+
agentName: "Analyst",
|
|
52
|
+
taskId: TASK_ID,
|
|
53
|
+
output: `### Summary\n\n**Answer:** ${"This is a detailed prose finding. ".repeat(220)}`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(text.length).toBeLessThanOrEqual(MAX_INLINE_OUTPUT_MESSAGE_LENGTH);
|
|
57
|
+
expect(text).toContain("✅ *Analyst* completed with output");
|
|
58
|
+
expect(text).toContain("*Summary*");
|
|
59
|
+
expect(text).toContain("*Answer:*");
|
|
60
|
+
expect(text).not.toContain("###");
|
|
61
|
+
expect(text).not.toContain("**Answer:**");
|
|
62
|
+
expect(text).toContain("…(full output in task");
|
|
63
|
+
expect(text).toContain("|`abcdef12`>");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
closeDb,
|
|
5
5
|
createAgent,
|
|
6
6
|
createTaskExtended,
|
|
7
|
+
createUser,
|
|
7
8
|
getLatestActiveTaskInThread,
|
|
8
9
|
initDb,
|
|
9
10
|
} from "../be/db";
|
|
11
|
+
import { type IdentityActor, linkIdentity } from "../be/users";
|
|
10
12
|
import {
|
|
11
13
|
bufferThreadMessage,
|
|
12
14
|
getBufferMessageCount,
|
|
@@ -14,6 +16,8 @@ import {
|
|
|
14
16
|
isThreadBuffered,
|
|
15
17
|
} from "../slack/thread-buffer";
|
|
16
18
|
|
|
19
|
+
const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
|
|
20
|
+
|
|
17
21
|
const TEST_DB_PATH = "./test-slack-thread-buffer.sqlite";
|
|
18
22
|
|
|
19
23
|
beforeAll(() => {
|
|
@@ -125,6 +129,30 @@ describe("Slack thread buffer", () => {
|
|
|
125
129
|
expect(task!.slackChannelId).toBe(channelId);
|
|
126
130
|
expect(task!.slackThreadTs).toBe(threadTs);
|
|
127
131
|
});
|
|
132
|
+
|
|
133
|
+
test("in-body <@U…> mentions are rewritten via the identity primitive — resolved and unknown", async () => {
|
|
134
|
+
const channelId = "C601";
|
|
135
|
+
const threadTs = "6001.0001";
|
|
136
|
+
const linked = createUser({ name: "Luis", email: "luis-buf@example.com" });
|
|
137
|
+
// Slack user ids are uppercase-alphanumeric only (matches the
|
|
138
|
+
// `/<@([A-Z0-9]+)>/g` mention regex) — no underscores.
|
|
139
|
+
linkIdentity(linked.id, "slack", "U1000LINKED", SYSTEM_ACTOR);
|
|
140
|
+
|
|
141
|
+
bufferThreadMessage(
|
|
142
|
+
channelId,
|
|
143
|
+
threadTs,
|
|
144
|
+
"cc <@U1000LINKED> and <@U2000UNKNOWN>",
|
|
145
|
+
"U1",
|
|
146
|
+
"6001.0010",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
150
|
+
|
|
151
|
+
const task = getLatestActiveTaskInThread(channelId, threadTs);
|
|
152
|
+
expect(task).not.toBeNull();
|
|
153
|
+
expect(task!.task).toContain("<@U1000LINKED|Luis>");
|
|
154
|
+
expect(task!.task).toContain("<@U2000UNKNOWN> (unknown user)");
|
|
155
|
+
});
|
|
128
156
|
});
|
|
129
157
|
|
|
130
158
|
describe("flush sets dependsOn to latest active task in thread", () => {
|