@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.
Files changed (101) hide show
  1. package/README.md +1 -1
  2. package/dist/{checksum-P9C2JlRk.mjs → checksum-CtOagryS.mjs} +2 -2
  3. package/dist/{checksum-P9C2JlRk.mjs.map → checksum-CtOagryS.mjs.map} +1 -1
  4. package/dist/errors/index.d.cts +2 -2
  5. package/dist/errors/index.d.mts +2 -2
  6. package/dist/errors/index.mjs +1 -1
  7. package/dist/flow/index.cjs +1 -1
  8. package/dist/flow/index.d.cts +5 -5
  9. package/dist/flow/index.d.mts +5 -5
  10. package/dist/flow/index.mjs +1 -1
  11. package/dist/{flow-DkTE3siV.cjs → flow-ChADffZ5.cjs} +1 -1
  12. package/dist/{flow-IgE8hj7H.mjs → flow-_J9-Dm_m.mjs} +2 -2
  13. package/dist/flow-_J9-Dm_m.mjs.map +1 -0
  14. package/dist/{index-CrZopnP9.d.cts → index-4VDJDcWM.d.cts} +227 -241
  15. package/dist/index-4VDJDcWM.d.cts.map +1 -0
  16. package/dist/{index-BPBI84iT.d.mts → index-Bi9YYid8.d.mts} +2 -2
  17. package/dist/{index-BPBI84iT.d.mts.map → index-Bi9YYid8.d.mts.map} +1 -1
  18. package/dist/{index-BteFEg-c.d.mts → index-Cbf1OPLp.d.mts} +2 -2
  19. package/dist/{index-BteFEg-c.d.mts.map → index-Cbf1OPLp.d.mts.map} +1 -1
  20. package/dist/{index-DMfADSSJ.d.cts → index-De4wQJwR.d.cts} +2 -2
  21. package/dist/{index-DMfADSSJ.d.cts.map → index-De4wQJwR.d.cts.map} +1 -1
  22. package/dist/{index-DHt7Ht_J.d.mts → index-RgOX4psL.d.mts} +305 -139
  23. package/dist/index-RgOX4psL.d.mts.map +1 -0
  24. package/dist/{index-DubOIur4.d.cts → index-qZ90PVNl.d.cts} +2 -2
  25. package/dist/index-qZ90PVNl.d.cts.map +1 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.cts +5 -5
  28. package/dist/index.d.mts +5 -5
  29. package/dist/index.mjs +1 -1
  30. package/dist/{stream-limiter-DFtRZczp.mjs → stream-limiter-D9KSAaoY.mjs} +2 -2
  31. package/dist/{stream-limiter-DFtRZczp.mjs.map → stream-limiter-D9KSAaoY.mjs.map} +1 -1
  32. package/dist/streams/index.d.cts +2 -2
  33. package/dist/streams/index.d.mts +2 -2
  34. package/dist/streams/index.mjs +1 -1
  35. package/dist/testing/index.cjs +1 -0
  36. package/dist/testing/index.d.cts +110 -0
  37. package/dist/testing/index.d.cts.map +1 -0
  38. package/dist/testing/index.d.mts +110 -0
  39. package/dist/testing/index.d.mts.map +1 -0
  40. package/dist/testing/index.mjs +2 -0
  41. package/dist/testing/index.mjs.map +1 -0
  42. package/dist/types/index.d.cts +5 -5
  43. package/dist/types/index.d.mts +5 -5
  44. package/dist/types/index.mjs +1 -1
  45. package/dist/{types-DGZ892my.mjs → types-BI_KmpTc.mjs} +2 -2
  46. package/dist/types-BI_KmpTc.mjs.map +1 -0
  47. package/dist/upload/index.d.cts +5 -5
  48. package/dist/upload/index.d.mts +5 -5
  49. package/dist/upload/index.mjs +1 -1
  50. package/dist/{upload-DJTptYqV.mjs → upload-Yj5lrtZo.mjs} +2 -2
  51. package/dist/{upload-DJTptYqV.mjs.map → upload-Yj5lrtZo.mjs.map} +1 -1
  52. package/dist/{uploadista-error-9yLWP7TC.d.cts → uploadista-error-BQLhNZcY.d.cts} +1 -1
  53. package/dist/{uploadista-error-9yLWP7TC.d.cts.map → uploadista-error-BQLhNZcY.d.cts.map} +1 -1
  54. package/dist/{uploadista-error-nZ_q-EZy.mjs → uploadista-error-Buscq-FR.mjs} +1 -1
  55. package/dist/{uploadista-error-nZ_q-EZy.mjs.map → uploadista-error-Buscq-FR.mjs.map} +1 -1
  56. package/dist/{uploadista-error-CBkvsyZ3.d.mts → uploadista-error-DUWw6OqS.d.mts} +1 -1
  57. package/dist/{uploadista-error-CBkvsyZ3.d.mts.map → uploadista-error-DUWw6OqS.d.mts.map} +1 -1
  58. package/dist/utils/index.d.cts +2 -2
  59. package/dist/utils/index.d.mts +2 -2
  60. package/dist/utils/index.mjs +1 -1
  61. package/dist/{utils-BicUw_lt.mjs → utils-BWiu6lqv.mjs} +2 -2
  62. package/dist/{utils-BicUw_lt.mjs.map → utils-BWiu6lqv.mjs.map} +1 -1
  63. package/package.json +14 -6
  64. package/src/flow/node.ts +4 -4
  65. package/src/flow/nodes/transform-node.ts +23 -2
  66. package/src/flow/plugins/credential-provider.ts +1 -1
  67. package/src/flow/plugins/image-ai-plugin.ts +1 -1
  68. package/src/flow/plugins/image-plugin.ts +1 -1
  69. package/src/flow/plugins/video-plugin.ts +1 -1
  70. package/src/flow/plugins/zip-plugin.ts +1 -1
  71. package/src/flow/types/type-utils.ts +14 -3
  72. package/src/testing/index.ts +14 -0
  73. package/src/testing/mock-image-ai-plugin.ts +33 -0
  74. package/src/testing/mock-image-plugin.ts +56 -0
  75. package/src/testing/mock-upload-server.ts +176 -0
  76. package/src/testing/mock-video-plugin.ts +94 -0
  77. package/src/testing/mock-zip-plugin.ts +41 -0
  78. package/src/types/data-store.ts +1 -1
  79. package/{src/errors/__tests__ → tests/errors}/uploadista-error.test.ts +23 -19
  80. package/{src → tests}/flow/edge.test.ts +1 -1
  81. package/tests/flow/flow.test.ts +853 -0
  82. package/tests/flow/node.test.ts +757 -0
  83. package/{src → tests}/streams/stream-limiter.test.ts +2 -2
  84. package/tests/types/typed-event-emitter.test.ts +282 -0
  85. package/{src → tests}/utils/debounce.test.ts +1 -1
  86. package/{src → tests}/utils/once.test.ts +1 -1
  87. package/tests/utils/test-layers.ts +183 -0
  88. package/{src → tests}/utils/throttle.test.ts +1 -1
  89. package/tsdown.config.ts +1 -0
  90. package/type-tests/flow.test-d.ts +93 -0
  91. package/type-tests/type-utils.test-d.ts +104 -51
  92. package/vitest.config.ts +19 -1
  93. package/dist/flow-IgE8hj7H.mjs.map +0 -1
  94. package/dist/index-CrZopnP9.d.cts.map +0 -1
  95. package/dist/index-DHt7Ht_J.d.mts.map +0 -1
  96. package/dist/index-DubOIur4.d.cts.map +0 -1
  97. package/dist/types-DGZ892my.mjs.map +0 -1
  98. /package/dist/{errors-C0zLx77t.mjs → errors-DEFjN-xn.mjs} +0 -0
  99. /package/dist/{index-BtBZHVmz.d.cts → index-C-svZlpj.d.mts} +0 -0
  100. /package/dist/{index-DEHBdV_z.d.mts → index-_wQ5ClJU.d.cts} +0 -0
  101. /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
+ });