@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,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Flow Node creation, execution, and lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Node creation with various configurations
|
|
6
|
+
* - Input/output validation
|
|
7
|
+
* - Node execution lifecycle
|
|
8
|
+
* - Retry logic with exponential backoff
|
|
9
|
+
* - Conditional node evaluation
|
|
10
|
+
* - Multi-input and multi-output nodes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { it } from "@effect/vitest";
|
|
14
|
+
import { Effect } from "effect";
|
|
15
|
+
import { describe, expect } from "vitest";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { UploadistaError } from "../../src/errors";
|
|
18
|
+
import {
|
|
19
|
+
type ConditionField,
|
|
20
|
+
type ConditionOperator,
|
|
21
|
+
createFlowNode,
|
|
22
|
+
NodeType,
|
|
23
|
+
} from "../../src/flow/node";
|
|
24
|
+
|
|
25
|
+
describe("Flow Node", () => {
|
|
26
|
+
describe("Node Creation", () => {
|
|
27
|
+
it.effect("should create a basic node with all required fields", () =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const node = yield* createFlowNode({
|
|
30
|
+
id: "test-node-1",
|
|
31
|
+
name: "Test Node",
|
|
32
|
+
description: "A test node",
|
|
33
|
+
type: NodeType.process,
|
|
34
|
+
inputSchema: z.object({ value: z.string() }),
|
|
35
|
+
outputSchema: z.object({ result: z.string() }),
|
|
36
|
+
run: ({ data }) =>
|
|
37
|
+
Effect.succeed({ type: "complete", data: { result: data.value } }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(node.id).toBe("test-node-1");
|
|
41
|
+
expect(node.name).toBe("Test Node");
|
|
42
|
+
expect(node.description).toBe("A test node");
|
|
43
|
+
expect(node.type).toBe(NodeType.process);
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
it.effect("should create nodes of different types", () =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const inputNode = yield* createFlowNode({
|
|
50
|
+
id: "input",
|
|
51
|
+
name: "Input",
|
|
52
|
+
description: "Input node",
|
|
53
|
+
type: NodeType.input,
|
|
54
|
+
inputSchema: z.object({ data: z.string() }),
|
|
55
|
+
outputSchema: z.object({ data: z.string() }),
|
|
56
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const processNode = yield* createFlowNode({
|
|
60
|
+
id: "process",
|
|
61
|
+
name: "Process",
|
|
62
|
+
description: "Process node",
|
|
63
|
+
type: NodeType.process,
|
|
64
|
+
inputSchema: z.object({ data: z.string() }),
|
|
65
|
+
outputSchema: z.object({ data: z.string() }),
|
|
66
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const outputNode = yield* createFlowNode({
|
|
70
|
+
id: "output",
|
|
71
|
+
name: "Output",
|
|
72
|
+
description: "Output node",
|
|
73
|
+
type: NodeType.output,
|
|
74
|
+
inputSchema: z.object({ data: z.string() }),
|
|
75
|
+
outputSchema: z.object({ data: z.string() }),
|
|
76
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(inputNode.type).toBe(NodeType.input);
|
|
80
|
+
expect(processNode.type).toBe(NodeType.process);
|
|
81
|
+
expect(outputNode.type).toBe(NodeType.output);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
it.effect("should create node with retry configuration", () =>
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const node = yield* createFlowNode({
|
|
88
|
+
id: "retry-node",
|
|
89
|
+
name: "Retry Node",
|
|
90
|
+
description: "Node with retry logic",
|
|
91
|
+
type: NodeType.process,
|
|
92
|
+
inputSchema: z.object({ value: z.string() }),
|
|
93
|
+
outputSchema: z.object({ value: z.string() }),
|
|
94
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
95
|
+
retry: {
|
|
96
|
+
maxRetries: 3,
|
|
97
|
+
retryDelay: 1000,
|
|
98
|
+
exponentialBackoff: true,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(node.retry).toEqual({
|
|
103
|
+
maxRetries: 3,
|
|
104
|
+
retryDelay: 1000,
|
|
105
|
+
exponentialBackoff: true,
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
it.effect("should create pausable node", () =>
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const node = yield* createFlowNode({
|
|
113
|
+
id: "pausable-node",
|
|
114
|
+
name: "Pausable Node",
|
|
115
|
+
description: "Node that can pause",
|
|
116
|
+
type: NodeType.process,
|
|
117
|
+
inputSchema: z.object({ value: z.string() }),
|
|
118
|
+
outputSchema: z.object({ value: z.string() }),
|
|
119
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
120
|
+
pausable: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(node.pausable).toBe(true);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it.effect("should create multi-input node", () =>
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
const node = yield* createFlowNode({
|
|
130
|
+
id: "multi-input-node",
|
|
131
|
+
name: "Multi Input",
|
|
132
|
+
description: "Accepts multiple inputs",
|
|
133
|
+
type: NodeType.merge,
|
|
134
|
+
inputSchema: z.record(z.string(), z.object({ value: z.string() })),
|
|
135
|
+
outputSchema: z.object({ value: z.string() }),
|
|
136
|
+
run: ({ data }) =>
|
|
137
|
+
Effect.succeed({
|
|
138
|
+
type: "complete",
|
|
139
|
+
data: { value: Object.keys(data).join(",") },
|
|
140
|
+
}),
|
|
141
|
+
multiInput: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(node.multiInput).toBe(true);
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
it.effect("should create multi-output node", () =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const node = yield* createFlowNode({
|
|
151
|
+
id: "multi-output-node",
|
|
152
|
+
name: "Multi Output",
|
|
153
|
+
description: "Produces multiple outputs",
|
|
154
|
+
type: NodeType.multiplex,
|
|
155
|
+
inputSchema: z.object({ value: z.string() }),
|
|
156
|
+
outputSchema: z.object({ value: z.string() }),
|
|
157
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
158
|
+
multiOutput: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(node.multiOutput).toBe(true);
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
it.effect("should create conditional node with condition", () =>
|
|
166
|
+
Effect.gen(function* () {
|
|
167
|
+
const condition = {
|
|
168
|
+
field: "mimeType" as ConditionField,
|
|
169
|
+
operator: "equals" as ConditionOperator,
|
|
170
|
+
value: "image/jpeg",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const node = yield* createFlowNode({
|
|
174
|
+
id: "conditional-node",
|
|
175
|
+
name: "Conditional",
|
|
176
|
+
description: "Routes based on condition",
|
|
177
|
+
type: NodeType.conditional,
|
|
178
|
+
inputSchema: z.object({ mimeType: z.string() }),
|
|
179
|
+
outputSchema: z.object({ mimeType: z.string() }),
|
|
180
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
181
|
+
condition,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(node.condition).toEqual(condition);
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("Node Execution", () => {
|
|
190
|
+
it.effect("should execute node with valid input", () =>
|
|
191
|
+
Effect.gen(function* () {
|
|
192
|
+
const node = yield* createFlowNode({
|
|
193
|
+
id: "exec-node",
|
|
194
|
+
name: "Execution Node",
|
|
195
|
+
description: "Test execution",
|
|
196
|
+
type: NodeType.process,
|
|
197
|
+
inputSchema: z.object({ value: z.string() }),
|
|
198
|
+
outputSchema: z.object({ result: z.string() }),
|
|
199
|
+
run: ({ data }) =>
|
|
200
|
+
Effect.succeed({
|
|
201
|
+
type: "complete",
|
|
202
|
+
data: { result: `processed-${data.value}` },
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = yield* node.run({
|
|
207
|
+
data: { value: "test" },
|
|
208
|
+
jobId: "job-1",
|
|
209
|
+
storageId: "storage-1",
|
|
210
|
+
flowId: "flow-1",
|
|
211
|
+
clientId: null,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result.type).toBe("complete");
|
|
215
|
+
if (result.type === "complete") {
|
|
216
|
+
expect(result.data.result).toBe("processed-test");
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
it.effect("should fail on invalid input", () =>
|
|
222
|
+
Effect.gen(function* () {
|
|
223
|
+
const node = yield* createFlowNode({
|
|
224
|
+
id: "strict-node",
|
|
225
|
+
name: "Strict Node",
|
|
226
|
+
description: "Strict validation",
|
|
227
|
+
type: NodeType.process,
|
|
228
|
+
inputSchema: z.object({
|
|
229
|
+
value: z.string().min(5),
|
|
230
|
+
}),
|
|
231
|
+
outputSchema: z.object({ value: z.string() }),
|
|
232
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = yield* Effect.either(
|
|
236
|
+
node.run({
|
|
237
|
+
data: { value: "abc" }, // Too short
|
|
238
|
+
jobId: "job-1",
|
|
239
|
+
storageId: "storage-1",
|
|
240
|
+
flowId: "flow-1",
|
|
241
|
+
clientId: null,
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(result._tag).toBe("Left");
|
|
246
|
+
if (result._tag === "Left") {
|
|
247
|
+
expect(result.left).toBeInstanceOf(UploadistaError);
|
|
248
|
+
expect(result.left.code).toBe("FLOW_INPUT_VALIDATION_ERROR");
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
it.effect("should fail on invalid output", () =>
|
|
254
|
+
Effect.gen(function* () {
|
|
255
|
+
const node = yield* createFlowNode({
|
|
256
|
+
id: "bad-output-node",
|
|
257
|
+
name: "Bad Output",
|
|
258
|
+
description: "Produces invalid output",
|
|
259
|
+
type: NodeType.process,
|
|
260
|
+
inputSchema: z.object({ value: z.string() }),
|
|
261
|
+
outputSchema: z.object({
|
|
262
|
+
value: z.string(),
|
|
263
|
+
required: z.number(),
|
|
264
|
+
}),
|
|
265
|
+
run: ({ data }) =>
|
|
266
|
+
// Return incomplete output
|
|
267
|
+
Effect.succeed({
|
|
268
|
+
type: "complete",
|
|
269
|
+
data: { value: data.value },
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = yield* Effect.either(
|
|
274
|
+
node.run({
|
|
275
|
+
data: { value: "test" },
|
|
276
|
+
jobId: "job-1",
|
|
277
|
+
storageId: "storage-1",
|
|
278
|
+
flowId: "flow-1",
|
|
279
|
+
clientId: null,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(result._tag).toBe("Left");
|
|
284
|
+
if (result._tag === "Left") {
|
|
285
|
+
expect(result.left).toBeInstanceOf(UploadistaError);
|
|
286
|
+
expect(result.left.code).toBe("FLOW_OUTPUT_VALIDATION_ERROR");
|
|
287
|
+
}
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
it.effect("should pass context parameters to run function", () =>
|
|
292
|
+
Effect.gen(function* () {
|
|
293
|
+
const contextNode = yield* createFlowNode({
|
|
294
|
+
id: "context-node",
|
|
295
|
+
name: "Context Node",
|
|
296
|
+
description: "Uses context parameters",
|
|
297
|
+
type: NodeType.process,
|
|
298
|
+
inputSchema: z.object({ value: z.string() }),
|
|
299
|
+
outputSchema: z.object({
|
|
300
|
+
value: z.string(),
|
|
301
|
+
jobId: z.string(),
|
|
302
|
+
storageId: z.string(),
|
|
303
|
+
flowId: z.string(),
|
|
304
|
+
hasClientId: z.boolean(),
|
|
305
|
+
}),
|
|
306
|
+
run: ({ data, jobId, storageId, flowId, clientId }) =>
|
|
307
|
+
Effect.succeed({
|
|
308
|
+
type: "complete",
|
|
309
|
+
data: {
|
|
310
|
+
value: data.value,
|
|
311
|
+
jobId,
|
|
312
|
+
storageId,
|
|
313
|
+
flowId,
|
|
314
|
+
hasClientId: clientId !== null,
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const result = yield* contextNode.run({
|
|
320
|
+
data: { value: "test" },
|
|
321
|
+
jobId: "test-job",
|
|
322
|
+
storageId: "test-storage",
|
|
323
|
+
flowId: "test-flow",
|
|
324
|
+
clientId: "test-client",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.type).toBe("complete");
|
|
328
|
+
if (result.type === "complete") {
|
|
329
|
+
expect(result.data.jobId).toBe("test-job");
|
|
330
|
+
expect(result.data.storageId).toBe("test-storage");
|
|
331
|
+
expect(result.data.flowId).toBe("test-flow");
|
|
332
|
+
expect(result.data.hasClientId).toBe(true);
|
|
333
|
+
}
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
it.effect("should handle waiting state from pausable nodes", () =>
|
|
338
|
+
Effect.gen(function* () {
|
|
339
|
+
const pausableNode = yield* createFlowNode({
|
|
340
|
+
id: "waiting-node",
|
|
341
|
+
name: "Waiting Node",
|
|
342
|
+
description: "Returns waiting state",
|
|
343
|
+
type: NodeType.process,
|
|
344
|
+
inputSchema: z.object({ value: z.string() }),
|
|
345
|
+
outputSchema: z.object({ value: z.string() }),
|
|
346
|
+
run: ({ data }) =>
|
|
347
|
+
Effect.succeed({
|
|
348
|
+
type: "waiting" as const,
|
|
349
|
+
partialData: { value: data.value, reason: "Waiting for external input" },
|
|
350
|
+
}),
|
|
351
|
+
pausable: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const result = yield* pausableNode.run({
|
|
355
|
+
data: { value: "test" },
|
|
356
|
+
jobId: "job-1",
|
|
357
|
+
storageId: "storage-1",
|
|
358
|
+
flowId: "flow-1",
|
|
359
|
+
clientId: null,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(result.type).toBe("waiting");
|
|
363
|
+
if (result.type === "waiting") {
|
|
364
|
+
expect(result.partialData).toBeDefined();
|
|
365
|
+
}
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("Retry Logic", () => {
|
|
371
|
+
it.effect("should attempt retry on failure", () =>
|
|
372
|
+
Effect.gen(function* () {
|
|
373
|
+
let attempts = 0;
|
|
374
|
+
|
|
375
|
+
const retryNode = yield* createFlowNode({
|
|
376
|
+
id: "retry-test",
|
|
377
|
+
name: "Retry Test",
|
|
378
|
+
description: "Tests retry logic",
|
|
379
|
+
type: NodeType.process,
|
|
380
|
+
inputSchema: z.object({ value: z.string() }),
|
|
381
|
+
outputSchema: z.object({ value: z.string(), attempts: z.number() }),
|
|
382
|
+
run: ({ data }) =>
|
|
383
|
+
Effect.gen(function* () {
|
|
384
|
+
attempts++;
|
|
385
|
+
// Fail first 2 attempts, succeed on 3rd
|
|
386
|
+
if (attempts < 3) {
|
|
387
|
+
return yield* Effect.fail(
|
|
388
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
389
|
+
body: "Temporary failure",
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
type: "complete" as const,
|
|
395
|
+
data: { value: data.value, attempts },
|
|
396
|
+
};
|
|
397
|
+
}),
|
|
398
|
+
retry: {
|
|
399
|
+
maxRetries: 3,
|
|
400
|
+
retryDelay: 100,
|
|
401
|
+
exponentialBackoff: false,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Note: Current implementation may not have retry logic at node level
|
|
406
|
+
// This test documents expected behavior
|
|
407
|
+
const result = yield* Effect.either(
|
|
408
|
+
retryNode.run({
|
|
409
|
+
data: { value: "test" },
|
|
410
|
+
jobId: "job-1",
|
|
411
|
+
storageId: "storage-1",
|
|
412
|
+
flowId: "flow-1",
|
|
413
|
+
clientId: null,
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Should eventually succeed after retries
|
|
418
|
+
// If retry not implemented, test will document this as a TODO
|
|
419
|
+
if (result._tag === "Right") {
|
|
420
|
+
expect((result.right as { type: string }).type).toBe("complete");
|
|
421
|
+
}
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
it.effect("should use exponential backoff when configured", () =>
|
|
426
|
+
Effect.gen(function* () {
|
|
427
|
+
const delays: number[] = [];
|
|
428
|
+
let attempts = 0;
|
|
429
|
+
|
|
430
|
+
const backoffNode = yield* createFlowNode({
|
|
431
|
+
id: "backoff-test",
|
|
432
|
+
name: "Backoff Test",
|
|
433
|
+
description: "Tests exponential backoff",
|
|
434
|
+
type: NodeType.process,
|
|
435
|
+
inputSchema: z.object({ value: z.string() }),
|
|
436
|
+
outputSchema: z.object({ value: z.string() }),
|
|
437
|
+
run: ({ data }) =>
|
|
438
|
+
Effect.gen(function* () {
|
|
439
|
+
attempts++;
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
delays.push(now);
|
|
442
|
+
|
|
443
|
+
if (attempts < 4) {
|
|
444
|
+
return yield* Effect.fail(
|
|
445
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
446
|
+
body: "Temporary failure",
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
type: "complete" as const,
|
|
452
|
+
data: { value: data.value },
|
|
453
|
+
};
|
|
454
|
+
}),
|
|
455
|
+
retry: {
|
|
456
|
+
maxRetries: 4,
|
|
457
|
+
retryDelay: 100,
|
|
458
|
+
exponentialBackoff: true,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Test exponential backoff timing
|
|
463
|
+
// This is a documentation test - retry logic may not be implemented yet
|
|
464
|
+
const result = yield* Effect.either(
|
|
465
|
+
backoffNode.run({
|
|
466
|
+
data: { value: "test" },
|
|
467
|
+
jobId: "job-1",
|
|
468
|
+
storageId: "storage-1",
|
|
469
|
+
flowId: "flow-1",
|
|
470
|
+
clientId: null,
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// If retry is implemented, delays should increase exponentially
|
|
475
|
+
// Current implementation: document expected behavior
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
it.effect("should respect maxRetries limit", () =>
|
|
480
|
+
Effect.gen(function* () {
|
|
481
|
+
let attempts = 0;
|
|
482
|
+
|
|
483
|
+
const limitedRetryNode = yield* createFlowNode({
|
|
484
|
+
id: "limited-retry",
|
|
485
|
+
name: "Limited Retry",
|
|
486
|
+
description: "Tests retry limit",
|
|
487
|
+
type: NodeType.process,
|
|
488
|
+
inputSchema: z.object({ value: z.string() }),
|
|
489
|
+
outputSchema: z.object({ value: z.string() }),
|
|
490
|
+
run: () =>
|
|
491
|
+
Effect.gen(function* () {
|
|
492
|
+
attempts++;
|
|
493
|
+
// Always fail
|
|
494
|
+
return yield* Effect.fail(
|
|
495
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
496
|
+
body: "Permanent failure",
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
}),
|
|
500
|
+
retry: {
|
|
501
|
+
maxRetries: 2,
|
|
502
|
+
retryDelay: 10,
|
|
503
|
+
exponentialBackoff: false,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const result = yield* Effect.either(
|
|
508
|
+
limitedRetryNode.run({
|
|
509
|
+
data: { value: "test" },
|
|
510
|
+
jobId: "job-1",
|
|
511
|
+
storageId: "storage-1",
|
|
512
|
+
flowId: "flow-1",
|
|
513
|
+
clientId: null,
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(result._tag).toBe("Left");
|
|
518
|
+
// Should have attempted initial + 2 retries = 3 total
|
|
519
|
+
// Note: May not be implemented yet - test documents expected behavior
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe("Conditional Nodes", () => {
|
|
525
|
+
it.effect("should create conditional node with various operators", () =>
|
|
526
|
+
Effect.gen(function* () {
|
|
527
|
+
const operators: ConditionOperator[] = [
|
|
528
|
+
"equals",
|
|
529
|
+
"notEquals",
|
|
530
|
+
"greaterThan",
|
|
531
|
+
"lessThan",
|
|
532
|
+
"contains",
|
|
533
|
+
"startsWith",
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
for (const operator of operators) {
|
|
537
|
+
const node = yield* createFlowNode({
|
|
538
|
+
id: `cond-${operator}`,
|
|
539
|
+
name: `Conditional ${operator}`,
|
|
540
|
+
description: `Test ${operator} operator`,
|
|
541
|
+
type: NodeType.conditional,
|
|
542
|
+
inputSchema: z.object({ value: z.string() }),
|
|
543
|
+
outputSchema: z.object({ value: z.string() }),
|
|
544
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
545
|
+
condition: {
|
|
546
|
+
field: "mimeType",
|
|
547
|
+
operator,
|
|
548
|
+
value: "test-value",
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(node.condition?.operator).toBe(operator);
|
|
553
|
+
}
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
it.effect("should support different condition fields", () =>
|
|
558
|
+
Effect.gen(function* () {
|
|
559
|
+
const fields: ConditionField[] = [
|
|
560
|
+
"mimeType",
|
|
561
|
+
"size",
|
|
562
|
+
"width",
|
|
563
|
+
"height",
|
|
564
|
+
"extension",
|
|
565
|
+
];
|
|
566
|
+
|
|
567
|
+
for (const field of fields) {
|
|
568
|
+
const node = yield* createFlowNode({
|
|
569
|
+
id: `cond-field-${field}`,
|
|
570
|
+
name: `Conditional ${field}`,
|
|
571
|
+
description: `Test ${field} field`,
|
|
572
|
+
type: NodeType.conditional,
|
|
573
|
+
inputSchema: z.object({
|
|
574
|
+
[field]: z.union([z.string(), z.number()]),
|
|
575
|
+
}),
|
|
576
|
+
outputSchema: z.object({
|
|
577
|
+
[field]: z.union([z.string(), z.number()]),
|
|
578
|
+
}),
|
|
579
|
+
run: ({ data }) => Effect.succeed({ type: "complete", data }),
|
|
580
|
+
condition: {
|
|
581
|
+
field,
|
|
582
|
+
operator: "equals",
|
|
583
|
+
value:
|
|
584
|
+
field === "mimeType" || field === "extension" ? "test" : 100,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
expect(node.condition?.field).toBe(field);
|
|
589
|
+
}
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("Complex Schemas", () => {
|
|
595
|
+
it.effect("should handle nested object schemas", () =>
|
|
596
|
+
Effect.gen(function* () {
|
|
597
|
+
const node = yield* createFlowNode({
|
|
598
|
+
id: "nested-schema",
|
|
599
|
+
name: "Nested Schema",
|
|
600
|
+
description: "Complex nested types",
|
|
601
|
+
type: NodeType.process,
|
|
602
|
+
inputSchema: z.object({
|
|
603
|
+
file: z.object({
|
|
604
|
+
name: z.string(),
|
|
605
|
+
size: z.number(),
|
|
606
|
+
metadata: z.object({
|
|
607
|
+
mimeType: z.string(),
|
|
608
|
+
dimensions: z.object({
|
|
609
|
+
width: z.number(),
|
|
610
|
+
height: z.number(),
|
|
611
|
+
}),
|
|
612
|
+
}),
|
|
613
|
+
}),
|
|
614
|
+
}),
|
|
615
|
+
outputSchema: z.object({
|
|
616
|
+
processed: z.boolean(),
|
|
617
|
+
originalSize: z.number(),
|
|
618
|
+
}),
|
|
619
|
+
run: ({ data }) =>
|
|
620
|
+
Effect.succeed({
|
|
621
|
+
type: "complete",
|
|
622
|
+
data: {
|
|
623
|
+
processed: true,
|
|
624
|
+
originalSize: data.file.size,
|
|
625
|
+
},
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const result = yield* node.run({
|
|
630
|
+
data: {
|
|
631
|
+
file: {
|
|
632
|
+
name: "test.jpg",
|
|
633
|
+
size: 1024,
|
|
634
|
+
metadata: {
|
|
635
|
+
mimeType: "image/jpeg",
|
|
636
|
+
dimensions: {
|
|
637
|
+
width: 800,
|
|
638
|
+
height: 600,
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
jobId: "job-1",
|
|
644
|
+
storageId: "storage-1",
|
|
645
|
+
flowId: "flow-1",
|
|
646
|
+
clientId: null,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(result.type).toBe("complete");
|
|
650
|
+
if (result.type === "complete") {
|
|
651
|
+
expect(result.data.processed).toBe(true);
|
|
652
|
+
expect(result.data.originalSize).toBe(1024);
|
|
653
|
+
}
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
it.effect("should handle array schemas", () =>
|
|
658
|
+
Effect.gen(function* () {
|
|
659
|
+
const node = yield* createFlowNode({
|
|
660
|
+
id: "array-schema",
|
|
661
|
+
name: "Array Schema",
|
|
662
|
+
description: "Handles arrays",
|
|
663
|
+
type: NodeType.process,
|
|
664
|
+
inputSchema: z.object({
|
|
665
|
+
items: z.array(z.object({ id: z.string(), value: z.number() })),
|
|
666
|
+
}),
|
|
667
|
+
outputSchema: z.object({
|
|
668
|
+
total: z.number(),
|
|
669
|
+
count: z.number(),
|
|
670
|
+
}),
|
|
671
|
+
run: ({ data }) =>
|
|
672
|
+
Effect.succeed({
|
|
673
|
+
type: "complete",
|
|
674
|
+
data: {
|
|
675
|
+
total: data.items.reduce((sum, item) => sum + item.value, 0),
|
|
676
|
+
count: data.items.length,
|
|
677
|
+
},
|
|
678
|
+
}),
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const result = yield* node.run({
|
|
682
|
+
data: {
|
|
683
|
+
items: [
|
|
684
|
+
{ id: "1", value: 10 },
|
|
685
|
+
{ id: "2", value: 20 },
|
|
686
|
+
{ id: "3", value: 30 },
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
jobId: "job-1",
|
|
690
|
+
storageId: "storage-1",
|
|
691
|
+
flowId: "flow-1",
|
|
692
|
+
clientId: null,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(result.type).toBe("complete");
|
|
696
|
+
if (result.type === "complete") {
|
|
697
|
+
expect(result.data.total).toBe(60);
|
|
698
|
+
expect(result.data.count).toBe(3);
|
|
699
|
+
}
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
it.effect("should handle union schemas", () =>
|
|
704
|
+
Effect.gen(function* () {
|
|
705
|
+
const node = yield* createFlowNode({
|
|
706
|
+
id: "union-schema",
|
|
707
|
+
name: "Union Schema",
|
|
708
|
+
description: "Handles unions",
|
|
709
|
+
type: NodeType.process,
|
|
710
|
+
inputSchema: z.object({
|
|
711
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
712
|
+
}),
|
|
713
|
+
outputSchema: z.object({
|
|
714
|
+
type: z.string(),
|
|
715
|
+
stringified: z.string(),
|
|
716
|
+
}),
|
|
717
|
+
run: ({ data }) =>
|
|
718
|
+
Effect.succeed({
|
|
719
|
+
type: "complete",
|
|
720
|
+
data: {
|
|
721
|
+
type: typeof data.value,
|
|
722
|
+
stringified: String(data.value),
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Test with string
|
|
728
|
+
const result1 = yield* node.run({
|
|
729
|
+
data: { value: "test" },
|
|
730
|
+
jobId: "job-1",
|
|
731
|
+
storageId: "storage-1",
|
|
732
|
+
flowId: "flow-1",
|
|
733
|
+
clientId: null,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(result1.type).toBe("complete");
|
|
737
|
+
if (result1.type === "complete") {
|
|
738
|
+
expect(result1.data.type).toBe("string");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Test with number
|
|
742
|
+
const result2 = yield* node.run({
|
|
743
|
+
data: { value: 42 },
|
|
744
|
+
jobId: "job-1",
|
|
745
|
+
storageId: "storage-1",
|
|
746
|
+
flowId: "flow-1",
|
|
747
|
+
clientId: null,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
expect(result2.type).toBe("complete");
|
|
751
|
+
if (result2.type === "complete") {
|
|
752
|
+
expect(result2.data.type).toBe("number");
|
|
753
|
+
}
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
756
|
+
});
|
|
757
|
+
});
|