@desplega.ai/agent-swarm 1.152.1 → 1.153.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 -1
- package/dist/{acp-adapter-4s7q25ft.js → acp-adapter-ahmjxhek.js} +4 -4
- package/dist/{actions-cs6rhmvc.js → actions-jzrfpkmn.js} +30 -26
- package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
- package/dist/{app-j54aq0dj.js → app-03na2vwb.js} +9 -6
- package/dist/{assistant-w1pgn7vf.js → assistant-hfkxvhgt.js} +33 -29
- package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
- package/dist/{boot-reembed-smy30z32.js → boot-reembed-c98xrad3.js} +8 -6
- package/dist/{boot-reembed-42vgkrrj.js → boot-reembed-zt3qm9xn.js} +9 -7
- package/dist/{boot-scrub-logs-b6s72fsw.js → boot-scrub-logs-jcqd8ksj.js} +6 -4
- package/dist/{claude-adapter-fh7rmcj9.js → claude-adapter-mgryg8g3.js} +17 -15
- package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
- package/dist/{claude-sdk-session-xhynatn9.js → claude-sdk-session-jjf4zss1.js} +17 -15
- package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
- package/dist/{cli-fm4ytsdk.js → cli-0d9nbqxf.js} +1 -1
- package/dist/cli-0x5qzvkg.js +4279 -0
- package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
- package/dist/{cli-wbyk465z.js → cli-1hwnb4hc.js} +1 -1
- package/dist/{cli-sakxd2rg.js → cli-3bcmp015.js} +5 -5
- package/dist/cli-3er6gcsn.js +1849 -0
- package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
- package/dist/{cli-sqmh7jj1.js → cli-5mbmvxnn.js} +5 -5
- package/dist/{cli-v9sk4dzw.js → cli-5taqtpze.js} +235 -8
- package/dist/{cli-dsyg66vt.js → cli-6ks9xf1n.js} +7 -5
- package/dist/cli-7g9s78jf.js +2827 -0
- package/dist/{cli-jtvs5m8q.js → cli-7pnz2v6s.js} +2 -2
- package/dist/{cli-jytf5mw9.js → cli-83wkg402.js} +2 -2
- package/dist/{cli-3pft2z6r.js → cli-a9scg8sv.js} +12 -10
- package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
- package/dist/{cli-ht5rs63n.js → cli-b2g6mzjc.js} +15 -6
- package/dist/{cli-3g61md6v.js → cli-bw0dfwkr.js} +1 -1
- package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
- package/dist/{cli-ks8a1cgq.js → cli-cdq08v3r.js} +2 -2
- package/dist/cli-cgj4hc12.js +18902 -0
- package/dist/cli-d6hk10xx.js +14075 -0
- package/dist/{cli-bdt7w021.js → cli-djvxhnc4.js} +31 -3324
- package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
- package/dist/{cli-b4dyetqt.js → cli-eazstkad.js} +2 -2
- package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
- package/dist/cli-h38x0qhg.js +6912 -0
- package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
- package/dist/{pi-mono-adapter-jcqt71cg.js → cli-ja7mrygn.js} +67343 -101083
- package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
- package/dist/{cli-y8tayczk.js → cli-m04pt18h.js} +2 -2
- package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
- package/dist/{cli-j4nv2wqy.js → cli-mzgyhxbe.js} +10 -4
- package/dist/{cli-wfve107r.js → cli-n0q9b7j4.js} +46 -1870
- package/dist/cli-n7h1kqpf.js +29 -0
- package/dist/{cli-ng4q6a9t.js → cli-nq0kgyxe.js} +7 -7
- package/dist/{cli-72tgtzr1.js → cli-qrqhc31j.js} +151 -125
- package/dist/{cli-vbk025fm.js → cli-r8tdds6s.js} +1 -1
- package/dist/{cli-ch38q5z4.js → cli-rj7de4b2.js} +6788 -2541
- package/dist/{cli-ftdjz1mk.js → cli-s5s1f2yn.js} +56 -40
- package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
- package/dist/{cli-7bwaerjt.js → cli-tdh4hjr2.js} +3 -2
- package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
- package/dist/{cli-kf8qp2vf.js → cli-vw03hg5t.js} +3 -2
- package/dist/{cli-gfez0qmk.js → cli-ybpp1srm.js} +6 -6
- package/dist/cli-yn0a417a.js +4069 -0
- package/dist/{cli-gxf2ebyz.js → cli-ysc1kbet.js} +2 -2
- package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
- package/dist/cli-zcsyfw2z.js +3335 -0
- package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
- package/dist/cli-zy5w9gpt.js +230 -0
- package/dist/cli.js +26 -34
- package/dist/{codex-adapter-k6yq95sn.js → codex-adapter-8548daap.js} +12 -11
- package/dist/{codex-session-runner-3rp84pk8.js → codex-session-runner-2dad8p4x.js} +12 -11
- package/dist/{commands-pbr5jzy5.js → commands-355dbpa8.js} +6 -4
- package/dist/{db-z0xaa59a.js → db-4jjvwz1s.js} +6 -4
- package/dist/{dispatcher-pwpgsj0x.js → dispatcher-ca590nkz.js} +29 -25
- package/dist/dsh-adapter-rgvj6ykp.js +19 -0
- package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
- package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-xtw7tggr.js} +33 -73
- package/dist/{google-vertex-fr665tqs.js → google-vertex-zdqpms6n.js} +38 -68
- package/dist/{handlers-5mzk4mbg.js → handlers-2y6dqpvf.js} +32 -28
- package/dist/{hook-8bz33zee.js → hook-0s8knepr.js} +11 -9
- package/dist/{hook-kbh84zb1.js → hook-k0gec254.js} +12 -10
- package/dist/{http-pppxg5vr.js → http-jp2nb4zw.js} +300 -237
- package/dist/{index-k1hq1qt6.js → index-0wzg3sp3.js} +39 -24
- package/dist/{index-68bd9b5f.js → index-3mq6ce93.js} +29 -25
- package/dist/{index-z0zwjtkn.js → index-42s7eezq.js} +29 -25
- package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
- package/dist/{index-y0te09pn.js → index-b56xkbc7.js} +8 -6
- package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
- package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
- package/dist/{index-mjpxsr6k.js → index-s37dbevy.js} +30 -26
- package/dist/index-w097eg0x.js +5 -0
- package/dist/jiti-loader-b4837mtm.js +34 -0
- package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
- package/dist/{keepalive-xvmwnh1d.js → keepalive-9pfw7t02.js} +8 -6
- package/dist/{lead-pf8nvxt8.js → lead-7vcmcdq4.js} +44 -39
- package/dist/{maintenance-jk4q0wgg.js → maintenance-x277jbpd.js} +9 -7
- package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
- package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
- package/dist/{oauth-refresh-sweep-6207x9sj.js → oauth-refresh-sweep-5fwa7th2.js} +7 -5
- package/dist/{onboard-b0csgm0q.js → onboard-17sv0f51.js} +103 -35
- package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
- package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
- package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
- package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
- package/dist/{otel-impl-b6rfehth.js → otel-impl-srjy4be4.js} +4 -4
- package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
- package/dist/pi-mono-adapter-946a0sdd.js +1316 -0
- package/dist/{pricing-refresh-4e201wtd.js → pricing-refresh-vetcm563.js} +8 -6
- package/dist/{rbac-roles-mkc5n5qa.js → rbac-roles-cfzgr6r1.js} +8 -6
- package/dist/{rbac-roles-zp6e4geh.js → rbac-roles-cmxaddmy.js} +7 -5
- package/dist/{render-v2-4fazz9tf.js → render-v2-f9hzff72.js} +9 -6
- package/dist/{seed-pricing-thxpn9vn.js → seed-pricing-jggk6rsy.js} +7 -5
- package/dist/{setup-wb0z8cat.js → setup-8mfceb9t.js} +4 -5
- package/dist/virtual-modules-ax661xp0.js +54 -0
- package/dist/{work-objects-h3498kwe.js → work-objects-vdsdcczj.js} +32 -28
- package/dist/{worker-2m2tzxp3.js → worker-cd0nhyv1.js} +44 -39
- package/openapi.json +36 -14
- package/package.json +6 -6
- package/src/be/automation-preflight.ts +6 -4
- package/src/be/extensions/db.ts +35 -4
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +213 -0
- package/src/be/rbac-audit.ts +2 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/onboard/compose-generator.ts +6 -1
- package/src/commands/onboard/env-generator.ts +6 -1
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/integration-slack.tsx +56 -16
- package/src/commands/onboard/steps/review.tsx +5 -0
- package/src/commands/onboard/types.ts +6 -0
- package/src/commands/onboard.tsx +5 -1
- package/src/commands/provider-credentials.ts +14 -2
- package/src/extensions/lifecycle.ts +15 -21
- package/src/http/agents.ts +2 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +5 -3
- package/src/http/extensions.ts +108 -34
- package/src/http/status.ts +22 -10
- package/src/prompts/base-prompt.ts +15 -7
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/codex-models.ts +15 -1
- package/src/providers/dsh-adapter.ts +248 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/modelsdev-reasoning.json +50 -0
- package/src/rbac/legacy-policy.ts +15 -1
- package/src/rbac/types.ts +2 -0
- package/src/server.ts +7 -1
- package/src/slack/app.ts +32 -10
- package/src/slack/assistant.ts +10 -1
- package/src/slack/config.ts +29 -0
- package/src/slack/handlers.ts +10 -1
- package/src/slack/thread-buffer.ts +14 -3
- package/src/telemetry.ts +2 -3
- package/src/tests/agents-harness-provider.test.ts +19 -0
- package/src/tests/agents-list-model-display.test.ts +60 -1
- package/src/tests/base-prompt.test.ts +25 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-gpt6-release.test.ts +97 -0
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/dsh-adapter.test.ts +276 -0
- package/src/tests/e2e-nightly-report.test.ts +3 -3
- package/src/tests/e2e-redact.test.ts +2 -0
- package/src/tests/env-flag.test.ts +6 -0
- package/src/tests/extensions-db.test.ts +41 -0
- package/src/tests/extensions-http.test.ts +148 -4
- package/src/tests/extensions-lifecycle.test.ts +18 -0
- package/src/tests/extensions-mcp-tools.test.ts +9 -5
- package/src/tests/internal-ai/complete-structured.test.ts +32 -9
- package/src/tests/internal-ai/credentials.test.ts +4 -4
- package/src/tests/internal-ai/models.test.ts +20 -0
- package/src/tests/migration-046-budgets.test.ts +2 -0
- package/src/tests/model-control.test.ts +2 -2
- package/src/tests/onboard-compose.test.ts +14 -0
- package/src/tests/onboard-env.test.ts +17 -0
- package/src/tests/onboard-manifest.test.ts +12 -0
- package/src/tests/opus-5-5-registry.test.ts +48 -0
- package/src/tests/pricing-refresh.test.ts +1 -1
- package/src/tests/pricing-routes.test.ts +15 -3
- package/src/tests/provider-registration.test.ts +5 -2
- package/src/tests/rbac-admission-e2e.test.ts +54 -0
- package/src/tests/rbac-e2e-helpers.ts +2 -0
- package/src/tests/rbac-engine.test.ts +32 -0
- package/src/tests/rbac-wire-e2e.test.ts +33 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/reload-config.test.ts +33 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/slack-assistant.test.ts +134 -0
- package/src/tests/slack-config.test.ts +136 -0
- package/src/tests/slack-mode-lifecycle.test.ts +69 -0
- package/src/tests/status.test.ts +22 -2
- package/src/tests/task-tool-preload-mcp.test.ts +3 -1
- package/src/tests/telemetry-init.test.ts +18 -0
- package/src/tests/workflow-llm.test.ts +3 -3
- package/src/tools/extension-common.ts +4 -0
- package/src/tools/extension-install.ts +1 -1
- package/src/tools/extension-list.ts +4 -3
- package/src/types.ts +17 -8
- package/src/utils/context-window.ts +1 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/internal-ai/complete-structured.ts +8 -0
- package/src/utils/internal-ai/models.ts +5 -5
- package/templates/schedules/daily-status-report/content.md +25 -12
- package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
- package/templates/skills/swarm-extensions/content.md +9 -5
- package/dist/cli-4wppfkcm.js +0 -16384
- package/dist/cli-55qrx3q8.js +0 -6041
- package/dist/cli-cg4vnyh4.js +0 -305
- package/dist/cli-jyetr64j.js +0 -52
- package/dist/cli-pp9ax9pa.js +0 -520
- package/dist/cli-qa66qaf0.js +0 -6076
- package/dist/cli-qghpvca2.js +0 -590
- package/dist/cli-rembwgt8.js +0 -861
- package/dist/cli-tcvj7j2r.js +0 -1971
- package/dist/cli-vfcth11k.js +0 -36
- package/dist/cli-w1d1bgkk.js +0 -1422
- package/dist/cli-y6x0gqtg.js +0 -5977
- package/dist/es5-tj3tn4jj.js +0 -844
- package/dist/index-1kq5hw2p.js +0 -467
- package/dist/index-1p8wdstt.js +0 -7
- package/dist/index-8fdjzb2f.js +0 -100
- package/dist/index-bxmwzbgw.js +0 -365
- package/dist/index-nzerxd5z.js +0 -7
- package/dist/index-pyymnzat.js +0 -1168
- package/dist/index-qv6atbab.js +0 -619
- package/dist/index-rja32qa7.js +0 -801
- package/dist/index-tzg5srfp.js +0 -211
- package/dist/index-vhncw983.js +0 -90
- package/dist/index-xk036zsk.js +0 -859
- package/dist/index-zyazbzcb.js +0 -6
- package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { Database } from "bun:sqlite";
|
|
11
11
|
import { afterAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
13
14
|
import {
|
|
14
15
|
api,
|
|
15
16
|
makeScratchDir,
|
|
@@ -483,3 +484,56 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
483
484
|
);
|
|
484
485
|
});
|
|
485
486
|
});
|
|
487
|
+
|
|
488
|
+
test.each(["true", "false"])("worker extension ownership with RBAC_ENABLED=%s", async (enabled) => {
|
|
489
|
+
const scratch = await makeScratchDir();
|
|
490
|
+
const instance = await spawnSwarmServer({
|
|
491
|
+
dbPath: join(scratch, "extension-admission.sqlite"),
|
|
492
|
+
logPath: join(scratch, "extension-admission.log"),
|
|
493
|
+
env: { RBAC_ENABLED: enabled },
|
|
494
|
+
});
|
|
495
|
+
try {
|
|
496
|
+
await registerAgent(instance.base, WORKER_A, "extension-owner", false);
|
|
497
|
+
const bundle = await loadBundleFixture("minimal");
|
|
498
|
+
const installed = await api(instance.base, "POST", "/api/extensions/install", {
|
|
499
|
+
agentId: WORKER_A,
|
|
500
|
+
body: bundle,
|
|
501
|
+
});
|
|
502
|
+
expect(installed.status).toBe(200);
|
|
503
|
+
expect(installed.body.extension).toMatchObject({
|
|
504
|
+
createdByAgentId: WORKER_A,
|
|
505
|
+
enabled: false,
|
|
506
|
+
status: "disabled",
|
|
507
|
+
});
|
|
508
|
+
const path = `/api/extensions/${installed.body.extension.id}`;
|
|
509
|
+
for (const operation of ["enable", "disable", "activate-version"]) {
|
|
510
|
+
expect(
|
|
511
|
+
(
|
|
512
|
+
await api(instance.base, "POST", `${path}/${operation}`, {
|
|
513
|
+
agentId: WORKER_A,
|
|
514
|
+
body: { version: 1 },
|
|
515
|
+
})
|
|
516
|
+
).status,
|
|
517
|
+
).toBe(403);
|
|
518
|
+
}
|
|
519
|
+
const operatorBundle = { ...bundle, manifest: { ...bundle.manifest, name: "operator-owned" } };
|
|
520
|
+
const operator = await api(instance.base, "POST", "/api/extensions/install", {
|
|
521
|
+
body: operatorBundle,
|
|
522
|
+
});
|
|
523
|
+
expect(operator.status).toBe(200);
|
|
524
|
+
expect(operator.body.extension.createdByAgentId).toBeNull();
|
|
525
|
+
for (const method of ["PATCH", "DELETE"]) {
|
|
526
|
+
expect(
|
|
527
|
+
(
|
|
528
|
+
await api(instance.base, method, `/api/extensions/${operator.body.extension.id}`, {
|
|
529
|
+
agentId: WORKER_A,
|
|
530
|
+
...(method === "PATCH" ? { body: { priority: 1 } } : {}),
|
|
531
|
+
})
|
|
532
|
+
).status,
|
|
533
|
+
).toBe(403);
|
|
534
|
+
}
|
|
535
|
+
} finally {
|
|
536
|
+
await instance.stop();
|
|
537
|
+
await removeScratchDir(scratch);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
@@ -78,6 +78,8 @@ export async function spawnSwarmServer(opts: {
|
|
|
78
78
|
API_KEY: E2E_API_KEY,
|
|
79
79
|
AGENT_SWARM_API_KEY: E2E_API_KEY,
|
|
80
80
|
PORT: String(port),
|
|
81
|
+
// MCP proxy tools must call this scratch server, never an inherited live URL.
|
|
82
|
+
MCP_BASE_URL: `http://localhost:${port}`,
|
|
81
83
|
SLACK_DISABLE: "true",
|
|
82
84
|
GITHUB_DISABLE: "true",
|
|
83
85
|
JIRA_DISABLE: "true",
|
|
@@ -255,6 +255,38 @@ describe("lead-or-operator-or-user verbs", () => {
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
describe("extension ownership", () => {
|
|
259
|
+
test("workers create extensions and write only their own existing resource", () => {
|
|
260
|
+
const worker = { kind: "agent", agentId: "extension-owner", isLead: false } as const;
|
|
261
|
+
for (const resource of [
|
|
262
|
+
{ kind: "extension" },
|
|
263
|
+
{ kind: "extension", extensionId: "ext-1", createdByAgentId: worker.agentId },
|
|
264
|
+
] as const) {
|
|
265
|
+
expect(
|
|
266
|
+
can({ principal: worker, verb: "extension.write", resource, source: "http" }).allow,
|
|
267
|
+
).toBe(true);
|
|
268
|
+
}
|
|
269
|
+
for (const createdByAgentId of [null, "other-agent"]) {
|
|
270
|
+
expect(
|
|
271
|
+
can({
|
|
272
|
+
principal: worker,
|
|
273
|
+
verb: "extension.write",
|
|
274
|
+
resource: { kind: "extension", extensionId: "ext-1", createdByAgentId },
|
|
275
|
+
source: "http",
|
|
276
|
+
}).allow,
|
|
277
|
+
).toBe(false);
|
|
278
|
+
}
|
|
279
|
+
expect(
|
|
280
|
+
can({
|
|
281
|
+
principal: { ...worker, agentId: "" },
|
|
282
|
+
verb: "extension.write",
|
|
283
|
+
resource: { kind: "extension" },
|
|
284
|
+
source: "http",
|
|
285
|
+
}).allow,
|
|
286
|
+
).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
258
290
|
describe("lead-or-resource-owner verbs", () => {
|
|
259
291
|
const expected: Expected = {
|
|
260
292
|
lead: true,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { Database } from "bun:sqlite";
|
|
19
19
|
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
21
22
|
import {
|
|
22
23
|
api,
|
|
23
24
|
LEAD,
|
|
@@ -119,6 +120,38 @@ afterAll(async () => {
|
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
describe("MCP gate matrix", () => {
|
|
123
|
+
test("worker MCP installs retain ownership and lifecycle calls remain lead-only", async () => {
|
|
124
|
+
const bundle = await loadBundleFixture("minimal");
|
|
125
|
+
bundle.manifest.name = "worker-owned-wire";
|
|
126
|
+
const installed = await mcpCall(base, WORKER_A, sidA, "extension-install", bundle);
|
|
127
|
+
expect(installed.structuredContent).toMatchObject({ success: true });
|
|
128
|
+
expect(installed.structuredContent).toMatchObject({
|
|
129
|
+
createdByAgentId: WORKER_A,
|
|
130
|
+
enabled: false,
|
|
131
|
+
status: "disabled",
|
|
132
|
+
});
|
|
133
|
+
expectRow({ type: "agent", id: WORKER_A }, "extension.write", "allow", null, "http");
|
|
134
|
+
const id = installed.structuredContent.id;
|
|
135
|
+
const listed = await mcpCall(base, WORKER_A, sidA, "extension-list", {});
|
|
136
|
+
expect(listed.structuredContent.extensions).toContainEqual(
|
|
137
|
+
expect.objectContaining({ id, createdByAgentId: WORKER_A }),
|
|
138
|
+
);
|
|
139
|
+
for (const tool of ["extension-enable", "extension-disable", "extension-activate-version"]) {
|
|
140
|
+
const denied = await mcpCall(base, WORKER_A, sidA, tool, {
|
|
141
|
+
id,
|
|
142
|
+
...(tool === "extension-activate-version" ? { version: 1 } : {}),
|
|
143
|
+
});
|
|
144
|
+
expect(denied.structuredContent.success).toBe(false);
|
|
145
|
+
expectRow(
|
|
146
|
+
{ type: "agent", id: WORKER_A },
|
|
147
|
+
"extension.activate",
|
|
148
|
+
"deny",
|
|
149
|
+
"requires operator/user authentication, or a lead with EXTENSION_ALLOW_LEAD_ACTIVATION enabled",
|
|
150
|
+
"http",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
122
155
|
test("config: set/delete lead-gated; includeSecrets masks for non-lead (DES-445 follow-up)", async () => {
|
|
123
156
|
// set-config (non-credential key): worker denied, lead allowed.
|
|
124
157
|
const setWorker = await mcpCall(base, WORKER_A, sidA, "set-config", {
|
|
@@ -22,6 +22,14 @@ describe("REASONING_EFFORT_LEVELS", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
describe("reasoningCapability — cache-sourced levels", () => {
|
|
25
|
+
test("Claude Opus 5.5 exposes supported CLI effort levels", () => {
|
|
26
|
+
expect(reasoningCapability("claude", "claude-opus-5-5").levels).toEqual([
|
|
27
|
+
"low",
|
|
28
|
+
"medium",
|
|
29
|
+
"high",
|
|
30
|
+
"xhigh",
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
25
33
|
test("claude claude-opus-4-8: levels come from reasoning_options.effort, not the fallback", () => {
|
|
26
34
|
const cap = reasoningCapability("claude", "claude-opus-4-8");
|
|
27
35
|
expect(cap.supported).toBe(true);
|
|
@@ -323,6 +323,39 @@ describe("reload-config", () => {
|
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
test("reload applies HTTP mode without reporting a Slack socket as reinitialized", async () => {
|
|
327
|
+
const keys = [
|
|
328
|
+
"SLACK_MODE",
|
|
329
|
+
"SLACK_DISABLE",
|
|
330
|
+
"SLACK_BOT_TOKEN",
|
|
331
|
+
"SLACK_SIGNING_SECRET",
|
|
332
|
+
] as const;
|
|
333
|
+
const originals = new Map(keys.map((key) => [key, process.env[key]]));
|
|
334
|
+
process.env.SLACK_DISABLE = "false";
|
|
335
|
+
process.env.SLACK_BOT_TOKEN = "xoxb-test";
|
|
336
|
+
process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
|
|
337
|
+
const config = await upsertSwarmConfig({
|
|
338
|
+
scope: "global",
|
|
339
|
+
key: "SLACK_MODE",
|
|
340
|
+
value: "http",
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const result = await reloadGlobalConfigsAndIntegrations();
|
|
345
|
+
expect(result.keysUpdated).toContain("SLACK_MODE");
|
|
346
|
+
expect(result.integrationsReinitialized).not.toContain("slack");
|
|
347
|
+
expect(process.env.SLACK_MODE).toBe("http");
|
|
348
|
+
} finally {
|
|
349
|
+
await deleteSwarmConfig(config.id);
|
|
350
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
351
|
+
for (const key of keys) {
|
|
352
|
+
const value = originals.get(key);
|
|
353
|
+
if (value === undefined) delete process.env[key];
|
|
354
|
+
else process.env[key] = value;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
326
359
|
// Regression: injection used to be one-way. Deleting a global row left the
|
|
327
360
|
// previously-injected value live in process.env until the process restarted,
|
|
328
361
|
// so "reset to default" in the dashboard silently did nothing.
|
|
@@ -133,6 +133,7 @@ describe("normalizeModelKey()", () => {
|
|
|
133
133
|
expect(normalizeModelKey("claude", "claude-opus-4-7")).toBe("claude-opus-4-7");
|
|
134
134
|
expect(normalizeModelKey("claude", "claude-fable-5-1")).toBe("claude-fable-5-1");
|
|
135
135
|
expect(normalizeModelKey("claude", "claude-mythos-5-1")).toBe("claude-mythos-5-1");
|
|
136
|
+
expect(normalizeModelKey("claude", "claude-opus-5-5")).toBe("claude-opus-5-5");
|
|
136
137
|
expect(normalizeModelKey("claude", "claude-opus-5")).toBe("claude-opus-5");
|
|
137
138
|
expect(normalizeModelKey("claude", "claude-fable-5")).toBe("claude-fable-5");
|
|
138
139
|
expect(normalizeModelKey("claude", "claude-mythos-5")).toBe("claude-mythos-5");
|
|
@@ -6,9 +6,15 @@ import {
|
|
|
6
6
|
createTaskExtended,
|
|
7
7
|
getAgentWorkingOnThread,
|
|
8
8
|
getLeadAgent,
|
|
9
|
+
getMostRecentTaskInThread,
|
|
9
10
|
getTaskById,
|
|
10
11
|
initDb,
|
|
11
12
|
} from "../be/db";
|
|
13
|
+
import { createAssistant } from "../slack/assistant";
|
|
14
|
+
import { getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
|
|
15
|
+
|
|
16
|
+
/** The single function Bolt's `Assistant` stores for the `message` event — not part of its public type. */
|
|
17
|
+
type AssistantUserMessageHandler = (args: Record<string, unknown>) => Promise<void>;
|
|
12
18
|
|
|
13
19
|
process.env.SLACK_RENDER_V2 = "false";
|
|
14
20
|
|
|
@@ -121,6 +127,134 @@ describe("assistant userMessage routing — follow-up (working agent exists)", (
|
|
|
121
127
|
});
|
|
122
128
|
});
|
|
123
129
|
|
|
130
|
+
describe("assistant DM path self-mention rendering — production ingestion path", () => {
|
|
131
|
+
// Drives `createAssistant()`'s real `userMessage` handler end-to-end (the
|
|
132
|
+
// production DM path), rather than calling `rewriteSlackMentions` in
|
|
133
|
+
// isolation. It must fail if the fix at src/slack/assistant.ts (the
|
|
134
|
+
// `cachedBotUserId ?? undefined` argument to `rewriteSlackMentions`) is
|
|
135
|
+
// reverted.
|
|
136
|
+
const BOT_USER_ID = "U0PRODBOT99";
|
|
137
|
+
const OTHER_USER_ID = "U0PRODOTHR1";
|
|
138
|
+
const CHANNEL_ID = "D_PROD_DM_SELF_MENTION";
|
|
139
|
+
const THREAD_TS = "5551112223.000001";
|
|
140
|
+
|
|
141
|
+
test("resolves the bot's own mention and leaves another user's mention correct", async () => {
|
|
142
|
+
const assistant = createAssistant();
|
|
143
|
+
const userMessage = (assistant as unknown as { userMessage: AssistantUserMessageHandler[] })
|
|
144
|
+
.userMessage[0];
|
|
145
|
+
|
|
146
|
+
const client = {
|
|
147
|
+
auth: { test: async () => ({ user_id: BOT_USER_ID }) },
|
|
148
|
+
reactions: { add: async () => ({}) },
|
|
149
|
+
chat: { postMessage: async () => ({}) },
|
|
150
|
+
users: { info: async () => ({ user: undefined }) },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const message = {
|
|
154
|
+
channel: CHANNEL_ID,
|
|
155
|
+
ts: THREAD_TS,
|
|
156
|
+
thread_ts: THREAD_TS,
|
|
157
|
+
text: `<@${BOT_USER_ID}> can you loop in <@${OTHER_USER_ID}> on this?`,
|
|
158
|
+
user: "U_PROD_REQUESTER",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
await userMessage({
|
|
162
|
+
message,
|
|
163
|
+
body: { event_id: "Ev_PROD_DM_SELF_MENTION_1" },
|
|
164
|
+
say: async () => {},
|
|
165
|
+
setStatus: async () => ({}),
|
|
166
|
+
setTitle: async () => ({}),
|
|
167
|
+
getThreadContext: async () => undefined,
|
|
168
|
+
client,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const task = await getMostRecentTaskInThread(CHANNEL_ID, THREAD_TS);
|
|
172
|
+
expect(task).toBeDefined();
|
|
173
|
+
expect(task!.task).toBe(
|
|
174
|
+
`<@${BOT_USER_ID}> (that's you) can you loop in <@${OTHER_USER_ID}> (unknown user) on this?`,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("assistant DM path self-mention rendering — buffered follow-up path (ADDITIVE_SLACK)", () => {
|
|
180
|
+
// With ADDITIVE_SLACK=true and an active agent already on the DM thread,
|
|
181
|
+
// userMessage buffers the message and returns before the direct
|
|
182
|
+
// rewriteSlackMentions(..., cachedBotUserId) call runs. The bot ID must
|
|
183
|
+
// still reach the later flush in thread-buffer.ts, or the bot's own
|
|
184
|
+
// mention renders as "(unknown user)" instead of "(that's you)".
|
|
185
|
+
// `cachedBotUserId` in assistant.ts is a module-level singleton resolved
|
|
186
|
+
// once per process, so this must reuse the ID the earlier describe block
|
|
187
|
+
// already cached via auth.test() — a fresh ID here would make this
|
|
188
|
+
// message look like it mentions "someone else" and never reach the
|
|
189
|
+
// buffered branch at all.
|
|
190
|
+
const BOT_USER_ID = "U0PRODBOT99";
|
|
191
|
+
const OTHER_USER_ID = "U0BUFOTHR01";
|
|
192
|
+
const CHANNEL_ID = "D_BUF_DM_SELF_MENTION";
|
|
193
|
+
const THREAD_TS = "5552223334.000001";
|
|
194
|
+
|
|
195
|
+
const originalAdditiveSlack = process.env.ADDITIVE_SLACK;
|
|
196
|
+
|
|
197
|
+
beforeAll(() => {
|
|
198
|
+
process.env.ADDITIVE_SLACK = "true";
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterAll(() => {
|
|
202
|
+
if (originalAdditiveSlack === undefined) delete process.env.ADDITIVE_SLACK;
|
|
203
|
+
else process.env.ADDITIVE_SLACK = originalAdditiveSlack;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("bot's own mention resolves through the buffered flush", async () => {
|
|
207
|
+
const worker = await createAgent({ name: "BufferedDmWorker", isLead: false, status: "idle" });
|
|
208
|
+
await createTaskExtended("original task in buffered DM thread", {
|
|
209
|
+
agentId: worker.id,
|
|
210
|
+
source: "slack",
|
|
211
|
+
slackChannelId: CHANNEL_ID,
|
|
212
|
+
slackThreadTs: THREAD_TS,
|
|
213
|
+
slackUserId: "U_BUF_REQUESTER",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const assistant = createAssistant();
|
|
217
|
+
const userMessage = (assistant as unknown as { userMessage: AssistantUserMessageHandler[] })
|
|
218
|
+
.userMessage[0];
|
|
219
|
+
|
|
220
|
+
const client = {
|
|
221
|
+
auth: { test: async () => ({ user_id: BOT_USER_ID }) },
|
|
222
|
+
reactions: { add: async () => ({}) },
|
|
223
|
+
chat: { postMessage: async () => ({}) },
|
|
224
|
+
users: { info: async () => ({ user: undefined }) },
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const message = {
|
|
228
|
+
channel: CHANNEL_ID,
|
|
229
|
+
ts: "5552223334.000002",
|
|
230
|
+
thread_ts: THREAD_TS,
|
|
231
|
+
text: `<@${BOT_USER_ID}> can you loop in <@${OTHER_USER_ID}> on this?`,
|
|
232
|
+
user: "U_BUF_REQUESTER",
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
await userMessage({
|
|
236
|
+
message,
|
|
237
|
+
body: { event_id: "Ev_BUF_DM_SELF_MENTION_1" },
|
|
238
|
+
say: async () => {},
|
|
239
|
+
setStatus: async () => ({}),
|
|
240
|
+
setTitle: async () => ({}),
|
|
241
|
+
getThreadContext: async () => undefined,
|
|
242
|
+
client,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Buffered, not turned into a task synchronously.
|
|
246
|
+
expect(getBufferMessageCount(`${CHANNEL_ID}:${THREAD_TS}`)).toBe(1);
|
|
247
|
+
|
|
248
|
+
await instantFlush(`${CHANNEL_ID}:${THREAD_TS}`);
|
|
249
|
+
|
|
250
|
+
const flushed = await getMostRecentTaskInThread(CHANNEL_ID, THREAD_TS);
|
|
251
|
+
expect(flushed).toBeDefined();
|
|
252
|
+
expect(flushed!.task).toContain(`<@${BOT_USER_ID}> (that's you)`);
|
|
253
|
+
expect(flushed!.task).not.toContain(`<@${BOT_USER_ID}> (unknown user)`);
|
|
254
|
+
expect(flushed!.task).toContain(`<@${OTHER_USER_ID}> (unknown user)`);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
124
258
|
describe("assistant thread title truncation", () => {
|
|
125
259
|
test("short messages stay as-is", () => {
|
|
126
260
|
const text = "Check agent status";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { startSlackMock, stopSlackMock } from "../../scripts/e2e/slack";
|
|
3
|
+
import { closeDb, initDb } from "../be/db";
|
|
4
|
+
import { getEnabledCapabilities } from "../server";
|
|
5
|
+
import { getSlackApp, startSlackApp, stopSlackApp } from "../slack/app";
|
|
6
|
+
import { getSlackConfiguration, isSlackConfigured } from "../slack/config";
|
|
7
|
+
|
|
8
|
+
const SLACK_ENV_KEYS = [
|
|
9
|
+
"NODE_ENV",
|
|
10
|
+
"SLACK_ALLOW_DEV_SOCKET_MODE",
|
|
11
|
+
"CAPABILITIES",
|
|
12
|
+
"SLACK_MODE",
|
|
13
|
+
"SLACK_DISABLE",
|
|
14
|
+
"SLACK_BOT_TOKEN",
|
|
15
|
+
"SLACK_APP_TOKEN",
|
|
16
|
+
"SLACK_SIGNING_SECRET",
|
|
17
|
+
"SLACK_API_URL",
|
|
18
|
+
] as const;
|
|
19
|
+
const originalEnv = new Map(SLACK_ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
for (const key of SLACK_ENV_KEYS) delete process.env[key];
|
|
23
|
+
process.env.NODE_ENV = "test";
|
|
24
|
+
process.env.SLACK_DISABLE = "false";
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await stopSlackApp();
|
|
29
|
+
for (const key of SLACK_ENV_KEYS) {
|
|
30
|
+
const value = originalEnv.get(key);
|
|
31
|
+
if (value === undefined) delete process.env[key];
|
|
32
|
+
else process.env[key] = value;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("Slack transport configuration", () => {
|
|
37
|
+
test("defaults to Socket Mode and requires the existing token pair", () => {
|
|
38
|
+
expect(getSlackConfiguration({})).toEqual({
|
|
39
|
+
disabled: false,
|
|
40
|
+
mode: "socket",
|
|
41
|
+
missingCredentials: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
|
|
42
|
+
});
|
|
43
|
+
expect(getEnabledCapabilities()).toContain("slack");
|
|
44
|
+
expect(isSlackConfigured({ SLACK_BOT_TOKEN: "xoxb-test", SLACK_APP_TOKEN: "xapp-test" })).toBe(
|
|
45
|
+
true,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("HTTP mode requires bot and signing credentials without an app token", () => {
|
|
50
|
+
const env = {
|
|
51
|
+
SLACK_MODE: "http",
|
|
52
|
+
SLACK_BOT_TOKEN: "xoxb-test",
|
|
53
|
+
SLACK_SIGNING_SECRET: "synthetic-signing-secret",
|
|
54
|
+
};
|
|
55
|
+
expect(getSlackConfiguration(env)).toEqual({
|
|
56
|
+
disabled: false,
|
|
57
|
+
mode: "http",
|
|
58
|
+
missingCredentials: [],
|
|
59
|
+
});
|
|
60
|
+
expect(isSlackConfigured(env)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("disabled and invalid configurations fail closed", () => {
|
|
64
|
+
expect(
|
|
65
|
+
isSlackConfigured({
|
|
66
|
+
SLACK_DISABLE: "true",
|
|
67
|
+
SLACK_BOT_TOKEN: "xoxb-test",
|
|
68
|
+
SLACK_APP_TOKEN: "xapp-test",
|
|
69
|
+
}),
|
|
70
|
+
).toBe(false);
|
|
71
|
+
expect(
|
|
72
|
+
getSlackConfiguration({
|
|
73
|
+
SLACK_MODE: "webhook",
|
|
74
|
+
SLACK_BOT_TOKEN: "xoxb-test",
|
|
75
|
+
SLACK_APP_TOKEN: "xapp-test",
|
|
76
|
+
}),
|
|
77
|
+
).toMatchObject({ mode: null });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("HTTP selection remains unavailable and never creates a socket app", async () => {
|
|
81
|
+
process.env.SLACK_MODE = "http";
|
|
82
|
+
expect(getEnabledCapabilities()).not.toContain("slack");
|
|
83
|
+
process.env.SLACK_BOT_TOKEN = "xoxb-test";
|
|
84
|
+
process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
|
|
85
|
+
delete process.env.SLACK_APP_TOKEN;
|
|
86
|
+
|
|
87
|
+
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
88
|
+
try {
|
|
89
|
+
expect(await startSlackApp()).toBe(false);
|
|
90
|
+
expect(getSlackApp()).toBeNull();
|
|
91
|
+
expect(error).toHaveBeenCalledWith(
|
|
92
|
+
"[Slack] HTTP mode is configured but unavailable until the HTTP receiver is installed",
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
error.mockRestore();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test.each([
|
|
101
|
+
"disabled",
|
|
102
|
+
"invalid mode",
|
|
103
|
+
"missing credentials",
|
|
104
|
+
"HTTP unavailable",
|
|
105
|
+
"socket blocked",
|
|
106
|
+
])("failed init is retryable without stop: %s", async (reason) => {
|
|
107
|
+
const slack = await startSlackMock(false);
|
|
108
|
+
initDb(":memory:");
|
|
109
|
+
try {
|
|
110
|
+
Object.assign(process.env, slack.mock.env);
|
|
111
|
+
process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
|
|
112
|
+
if (reason === "disabled") process.env.SLACK_DISABLE = "true";
|
|
113
|
+
if (reason === "invalid mode") process.env.SLACK_MODE = "invalid";
|
|
114
|
+
if (reason === "missing credentials") delete process.env.SLACK_APP_TOKEN;
|
|
115
|
+
if (reason === "HTTP unavailable") process.env.SLACK_MODE = "http";
|
|
116
|
+
if (reason === "socket blocked") process.env.NODE_ENV = "development";
|
|
117
|
+
|
|
118
|
+
expect(await startSlackApp()).toBe(false);
|
|
119
|
+
expect(getSlackApp()).toBeNull();
|
|
120
|
+
expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(0);
|
|
121
|
+
|
|
122
|
+
Object.assign(process.env, slack.mock.env);
|
|
123
|
+
process.env.SLACK_DISABLE = "false";
|
|
124
|
+
process.env.SLACK_MODE = "socket";
|
|
125
|
+
process.env.NODE_ENV = "test";
|
|
126
|
+
// Deliberately retry without stopSlackApp: failed initialization must not latch.
|
|
127
|
+
expect(await startSlackApp()).toBe(true);
|
|
128
|
+
await slack.mock.waitForConnection(10_000);
|
|
129
|
+
expect(slack.mock.connectionCount).toBe(1);
|
|
130
|
+
expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(1);
|
|
131
|
+
} finally {
|
|
132
|
+
await stopSlackApp();
|
|
133
|
+
await stopSlackMock(slack, false);
|
|
134
|
+
closeDb();
|
|
135
|
+
}
|
|
136
|
+
}, 20_000);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createApiClient, pollUntil } from "../../scripts/e2e/http";
|
|
3
|
+
import { startSlackMock, stopSlackMock } from "../../scripts/e2e/slack";
|
|
4
|
+
import { type Sut, startSut, stopSut } from "../../scripts/e2e/sut";
|
|
5
|
+
|
|
6
|
+
test("config reload disconnects a live socket for HTTP and can return to Socket Mode", async () => {
|
|
7
|
+
const slack = await startSlackMock(false);
|
|
8
|
+
let sut: Sut | undefined;
|
|
9
|
+
try {
|
|
10
|
+
sut = await startSut(false, slack.mock.env, {
|
|
11
|
+
SLACK_SIGNING_SECRET: "synthetic-signing-secret",
|
|
12
|
+
HEARTBEAT_DISABLE: "true",
|
|
13
|
+
});
|
|
14
|
+
await slack.mock.waitForConnection(10_000);
|
|
15
|
+
expect(slack.mock.connectionCount).toBe(1);
|
|
16
|
+
const opened = slack.mock.apiCalls("apps.connections.open").length;
|
|
17
|
+
const api = createApiClient(sut.baseUrl, sut.apiKey);
|
|
18
|
+
const http = await api("PUT", "/api/config", {
|
|
19
|
+
body: { scope: "global", key: "SLACK_MODE", value: "http" },
|
|
20
|
+
});
|
|
21
|
+
expect(http.status).toBe(200);
|
|
22
|
+
expect(await pollUntil(() => slack.mock.connectionCount === 0, 10_000, 50)).toBe(true);
|
|
23
|
+
const reload = await api("POST", "/api/config/reload");
|
|
24
|
+
expect(reload.status).toBe(200);
|
|
25
|
+
expect(reload.json).toMatchObject({ success: true });
|
|
26
|
+
expect(
|
|
27
|
+
(reload.json as { integrationsReinitialized: string[] }).integrationsReinitialized,
|
|
28
|
+
).not.toContain("slack");
|
|
29
|
+
expect(slack.mock.apiCalls("apps.connections.open").length).toBe(opened);
|
|
30
|
+
const socket = await api("PUT", "/api/config", {
|
|
31
|
+
body: { scope: "global", key: "SLACK_MODE", value: "socket" },
|
|
32
|
+
});
|
|
33
|
+
expect(socket.status).toBe(200);
|
|
34
|
+
expect(await pollUntil(() => slack.mock.connectionCount === 1, 10_000, 50)).toBe(true);
|
|
35
|
+
expect(slack.mock.apiCalls("apps.connections.open").length).toBe(opened + 1);
|
|
36
|
+
} finally {
|
|
37
|
+
if (sut) await stopSut(sut, false);
|
|
38
|
+
await stopSlackMock(slack, false);
|
|
39
|
+
}
|
|
40
|
+
}, 30_000);
|
|
41
|
+
|
|
42
|
+
test("config reload recovers from an initial unavailable HTTP transport", async () => {
|
|
43
|
+
const slack = await startSlackMock(false);
|
|
44
|
+
let sut: Sut | undefined;
|
|
45
|
+
try {
|
|
46
|
+
sut = await startSut(false, slack.mock.env, {
|
|
47
|
+
SLACK_MODE: "http",
|
|
48
|
+
SLACK_SIGNING_SECRET: "synthetic-signing-secret",
|
|
49
|
+
HEARTBEAT_DISABLE: "true",
|
|
50
|
+
});
|
|
51
|
+
const api = createApiClient(sut.baseUrl, sut.apiKey);
|
|
52
|
+
const initialReload = await api("POST", "/api/config/reload");
|
|
53
|
+
expect(initialReload.status).toBe(200);
|
|
54
|
+
expect(
|
|
55
|
+
(initialReload.json as { integrationsReinitialized: string[] }).integrationsReinitialized,
|
|
56
|
+
).not.toContain("slack");
|
|
57
|
+
expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(0);
|
|
58
|
+
|
|
59
|
+
const socket = await api("PUT", "/api/config", {
|
|
60
|
+
body: { scope: "global", key: "SLACK_MODE", value: "socket" },
|
|
61
|
+
});
|
|
62
|
+
expect(socket.status).toBe(200);
|
|
63
|
+
expect(await pollUntil(() => slack.mock.connectionCount === 1, 10_000, 50)).toBe(true);
|
|
64
|
+
expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(1);
|
|
65
|
+
} finally {
|
|
66
|
+
if (sut) await stopSut(sut, false);
|
|
67
|
+
await stopSlackMock(slack, false);
|
|
68
|
+
}
|
|
69
|
+
}, 30_000);
|
package/src/tests/status.test.ts
CHANGED
|
@@ -96,6 +96,8 @@ const ENV_KEYS_TO_RESET = [
|
|
|
96
96
|
"DEVIN_ORG_ID",
|
|
97
97
|
"SLACK_BOT_TOKEN",
|
|
98
98
|
"SLACK_APP_TOKEN",
|
|
99
|
+
"SLACK_SIGNING_SECRET",
|
|
100
|
+
"SLACK_MODE",
|
|
99
101
|
"SLACK_DISABLE",
|
|
100
102
|
"GITHUB_WEBHOOK_SECRET",
|
|
101
103
|
"GITHUB_APP_ID",
|
|
@@ -409,20 +411,38 @@ describe("setup milestones", () => {
|
|
|
409
411
|
});
|
|
410
412
|
});
|
|
411
413
|
|
|
412
|
-
test("slack:
|
|
414
|
+
test("slack: reports mode-aware credentials as configured, not live-verified", async () => {
|
|
413
415
|
process.env.SLACK_BOT_TOKEN = "xoxb-test";
|
|
414
416
|
const a = await buildStatusPayload();
|
|
415
417
|
expect(getMilestone(a, "slack").state).toBe("unverified");
|
|
416
418
|
|
|
417
419
|
process.env.SLACK_APP_TOKEN = "xapp-test";
|
|
418
420
|
const b = await buildStatusPayload();
|
|
419
|
-
expect(getMilestone(b, "slack").state).toBe("
|
|
421
|
+
expect(getMilestone(b, "slack").state).toBe("configured");
|
|
422
|
+
expect(getMilestone(b, "slack").hint).toContain("not been verified");
|
|
423
|
+
|
|
424
|
+
process.env.SLACK_MODE = "http";
|
|
425
|
+
delete process.env.SLACK_APP_TOKEN;
|
|
426
|
+
process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
|
|
427
|
+
const http = await buildStatusPayload();
|
|
428
|
+
expect(getMilestone(http, "slack").state).toBe("unverified");
|
|
429
|
+
expect(getMilestone(http, "slack").hint).toContain("unavailable");
|
|
420
430
|
|
|
421
431
|
process.env.SLACK_DISABLE = "true";
|
|
422
432
|
const c = await buildStatusPayload();
|
|
423
433
|
expect(getMilestone(c, "slack").state).toBe("unverified");
|
|
424
434
|
});
|
|
425
435
|
|
|
436
|
+
test("slack: invalid mode fails closed with a specific hint", async () => {
|
|
437
|
+
process.env.SLACK_MODE = "webhook";
|
|
438
|
+
process.env.SLACK_BOT_TOKEN = "xoxb-test";
|
|
439
|
+
process.env.SLACK_APP_TOKEN = "xapp-test";
|
|
440
|
+
|
|
441
|
+
const milestone = getMilestone(await buildStatusPayload(), "slack");
|
|
442
|
+
expect(milestone.state).toBe("unverified");
|
|
443
|
+
expect(milestone.hint).toContain("Invalid SLACK_MODE");
|
|
444
|
+
});
|
|
445
|
+
|
|
426
446
|
test("github: needs webhook secret + app id + private key", async () => {
|
|
427
447
|
process.env.GITHUB_WEBHOOK_SECRET = "secret";
|
|
428
448
|
process.env.GITHUB_APP_ID = "12345";
|
|
@@ -73,6 +73,8 @@ async function pins(client: Client): Promise<string[]> {
|
|
|
73
73
|
.sort();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Eight real MCP sessions approach the 10s CI default under parallel runner load.
|
|
77
|
+
// Keep the session-lifetime assertions together with room for connection setup.
|
|
76
78
|
test("wire response is default-on, per-task, additive, and fixed for one MCP session", async () => {
|
|
77
79
|
const task = await createTaskExtended("review fixture", { agentId, taskType: "review" });
|
|
78
80
|
const other = await createTaskExtended("other fixture", { agentId, taskType: "other" });
|
|
@@ -115,7 +117,7 @@ test("wire response is default-on, per-task, additive, and fixed for one MCP ses
|
|
|
115
117
|
await configure("TASK_TOOL_PRELOAD_ENABLED", "true");
|
|
116
118
|
await configure("TASK_TOOL_MANIFESTS", "invalid deployment JSON");
|
|
117
119
|
expect(await pins(await connect(task.id))).toEqual([]);
|
|
118
|
-
});
|
|
120
|
+
}, 30_000);
|
|
119
121
|
|
|
120
122
|
test("a manifest cannot expand the scripts-only capability surface", async () => {
|
|
121
123
|
const task = await createTaskExtended("script fixture", { agentId, taskType: "script" });
|
|
@@ -794,6 +794,24 @@ describe("initTelemetry", () => {
|
|
|
794
794
|
).toBe(false);
|
|
795
795
|
});
|
|
796
796
|
|
|
797
|
+
test("Slack HTTP mode uses the signing secret instead of an app token", () => {
|
|
798
|
+
expect(
|
|
799
|
+
_hasSlackChannel({
|
|
800
|
+
SLACK_MODE: "http",
|
|
801
|
+
SLACK_BOT_TOKEN: "xoxb-1",
|
|
802
|
+
SLACK_SIGNING_SECRET: "synthetic-signing-secret",
|
|
803
|
+
}),
|
|
804
|
+
).toBe(true);
|
|
805
|
+
expect(_hasSlackChannel({ SLACK_MODE: "http", SLACK_BOT_TOKEN: "xoxb-1" })).toBe(false);
|
|
806
|
+
expect(
|
|
807
|
+
_hasSlackChannel({
|
|
808
|
+
SLACK_MODE: "invalid",
|
|
809
|
+
SLACK_BOT_TOKEN: "xoxb-1",
|
|
810
|
+
SLACK_APP_TOKEN: "xapp-1",
|
|
811
|
+
}),
|
|
812
|
+
).toBe(false);
|
|
813
|
+
});
|
|
814
|
+
|
|
797
815
|
test("Email requires AGENTMAIL_WEBHOOK_SECRET", () => {
|
|
798
816
|
expect(_hasEmailChannel({})).toBe(false);
|
|
799
817
|
expect(_hasEmailChannel({ AGENTMAIL_WEBHOOK_SECRET: "whsec_1" })).toBe(true);
|