@desplega.ai/agent-swarm 2.7.4 → 2.7.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/README.md +1 -1
- package/dist/acp-adapter-g3ads55s.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
- package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
- package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
- package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
- package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
- package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
- package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
- package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
- package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
- package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
- package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
- package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
- package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
- package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
- package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
- package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
- package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
- package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
- package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
- package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
- package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
- package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
- package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
- package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
- package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
- package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
- package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
- package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
- package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
- package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
- package/dist/cli-zh3fp8v2.js +151 -0
- package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
- package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
- package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
- package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
- package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
- package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
- package/dist/fx-adapter-gt6pncj2.js +18 -0
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
- package/dist/handlers-vwmba1m2.js +31 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
- package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
- package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
- package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
- package/dist/index-5r71rf98.js +32 -0
- package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
- package/dist/index-anmwxs87.js +29 -0
- package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
- package/dist/index-spp0ra5c.js +36 -0
- package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
- package/dist/lead-fjy418cw.js +69 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
- package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
- package/dist/worker-0c5tyzqe.js +69 -0
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +97 -32
- package/package.json +1 -1
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db.ts +8 -4
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/mcp-bridge.ts +4 -0
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/scheduler/scheduler.ts +36 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +5 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/handlers.ts +18 -5
- package/src/slack/left-threads.ts +65 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/explicit-model-validation.test.ts +99 -0
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +3 -2
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +431 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +19 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { createServer, type Server } from "node:http";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
closeDb,
|
|
8
|
+
createAgent,
|
|
9
|
+
createTaskExtended,
|
|
10
|
+
createWorkflow,
|
|
11
|
+
deleteSwarmConfig,
|
|
12
|
+
getSlackTasksInThread,
|
|
13
|
+
initDb,
|
|
14
|
+
updateWorkflow,
|
|
15
|
+
upsertSwarmConfig,
|
|
16
|
+
} from "../be/db";
|
|
17
|
+
import { __resetInjectedEnvTracking, loadGlobalConfigsIntoEnv } from "../http/core";
|
|
18
|
+
import { invokeToolInProcess, resetBridgeServerForTesting } from "../http/mcp-bridge";
|
|
19
|
+
import { handleTasks } from "../http/tasks";
|
|
20
|
+
import { reservedNamespaceError } from "../kv-reserved-namespaces";
|
|
21
|
+
import { createServer as createMcpServer } from "../server";
|
|
22
|
+
import { registerMessageHandler, resetSlackHandlerCachesForTesting } from "../slack/handlers";
|
|
23
|
+
import { isSlackThreadLeft, SLACK_LEFT_THREAD_KV_NAMESPACE } from "../slack/left-threads";
|
|
24
|
+
import { bufferThreadMessage, instantFlush } from "../slack/thread-buffer";
|
|
25
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
26
|
+
import { registerSlackLeaveThreadTool } from "../tools/slack-leave-thread";
|
|
27
|
+
import type { Agent, AgentTask } from "../types";
|
|
28
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
29
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
30
|
+
import { BaseExecutor, type ExecutorResult } from "../workflows/executors/base";
|
|
31
|
+
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
32
|
+
import { handleEventTrigger } from "../workflows/triggers";
|
|
33
|
+
import { listenOnFreePort } from "./test-net";
|
|
34
|
+
|
|
35
|
+
const TEST_DB_PATH = "./test-slack-leave-thread.sqlite";
|
|
36
|
+
const BOT_USER_ID = "U_LEAVE_BOT";
|
|
37
|
+
const HUMAN = "U_LEAVE_HUMAN";
|
|
38
|
+
|
|
39
|
+
const previousEnv = {
|
|
40
|
+
SLACK_RENDER_V2: process.env.SLACK_RENDER_V2,
|
|
41
|
+
ADDITIVE_SLACK: process.env.ADDITIVE_SLACK,
|
|
42
|
+
SLACK_BLOCK_EXTERNAL_CHANNELS: process.env.SLACK_BLOCK_EXTERNAL_CHANNELS,
|
|
43
|
+
SLACK_LEAVE_THREAD_DISABLE: process.env.SLACK_LEAVE_THREAD_DISABLE,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Handler = (args: Record<string, unknown>) => Promise<void>;
|
|
47
|
+
const handlers = new Map<string, Handler>();
|
|
48
|
+
|
|
49
|
+
const client = {
|
|
50
|
+
auth: { test: async () => ({ user_id: BOT_USER_ID, bot_id: "B_LEAVE_BOT" }) },
|
|
51
|
+
conversations: {
|
|
52
|
+
info: async () => ({ ok: true, channel: {} }),
|
|
53
|
+
replies: async () => ({ ok: true, messages: [] }),
|
|
54
|
+
},
|
|
55
|
+
chat: { postMessage: async () => ({ ok: true, ts: "1.1" }) },
|
|
56
|
+
reactions: { add: async () => ({ ok: true }) },
|
|
57
|
+
users: { info: async () => ({ ok: true, user: { profile: {} } }) },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type RegisteredTool = {
|
|
61
|
+
handler: (
|
|
62
|
+
args: unknown,
|
|
63
|
+
extra: unknown,
|
|
64
|
+
) => Promise<{ structuredContent: { success: boolean; message: string } }>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function buildTool(): RegisteredTool {
|
|
68
|
+
const server = new McpServer({ name: "slack-leave-thread-test", version: "1.0.0" });
|
|
69
|
+
registerSlackLeaveThreadTool(server);
|
|
70
|
+
const tool = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
71
|
+
._registeredTools["slack-leave-thread"];
|
|
72
|
+
if (!tool) throw new Error("slack-leave-thread tool not registered");
|
|
73
|
+
return tool;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function meta(agentId: string) {
|
|
77
|
+
return { sessionId: "s1", requestInfo: { headers: { "x-agent-id": agentId } } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let counter = 0;
|
|
81
|
+
async function sendThreadReply(channel: string, threadTs: string, text: string) {
|
|
82
|
+
counter++;
|
|
83
|
+
await handlers.get("message")!({
|
|
84
|
+
event: {
|
|
85
|
+
channel,
|
|
86
|
+
ts: `18000.${String(counter).padStart(4, "0")}`,
|
|
87
|
+
thread_ts: threadTs,
|
|
88
|
+
text,
|
|
89
|
+
user: HUMAN,
|
|
90
|
+
},
|
|
91
|
+
body: { event_id: `evt_leave_${counter}` },
|
|
92
|
+
client,
|
|
93
|
+
say: async () => ({ ok: true, ts: "1.2" }),
|
|
94
|
+
});
|
|
95
|
+
return getSlackTasksInThread(channel, threadTs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let lead: Agent;
|
|
99
|
+
let worker: Agent;
|
|
100
|
+
let otherWorker: Agent;
|
|
101
|
+
|
|
102
|
+
async function seedThread(channel: string, threadTs: string) {
|
|
103
|
+
// As the Slack handlers create it: only Slack ingress writes slackTriggerMessageTs.
|
|
104
|
+
return createTaskExtended("original ask", {
|
|
105
|
+
agentId: worker.id,
|
|
106
|
+
source: "slack",
|
|
107
|
+
slackChannelId: channel,
|
|
108
|
+
slackThreadTs: threadTs,
|
|
109
|
+
slackTriggerMessageTs: threadTs,
|
|
110
|
+
slackUserId: HUMAN,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Counts how many times a workflow node actually runs. */
|
|
115
|
+
class RecordingExecutor extends BaseExecutor<
|
|
116
|
+
typeof RecordingExecutor.schema,
|
|
117
|
+
typeof RecordingExecutor.outSchema
|
|
118
|
+
> {
|
|
119
|
+
static readonly schema = z.object({ template: z.string().optional() });
|
|
120
|
+
static readonly outSchema = z.object({ sent: z.boolean() });
|
|
121
|
+
static runs = 0;
|
|
122
|
+
|
|
123
|
+
readonly type = "notify";
|
|
124
|
+
readonly mode = "instant" as const;
|
|
125
|
+
readonly configSchema = RecordingExecutor.schema;
|
|
126
|
+
readonly outputSchema = RecordingExecutor.outSchema;
|
|
127
|
+
|
|
128
|
+
protected async execute(): Promise<ExecutorResult<z.infer<typeof RecordingExecutor.outSchema>>> {
|
|
129
|
+
RecordingExecutor.runs++;
|
|
130
|
+
return { status: "success", output: { sent: true } };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let routeServer: Server;
|
|
135
|
+
let routeBaseUrl: string;
|
|
136
|
+
|
|
137
|
+
beforeAll(async () => {
|
|
138
|
+
initDb(TEST_DB_PATH);
|
|
139
|
+
process.env.SLACK_RENDER_V2 = "false";
|
|
140
|
+
process.env.ADDITIVE_SLACK = "false";
|
|
141
|
+
process.env.SLACK_BLOCK_EXTERNAL_CHANNELS = "false";
|
|
142
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
143
|
+
lead = await createAgent({ name: "leave-lead", isLead: true, status: "idle", capabilities: [] });
|
|
144
|
+
worker = await createAgent({
|
|
145
|
+
name: "leave-worker",
|
|
146
|
+
isLead: false,
|
|
147
|
+
status: "idle",
|
|
148
|
+
capabilities: [],
|
|
149
|
+
});
|
|
150
|
+
otherWorker = await createAgent({
|
|
151
|
+
name: "leave-other",
|
|
152
|
+
isLead: false,
|
|
153
|
+
status: "idle",
|
|
154
|
+
capabilities: [],
|
|
155
|
+
});
|
|
156
|
+
registerMessageHandler({
|
|
157
|
+
event: (type: string, handler: Handler) => {
|
|
158
|
+
handlers.set(type, handler);
|
|
159
|
+
},
|
|
160
|
+
} as never);
|
|
161
|
+
|
|
162
|
+
routeServer = createServer(async (req, res) => {
|
|
163
|
+
setRequestAuth(req, { kind: "operator", fingerprint: "test-key" });
|
|
164
|
+
const url = req.url ?? "/";
|
|
165
|
+
const handled = await handleTasks(
|
|
166
|
+
req,
|
|
167
|
+
res,
|
|
168
|
+
url.split("?")[0]?.split("/").filter(Boolean) ?? [],
|
|
169
|
+
new URLSearchParams(url.split("?")[1] ?? ""),
|
|
170
|
+
req.headers["x-agent-id"] as string | undefined,
|
|
171
|
+
);
|
|
172
|
+
if (!handled) res.writeHead(404).end();
|
|
173
|
+
});
|
|
174
|
+
routeBaseUrl = `http://127.0.0.1:${await listenOnFreePort(routeServer)}`;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterAll(async () => {
|
|
178
|
+
await new Promise<void>((resolve) => routeServer.close(() => resolve()));
|
|
179
|
+
closeDb();
|
|
180
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
181
|
+
try {
|
|
182
|
+
unlinkSync(`${TEST_DB_PATH}${suffix}`);
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const [key, value] of Object.entries(previousEnv)) {
|
|
188
|
+
if (value === undefined) delete process.env[key];
|
|
189
|
+
else process.env[key] = value;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
resetSlackHandlerCachesForTesting();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("slack-leave-thread", () => {
|
|
198
|
+
test("a left thread skips a plain reply; a mention clears the state and is processed", async () => {
|
|
199
|
+
const task = await seedThread("C_LEAVE_A", "1800.0001");
|
|
200
|
+
const result = await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
201
|
+
expect(result.structuredContent.success).toBe(true);
|
|
202
|
+
expect(await isSlackThreadLeft("C_LEAVE_A", "1800.0001")).toBe(true);
|
|
203
|
+
|
|
204
|
+
expect(await sendThreadReply("C_LEAVE_A", "1800.0001", "thanks, we'll sort it")).toHaveLength(
|
|
205
|
+
1,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const afterMention = await sendThreadReply(
|
|
209
|
+
"C_LEAVE_A",
|
|
210
|
+
"1800.0001",
|
|
211
|
+
`<@${BOT_USER_ID}> one more thing`,
|
|
212
|
+
);
|
|
213
|
+
expect(afterMention).toHaveLength(2);
|
|
214
|
+
expect(await isSlackThreadLeft("C_LEAVE_A", "1800.0001")).toBe(false);
|
|
215
|
+
|
|
216
|
+
// Back in the thread: plain replies follow up again.
|
|
217
|
+
expect(await sendThreadReply("C_LEAVE_A", "1800.0001", "and also this")).toHaveLength(3);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("another thread is unaffected", async () => {
|
|
221
|
+
await seedThread("C_LEAVE_B", "1900.0001");
|
|
222
|
+
await buildTool().handler({ channelId: "C_LEAVE_B", threadTs: "1900.0002" }, meta(lead.id));
|
|
223
|
+
|
|
224
|
+
expect(await isSlackThreadLeft("C_LEAVE_B", "1900.0002")).toBe(true);
|
|
225
|
+
expect(await sendThreadReply("C_LEAVE_B", "1900.0001", "plain follow-up")).toHaveLength(2);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("refuses a task the agent has no context for, and missing arguments", async () => {
|
|
229
|
+
const task = await seedThread("C_LEAVE_C", "2000.0001");
|
|
230
|
+
const tool = buildTool();
|
|
231
|
+
|
|
232
|
+
const foreign = await tool.handler({ taskId: task.id }, meta(otherWorker.id));
|
|
233
|
+
expect(foreign.structuredContent.success).toBe(false);
|
|
234
|
+
expect(await isSlackThreadLeft("C_LEAVE_C", "2000.0001")).toBe(false);
|
|
235
|
+
|
|
236
|
+
const missing = await tool.handler({ channelId: "C_LEAVE_C" }, meta(worker.id));
|
|
237
|
+
expect(missing.structuredContent.success).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("only a lead can leave an arbitrary thread by channelId; KV callers cannot write the state", async () => {
|
|
241
|
+
const tool = buildTool();
|
|
242
|
+
const direct = await tool.handler(
|
|
243
|
+
{ channelId: "C_LEAVE_D", threadTs: "2100.0001" },
|
|
244
|
+
meta(worker.id),
|
|
245
|
+
);
|
|
246
|
+
expect(direct.structuredContent.success).toBe(false);
|
|
247
|
+
expect(await isSlackThreadLeft("C_LEAVE_D", "2100.0001")).toBe(false);
|
|
248
|
+
|
|
249
|
+
expect(reservedNamespaceError(SLACK_LEFT_THREAD_KV_NAMESPACE)).not.toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("a non-lead cannot leave a thread through a task with hand-set Slack context", async () => {
|
|
253
|
+
const forged = await createTaskExtended("forged", {
|
|
254
|
+
agentId: worker.id,
|
|
255
|
+
creatorAgentId: worker.id,
|
|
256
|
+
source: "mcp",
|
|
257
|
+
slackChannelId: "C_LEAVE_E",
|
|
258
|
+
slackThreadTs: "2200.0001",
|
|
259
|
+
});
|
|
260
|
+
const tool = buildTool();
|
|
261
|
+
|
|
262
|
+
const denied = await tool.handler({ taskId: forged.id }, meta(worker.id));
|
|
263
|
+
expect(denied.structuredContent.success).toBe(false);
|
|
264
|
+
expect(await isSlackThreadLeft("C_LEAVE_E", "2200.0001")).toBe(false);
|
|
265
|
+
|
|
266
|
+
const byLead = await tool.handler({ taskId: forged.id }, meta(lead.id));
|
|
267
|
+
expect(byLead.structuredContent.success).toBe(true);
|
|
268
|
+
expect(await isSlackThreadLeft("C_LEAVE_E", "2200.0001")).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("a non-lead cannot leave a thread through a task it created over REST with source slack", async () => {
|
|
272
|
+
const channel = "C_FORGED_HTTP";
|
|
273
|
+
const threadTs = "1700000000.000001";
|
|
274
|
+
const res = await fetch(`${routeBaseUrl}/api/tasks`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: { "content-type": "application/json", "x-agent-id": worker.id },
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
task: "forged slack task",
|
|
279
|
+
agentId: worker.id,
|
|
280
|
+
routingReason: "skill",
|
|
281
|
+
source: "slack",
|
|
282
|
+
contextKey: slackContextKey({ channelId: channel, threadTs }),
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
expect(res.status).toBe(201);
|
|
286
|
+
const forged = (await res.json()) as AgentTask;
|
|
287
|
+
expect(forged.source).toBe("slack");
|
|
288
|
+
expect(forged.slackChannelId).toBe(channel);
|
|
289
|
+
expect(forged.slackThreadTs).toBe(threadTs);
|
|
290
|
+
|
|
291
|
+
const denied = await buildTool().handler({ taskId: forged.id }, meta(worker.id));
|
|
292
|
+
expect(denied.structuredContent.success).toBe(false);
|
|
293
|
+
expect(await isSlackThreadLeft(channel, threadTs)).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a plain reply in a left thread starts no workflow; a mention still does", async () => {
|
|
297
|
+
const workflow = await createWorkflow({
|
|
298
|
+
name: "leave-thread-slack-listener",
|
|
299
|
+
triggers: [{ type: "event", eventName: "slack.message" }],
|
|
300
|
+
definition: { nodes: [{ id: "n1", type: "notify", config: { template: "ping" } }] },
|
|
301
|
+
});
|
|
302
|
+
const registry = new ExecutorRegistry();
|
|
303
|
+
registry.register(new RecordingExecutor());
|
|
304
|
+
const emitted: unknown[] = [];
|
|
305
|
+
const started: Promise<string[]>[] = [];
|
|
306
|
+
const listener = (payload: unknown) => {
|
|
307
|
+
emitted.push(payload);
|
|
308
|
+
started.push(handleEventTrigger("slack.message", payload, registry));
|
|
309
|
+
};
|
|
310
|
+
workflowEventBus.on("slack.message", listener);
|
|
311
|
+
RecordingExecutor.runs = 0;
|
|
312
|
+
try {
|
|
313
|
+
const task = await seedThread("C_LEAVE_F", "2300.0001");
|
|
314
|
+
await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
315
|
+
|
|
316
|
+
await sendThreadReply("C_LEAVE_F", "2300.0001", "people talking among themselves");
|
|
317
|
+
const mutedRuns = (await Promise.all(started)).flat();
|
|
318
|
+
expect(emitted).toHaveLength(0);
|
|
319
|
+
expect(mutedRuns).toHaveLength(0);
|
|
320
|
+
expect(RecordingExecutor.runs).toBe(0);
|
|
321
|
+
|
|
322
|
+
await sendThreadReply("C_LEAVE_F", "2300.0001", `<@${BOT_USER_ID}> back in`);
|
|
323
|
+
const mentionRuns = (await Promise.all(started)).flat();
|
|
324
|
+
expect(emitted).toHaveLength(1);
|
|
325
|
+
expect(mentionRuns).toHaveLength(1);
|
|
326
|
+
expect(RecordingExecutor.runs).toBe(1);
|
|
327
|
+
} finally {
|
|
328
|
+
workflowEventBus.off("slack.message", listener);
|
|
329
|
+
await updateWorkflow(workflow.id, { enabled: false });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("replies buffered before the thread was left create no task when they flush", async () => {
|
|
334
|
+
const task = await seedThread("C_LEAVE_G", "2400.0001");
|
|
335
|
+
bufferThreadMessage("C_LEAVE_G", "2400.0001", "still chatting", HUMAN, "2400.0002");
|
|
336
|
+
await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
337
|
+
|
|
338
|
+
await instantFlush("C_LEAVE_G:2400.0001");
|
|
339
|
+
expect(await getSlackTasksInThread("C_LEAVE_G", "2400.0001")).toHaveLength(1);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("SLACK_LEAVE_THREAD_DISABLE", () => {
|
|
344
|
+
function registeredTools(server: McpServer): Record<string, unknown> {
|
|
345
|
+
return (server as unknown as { _registeredTools: Record<string, unknown> })._registeredTools;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
test("set at session creation, the tool is absent from the MCP surface", async () => {
|
|
349
|
+
expect(registeredTools(await createMcpServer())["slack-leave-thread"]).toBeDefined();
|
|
350
|
+
|
|
351
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
352
|
+
try {
|
|
353
|
+
const tools = registeredTools(await createMcpServer());
|
|
354
|
+
expect(tools["slack-leave-thread"]).toBeUndefined();
|
|
355
|
+
expect(tools["slack-reply"]).toBeDefined();
|
|
356
|
+
} finally {
|
|
357
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The scripts bridge caches the full-surface server it builds on first use.
|
|
362
|
+
test("the cached scripts bridge follows the flag per call, not the flag at first build", async () => {
|
|
363
|
+
function bridgeLeave(taskId: string) {
|
|
364
|
+
return invokeToolInProcess({
|
|
365
|
+
toolName: "slack-leave-thread",
|
|
366
|
+
args: { taskId },
|
|
367
|
+
agentId: worker.id,
|
|
368
|
+
callOrigin: "script-sdk",
|
|
369
|
+
}) as Promise<{ structuredContent: { success: boolean; message: string } }>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
resetBridgeServerForTesting();
|
|
373
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
374
|
+
try {
|
|
375
|
+
const refused = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0001")).id);
|
|
376
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
377
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
378
|
+
|
|
379
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
380
|
+
const left = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0002")).id);
|
|
381
|
+
expect(left.structuredContent.success).toBe(true);
|
|
382
|
+
expect(await isSlackThreadLeft("C_LEAVE_I", "2600.0002")).toBe(true);
|
|
383
|
+
} finally {
|
|
384
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
resetBridgeServerForTesting();
|
|
388
|
+
try {
|
|
389
|
+
expect(
|
|
390
|
+
(await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0003")).id)).structuredContent
|
|
391
|
+
.success,
|
|
392
|
+
).toBe(true);
|
|
393
|
+
|
|
394
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
395
|
+
const refused = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0004")).id);
|
|
396
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
397
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
398
|
+
expect(await isSlackThreadLeft("C_LEAVE_I", "2600.0004")).toBe(false);
|
|
399
|
+
} finally {
|
|
400
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
401
|
+
resetBridgeServerForTesting();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("set through a global config reload after registration, the call fails naming the flag", async () => {
|
|
406
|
+
const task = await seedThread("C_LEAVE_H", "2500.0001");
|
|
407
|
+
const tool = buildTool();
|
|
408
|
+
const row = await upsertSwarmConfig({
|
|
409
|
+
scope: "global",
|
|
410
|
+
key: "SLACK_LEAVE_THREAD_DISABLE",
|
|
411
|
+
value: "true",
|
|
412
|
+
});
|
|
413
|
+
try {
|
|
414
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
415
|
+
const refused = await tool.handler({ taskId: task.id }, meta(worker.id));
|
|
416
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
417
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
418
|
+
expect(await isSlackThreadLeft("C_LEAVE_H", "2500.0001")).toBe(false);
|
|
419
|
+
|
|
420
|
+
await deleteSwarmConfig(row.id);
|
|
421
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
422
|
+
const left = await tool.handler({ taskId: task.id }, meta(worker.id));
|
|
423
|
+
expect(left.structuredContent.success).toBe(true);
|
|
424
|
+
expect(await isSlackThreadLeft("C_LEAVE_H", "2500.0001")).toBe(true);
|
|
425
|
+
} finally {
|
|
426
|
+
await deleteSwarmConfig(row.id);
|
|
427
|
+
__resetInjectedEnvTracking();
|
|
428
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
@@ -181,6 +181,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
181
181
|
"slack-list-channels",
|
|
182
182
|
"slack-delete",
|
|
183
183
|
"slack-update",
|
|
184
|
+
"slack-leave-thread",
|
|
184
185
|
];
|
|
185
186
|
|
|
186
187
|
for (const name of slackTools) {
|
|
@@ -379,7 +380,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
379
380
|
// the six Chat SDK tools (chat-* + register/unregister-chat-channel),
|
|
380
381
|
// and the seven app row tools plus app-action-run.
|
|
381
382
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
382
|
-
expect(count).toBeLessThanOrEqual(
|
|
383
|
+
expect(count).toBeLessThanOrEqual(170);
|
|
383
384
|
});
|
|
384
385
|
|
|
385
386
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -945,6 +945,48 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
945
945
|
expect(run2!.status).toBe("skipped");
|
|
946
946
|
expect(run2!.error).toBe("cooldown");
|
|
947
947
|
});
|
|
948
|
+
|
|
949
|
+
test("simultaneous triggers start one run; the other records a cooldown skip", async () => {
|
|
950
|
+
const registry = createTestRegistry();
|
|
951
|
+
const def: WorkflowDefinition = {
|
|
952
|
+
nodes: [{ id: "a", type: "echo", config: { message: "hi" } }],
|
|
953
|
+
};
|
|
954
|
+
const workflow = await makeWorkflow(def, { cooldown: { hours: 20, from: "start" } });
|
|
955
|
+
|
|
956
|
+
// Both pass the pre-transaction check before either run row exists.
|
|
957
|
+
const runIds = await Promise.all([
|
|
958
|
+
startWorkflowExecution(workflow, {}, registry),
|
|
959
|
+
startWorkflowExecution(workflow, {}, registry),
|
|
960
|
+
]);
|
|
961
|
+
const statuses = await Promise.all(
|
|
962
|
+
runIds.map(async (id) => (await getWorkflowRun(id))!.status),
|
|
963
|
+
);
|
|
964
|
+
expect(statuses.sort()).toEqual(["completed", "skipped"]);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test('from: "start" ignores a long successful run that finished inside the window', async () => {
|
|
968
|
+
const { createWorkflowRun, updateWorkflowRun } = await import("../be/db");
|
|
969
|
+
const def: WorkflowDefinition = {
|
|
970
|
+
nodes: [{ id: "a", type: "echo", config: {} }],
|
|
971
|
+
};
|
|
972
|
+
const workflow = await makeWorkflow(def);
|
|
973
|
+
|
|
974
|
+
// Started 24h ago, finished 1h ago: 24h since its start, 1h since its finish.
|
|
975
|
+
const runId = crypto.randomUUID();
|
|
976
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
977
|
+
await getDbClient().run("UPDATE workflow_runs SET startedAt = ? WHERE id = ?", [
|
|
978
|
+
new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
979
|
+
runId,
|
|
980
|
+
]);
|
|
981
|
+
await updateWorkflowRun(runId, {
|
|
982
|
+
status: "completed",
|
|
983
|
+
finishedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
expect(await shouldSkipCooldown(workflow.id, { hours: 20, from: "start" })).toBe(false);
|
|
987
|
+
// Control: the default also counts from the successful finish.
|
|
988
|
+
expect(await shouldSkipCooldown(workflow.id, { hours: 20 })).toBe(true);
|
|
989
|
+
});
|
|
948
990
|
});
|
|
949
991
|
|
|
950
992
|
// ─── Input Resolution ─────────────────────────────────────
|