@uploadista/core 0.0.13-beta.5 → 0.0.13
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/{checksum-P9C2JlRk.mjs → checksum-CtOagryS.mjs} +2 -2
- package/dist/{checksum-P9C2JlRk.mjs.map → checksum-CtOagryS.mjs.map} +1 -1
- package/dist/errors/index.d.cts +2 -2
- package/dist/errors/index.d.mts +2 -2
- package/dist/errors/index.mjs +1 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +5 -5
- package/dist/flow/index.d.mts +5 -5
- package/dist/flow/index.mjs +1 -1
- package/dist/{flow-DkTE3siV.cjs → flow-ChADffZ5.cjs} +1 -1
- package/dist/{flow-IgE8hj7H.mjs → flow-_J9-Dm_m.mjs} +2 -2
- package/dist/flow-_J9-Dm_m.mjs.map +1 -0
- package/dist/{index-CrZopnP9.d.cts → index-4VDJDcWM.d.cts} +227 -241
- package/dist/index-4VDJDcWM.d.cts.map +1 -0
- package/dist/{index-BPBI84iT.d.mts → index-Bi9YYid8.d.mts} +2 -2
- package/dist/{index-BPBI84iT.d.mts.map → index-Bi9YYid8.d.mts.map} +1 -1
- package/dist/{index-BteFEg-c.d.mts → index-Cbf1OPLp.d.mts} +2 -2
- package/dist/{index-BteFEg-c.d.mts.map → index-Cbf1OPLp.d.mts.map} +1 -1
- package/dist/{index-DMfADSSJ.d.cts → index-De4wQJwR.d.cts} +2 -2
- package/dist/{index-DMfADSSJ.d.cts.map → index-De4wQJwR.d.cts.map} +1 -1
- package/dist/{index-DHt7Ht_J.d.mts → index-RgOX4psL.d.mts} +305 -139
- package/dist/index-RgOX4psL.d.mts.map +1 -0
- package/dist/{index-DubOIur4.d.cts → index-qZ90PVNl.d.cts} +2 -2
- package/dist/index-qZ90PVNl.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/{stream-limiter-DFtRZczp.mjs → stream-limiter-D9KSAaoY.mjs} +2 -2
- package/dist/{stream-limiter-DFtRZczp.mjs.map → stream-limiter-D9KSAaoY.mjs.map} +1 -1
- package/dist/streams/index.d.cts +2 -2
- package/dist/streams/index.d.mts +2 -2
- package/dist/streams/index.mjs +1 -1
- package/dist/testing/index.cjs +1 -0
- package/dist/testing/index.d.cts +110 -0
- package/dist/testing/index.d.cts.map +1 -0
- package/dist/testing/index.d.mts +110 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +2 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -1
- package/dist/{types-DGZ892my.mjs → types-BI_KmpTc.mjs} +2 -2
- package/dist/types-BI_KmpTc.mjs.map +1 -0
- package/dist/upload/index.d.cts +5 -5
- package/dist/upload/index.d.mts +5 -5
- package/dist/upload/index.mjs +1 -1
- package/dist/{upload-DJTptYqV.mjs → upload-Yj5lrtZo.mjs} +2 -2
- package/dist/{upload-DJTptYqV.mjs.map → upload-Yj5lrtZo.mjs.map} +1 -1
- package/dist/{uploadista-error-9yLWP7TC.d.cts → uploadista-error-BQLhNZcY.d.cts} +1 -1
- package/dist/{uploadista-error-9yLWP7TC.d.cts.map → uploadista-error-BQLhNZcY.d.cts.map} +1 -1
- package/dist/{uploadista-error-nZ_q-EZy.mjs → uploadista-error-Buscq-FR.mjs} +1 -1
- package/dist/{uploadista-error-nZ_q-EZy.mjs.map → uploadista-error-Buscq-FR.mjs.map} +1 -1
- package/dist/{uploadista-error-CBkvsyZ3.d.mts → uploadista-error-DUWw6OqS.d.mts} +1 -1
- package/dist/{uploadista-error-CBkvsyZ3.d.mts.map → uploadista-error-DUWw6OqS.d.mts.map} +1 -1
- package/dist/utils/index.d.cts +2 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-BicUw_lt.mjs → utils-BWiu6lqv.mjs} +2 -2
- package/dist/{utils-BicUw_lt.mjs.map → utils-BWiu6lqv.mjs.map} +1 -1
- package/package.json +14 -6
- package/src/flow/node.ts +4 -4
- package/src/flow/nodes/transform-node.ts +23 -2
- package/src/flow/plugins/credential-provider.ts +1 -1
- package/src/flow/plugins/image-ai-plugin.ts +1 -1
- package/src/flow/plugins/image-plugin.ts +1 -1
- package/src/flow/plugins/video-plugin.ts +1 -1
- package/src/flow/plugins/zip-plugin.ts +1 -1
- package/src/flow/types/type-utils.ts +14 -3
- package/src/testing/index.ts +14 -0
- package/src/testing/mock-image-ai-plugin.ts +33 -0
- package/src/testing/mock-image-plugin.ts +56 -0
- package/src/testing/mock-upload-server.ts +176 -0
- package/src/testing/mock-video-plugin.ts +94 -0
- package/src/testing/mock-zip-plugin.ts +41 -0
- package/src/types/data-store.ts +1 -1
- package/{src/errors/__tests__ → tests/errors}/uploadista-error.test.ts +23 -19
- package/{src → tests}/flow/edge.test.ts +1 -1
- package/tests/flow/flow.test.ts +853 -0
- package/tests/flow/node.test.ts +757 -0
- package/{src → tests}/streams/stream-limiter.test.ts +2 -2
- package/tests/types/typed-event-emitter.test.ts +282 -0
- package/{src → tests}/utils/debounce.test.ts +1 -1
- package/{src → tests}/utils/once.test.ts +1 -1
- package/tests/utils/test-layers.ts +183 -0
- package/{src → tests}/utils/throttle.test.ts +1 -1
- package/tsdown.config.ts +1 -0
- package/type-tests/flow.test-d.ts +93 -0
- package/type-tests/type-utils.test-d.ts +104 -51
- package/vitest.config.ts +19 -1
- package/dist/flow-IgE8hj7H.mjs.map +0 -1
- package/dist/index-CrZopnP9.d.cts.map +0 -1
- package/dist/index-DHt7Ht_J.d.mts.map +0 -1
- package/dist/index-DubOIur4.d.cts.map +0 -1
- package/dist/types-DGZ892my.mjs.map +0 -1
- /package/dist/{errors-C0zLx77t.mjs → errors-DEFjN-xn.mjs} +0 -0
- /package/dist/{index-BtBZHVmz.d.cts → index-C-svZlpj.d.mts} +0 -0
- /package/dist/{index-DEHBdV_z.d.mts → index-_wQ5ClJU.d.cts} +0 -0
- /package/dist/{streams-CJKKIAwy.mjs → streams-DPU17bYp.mjs} +0 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Flow Engine DAG processing and validation
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Flow creation and configuration
|
|
6
|
+
* - DAG validation and cycle detection
|
|
7
|
+
* - Node execution lifecycle
|
|
8
|
+
* - Edge connection and data flow
|
|
9
|
+
* - Result type handling (success, error, cancellation)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Effect } from "effect";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { UploadistaError } from "../../src/errors";
|
|
16
|
+
import { createFlow } from "../../src/flow";
|
|
17
|
+
import { createFlowNode, NodeType } from "../../src/flow/node";
|
|
18
|
+
|
|
19
|
+
describe("Flow Engine", () => {
|
|
20
|
+
describe("Flow Creation", () => {
|
|
21
|
+
it("should create a flow with name and id", () =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const flow = yield* createFlow({
|
|
24
|
+
flowId: "test-flow-1",
|
|
25
|
+
name: "Test Flow",
|
|
26
|
+
inputSchema: z.object({ value: z.string() }),
|
|
27
|
+
outputSchema: z.object({ result: z.string() }),
|
|
28
|
+
nodes: {},
|
|
29
|
+
edges: [],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(flow.id).toBe("test-flow-1");
|
|
33
|
+
expect(flow.name).toBe("Test Flow");
|
|
34
|
+
}).pipe(Effect.runPromise));
|
|
35
|
+
|
|
36
|
+
it("should create a flow with nodes and edges", () =>
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const inputNode = yield* createFlowNode({
|
|
39
|
+
id: "input-1",
|
|
40
|
+
name: "Input Node",
|
|
41
|
+
description: "Test input",
|
|
42
|
+
type: NodeType.input,
|
|
43
|
+
inputSchema: z.object({ value: z.string() }),
|
|
44
|
+
outputSchema: z.object({ value: z.string() }),
|
|
45
|
+
run: ({ data }) =>
|
|
46
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const outputNode = yield* createFlowNode({
|
|
50
|
+
id: "output-1",
|
|
51
|
+
name: "Output Node",
|
|
52
|
+
description: "Test output",
|
|
53
|
+
type: NodeType.output,
|
|
54
|
+
inputSchema: z.object({ value: z.string() }),
|
|
55
|
+
outputSchema: z.object({ value: z.string() }),
|
|
56
|
+
run: ({ data }) =>
|
|
57
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const flow = yield* createFlow({
|
|
61
|
+
flowId: "test-flow-2",
|
|
62
|
+
name: "Test Flow with Nodes",
|
|
63
|
+
inputSchema: z.object({ value: z.string() }),
|
|
64
|
+
outputSchema: z.object({ value: z.string() }),
|
|
65
|
+
nodes: {
|
|
66
|
+
"input-1": inputNode,
|
|
67
|
+
"output-1": outputNode,
|
|
68
|
+
},
|
|
69
|
+
edges: [{ source: "input-1", target: "output-1" }],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(flow.nodes).toHaveLength(2);
|
|
73
|
+
expect(flow.edges).toHaveLength(1);
|
|
74
|
+
expect(flow.nodes[0]?.id).toBe("input-1");
|
|
75
|
+
expect(flow.nodes[1]?.id).toBe("output-1");
|
|
76
|
+
}).pipe(Effect.runPromise));
|
|
77
|
+
|
|
78
|
+
it("should handle empty flow creation", () =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const flow = yield* createFlow({
|
|
81
|
+
flowId: "empty-flow",
|
|
82
|
+
name: "Empty Flow",
|
|
83
|
+
inputSchema: z.object({}),
|
|
84
|
+
outputSchema: z.object({}),
|
|
85
|
+
nodes: {},
|
|
86
|
+
edges: [],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(flow.nodes).toHaveLength(0);
|
|
90
|
+
expect(flow.edges).toHaveLength(0);
|
|
91
|
+
}).pipe(Effect.runPromise));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("DAG Validation", () => {
|
|
95
|
+
it("should detect cycles in the graph", () =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
const node1 = yield* createFlowNode({
|
|
98
|
+
id: "node-1",
|
|
99
|
+
name: "Node 1",
|
|
100
|
+
description: "First node",
|
|
101
|
+
type: NodeType.process,
|
|
102
|
+
inputSchema: z.object({ value: z.string() }),
|
|
103
|
+
outputSchema: z.object({ value: z.string() }),
|
|
104
|
+
run: ({ data }) =>
|
|
105
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const node2 = yield* createFlowNode({
|
|
109
|
+
id: "node-2",
|
|
110
|
+
name: "Node 2",
|
|
111
|
+
description: "Second node",
|
|
112
|
+
type: NodeType.process,
|
|
113
|
+
inputSchema: z.object({ value: z.string() }),
|
|
114
|
+
outputSchema: z.object({ value: z.string() }),
|
|
115
|
+
run: ({ data }) =>
|
|
116
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Attempt to create flow with cycle - this should either fail or be detected during execution
|
|
120
|
+
const result = yield* Effect.either(
|
|
121
|
+
createFlow({
|
|
122
|
+
flowId: "cycle-flow",
|
|
123
|
+
name: "Flow with Cycle",
|
|
124
|
+
inputSchema: z.object({ value: z.string() }),
|
|
125
|
+
outputSchema: z.object({ value: z.string() }),
|
|
126
|
+
nodes: {
|
|
127
|
+
"node-1": node1,
|
|
128
|
+
"node-2": node2,
|
|
129
|
+
},
|
|
130
|
+
edges: [
|
|
131
|
+
{ source: "node-1", target: "node-2" },
|
|
132
|
+
{ source: "node-2", target: "node-1" },
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Note: Current implementation may or may not detect cycles at creation time
|
|
138
|
+
// This test documents the expected behavior - cycle detection should happen
|
|
139
|
+
// either at creation or execution time
|
|
140
|
+
if (result._tag === "Right") {
|
|
141
|
+
// If flow creation succeeds, execution should detect the cycle
|
|
142
|
+
const flow = result.right;
|
|
143
|
+
const runResult = yield* Effect.either(
|
|
144
|
+
flow.run({
|
|
145
|
+
inputs: { "node-1": { value: "test" } },
|
|
146
|
+
storageId: "test-storage",
|
|
147
|
+
jobId: "test-job",
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Execution should fail with cycle detection or timeout
|
|
152
|
+
// This is a known issue to be fixed - test documents expected behavior
|
|
153
|
+
expect(runResult._tag).toBe("Left");
|
|
154
|
+
}
|
|
155
|
+
}).pipe(Effect.runPromise));
|
|
156
|
+
|
|
157
|
+
it("should allow valid DAG structures", () =>
|
|
158
|
+
Effect.gen(function* () {
|
|
159
|
+
const inputNode = yield* createFlowNode({
|
|
160
|
+
id: "input",
|
|
161
|
+
name: "Input",
|
|
162
|
+
description: "Input node",
|
|
163
|
+
type: NodeType.input,
|
|
164
|
+
inputSchema: z.object({ value: z.string() }),
|
|
165
|
+
outputSchema: z.object({ value: z.string() }),
|
|
166
|
+
run: ({ data }) =>
|
|
167
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const process1 = yield* createFlowNode({
|
|
171
|
+
id: "process-1",
|
|
172
|
+
name: "Process 1",
|
|
173
|
+
description: "First processor",
|
|
174
|
+
type: NodeType.process,
|
|
175
|
+
inputSchema: z.object({ value: z.string() }),
|
|
176
|
+
outputSchema: z.object({ value: z.string() }),
|
|
177
|
+
run: ({ data }) =>
|
|
178
|
+
Effect.succeed({
|
|
179
|
+
type: "complete",
|
|
180
|
+
data: { value: `${data.value}-processed-1` },
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const process2 = yield* createFlowNode({
|
|
185
|
+
id: "process-2",
|
|
186
|
+
name: "Process 2",
|
|
187
|
+
description: "Second processor",
|
|
188
|
+
type: NodeType.process,
|
|
189
|
+
inputSchema: z.object({ value: z.string() }),
|
|
190
|
+
outputSchema: z.object({ value: z.string() }),
|
|
191
|
+
run: ({ data }) =>
|
|
192
|
+
Effect.succeed({
|
|
193
|
+
type: "complete",
|
|
194
|
+
data: { value: `${data.value}-processed-2` },
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const outputNode = yield* createFlowNode({
|
|
199
|
+
id: "output",
|
|
200
|
+
name: "Output",
|
|
201
|
+
description: "Output node",
|
|
202
|
+
type: NodeType.output,
|
|
203
|
+
inputSchema: z.object({ value: z.string() }),
|
|
204
|
+
outputSchema: z.object({ value: z.string() }),
|
|
205
|
+
run: ({ data }) =>
|
|
206
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Valid DAG: input -> process-1 -> process-2 -> output
|
|
210
|
+
const flow = yield* createFlow({
|
|
211
|
+
flowId: "valid-dag",
|
|
212
|
+
name: "Valid DAG Flow",
|
|
213
|
+
inputSchema: z.object({ value: z.string() }),
|
|
214
|
+
outputSchema: z.object({ value: z.string() }),
|
|
215
|
+
nodes: {
|
|
216
|
+
input: inputNode,
|
|
217
|
+
"process-1": process1,
|
|
218
|
+
"process-2": process2,
|
|
219
|
+
output: outputNode,
|
|
220
|
+
},
|
|
221
|
+
edges: [
|
|
222
|
+
{ source: "input", target: "process-1" },
|
|
223
|
+
{ source: "process-1", target: "process-2" },
|
|
224
|
+
{ source: "process-2", target: "output" },
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(flow.nodes).toHaveLength(4);
|
|
229
|
+
expect(flow.edges).toHaveLength(3);
|
|
230
|
+
}).pipe(Effect.runPromise));
|
|
231
|
+
|
|
232
|
+
it("should handle disconnected nodes", () =>
|
|
233
|
+
Effect.gen(function* () {
|
|
234
|
+
const node1 = yield* createFlowNode({
|
|
235
|
+
id: "disconnected-1",
|
|
236
|
+
name: "Disconnected 1",
|
|
237
|
+
description: "First disconnected node",
|
|
238
|
+
type: NodeType.process,
|
|
239
|
+
inputSchema: z.object({ value: z.string() }),
|
|
240
|
+
outputSchema: z.object({ value: z.string() }),
|
|
241
|
+
run: ({ data }) =>
|
|
242
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const node2 = yield* createFlowNode({
|
|
246
|
+
id: "disconnected-2",
|
|
247
|
+
name: "Disconnected 2",
|
|
248
|
+
description: "Second disconnected node",
|
|
249
|
+
type: NodeType.process,
|
|
250
|
+
inputSchema: z.object({ value: z.string() }),
|
|
251
|
+
outputSchema: z.object({ value: z.string() }),
|
|
252
|
+
run: ({ data }) =>
|
|
253
|
+
Effect.succeed({ type: "complete", data: { value: data.value } }),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Create flow with no edges - disconnected nodes
|
|
257
|
+
const flow = yield* createFlow({
|
|
258
|
+
flowId: "disconnected-flow",
|
|
259
|
+
name: "Disconnected Flow",
|
|
260
|
+
inputSchema: z.object({ value: z.string() }),
|
|
261
|
+
outputSchema: z.object({ value: z.string() }),
|
|
262
|
+
nodes: {
|
|
263
|
+
"disconnected-1": node1,
|
|
264
|
+
"disconnected-2": node2,
|
|
265
|
+
},
|
|
266
|
+
edges: [],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(flow.nodes).toHaveLength(2);
|
|
270
|
+
expect(flow.edges).toHaveLength(0);
|
|
271
|
+
}).pipe(Effect.runPromise));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Node Execution Lifecycle", () => {
|
|
275
|
+
it("should execute a single node successfully", () =>
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const inputNode = yield* createFlowNode({
|
|
278
|
+
id: "input-node",
|
|
279
|
+
name: "Input Node",
|
|
280
|
+
description: "Test input node",
|
|
281
|
+
type: NodeType.input,
|
|
282
|
+
inputSchema: z.object({ value: z.string() }),
|
|
283
|
+
outputSchema: z.object({ value: z.string() }),
|
|
284
|
+
run: ({ data }) =>
|
|
285
|
+
Effect.succeed({
|
|
286
|
+
type: "complete",
|
|
287
|
+
data: { value: data.value },
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const outputNode = yield* createFlowNode({
|
|
292
|
+
id: "output-node",
|
|
293
|
+
name: "Output Node",
|
|
294
|
+
description: "Test output node",
|
|
295
|
+
type: NodeType.output,
|
|
296
|
+
inputSchema: z.object({ value: z.string() }),
|
|
297
|
+
outputSchema: z.object({ result: z.string() }),
|
|
298
|
+
run: ({ data }) =>
|
|
299
|
+
Effect.succeed({
|
|
300
|
+
type: "complete",
|
|
301
|
+
data: { result: `processed-${data.value}` },
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const flow = yield* createFlow({
|
|
306
|
+
flowId: "single-node-flow",
|
|
307
|
+
name: "Single Node Flow",
|
|
308
|
+
inputSchema: z.object({ value: z.string() }),
|
|
309
|
+
outputSchema: z.object({ result: z.string() }),
|
|
310
|
+
nodes: {
|
|
311
|
+
"input-node": inputNode,
|
|
312
|
+
"output-node": outputNode,
|
|
313
|
+
},
|
|
314
|
+
edges: [{ source: "input-node", target: "output-node" }],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const result = yield* flow.run({
|
|
318
|
+
inputs: { "input-node": { value: "test" } },
|
|
319
|
+
storageId: "test-storage",
|
|
320
|
+
jobId: "test-job",
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(result.type).toBe("completed");
|
|
324
|
+
if (result.type === "completed") {
|
|
325
|
+
expect(result.result["output-node"]).toEqual({
|
|
326
|
+
result: "processed-test",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}).pipe(Effect.runPromise));
|
|
330
|
+
|
|
331
|
+
it("should execute multiple nodes in sequence", () =>
|
|
332
|
+
Effect.gen(function* () {
|
|
333
|
+
const node1 = yield* createFlowNode({
|
|
334
|
+
id: "seq-node-1",
|
|
335
|
+
name: "Sequential Node 1",
|
|
336
|
+
description: "First node in sequence",
|
|
337
|
+
type: NodeType.input,
|
|
338
|
+
inputSchema: z.object({ value: z.number() }),
|
|
339
|
+
outputSchema: z.object({ value: z.number() }),
|
|
340
|
+
run: ({ data }) =>
|
|
341
|
+
Effect.succeed({
|
|
342
|
+
type: "complete",
|
|
343
|
+
data: { value: data.value + 1 },
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const node2 = yield* createFlowNode({
|
|
348
|
+
id: "seq-node-2",
|
|
349
|
+
name: "Sequential Node 2",
|
|
350
|
+
description: "Second node in sequence",
|
|
351
|
+
type: NodeType.process,
|
|
352
|
+
inputSchema: z.object({ value: z.number() }),
|
|
353
|
+
outputSchema: z.object({ value: z.number() }),
|
|
354
|
+
run: ({ data }) =>
|
|
355
|
+
Effect.succeed({
|
|
356
|
+
type: "complete",
|
|
357
|
+
data: { value: data.value * 2 },
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const node3 = yield* createFlowNode({
|
|
362
|
+
id: "seq-node-3",
|
|
363
|
+
name: "Sequential Node 3",
|
|
364
|
+
description: "Third node in sequence",
|
|
365
|
+
type: NodeType.output,
|
|
366
|
+
inputSchema: z.object({ value: z.number() }),
|
|
367
|
+
outputSchema: z.object({ value: z.number() }),
|
|
368
|
+
run: ({ data }) =>
|
|
369
|
+
Effect.succeed({
|
|
370
|
+
type: "complete",
|
|
371
|
+
data: { value: data.value + 10 },
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const flow = yield* createFlow({
|
|
376
|
+
flowId: "sequential-flow",
|
|
377
|
+
name: "Sequential Flow",
|
|
378
|
+
inputSchema: z.object({ value: z.number() }),
|
|
379
|
+
outputSchema: z.object({ value: z.number() }),
|
|
380
|
+
nodes: {
|
|
381
|
+
"seq-node-1": node1,
|
|
382
|
+
"seq-node-2": node2,
|
|
383
|
+
"seq-node-3": node3,
|
|
384
|
+
},
|
|
385
|
+
edges: [
|
|
386
|
+
{ source: "seq-node-1", target: "seq-node-2" },
|
|
387
|
+
{ source: "seq-node-2", target: "seq-node-3" },
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const result = yield* flow.run({
|
|
392
|
+
inputs: { "seq-node-1": { value: 5 } },
|
|
393
|
+
storageId: "test-storage",
|
|
394
|
+
jobId: "test-job",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Expected: (5 + 1) * 2 + 10 = 6 * 2 + 10 = 12 + 10 = 22
|
|
398
|
+
expect(result.type).toBe("completed");
|
|
399
|
+
if (result.type === "completed") {
|
|
400
|
+
expect(result.result["seq-node-3"].value).toBe(22);
|
|
401
|
+
}
|
|
402
|
+
}).pipe(Effect.runPromise));
|
|
403
|
+
|
|
404
|
+
it("should handle node execution errors gracefully", () =>
|
|
405
|
+
Effect.gen(function* () {
|
|
406
|
+
const failingNode = yield* createFlowNode({
|
|
407
|
+
id: "failing-node",
|
|
408
|
+
name: "Failing Node",
|
|
409
|
+
description: "Node that always fails",
|
|
410
|
+
type: NodeType.process,
|
|
411
|
+
inputSchema: z.object({ value: z.string() }),
|
|
412
|
+
outputSchema: z.object({ value: z.string() }),
|
|
413
|
+
run: () =>
|
|
414
|
+
Effect.fail(
|
|
415
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
416
|
+
body: "Intentional failure for testing",
|
|
417
|
+
}),
|
|
418
|
+
),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const flow = yield* createFlow({
|
|
422
|
+
flowId: "error-flow",
|
|
423
|
+
name: "Error Flow",
|
|
424
|
+
inputSchema: z.object({ value: z.string() }),
|
|
425
|
+
outputSchema: z.object({ value: z.string() }),
|
|
426
|
+
nodes: {
|
|
427
|
+
"failing-node": failingNode,
|
|
428
|
+
},
|
|
429
|
+
edges: [],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const result = yield* Effect.either(
|
|
433
|
+
flow.run({
|
|
434
|
+
inputs: { "failing-node": { value: "test" } },
|
|
435
|
+
storageId: "test-storage",
|
|
436
|
+
jobId: "test-job",
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(result._tag).toBe("Left");
|
|
441
|
+
if (result._tag === "Left") {
|
|
442
|
+
expect(result.left).toBeInstanceOf(UploadistaError);
|
|
443
|
+
}
|
|
444
|
+
}).pipe(Effect.runPromise));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("Edge Connection and Data Flow", () => {
|
|
448
|
+
it("should pass data through connected nodes", () =>
|
|
449
|
+
Effect.gen(function* () {
|
|
450
|
+
const sourceNode = yield* createFlowNode({
|
|
451
|
+
id: "source",
|
|
452
|
+
name: "Source",
|
|
453
|
+
description: "Source node",
|
|
454
|
+
type: NodeType.input,
|
|
455
|
+
inputSchema: z.object({ text: z.string() }),
|
|
456
|
+
outputSchema: z.object({
|
|
457
|
+
text: z.string(),
|
|
458
|
+
metadata: z.object({ processed: z.boolean() }),
|
|
459
|
+
}),
|
|
460
|
+
run: ({ data }) =>
|
|
461
|
+
Effect.succeed({
|
|
462
|
+
type: "complete",
|
|
463
|
+
data: { text: data.text, metadata: { processed: true } },
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const targetNode = yield* createFlowNode({
|
|
468
|
+
id: "target",
|
|
469
|
+
name: "Target",
|
|
470
|
+
description: "Target node",
|
|
471
|
+
type: NodeType.output,
|
|
472
|
+
inputSchema: z.object({
|
|
473
|
+
text: z.string(),
|
|
474
|
+
metadata: z.object({ processed: z.boolean() }),
|
|
475
|
+
}),
|
|
476
|
+
outputSchema: z.object({ result: z.string() }),
|
|
477
|
+
run: ({ data }) =>
|
|
478
|
+
Effect.succeed({
|
|
479
|
+
type: "complete",
|
|
480
|
+
data: {
|
|
481
|
+
result: `${data.text} (${data.metadata.processed ? "processed" : "unprocessed"})`,
|
|
482
|
+
},
|
|
483
|
+
}),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const flow = yield* createFlow({
|
|
487
|
+
flowId: "data-flow",
|
|
488
|
+
name: "Data Flow",
|
|
489
|
+
inputSchema: z.object({ text: z.string() }),
|
|
490
|
+
outputSchema: z.object({ result: z.string() }),
|
|
491
|
+
nodes: {
|
|
492
|
+
source: sourceNode,
|
|
493
|
+
target: targetNode,
|
|
494
|
+
},
|
|
495
|
+
edges: [
|
|
496
|
+
{
|
|
497
|
+
source: "source",
|
|
498
|
+
target: "target",
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const result = yield* flow.run({
|
|
504
|
+
inputs: { source: { text: "hello" } },
|
|
505
|
+
|
|
506
|
+
storageId: "test-storage",
|
|
507
|
+
jobId: "test-job",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
expect(result.type).toBe("completed");
|
|
511
|
+
if (result.type === "completed") {
|
|
512
|
+
expect(result.result.target.result).toBe("hello (processed)");
|
|
513
|
+
}
|
|
514
|
+
}).pipe(Effect.runPromise));
|
|
515
|
+
|
|
516
|
+
it("should handle multiple edges from single source", () =>
|
|
517
|
+
Effect.gen(function* () {
|
|
518
|
+
const sourceNode = yield* createFlowNode({
|
|
519
|
+
id: "multi-source",
|
|
520
|
+
name: "Multi Source",
|
|
521
|
+
description: "Source with multiple outputs",
|
|
522
|
+
type: NodeType.input,
|
|
523
|
+
inputSchema: z.object({ value: z.number() }),
|
|
524
|
+
outputSchema: z.object({ value: z.number() }),
|
|
525
|
+
run: ({ data }) =>
|
|
526
|
+
Effect.succeed({
|
|
527
|
+
type: "complete",
|
|
528
|
+
data: { value: data.value * 2 },
|
|
529
|
+
}),
|
|
530
|
+
multiOutput: true,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const target1 = yield* createFlowNode({
|
|
534
|
+
id: "target-1",
|
|
535
|
+
name: "Target 1",
|
|
536
|
+
description: "First target",
|
|
537
|
+
type: NodeType.output,
|
|
538
|
+
inputSchema: z.object({ value: z.number() }),
|
|
539
|
+
outputSchema: z.object({ value: z.number() }),
|
|
540
|
+
run: ({ data }) =>
|
|
541
|
+
Effect.succeed({
|
|
542
|
+
type: "complete",
|
|
543
|
+
data: { value: data.value + 1 },
|
|
544
|
+
}),
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const target2 = yield* createFlowNode({
|
|
548
|
+
id: "target-2",
|
|
549
|
+
name: "Target 2",
|
|
550
|
+
description: "Second target",
|
|
551
|
+
type: NodeType.output,
|
|
552
|
+
inputSchema: z.object({ value: z.number() }),
|
|
553
|
+
outputSchema: z.object({ value: z.number() }),
|
|
554
|
+
run: ({ data }) =>
|
|
555
|
+
Effect.succeed({
|
|
556
|
+
type: "complete",
|
|
557
|
+
data: { value: data.value + 2 },
|
|
558
|
+
}),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const flow = yield* createFlow({
|
|
562
|
+
flowId: "multi-edge-flow",
|
|
563
|
+
name: "Multi Edge Flow",
|
|
564
|
+
inputSchema: z.object({ value: z.number() }),
|
|
565
|
+
outputSchema: z.object({ value: z.number() }),
|
|
566
|
+
nodes: {
|
|
567
|
+
"multi-source": sourceNode,
|
|
568
|
+
"target-1": target1,
|
|
569
|
+
"target-2": target2,
|
|
570
|
+
},
|
|
571
|
+
edges: [
|
|
572
|
+
{
|
|
573
|
+
source: "multi-source",
|
|
574
|
+
target: "target-1",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
source: "multi-source",
|
|
578
|
+
target: "target-2",
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Flow should execute and fan out to both targets
|
|
584
|
+
const result = yield* flow.run({
|
|
585
|
+
inputs: { "multi-source": { value: 5 } },
|
|
586
|
+
|
|
587
|
+
storageId: "test-storage",
|
|
588
|
+
jobId: "test-job",
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
expect(result.type).toBe("completed");
|
|
592
|
+
}).pipe(Effect.runPromise));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe("Result Type Handling", () => {
|
|
596
|
+
it("should handle success results", () =>
|
|
597
|
+
Effect.gen(function* () {
|
|
598
|
+
const inputNode = yield* createFlowNode({
|
|
599
|
+
id: "input-node",
|
|
600
|
+
name: "Input Node",
|
|
601
|
+
description: "Input for success test",
|
|
602
|
+
type: NodeType.input,
|
|
603
|
+
inputSchema: z.object({ value: z.string() }),
|
|
604
|
+
outputSchema: z.object({ value: z.string() }),
|
|
605
|
+
run: ({ data }) =>
|
|
606
|
+
Effect.succeed({
|
|
607
|
+
type: "complete",
|
|
608
|
+
data: { value: data.value },
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const successNode = yield* createFlowNode({
|
|
613
|
+
id: "success-node",
|
|
614
|
+
name: "Success Node",
|
|
615
|
+
description: "Node that succeeds",
|
|
616
|
+
type: NodeType.output,
|
|
617
|
+
inputSchema: z.object({ value: z.string() }),
|
|
618
|
+
outputSchema: z.object({
|
|
619
|
+
result: z.string(),
|
|
620
|
+
status: z.literal("success"),
|
|
621
|
+
}),
|
|
622
|
+
run: ({ data }) =>
|
|
623
|
+
Effect.succeed({
|
|
624
|
+
type: "complete",
|
|
625
|
+
data: { result: data.value, status: "success" as const },
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const flow = yield* createFlow({
|
|
630
|
+
flowId: "success-flow",
|
|
631
|
+
name: "Success Flow",
|
|
632
|
+
inputSchema: z.object({ value: z.string() }),
|
|
633
|
+
outputSchema: z.object({
|
|
634
|
+
result: z.string(),
|
|
635
|
+
status: z.literal("success"),
|
|
636
|
+
}),
|
|
637
|
+
nodes: {
|
|
638
|
+
"input-node": inputNode,
|
|
639
|
+
"success-node": successNode,
|
|
640
|
+
},
|
|
641
|
+
edges: [{ source: "input-node", target: "success-node" }],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const result = yield* flow.run({
|
|
645
|
+
inputs: { "input-node": { value: "test" } },
|
|
646
|
+
|
|
647
|
+
storageId: "test-storage",
|
|
648
|
+
jobId: "test-job",
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(result.type).toBe("completed");
|
|
652
|
+
if (result.type === "completed") {
|
|
653
|
+
expect(result.result["success-node"]?.status).toBe("success");
|
|
654
|
+
expect(result.result["success-node"]?.result).toBe("test");
|
|
655
|
+
}
|
|
656
|
+
}).pipe(Effect.runPromise));
|
|
657
|
+
|
|
658
|
+
it("should handle error results", () =>
|
|
659
|
+
Effect.gen(function* () {
|
|
660
|
+
const inputNode = yield* createFlowNode({
|
|
661
|
+
id: "input-node",
|
|
662
|
+
name: "Input Node",
|
|
663
|
+
description: "Input for error test",
|
|
664
|
+
type: NodeType.input,
|
|
665
|
+
inputSchema: z.object({ value: z.string() }),
|
|
666
|
+
outputSchema: z.object({ value: z.string() }),
|
|
667
|
+
run: ({ data }) =>
|
|
668
|
+
Effect.succeed({
|
|
669
|
+
type: "complete",
|
|
670
|
+
data: { value: data.value },
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const errorNode = yield* createFlowNode({
|
|
675
|
+
id: "error-node",
|
|
676
|
+
name: "Error Node",
|
|
677
|
+
description: "Node that throws error",
|
|
678
|
+
type: NodeType.output,
|
|
679
|
+
inputSchema: z.object({ value: z.string() }),
|
|
680
|
+
outputSchema: z.object({ value: z.string() }),
|
|
681
|
+
run: () =>
|
|
682
|
+
Effect.fail(
|
|
683
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
684
|
+
body: "Test error message",
|
|
685
|
+
}),
|
|
686
|
+
),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const flow = yield* createFlow({
|
|
690
|
+
flowId: "error-result-flow",
|
|
691
|
+
name: "Error Result Flow",
|
|
692
|
+
inputSchema: z.object({ value: z.string() }),
|
|
693
|
+
outputSchema: z.object({ value: z.string() }),
|
|
694
|
+
nodes: {
|
|
695
|
+
"input-node": inputNode,
|
|
696
|
+
"error-node": errorNode,
|
|
697
|
+
},
|
|
698
|
+
edges: [{ source: "input-node", target: "error-node" }],
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const result = yield* Effect.either(
|
|
702
|
+
flow.run({
|
|
703
|
+
inputs: { "input-node": { value: "test" } },
|
|
704
|
+
storageId: "test-storage",
|
|
705
|
+
jobId: "test-job",
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result._tag).toBe("Left");
|
|
710
|
+
if (result._tag === "Left") {
|
|
711
|
+
const error = result.left;
|
|
712
|
+
expect(error).toBeInstanceOf(UploadistaError);
|
|
713
|
+
expect(error.body).toBe("Test error message");
|
|
714
|
+
}
|
|
715
|
+
}).pipe(Effect.runPromise));
|
|
716
|
+
|
|
717
|
+
it("should handle validation errors in input", () =>
|
|
718
|
+
Effect.gen(function* () {
|
|
719
|
+
const inputNode = yield* createFlowNode({
|
|
720
|
+
id: "input-node",
|
|
721
|
+
name: "Input Node",
|
|
722
|
+
description: "Input with validation",
|
|
723
|
+
type: NodeType.input,
|
|
724
|
+
inputSchema: z.object({
|
|
725
|
+
value: z.string(),
|
|
726
|
+
count: z.number(),
|
|
727
|
+
}),
|
|
728
|
+
outputSchema: z.object({
|
|
729
|
+
value: z.string(),
|
|
730
|
+
count: z.number(),
|
|
731
|
+
}),
|
|
732
|
+
run: ({ data }) =>
|
|
733
|
+
Effect.succeed({
|
|
734
|
+
type: "complete",
|
|
735
|
+
data: { value: data.value, count: data.count },
|
|
736
|
+
}),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const strictNode = yield* createFlowNode({
|
|
740
|
+
id: "strict-node",
|
|
741
|
+
name: "Strict Node",
|
|
742
|
+
description: "Node with strict input validation",
|
|
743
|
+
type: NodeType.output,
|
|
744
|
+
inputSchema: z.object({
|
|
745
|
+
value: z.string().min(5),
|
|
746
|
+
count: z.number().positive(),
|
|
747
|
+
}),
|
|
748
|
+
outputSchema: z.object({ value: z.string() }),
|
|
749
|
+
run: ({ data }) =>
|
|
750
|
+
Effect.succeed({
|
|
751
|
+
type: "complete",
|
|
752
|
+
data: { value: data.value },
|
|
753
|
+
}),
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const flow = yield* createFlow({
|
|
757
|
+
flowId: "validation-flow",
|
|
758
|
+
name: "Validation Flow",
|
|
759
|
+
inputSchema: z.object({
|
|
760
|
+
value: z.string(),
|
|
761
|
+
count: z.number(),
|
|
762
|
+
}),
|
|
763
|
+
outputSchema: z.object({ value: z.string() }),
|
|
764
|
+
nodes: {
|
|
765
|
+
"input-node": inputNode,
|
|
766
|
+
"strict-node": strictNode,
|
|
767
|
+
},
|
|
768
|
+
edges: [{ source: "input-node", target: "strict-node" }],
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Invalid input - too short string
|
|
772
|
+
const result = yield* Effect.either(
|
|
773
|
+
flow.run({
|
|
774
|
+
inputs: { "input-node": { value: "abc", count: 5 } },
|
|
775
|
+
storageId: "test-storage",
|
|
776
|
+
jobId: "test-job",
|
|
777
|
+
}),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
expect(result._tag).toBe("Left");
|
|
781
|
+
if (result._tag === "Left") {
|
|
782
|
+
const error = result.left;
|
|
783
|
+
expect(error).toBeInstanceOf(UploadistaError);
|
|
784
|
+
expect(error.code).toBe("FLOW_INPUT_VALIDATION_ERROR");
|
|
785
|
+
}
|
|
786
|
+
}).pipe(Effect.runPromise));
|
|
787
|
+
|
|
788
|
+
it("should handle validation errors in output", () =>
|
|
789
|
+
Effect.gen(function* () {
|
|
790
|
+
const inputNode = yield* createFlowNode({
|
|
791
|
+
id: "input-node",
|
|
792
|
+
name: "Input Node",
|
|
793
|
+
description: "Input for output validation test",
|
|
794
|
+
type: NodeType.input,
|
|
795
|
+
inputSchema: z.object({ value: z.string() }),
|
|
796
|
+
outputSchema: z.object({ value: z.string() }),
|
|
797
|
+
run: ({ data }) =>
|
|
798
|
+
Effect.succeed({
|
|
799
|
+
type: "complete",
|
|
800
|
+
data: { value: data.value },
|
|
801
|
+
}),
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const invalidOutputNode = yield* createFlowNode({
|
|
805
|
+
id: "invalid-output-node",
|
|
806
|
+
name: "Invalid Output Node",
|
|
807
|
+
description: "Node that produces invalid output",
|
|
808
|
+
type: NodeType.output,
|
|
809
|
+
inputSchema: z.object({ value: z.string() }),
|
|
810
|
+
outputSchema: z.object({
|
|
811
|
+
value: z.string(),
|
|
812
|
+
requiredField: z.string(),
|
|
813
|
+
}),
|
|
814
|
+
run: ({ data }) =>
|
|
815
|
+
// Intentionally return invalid output (missing requiredField)
|
|
816
|
+
Effect.succeed({
|
|
817
|
+
type: "complete",
|
|
818
|
+
data: { value: data.value },
|
|
819
|
+
}),
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const flow = yield* createFlow({
|
|
823
|
+
flowId: "invalid-output-flow",
|
|
824
|
+
name: "Invalid Output Flow",
|
|
825
|
+
inputSchema: z.object({ value: z.string() }),
|
|
826
|
+
outputSchema: z.object({
|
|
827
|
+
value: z.string(),
|
|
828
|
+
requiredField: z.string(),
|
|
829
|
+
}),
|
|
830
|
+
nodes: {
|
|
831
|
+
"input-node": inputNode,
|
|
832
|
+
"invalid-output-node": invalidOutputNode,
|
|
833
|
+
},
|
|
834
|
+
edges: [{ source: "input-node", target: "invalid-output-node" }],
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const result = yield* Effect.either(
|
|
838
|
+
flow.run({
|
|
839
|
+
inputs: { "input-node": { value: "test" } },
|
|
840
|
+
storageId: "test-storage",
|
|
841
|
+
jobId: "test-job",
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
expect(result._tag).toBe("Left");
|
|
846
|
+
if (result._tag === "Left") {
|
|
847
|
+
const error = result.left;
|
|
848
|
+
expect(error).toBeInstanceOf(UploadistaError);
|
|
849
|
+
expect(error.code).toBe("FLOW_OUTPUT_VALIDATION_ERROR");
|
|
850
|
+
}
|
|
851
|
+
}).pipe(Effect.runPromise));
|
|
852
|
+
});
|
|
853
|
+
});
|