@q1k-oss/btree-workflows 0.0.1

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 (203) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/CLAUDE.md +181 -0
  3. package/LICENSE +21 -0
  4. package/README.md +920 -0
  5. package/behaviour-tree-workflows-landing/index.html +16 -0
  6. package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
  7. package/behaviour-tree-workflows-landing/package.json +31 -0
  8. package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
  9. package/behaviour-tree-workflows-landing/src/App.css +103 -0
  10. package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
  11. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
  12. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
  13. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
  14. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
  15. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
  16. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
  17. package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
  18. package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
  19. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
  20. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
  21. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
  22. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
  23. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
  24. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
  25. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
  26. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
  27. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
  28. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
  29. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
  30. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
  31. package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
  32. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
  33. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
  34. package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
  35. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
  36. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
  37. package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
  38. package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
  39. package/demo-google-sheets.ts +181 -0
  40. package/demo-runtime-variables.ts +174 -0
  41. package/demo-template.ts +208 -0
  42. package/docs/ARCHITECTURE_SUMMARY.md +613 -0
  43. package/docs/NODE_REFERENCE.md +504 -0
  44. package/docs/README.md +53 -0
  45. package/docs/custom-nodes-architecture.md +826 -0
  46. package/docs/observability.md +175 -0
  47. package/docs/yaml-specification.md +990 -0
  48. package/examples/temporal/README.md +117 -0
  49. package/examples/temporal/activities.ts +373 -0
  50. package/examples/temporal/client.ts +115 -0
  51. package/examples/temporal/python-worker/activities.py +339 -0
  52. package/examples/temporal/python-worker/requirements.txt +12 -0
  53. package/examples/temporal/python-worker/worker.py +106 -0
  54. package/examples/temporal/worker.ts +66 -0
  55. package/examples/temporal/workflows.ts +6 -0
  56. package/examples/temporal/yaml-workflow-loader.ts +105 -0
  57. package/examples/yaml-test.ts +97 -0
  58. package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
  59. package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
  60. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
  61. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
  62. package/examples/yaml-workflows/05-order-processing.yaml +146 -0
  63. package/examples/yaml-workflows/06-activity-test.yaml +71 -0
  64. package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
  65. package/examples/yaml-workflows/08-file-processing.yaml +141 -0
  66. package/examples/yaml-workflows/09-http-request.yaml +137 -0
  67. package/examples/yaml-workflows/README.md +211 -0
  68. package/package.json +38 -0
  69. package/src/actions/code-execution.schema.ts +27 -0
  70. package/src/actions/code-execution.ts +218 -0
  71. package/src/actions/generate-file.test.ts +516 -0
  72. package/src/actions/generate-file.ts +166 -0
  73. package/src/actions/http-request.test.ts +784 -0
  74. package/src/actions/http-request.ts +228 -0
  75. package/src/actions/index.ts +20 -0
  76. package/src/actions/parse-file.test.ts +448 -0
  77. package/src/actions/parse-file.ts +139 -0
  78. package/src/actions/python-script.test.ts +439 -0
  79. package/src/actions/python-script.ts +154 -0
  80. package/src/base-node.test.ts +511 -0
  81. package/src/base-node.ts +605 -0
  82. package/src/behavior-tree.test.ts +431 -0
  83. package/src/behavior-tree.ts +283 -0
  84. package/src/blackboard.test.ts +222 -0
  85. package/src/blackboard.ts +192 -0
  86. package/src/composites/conditional.schema.ts +19 -0
  87. package/src/composites/conditional.test.ts +309 -0
  88. package/src/composites/conditional.ts +129 -0
  89. package/src/composites/for-each.schema.ts +23 -0
  90. package/src/composites/for-each.test.ts +254 -0
  91. package/src/composites/for-each.ts +132 -0
  92. package/src/composites/index.ts +15 -0
  93. package/src/composites/memory-sequence.schema.ts +19 -0
  94. package/src/composites/memory-sequence.test.ts +223 -0
  95. package/src/composites/memory-sequence.ts +98 -0
  96. package/src/composites/parallel.schema.ts +28 -0
  97. package/src/composites/parallel.test.ts +502 -0
  98. package/src/composites/parallel.ts +157 -0
  99. package/src/composites/reactive-sequence.schema.ts +19 -0
  100. package/src/composites/reactive-sequence.test.ts +170 -0
  101. package/src/composites/reactive-sequence.ts +85 -0
  102. package/src/composites/recovery.schema.ts +19 -0
  103. package/src/composites/recovery.test.ts +366 -0
  104. package/src/composites/recovery.ts +90 -0
  105. package/src/composites/selector.schema.ts +19 -0
  106. package/src/composites/selector.test.ts +387 -0
  107. package/src/composites/selector.ts +85 -0
  108. package/src/composites/sequence.schema.ts +19 -0
  109. package/src/composites/sequence.test.ts +337 -0
  110. package/src/composites/sequence.ts +72 -0
  111. package/src/composites/sub-tree.schema.ts +21 -0
  112. package/src/composites/sub-tree.test.ts +893 -0
  113. package/src/composites/sub-tree.ts +177 -0
  114. package/src/composites/while.schema.ts +24 -0
  115. package/src/composites/while.test.ts +381 -0
  116. package/src/composites/while.ts +149 -0
  117. package/src/data-store/index.ts +10 -0
  118. package/src/data-store/memory-store.ts +161 -0
  119. package/src/data-store/types.ts +94 -0
  120. package/src/debug/breakpoint.test.ts +47 -0
  121. package/src/debug/breakpoint.ts +30 -0
  122. package/src/debug/index.ts +17 -0
  123. package/src/debug/resume-point.test.ts +49 -0
  124. package/src/debug/resume-point.ts +29 -0
  125. package/src/decorators/delay.schema.ts +21 -0
  126. package/src/decorators/delay.test.ts +261 -0
  127. package/src/decorators/delay.ts +140 -0
  128. package/src/decorators/force-result.schema.ts +32 -0
  129. package/src/decorators/force-result.test.ts +133 -0
  130. package/src/decorators/force-result.ts +63 -0
  131. package/src/decorators/index.ts +13 -0
  132. package/src/decorators/invert.schema.ts +19 -0
  133. package/src/decorators/invert.test.ts +135 -0
  134. package/src/decorators/invert.ts +42 -0
  135. package/src/decorators/keep-running.schema.ts +20 -0
  136. package/src/decorators/keep-running.test.ts +105 -0
  137. package/src/decorators/keep-running.ts +49 -0
  138. package/src/decorators/precondition.schema.ts +19 -0
  139. package/src/decorators/precondition.test.ts +351 -0
  140. package/src/decorators/precondition.ts +139 -0
  141. package/src/decorators/repeat.schema.ts +21 -0
  142. package/src/decorators/repeat.test.ts +187 -0
  143. package/src/decorators/repeat.ts +94 -0
  144. package/src/decorators/run-once.schema.ts +19 -0
  145. package/src/decorators/run-once.test.ts +140 -0
  146. package/src/decorators/run-once.ts +61 -0
  147. package/src/decorators/soft-assert.schema.ts +19 -0
  148. package/src/decorators/soft-assert.test.ts +107 -0
  149. package/src/decorators/soft-assert.ts +68 -0
  150. package/src/decorators/timeout.schema.ts +21 -0
  151. package/src/decorators/timeout.test.ts +274 -0
  152. package/src/decorators/timeout.ts +159 -0
  153. package/src/errors.test.ts +63 -0
  154. package/src/errors.ts +34 -0
  155. package/src/events.test.ts +347 -0
  156. package/src/events.ts +183 -0
  157. package/src/index.ts +80 -0
  158. package/src/integrations/index.ts +30 -0
  159. package/src/integrations/integration-action.test.ts +571 -0
  160. package/src/integrations/integration-action.ts +233 -0
  161. package/src/integrations/piece-executor.ts +320 -0
  162. package/src/observability/execution-tracker.ts +320 -0
  163. package/src/observability/index.ts +23 -0
  164. package/src/observability/sinks.ts +138 -0
  165. package/src/observability/types.ts +130 -0
  166. package/src/registry-utils.ts +147 -0
  167. package/src/registry.test.ts +466 -0
  168. package/src/registry.ts +334 -0
  169. package/src/schemas/base.schema.ts +104 -0
  170. package/src/schemas/index.ts +223 -0
  171. package/src/schemas/integration.test.ts +238 -0
  172. package/src/schemas/tree-definition.schema.ts +170 -0
  173. package/src/schemas/validation.test.ts +146 -0
  174. package/src/schemas/validation.ts +122 -0
  175. package/src/scripting/index.ts +22 -0
  176. package/src/templates/template-loader.test.ts +281 -0
  177. package/src/templates/template-loader.ts +152 -0
  178. package/src/temporal-integration.test.ts +213 -0
  179. package/src/test-nodes.ts +259 -0
  180. package/src/types.ts +503 -0
  181. package/src/utilities/index.ts +17 -0
  182. package/src/utilities/log-message.test.ts +275 -0
  183. package/src/utilities/log-message.ts +134 -0
  184. package/src/utilities/regex-extract.test.ts +138 -0
  185. package/src/utilities/regex-extract.ts +108 -0
  186. package/src/utilities/variable-resolver.test.ts +416 -0
  187. package/src/utilities/variable-resolver.ts +318 -0
  188. package/src/utils/error-handler.test.ts +117 -0
  189. package/src/utils/error-handler.ts +48 -0
  190. package/src/utils/signal-check.test.ts +234 -0
  191. package/src/utils/signal-check.ts +140 -0
  192. package/src/yaml/errors.ts +143 -0
  193. package/src/yaml/index.ts +30 -0
  194. package/src/yaml/loader.ts +39 -0
  195. package/src/yaml/parser.ts +286 -0
  196. package/src/yaml/validation/semantic-validator.ts +196 -0
  197. package/templates/google-sheets/insert-row.yaml +76 -0
  198. package/templates/notification-sender.yaml +33 -0
  199. package/templates/order-validation.yaml +44 -0
  200. package/tsconfig.json +24 -0
  201. package/vitest.config.ts +25 -0
  202. package/workflows/order-processor.yaml +59 -0
  203. package/workflows/process-order-workflow.yaml +142 -0
@@ -0,0 +1,516 @@
1
+ /**
2
+ * GenerateFile Node Tests
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { Registry } from "../registry.js";
8
+ import { type TemporalContext, type BtreeActivities, NodeStatus } from "../types.js";
9
+ import { GenerateFile, type GenerateFileConfig } from "./generate-file.js";
10
+
11
+ describe("GenerateFile Node", () => {
12
+ let blackboard: ScopedBlackboard;
13
+ let registry: Registry;
14
+
15
+ beforeEach(() => {
16
+ blackboard = new ScopedBlackboard();
17
+ registry = new Registry();
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ describe("Construction and validation", () => {
22
+ it("should create node with valid config", () => {
23
+ const node = new GenerateFile({
24
+ id: "test",
25
+ format: "csv",
26
+ dataKey: "data",
27
+ filename: "export.csv",
28
+ storage: "temp",
29
+ outputKey: "fileResult",
30
+ });
31
+
32
+ expect(node).toBeDefined();
33
+ expect(node.id).toBe("test");
34
+ });
35
+
36
+ it("should require format", () => {
37
+ expect(() => {
38
+ new GenerateFile({
39
+ id: "test",
40
+ dataKey: "data",
41
+ filename: "export.csv",
42
+ storage: "temp",
43
+ outputKey: "fileResult",
44
+ } as GenerateFileConfig);
45
+ }).toThrow(/requires format/i);
46
+ });
47
+
48
+ it("should require dataKey", () => {
49
+ expect(() => {
50
+ new GenerateFile({
51
+ id: "test",
52
+ format: "csv",
53
+ filename: "export.csv",
54
+ storage: "temp",
55
+ outputKey: "fileResult",
56
+ } as GenerateFileConfig);
57
+ }).toThrow(/requires dataKey/i);
58
+ });
59
+
60
+ it("should require filename", () => {
61
+ expect(() => {
62
+ new GenerateFile({
63
+ id: "test",
64
+ format: "csv",
65
+ dataKey: "data",
66
+ storage: "temp",
67
+ outputKey: "fileResult",
68
+ } as GenerateFileConfig);
69
+ }).toThrow(/requires filename/i);
70
+ });
71
+
72
+ it("should require storage", () => {
73
+ expect(() => {
74
+ new GenerateFile({
75
+ id: "test",
76
+ format: "csv",
77
+ dataKey: "data",
78
+ filename: "export.csv",
79
+ outputKey: "fileResult",
80
+ } as GenerateFileConfig);
81
+ }).toThrow(/requires storage/i);
82
+ });
83
+
84
+ it("should require outputKey", () => {
85
+ expect(() => {
86
+ new GenerateFile({
87
+ id: "test",
88
+ format: "csv",
89
+ dataKey: "data",
90
+ filename: "export.csv",
91
+ storage: "temp",
92
+ } as GenerateFileConfig);
93
+ }).toThrow(/requires outputKey/i);
94
+ });
95
+ });
96
+
97
+ describe("Activity requirement", () => {
98
+ it("should fail without generateFile activity", async () => {
99
+ blackboard.set("data", [{ a: 1 }]);
100
+
101
+ const context: TemporalContext = {
102
+ blackboard,
103
+ treeRegistry: registry,
104
+ timestamp: Date.now(),
105
+ deltaTime: 0,
106
+ activities: undefined,
107
+ };
108
+
109
+ const node = new GenerateFile({
110
+ id: "test",
111
+ format: "csv",
112
+ dataKey: "data",
113
+ filename: "export.csv",
114
+ storage: "temp",
115
+ outputKey: "fileResult",
116
+ });
117
+
118
+ const status = await node.tick(context);
119
+
120
+ expect(status).toBe(NodeStatus.FAILURE);
121
+ expect(node.lastError).toContain("requires activities.generateFile");
122
+ });
123
+
124
+ it("should fail when activities object exists but generateFile is missing", async () => {
125
+ blackboard.set("data", [{ a: 1 }]);
126
+
127
+ const context: TemporalContext = {
128
+ blackboard,
129
+ treeRegistry: registry,
130
+ timestamp: Date.now(),
131
+ deltaTime: 0,
132
+ activities: {
133
+ executePieceAction: vi.fn(),
134
+ // generateFile is not provided
135
+ } as BtreeActivities,
136
+ };
137
+
138
+ const node = new GenerateFile({
139
+ id: "test",
140
+ format: "csv",
141
+ dataKey: "data",
142
+ filename: "export.csv",
143
+ storage: "temp",
144
+ outputKey: "fileResult",
145
+ });
146
+
147
+ const status = await node.tick(context);
148
+
149
+ expect(status).toBe(NodeStatus.FAILURE);
150
+ expect(node.lastError).toContain("requires activities.generateFile");
151
+ });
152
+ });
153
+
154
+ describe("Data validation", () => {
155
+ it("should fail if data is not an array", async () => {
156
+ blackboard.set("data", { notAnArray: true });
157
+
158
+ const mockGenerateActivity = vi.fn();
159
+
160
+ const context: TemporalContext = {
161
+ blackboard,
162
+ treeRegistry: registry,
163
+ timestamp: Date.now(),
164
+ deltaTime: 0,
165
+ activities: {
166
+ executePieceAction: vi.fn(),
167
+ generateFile: mockGenerateActivity,
168
+ },
169
+ };
170
+
171
+ const node = new GenerateFile({
172
+ id: "test",
173
+ format: "csv",
174
+ dataKey: "data",
175
+ filename: "export.csv",
176
+ storage: "temp",
177
+ outputKey: "fileResult",
178
+ });
179
+
180
+ const status = await node.tick(context);
181
+
182
+ expect(status).toBe(NodeStatus.FAILURE);
183
+ expect(node.lastError).toContain("is not an array");
184
+ expect(mockGenerateActivity).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it("should fail if data key does not exist", async () => {
188
+ // Don't set anything in blackboard
189
+
190
+ const mockGenerateActivity = vi.fn();
191
+
192
+ const context: TemporalContext = {
193
+ blackboard,
194
+ treeRegistry: registry,
195
+ timestamp: Date.now(),
196
+ deltaTime: 0,
197
+ activities: {
198
+ executePieceAction: vi.fn(),
199
+ generateFile: mockGenerateActivity,
200
+ },
201
+ };
202
+
203
+ const node = new GenerateFile({
204
+ id: "test",
205
+ format: "csv",
206
+ dataKey: "missingData",
207
+ filename: "export.csv",
208
+ storage: "temp",
209
+ outputKey: "fileResult",
210
+ });
211
+
212
+ const status = await node.tick(context);
213
+
214
+ expect(status).toBe(NodeStatus.FAILURE);
215
+ expect(node.lastError).toContain("is not an array");
216
+ });
217
+ });
218
+
219
+ describe("Execution with activity", () => {
220
+ it("should generate CSV file via activity", async () => {
221
+ blackboard.set("orders", [
222
+ { orderId: "1", amount: 100 },
223
+ { orderId: "2", amount: 200 },
224
+ ]);
225
+
226
+ const mockGenerateActivity = vi.fn().mockResolvedValue({
227
+ filename: "export.csv",
228
+ contentType: "text/csv",
229
+ size: 1024,
230
+ path: "/tmp/export.csv",
231
+ });
232
+
233
+ const context: TemporalContext = {
234
+ blackboard,
235
+ treeRegistry: registry,
236
+ timestamp: Date.now(),
237
+ deltaTime: 0,
238
+ activities: {
239
+ executePieceAction: vi.fn(),
240
+ generateFile: mockGenerateActivity,
241
+ },
242
+ };
243
+
244
+ const node = new GenerateFile({
245
+ id: "test",
246
+ format: "csv",
247
+ dataKey: "orders",
248
+ filename: "export.csv",
249
+ storage: "temp",
250
+ outputKey: "fileResult",
251
+ });
252
+
253
+ const status = await node.tick(context);
254
+
255
+ expect(status).toBe(NodeStatus.SUCCESS);
256
+ expect(mockGenerateActivity).toHaveBeenCalledWith(
257
+ expect.objectContaining({
258
+ format: "csv",
259
+ data: [
260
+ { orderId: "1", amount: 100 },
261
+ { orderId: "2", amount: 200 },
262
+ ],
263
+ filename: "export.csv",
264
+ storage: "temp",
265
+ })
266
+ );
267
+ });
268
+
269
+ it("should store file metadata in blackboard", async () => {
270
+ blackboard.set("data", [{ a: 1 }]);
271
+
272
+ const fileMetadata = {
273
+ filename: "export-123.csv",
274
+ contentType: "text/csv",
275
+ size: 2048,
276
+ path: "/storage/exports/export-123.csv",
277
+ url: "https://storage.example.com/exports/export-123.csv",
278
+ };
279
+
280
+ const mockGenerateActivity = vi.fn().mockResolvedValue(fileMetadata);
281
+
282
+ const context: TemporalContext = {
283
+ blackboard,
284
+ treeRegistry: registry,
285
+ timestamp: Date.now(),
286
+ deltaTime: 0,
287
+ activities: {
288
+ executePieceAction: vi.fn(),
289
+ generateFile: mockGenerateActivity,
290
+ },
291
+ };
292
+
293
+ const node = new GenerateFile({
294
+ id: "test",
295
+ format: "csv",
296
+ dataKey: "data",
297
+ filename: "export.csv",
298
+ storage: "persistent",
299
+ outputKey: "exportedFile",
300
+ });
301
+
302
+ await node.tick(context);
303
+
304
+ expect(blackboard.get("exportedFile")).toEqual(fileMetadata);
305
+ });
306
+
307
+ it("should resolve filename from variables", async () => {
308
+ blackboard.set("data", [{ a: 1 }]);
309
+ blackboard.set("timestamp", "2024-01-15");
310
+
311
+ const mockGenerateActivity = vi.fn().mockResolvedValue({
312
+ filename: "report-2024-01-15.csv",
313
+ contentType: "text/csv",
314
+ size: 512,
315
+ path: "/tmp/report-2024-01-15.csv",
316
+ });
317
+
318
+ const context: TemporalContext = {
319
+ blackboard,
320
+ treeRegistry: registry,
321
+ timestamp: Date.now(),
322
+ deltaTime: 0,
323
+ input: { reportType: "sales" },
324
+ activities: {
325
+ executePieceAction: vi.fn(),
326
+ generateFile: mockGenerateActivity,
327
+ },
328
+ };
329
+
330
+ const node = new GenerateFile({
331
+ id: "test",
332
+ format: "csv",
333
+ dataKey: "data",
334
+ filename: "${input.reportType}-${bb.timestamp}.csv",
335
+ storage: "temp",
336
+ outputKey: "fileResult",
337
+ });
338
+
339
+ await node.tick(context);
340
+
341
+ expect(mockGenerateActivity).toHaveBeenCalledWith(
342
+ expect.objectContaining({
343
+ filename: "sales-2024-01-15.csv",
344
+ })
345
+ );
346
+ });
347
+
348
+ it("should pass column definitions to activity", async () => {
349
+ blackboard.set("orders", [
350
+ { id: "1", customer: "Alice", total: 100 },
351
+ ]);
352
+
353
+ const mockGenerateActivity = vi.fn().mockResolvedValue({
354
+ filename: "orders.xlsx",
355
+ contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
356
+ size: 4096,
357
+ path: "/tmp/orders.xlsx",
358
+ });
359
+
360
+ const context: TemporalContext = {
361
+ blackboard,
362
+ treeRegistry: registry,
363
+ timestamp: Date.now(),
364
+ deltaTime: 0,
365
+ activities: {
366
+ executePieceAction: vi.fn(),
367
+ generateFile: mockGenerateActivity,
368
+ },
369
+ };
370
+
371
+ const node = new GenerateFile({
372
+ id: "test",
373
+ format: "xlsx",
374
+ dataKey: "orders",
375
+ columns: [
376
+ { header: "Order ID", key: "id", width: 10 },
377
+ { header: "Customer Name", key: "customer", width: 25 },
378
+ { header: "Total Amount", key: "total", width: 15 },
379
+ ],
380
+ filename: "orders.xlsx",
381
+ storage: "temp",
382
+ outputKey: "fileResult",
383
+ });
384
+
385
+ await node.tick(context);
386
+
387
+ expect(mockGenerateActivity).toHaveBeenCalledWith(
388
+ expect.objectContaining({
389
+ format: "xlsx",
390
+ columns: [
391
+ { header: "Order ID", key: "id", width: 10 },
392
+ { header: "Customer Name", key: "customer", width: 25 },
393
+ { header: "Total Amount", key: "total", width: 15 },
394
+ ],
395
+ })
396
+ );
397
+ });
398
+
399
+ it("should generate JSON file", async () => {
400
+ blackboard.set("data", [{ key: "value" }]);
401
+
402
+ const mockGenerateActivity = vi.fn().mockResolvedValue({
403
+ filename: "data.json",
404
+ contentType: "application/json",
405
+ size: 256,
406
+ path: "/tmp/data.json",
407
+ });
408
+
409
+ const context: TemporalContext = {
410
+ blackboard,
411
+ treeRegistry: registry,
412
+ timestamp: Date.now(),
413
+ deltaTime: 0,
414
+ activities: {
415
+ executePieceAction: vi.fn(),
416
+ generateFile: mockGenerateActivity,
417
+ },
418
+ };
419
+
420
+ const node = new GenerateFile({
421
+ id: "test",
422
+ format: "json",
423
+ dataKey: "data",
424
+ filename: "data.json",
425
+ storage: "temp",
426
+ outputKey: "fileResult",
427
+ });
428
+
429
+ await node.tick(context);
430
+
431
+ expect(mockGenerateActivity).toHaveBeenCalledWith(
432
+ expect.objectContaining({
433
+ format: "json",
434
+ })
435
+ );
436
+ });
437
+
438
+ it("should handle activity errors", async () => {
439
+ blackboard.set("data", [{ a: 1 }]);
440
+
441
+ const mockGenerateActivity = vi.fn().mockRejectedValue(
442
+ new Error("Disk full: cannot write file")
443
+ );
444
+
445
+ const context: TemporalContext = {
446
+ blackboard,
447
+ treeRegistry: registry,
448
+ timestamp: Date.now(),
449
+ deltaTime: 0,
450
+ activities: {
451
+ executePieceAction: vi.fn(),
452
+ generateFile: mockGenerateActivity,
453
+ },
454
+ };
455
+
456
+ const node = new GenerateFile({
457
+ id: "test",
458
+ format: "csv",
459
+ dataKey: "data",
460
+ filename: "export.csv",
461
+ storage: "temp",
462
+ outputKey: "fileResult",
463
+ });
464
+
465
+ const status = await node.tick(context);
466
+
467
+ expect(status).toBe(NodeStatus.FAILURE);
468
+ expect(node.lastError).toContain("Disk full");
469
+ });
470
+ });
471
+
472
+ describe("Node lifecycle", () => {
473
+ it("should clone correctly", () => {
474
+ const node = new GenerateFile({
475
+ id: "original",
476
+ format: "csv",
477
+ dataKey: "data",
478
+ filename: "export.csv",
479
+ storage: "persistent",
480
+ outputKey: "fileResult",
481
+ });
482
+
483
+ const cloned = node.clone() as GenerateFile;
484
+
485
+ expect(cloned.id).toBe("original");
486
+ });
487
+
488
+ it("should reset status correctly", async () => {
489
+ const context: TemporalContext = {
490
+ blackboard,
491
+ treeRegistry: registry,
492
+ timestamp: Date.now(),
493
+ deltaTime: 0,
494
+ // No activities - will fail
495
+ };
496
+
497
+ blackboard.set("data", [{ a: 1 }]);
498
+
499
+ const node = new GenerateFile({
500
+ id: "test",
501
+ format: "csv",
502
+ dataKey: "data",
503
+ filename: "export.csv",
504
+ storage: "temp",
505
+ outputKey: "fileResult",
506
+ });
507
+
508
+ await node.tick(context);
509
+ expect(node.status()).toBe(NodeStatus.FAILURE);
510
+
511
+ node.reset();
512
+ expect(node.status()).toBe(NodeStatus.IDLE);
513
+ expect(node.lastError).toBeUndefined();
514
+ });
515
+ });
516
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * GenerateFile Node
3
+ *
4
+ * Generates CSV/Excel/JSON files from data via a Temporal activity.
5
+ * This node requires the `generateFile` activity to be configured in the context -
6
+ * it does not support standalone/inline execution because file I/O requires
7
+ * capabilities outside the workflow sandbox.
8
+ *
9
+ * Features:
10
+ * - CSV, Excel (xlsx), and JSON output formats
11
+ * - Column definitions with header names and widths
12
+ * - Temporary or persistent storage
13
+ * - Result metadata stored in blackboard (filename, path, URL)
14
+ */
15
+
16
+ import { ActionNode } from "../base-node.js";
17
+ import { ConfigurationError } from "../errors.js";
18
+ import {
19
+ type TemporalContext,
20
+ type NodeConfiguration,
21
+ type GenerateFileRequest,
22
+ NodeStatus,
23
+ } from "../types.js";
24
+ import { resolveValue, type VariableContext } from "../utilities/variable-resolver.js";
25
+
26
+ /**
27
+ * Configuration for GenerateFile node
28
+ */
29
+ export interface GenerateFileConfig extends NodeConfiguration {
30
+ /** Output format */
31
+ format: "csv" | "xlsx" | "json";
32
+ /** Data source (blackboard key) */
33
+ dataKey: string;
34
+ /** Column definitions */
35
+ columns?: Array<{ header: string; key: string; width?: number }>;
36
+ /** Output filename template (supports ${input.x}, ${bb.x}) */
37
+ filename: string;
38
+ /** Storage type */
39
+ storage: "temp" | "persistent";
40
+ /** Output key for file metadata (path, url, size) */
41
+ outputKey: string;
42
+ }
43
+
44
+ /**
45
+ * GenerateFile Node
46
+ *
47
+ * Generates a file from data in the blackboard and stores file metadata.
48
+ * Requires the `generateFile` activity to be configured.
49
+ *
50
+ * @example YAML
51
+ * ```yaml
52
+ * type: GenerateFile
53
+ * id: export-report
54
+ * props:
55
+ * format: csv
56
+ * dataKey: "processedOrders"
57
+ * columns:
58
+ * - header: "Order ID"
59
+ * key: "orderId"
60
+ * - header: "Customer"
61
+ * key: "customerName"
62
+ * - header: "Total"
63
+ * key: "amount"
64
+ * width: 15
65
+ * filename: "orders-${bb.timestamp}.csv"
66
+ * storage: persistent
67
+ * outputKey: "exportedFile"
68
+ * ```
69
+ */
70
+ export class GenerateFile extends ActionNode {
71
+ private format: GenerateFileConfig["format"];
72
+ private dataKey: string;
73
+ private columns?: GenerateFileConfig["columns"];
74
+ private filename: string;
75
+ private storage: GenerateFileConfig["storage"];
76
+ private outputKey: string;
77
+
78
+ constructor(config: GenerateFileConfig) {
79
+ super(config);
80
+
81
+ if (!config.format) {
82
+ throw new ConfigurationError("GenerateFile requires format");
83
+ }
84
+
85
+ if (!config.dataKey) {
86
+ throw new ConfigurationError("GenerateFile requires dataKey");
87
+ }
88
+
89
+ if (!config.filename) {
90
+ throw new ConfigurationError("GenerateFile requires filename");
91
+ }
92
+
93
+ if (!config.storage) {
94
+ throw new ConfigurationError("GenerateFile requires storage");
95
+ }
96
+
97
+ if (!config.outputKey) {
98
+ throw new ConfigurationError("GenerateFile requires outputKey");
99
+ }
100
+
101
+ this.format = config.format;
102
+ this.dataKey = config.dataKey;
103
+ this.columns = config.columns;
104
+ this.filename = config.filename;
105
+ this.storage = config.storage;
106
+ this.outputKey = config.outputKey;
107
+ }
108
+
109
+ protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
110
+ // Validate activity is available
111
+ if (!context.activities?.generateFile) {
112
+ this._lastError =
113
+ "GenerateFile requires activities.generateFile to be configured. " +
114
+ "This activity handles file I/O outside the workflow sandbox.";
115
+ this.log(`Error: ${this._lastError}`);
116
+ return NodeStatus.FAILURE;
117
+ }
118
+
119
+ try {
120
+ const varCtx: VariableContext = {
121
+ blackboard: context.blackboard,
122
+ input: context.input,
123
+ testData: context.testData,
124
+ };
125
+
126
+ // Get data from blackboard
127
+ const data = context.blackboard.get(this.dataKey);
128
+ if (!Array.isArray(data)) {
129
+ this._lastError = `Data at '${this.dataKey}' is not an array (got ${typeof data})`;
130
+ this.log(`Error: ${this._lastError}`);
131
+ return NodeStatus.FAILURE;
132
+ }
133
+
134
+ const resolvedFilename = resolveValue(this.filename, varCtx) as string;
135
+
136
+ const request: GenerateFileRequest = {
137
+ format: this.format,
138
+ data: data as Record<string, unknown>[],
139
+ columns: this.columns,
140
+ filename: resolvedFilename,
141
+ storage: this.storage,
142
+ };
143
+
144
+ this.log(
145
+ `Generating ${this.format} file: ${resolvedFilename} ` +
146
+ `(${data.length} rows, storage: ${this.storage})`
147
+ );
148
+
149
+ const result = await context.activities.generateFile(request);
150
+
151
+ // Store file metadata in blackboard
152
+ context.blackboard.set(this.outputKey, result);
153
+
154
+ this.log(
155
+ `Generated file: ${result.filename} ` +
156
+ `(${result.size} bytes, path: ${result.path})`
157
+ );
158
+
159
+ return NodeStatus.SUCCESS;
160
+ } catch (error) {
161
+ this._lastError = error instanceof Error ? error.message : String(error);
162
+ this.log(`Generate file failed: ${this._lastError}`);
163
+ return NodeStatus.FAILURE;
164
+ }
165
+ }
166
+ }