@desplega.ai/agent-swarm 1.74.0 → 1.74.2
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/openapi.json +199 -1
- package/package.json +1 -1
- package/src/be/db.ts +278 -0
- package/src/be/migrations/049_wait_states.sql +30 -0
- package/src/be/migrations/050_wait_states_scope.sql +19 -0
- package/src/http/index.ts +2 -0
- package/src/http/trackers/jira.ts +84 -27
- package/src/http/trackers/linear.ts +67 -11
- package/src/http/utils.ts +15 -0
- package/src/http/workflow-events.ts +107 -0
- package/src/http/workflows.ts +55 -6
- package/src/jira/sync.ts +20 -7
- package/src/linear/gate.ts +122 -0
- package/src/linear/sync.ts +128 -0
- package/src/oauth/keepalive.ts +34 -13
- package/src/tests/ensure-token.test.ts +33 -0
- package/src/tests/linear-webhook.test.ts +383 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +617 -0
- package/src/tests/workflow-patch.test.ts +14 -14
- package/src/tests/workflow-wait-builtin-events.test.ts +279 -0
- package/src/tests/workflow-wait-event.test.ts +384 -0
- package/src/tests/workflow-wait-filter.test.ts +200 -0
- package/src/tests/workflow-wait-http.test.ts +177 -0
- package/src/tests/workflow-wait-recovery.test.ts +178 -0
- package/src/tests/workflow-wait-state-queries.test.ts +419 -0
- package/src/tests/workflow-wait-time.test.ts +255 -0
- package/src/tools/tracker/tracker-status.ts +7 -1
- package/src/tools/workflows/create-workflow.ts +16 -2
- package/src/tools/workflows/patch-workflow.ts +26 -6
- package/src/tools/workflows/trigger-workflow.ts +26 -1
- package/src/tools/workflows/update-workflow.ts +28 -2
- package/src/types.ts +48 -3
- package/src/workflows/definition.ts +2 -5
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/wait.ts +170 -0
- package/src/workflows/index.ts +18 -2
- package/src/workflows/json-schema-validator.ts +8 -1
- package/src/workflows/recovery.ts +55 -1
- package/src/workflows/resume.ts +272 -0
- package/src/workflows/wait-filter.ts +311 -0
- package/src/workflows/wait-poller.ts +63 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { WorkflowDefinition,
|
|
2
|
+
import type { WorkflowDefinition, WorkflowPatch } from "../types";
|
|
3
3
|
import { applyDefinitionPatch } from "../workflows/definition";
|
|
4
4
|
|
|
5
5
|
// ─── Helper ──────────────────────────────────────────────────
|
|
@@ -22,7 +22,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
22
22
|
// --- Delete ---
|
|
23
23
|
|
|
24
24
|
test("delete removes nodes", () => {
|
|
25
|
-
const patch:
|
|
25
|
+
const patch: WorkflowPatch = { delete: ["b"] };
|
|
26
26
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
27
27
|
expect(result.errors).toEqual([]);
|
|
28
28
|
expect(result.definition.nodes).toHaveLength(1);
|
|
@@ -30,7 +30,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("delete returns error for non-existent node", () => {
|
|
33
|
-
const patch:
|
|
33
|
+
const patch: WorkflowPatch = { delete: ["nonexistent"] };
|
|
34
34
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
35
35
|
expect(result.errors).toHaveLength(1);
|
|
36
36
|
expect(result.errors[0]).toContain("non-existent");
|
|
@@ -40,7 +40,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
40
40
|
// --- Create ---
|
|
41
41
|
|
|
42
42
|
test("create adds nodes", () => {
|
|
43
|
-
const patch:
|
|
43
|
+
const patch: WorkflowPatch = {
|
|
44
44
|
create: [{ id: "c", type: "agent-task", config: { template: "New" } }],
|
|
45
45
|
};
|
|
46
46
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
@@ -50,7 +50,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test("create returns error for duplicate ID", () => {
|
|
53
|
-
const patch:
|
|
53
|
+
const patch: WorkflowPatch = {
|
|
54
54
|
create: [{ id: "a", type: "agent-task", config: { template: "Dup" } }],
|
|
55
55
|
};
|
|
56
56
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
@@ -62,7 +62,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
62
62
|
// --- Update ---
|
|
63
63
|
|
|
64
64
|
test("update merges fields (shallow)", () => {
|
|
65
|
-
const patch:
|
|
65
|
+
const patch: WorkflowPatch = {
|
|
66
66
|
update: [{ nodeId: "b", node: { label: "Updated", config: { template: "Changed" } } }],
|
|
67
67
|
};
|
|
68
68
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
@@ -74,7 +74,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test("update returns error for non-existent node", () => {
|
|
77
|
-
const patch:
|
|
77
|
+
const patch: WorkflowPatch = {
|
|
78
78
|
update: [{ nodeId: "nonexistent", node: { label: "Nope" } }],
|
|
79
79
|
};
|
|
80
80
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
@@ -86,7 +86,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
86
86
|
// --- Combined operations ---
|
|
87
87
|
|
|
88
88
|
test("delete → create → update ordering works in single patch", () => {
|
|
89
|
-
const patch:
|
|
89
|
+
const patch: WorkflowPatch = {
|
|
90
90
|
delete: ["b"],
|
|
91
91
|
create: [{ id: "c", type: "script", config: {} }],
|
|
92
92
|
update: [{ nodeId: "a", node: { next: "c" } }],
|
|
@@ -101,7 +101,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test("delete + create same ID in one patch works (delete runs first)", () => {
|
|
104
|
-
const patch:
|
|
104
|
+
const patch: WorkflowPatch = {
|
|
105
105
|
delete: ["b"],
|
|
106
106
|
create: [{ id: "b", type: "script", config: { template: "Replacement" } }],
|
|
107
107
|
};
|
|
@@ -116,7 +116,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
116
116
|
// --- Multiple errors ---
|
|
117
117
|
|
|
118
118
|
test("collects multiple errors in one patch", () => {
|
|
119
|
-
const patch:
|
|
119
|
+
const patch: WorkflowPatch = {
|
|
120
120
|
delete: ["nonexistent1"],
|
|
121
121
|
create: [{ id: "a", type: "script", config: {} }], // duplicate
|
|
122
122
|
update: [{ nodeId: "nonexistent2", node: { label: "Nope" } }],
|
|
@@ -129,7 +129,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
129
129
|
|
|
130
130
|
test("preserves onNodeFailure when not patched", () => {
|
|
131
131
|
const def = makeDef([{ id: "a", type: "agent-task", config: {} }], "continue");
|
|
132
|
-
const patch:
|
|
132
|
+
const patch: WorkflowPatch = {
|
|
133
133
|
update: [{ nodeId: "a", node: { label: "Labeled" } }],
|
|
134
134
|
};
|
|
135
135
|
const result = applyDefinitionPatch(def, patch);
|
|
@@ -138,7 +138,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
test("patches onNodeFailure when provided", () => {
|
|
141
|
-
const patch:
|
|
141
|
+
const patch: WorkflowPatch = { onNodeFailure: "continue" };
|
|
142
142
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
143
143
|
expect(result.errors).toEqual([]);
|
|
144
144
|
expect(result.definition.onNodeFailure).toBe("continue");
|
|
@@ -147,7 +147,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
147
147
|
// --- Empty patch ---
|
|
148
148
|
|
|
149
149
|
test("empty patch returns definition unchanged with no errors", () => {
|
|
150
|
-
const patch:
|
|
150
|
+
const patch: WorkflowPatch = {};
|
|
151
151
|
const result = applyDefinitionPatch(baseDef, patch);
|
|
152
152
|
expect(result.errors).toEqual([]);
|
|
153
153
|
expect(result.definition.nodes).toEqual(baseDef.nodes);
|
|
@@ -159,7 +159,7 @@ describe("applyDefinitionPatch", () => {
|
|
|
159
159
|
test("does not mutate original definition", () => {
|
|
160
160
|
const original = makeDef([{ id: "a", type: "agent-task", config: { template: "Hello" } }]);
|
|
161
161
|
const originalNodes = [...original.nodes];
|
|
162
|
-
const patch:
|
|
162
|
+
const patch: WorkflowPatch = {
|
|
163
163
|
create: [{ id: "b", type: "script", config: {} }],
|
|
164
164
|
};
|
|
165
165
|
applyDefinitionPatch(original, patch);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 verification: a `wait` node correlates against an existing
|
|
3
|
+
* built-in `workflowEventBus` event (`task.completed`) without any new
|
|
4
|
+
* emit code in production.
|
|
5
|
+
*
|
|
6
|
+
* Why a fan-out shape (entry → [agent-task, wait])?
|
|
7
|
+
* --------------------------------------------------
|
|
8
|
+
* Linear `agent-task → wait { eventName: "task.completed" }` does NOT work —
|
|
9
|
+
* by the time execution reaches the wait, the upstream's `task.completed`
|
|
10
|
+
* has already fired and the bus event is gone. The wait must subscribe
|
|
11
|
+
* BEFORE the event fires, so it has to register on a parallel branch.
|
|
12
|
+
*
|
|
13
|
+
* What this test simulates
|
|
14
|
+
* ------------------------
|
|
15
|
+
* Spinning up a real agent-task involves provider work (Claude / Codex /
|
|
16
|
+
* etc.) which is too heavy for a unit test. Instead we exercise the same
|
|
17
|
+
* code path the real emit hits by emitting `task.completed` directly via
|
|
18
|
+
* `workflowEventBus.emit` with a payload shaped exactly like the one
|
|
19
|
+
* produced from `src/be/db.ts` (`{ taskId, output, agentId, workflowRunId,
|
|
20
|
+
* workflowRunStepId }`). This proves the wait correlates against the
|
|
21
|
+
* built-in event by `workflowRunId` filter — no production-code changes
|
|
22
|
+
* outside of Phases 1–3.
|
|
23
|
+
*/
|
|
24
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
25
|
+
import { unlink } from "node:fs/promises";
|
|
26
|
+
import * as db from "../be/db";
|
|
27
|
+
import {
|
|
28
|
+
closeDb,
|
|
29
|
+
createWorkflow,
|
|
30
|
+
deleteWorkflow,
|
|
31
|
+
getWaitStateByStepId,
|
|
32
|
+
getWorkflowRun,
|
|
33
|
+
getWorkflowRunStepsByRunId,
|
|
34
|
+
initDb,
|
|
35
|
+
} from "../be/db";
|
|
36
|
+
import type { Workflow, WorkflowDefinition } from "../types";
|
|
37
|
+
import { startWorkflowExecution } from "../workflows/engine";
|
|
38
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
39
|
+
import type { ExecutorDependencies } from "../workflows/executors/base";
|
|
40
|
+
import { createExecutorRegistry } from "../workflows/executors/registry";
|
|
41
|
+
import { _resetWaitBusSubscriptionsForTests, initWaitBusSubscriptions } from "../workflows/resume";
|
|
42
|
+
|
|
43
|
+
const TEST_DB_PATH = "./test-workflow-wait-builtin-events.sqlite";
|
|
44
|
+
|
|
45
|
+
const deps: ExecutorDependencies = {
|
|
46
|
+
db: db as typeof import("../be/db"),
|
|
47
|
+
eventBus: workflowEventBus,
|
|
48
|
+
interpolate: (t: string) => t,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createdWorkflowIds: string[] = [];
|
|
52
|
+
|
|
53
|
+
function makeWorkflow(name: string, def: WorkflowDefinition): Workflow {
|
|
54
|
+
const wf = createWorkflow({ name: `${name}-${Date.now()}-${Math.random()}`, definition: def });
|
|
55
|
+
createdWorkflowIds.push(wf.id);
|
|
56
|
+
return wf;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
try {
|
|
61
|
+
await unlink(TEST_DB_PATH);
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
initDb(TEST_DB_PATH);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
for (const id of createdWorkflowIds) {
|
|
70
|
+
try {
|
|
71
|
+
deleteWorkflow(id);
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
closeDb();
|
|
75
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
76
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
_resetWaitBusSubscriptionsForTests();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
async function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
|
|
85
|
+
const t0 = Date.now();
|
|
86
|
+
while (!predicate()) {
|
|
87
|
+
if (Date.now() - t0 > timeoutMs) throw new Error("waitFor: timeout");
|
|
88
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("WaitExecutor — built-in bus events (Phase 4 verification)", () => {
|
|
93
|
+
test("wait correlates against task.completed via workflowRunId filter (fan-out)", async () => {
|
|
94
|
+
const registry = createExecutorRegistry(deps);
|
|
95
|
+
|
|
96
|
+
// Fan-out shape: a script entry node spawns two parallel branches.
|
|
97
|
+
// Branch A is a placeholder for the agent-task that would fire
|
|
98
|
+
// task.completed. Branch B is the wait node, which subscribes to
|
|
99
|
+
// `task.completed` BEFORE the simulated emit happens. After the wait
|
|
100
|
+
// resolves it falls through to a terminal `notify` node.
|
|
101
|
+
const def: WorkflowDefinition = {
|
|
102
|
+
nodes: [
|
|
103
|
+
{
|
|
104
|
+
id: "entry",
|
|
105
|
+
type: "script",
|
|
106
|
+
config: { runtime: "bash", script: "echo go" },
|
|
107
|
+
next: ["w1", "tail"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "w1",
|
|
111
|
+
type: "wait",
|
|
112
|
+
config: {
|
|
113
|
+
mode: "event",
|
|
114
|
+
eventName: "task.completed",
|
|
115
|
+
// Built-in task.completed payload includes `workflowRunId` —
|
|
116
|
+
// we filter on it to correlate against this specific run.
|
|
117
|
+
// The matcher walks the dot-path, so this maps to
|
|
118
|
+
// payload.workflowRunId === <captured run id>.
|
|
119
|
+
// We can't interpolate {{trigger.runId}} here without an
|
|
120
|
+
// explicit `inputs` mapping, so the test rebuilds the def
|
|
121
|
+
// after `runId` is known and re-creates the workflow.
|
|
122
|
+
// For simplicity we use scope='run' which already enforces
|
|
123
|
+
// workflowRunId === waitState.workflowRunId — that's the
|
|
124
|
+
// canonical correlation path for built-in events.
|
|
125
|
+
scope: "run",
|
|
126
|
+
},
|
|
127
|
+
next: { event: "tail" },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "tail",
|
|
131
|
+
type: "notify",
|
|
132
|
+
config: { channel: "swarm", template: "done" },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
const wf = makeWorkflow("wait-builtin-task-completed", def);
|
|
137
|
+
const runId = await startWorkflowExecution(wf, {}, registry);
|
|
138
|
+
|
|
139
|
+
// Initialize the bus listener registry (parity with initWorkflows()).
|
|
140
|
+
initWaitBusSubscriptions(registry);
|
|
141
|
+
|
|
142
|
+
// The wait should be in 'waiting' status at this point — it registered
|
|
143
|
+
// its bus subscription during execute. The fan-out's other branch may
|
|
144
|
+
// already have run.
|
|
145
|
+
expect(getWorkflowRun(runId)?.status).toBe("waiting");
|
|
146
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
147
|
+
const w1 = steps.find((s) => s.nodeId === "w1");
|
|
148
|
+
expect(w1?.status).toBe("waiting");
|
|
149
|
+
|
|
150
|
+
const waitState = getWaitStateByStepId(w1!.id);
|
|
151
|
+
expect(waitState).not.toBeNull();
|
|
152
|
+
expect(waitState?.eventName).toBe("task.completed");
|
|
153
|
+
|
|
154
|
+
// Simulate the agent-task completing. The payload is exactly what
|
|
155
|
+
// `completeTask` in src/be/db.ts emits today — including
|
|
156
|
+
// workflowRunId, which the scope='run' matcher uses to correlate.
|
|
157
|
+
workflowEventBus.emit("task.completed", {
|
|
158
|
+
taskId: "fake-task-id-1",
|
|
159
|
+
output: '{"result":"ok"}',
|
|
160
|
+
agentId: "fake-agent-id",
|
|
161
|
+
workflowRunId: runId,
|
|
162
|
+
workflowRunStepId: "fake-step-id-on-some-other-run",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await waitFor(() => getWaitStateByStepId(w1!.id)?.status === "fired");
|
|
166
|
+
|
|
167
|
+
const fired = getWaitStateByStepId(w1!.id);
|
|
168
|
+
expect(fired?.status).toBe("fired");
|
|
169
|
+
const stored = fired?.firedPayload as { workflowRunId?: string; output?: string };
|
|
170
|
+
expect(stored?.workflowRunId).toBe(runId);
|
|
171
|
+
|
|
172
|
+
// The wait advanced via `event` port — the run should reach the tail.
|
|
173
|
+
const finalSteps = getWorkflowRunStepsByRunId(runId);
|
|
174
|
+
const w1After = finalSteps.find((s) => s.nodeId === "w1");
|
|
175
|
+
expect(w1After?.status).toBe("completed");
|
|
176
|
+
expect(w1After?.nextPort).toBe("event");
|
|
177
|
+
|
|
178
|
+
// tail should run (could be once or twice depending on convergence —
|
|
179
|
+
// we just assert it completed at least once).
|
|
180
|
+
const tailSteps = finalSteps.filter((s) => s.nodeId === "tail");
|
|
181
|
+
expect(tailSteps.length).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect(tailSteps.every((s) => s.status === "completed")).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("scope='run' rejects task.completed from a different run", async () => {
|
|
186
|
+
const registry = createExecutorRegistry(deps);
|
|
187
|
+
|
|
188
|
+
const def: WorkflowDefinition = {
|
|
189
|
+
nodes: [
|
|
190
|
+
{
|
|
191
|
+
id: "entry",
|
|
192
|
+
type: "script",
|
|
193
|
+
config: { runtime: "bash", script: "echo go" },
|
|
194
|
+
next: ["w1", "tail"],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "w1",
|
|
198
|
+
type: "wait",
|
|
199
|
+
config: { mode: "event", eventName: "task.completed", scope: "run" },
|
|
200
|
+
next: { event: "tail" },
|
|
201
|
+
},
|
|
202
|
+
{ id: "tail", type: "notify", config: { channel: "swarm", template: "done" } },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
const wf = makeWorkflow("wait-builtin-task-completed-other-run", def);
|
|
206
|
+
const runId = await startWorkflowExecution(wf, {}, registry);
|
|
207
|
+
|
|
208
|
+
initWaitBusSubscriptions(registry);
|
|
209
|
+
|
|
210
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
211
|
+
const w1 = steps.find((s) => s.nodeId === "w1");
|
|
212
|
+
|
|
213
|
+
// Emit a task.completed bound to a DIFFERENT run — must NOT resolve
|
|
214
|
+
// this wait because scope='run' enforces workflowRunId === this runId.
|
|
215
|
+
workflowEventBus.emit("task.completed", {
|
|
216
|
+
taskId: "task-from-elsewhere",
|
|
217
|
+
output: "ignored",
|
|
218
|
+
agentId: "x",
|
|
219
|
+
workflowRunId: "some-other-run-id-not-ours",
|
|
220
|
+
workflowRunStepId: "some-other-step",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Yield to listeners.
|
|
224
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
225
|
+
expect(getWaitStateByStepId(w1!.id)?.status).toBe("pending");
|
|
226
|
+
|
|
227
|
+
// Now emit one bound to OUR run — should resolve.
|
|
228
|
+
workflowEventBus.emit("task.completed", {
|
|
229
|
+
taskId: "task-for-us",
|
|
230
|
+
output: "ok",
|
|
231
|
+
agentId: "x",
|
|
232
|
+
workflowRunId: runId,
|
|
233
|
+
workflowRunStepId: "any-step",
|
|
234
|
+
});
|
|
235
|
+
await waitFor(() => getWaitStateByStepId(w1!.id)?.status === "fired");
|
|
236
|
+
expect(getWaitStateByStepId(w1!.id)?.status).toBe("fired");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("create-workflow accepts a wait-node-containing definition (Zod round-trip)", () => {
|
|
240
|
+
// Validation is purely Zod-driven via WorkflowDefinitionSchema. Wait
|
|
241
|
+
// node config is a `z.record` at the schema level; the WaitExecutor's
|
|
242
|
+
// own configSchema parses it at execution time. Here we just round-trip
|
|
243
|
+
// a definition with both time- and event-mode wait nodes through the
|
|
244
|
+
// public `createWorkflow` path to prove the schema accepts them.
|
|
245
|
+
const def: WorkflowDefinition = {
|
|
246
|
+
nodes: [
|
|
247
|
+
{
|
|
248
|
+
id: "time-wait",
|
|
249
|
+
type: "wait",
|
|
250
|
+
config: { mode: "time", durationMs: 30000 },
|
|
251
|
+
next: { default: "event-wait" },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "event-wait",
|
|
255
|
+
type: "wait",
|
|
256
|
+
config: {
|
|
257
|
+
mode: "event",
|
|
258
|
+
eventName: "demo.signal",
|
|
259
|
+
filter: { ok: true },
|
|
260
|
+
scope: "run",
|
|
261
|
+
timeoutMs: 60_000,
|
|
262
|
+
},
|
|
263
|
+
next: { event: "yay", timeout: "nay" },
|
|
264
|
+
},
|
|
265
|
+
{ id: "yay", type: "notify", config: { channel: "swarm", template: "ok" } },
|
|
266
|
+
{ id: "nay", type: "notify", config: { channel: "swarm", template: "timed out" } },
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const wf = makeWorkflow("wait-roundtrip", def);
|
|
270
|
+
expect(wf.id).toBeTruthy();
|
|
271
|
+
expect(wf.definition.nodes).toHaveLength(4);
|
|
272
|
+
const timeWait = wf.definition.nodes.find((n) => n.id === "time-wait");
|
|
273
|
+
expect(timeWait?.type).toBe("wait");
|
|
274
|
+
expect((timeWait?.config as { mode?: string })?.mode).toBe("time");
|
|
275
|
+
const eventWait = wf.definition.nodes.find((n) => n.id === "event-wait");
|
|
276
|
+
expect((eventWait?.config as { mode?: string })?.mode).toBe("event");
|
|
277
|
+
expect((eventWait?.config as { timeoutMs?: number })?.timeoutMs).toBe(60_000);
|
|
278
|
+
});
|
|
279
|
+
});
|