@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,139 @@
1
+ /**
2
+ * ParseFile Node
3
+ *
4
+ * Parses CSV/Excel files into structured data via a Temporal activity.
5
+ * This node requires the `parseFile` 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 and Excel (xlsx, xls) file format support
11
+ * - Column mapping/renaming
12
+ * - Parse options (skip rows, trim, date parsing)
13
+ * - Result stored in blackboard
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 ParseFileRequest,
22
+ NodeStatus,
23
+ } from "../types.js";
24
+ import { resolveValue, type VariableContext } from "../utilities/variable-resolver.js";
25
+
26
+ /**
27
+ * Configuration for ParseFile node
28
+ */
29
+ export interface ParseFileConfig extends NodeConfiguration {
30
+ /** Path to file (supports ${input.file}, ${bb.filePath}) */
31
+ file: string;
32
+ /** File format */
33
+ format?: "csv" | "xlsx" | "xls" | "auto";
34
+ /** Sheet name for Excel (default: first sheet) */
35
+ sheetName?: string;
36
+ /** Column mapping { "Original Name": "normalizedName" } */
37
+ columnMapping?: Record<string, string>;
38
+ /** Output key on blackboard */
39
+ outputKey: string;
40
+ /** Parse options */
41
+ options?: ParseFileRequest["options"];
42
+ }
43
+
44
+ /**
45
+ * ParseFile Node
46
+ *
47
+ * Parses CSV/Excel files into structured data and stores the result in blackboard.
48
+ * Requires the `parseFile` activity to be configured.
49
+ *
50
+ * @example YAML
51
+ * ```yaml
52
+ * type: ParseFile
53
+ * id: parse-orders
54
+ * props:
55
+ * file: "${input.orderFile}"
56
+ * format: csv
57
+ * columnMapping:
58
+ * "Order ID": "orderId"
59
+ * "Customer Name": "customerName"
60
+ * "Amount": "amount"
61
+ * outputKey: "orders"
62
+ * options:
63
+ * skipRows: 1
64
+ * trim: true
65
+ * ```
66
+ */
67
+ export class ParseFile extends ActionNode {
68
+ private file: string;
69
+ private format: ParseFileConfig["format"];
70
+ private sheetName?: string;
71
+ private columnMapping?: Record<string, string>;
72
+ private outputKey: string;
73
+ private options?: ParseFileRequest["options"];
74
+
75
+ constructor(config: ParseFileConfig) {
76
+ super(config);
77
+
78
+ if (!config.file) {
79
+ throw new ConfigurationError("ParseFile requires file");
80
+ }
81
+
82
+ if (!config.outputKey) {
83
+ throw new ConfigurationError("ParseFile requires outputKey");
84
+ }
85
+
86
+ this.file = config.file;
87
+ this.format = config.format || "auto";
88
+ this.sheetName = config.sheetName;
89
+ this.columnMapping = config.columnMapping;
90
+ this.outputKey = config.outputKey;
91
+ this.options = config.options;
92
+ }
93
+
94
+ protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
95
+ // Validate activity is available
96
+ if (!context.activities?.parseFile) {
97
+ this._lastError =
98
+ "ParseFile requires activities.parseFile to be configured. " +
99
+ "This activity handles file I/O outside the workflow sandbox.";
100
+ this.log(`Error: ${this._lastError}`);
101
+ return NodeStatus.FAILURE;
102
+ }
103
+
104
+ try {
105
+ const varCtx: VariableContext = {
106
+ blackboard: context.blackboard,
107
+ input: context.input,
108
+ testData: context.testData,
109
+ };
110
+
111
+ const resolvedFile = resolveValue(this.file, varCtx) as string;
112
+
113
+ const request: ParseFileRequest = {
114
+ file: resolvedFile,
115
+ format: this.format,
116
+ sheetName: this.sheetName,
117
+ columnMapping: this.columnMapping,
118
+ options: this.options,
119
+ };
120
+
121
+ this.log(`Parsing file: ${resolvedFile} (format: ${this.format || "auto"})`);
122
+ const result = await context.activities.parseFile(request);
123
+
124
+ // Store parsed data in blackboard
125
+ context.blackboard.set(this.outputKey, result.data);
126
+
127
+ this.log(
128
+ `Parsed ${result.rowCount} rows from ${resolvedFile}, ` +
129
+ `columns: [${result.columns.join(", ")}]`
130
+ );
131
+
132
+ return NodeStatus.SUCCESS;
133
+ } catch (error) {
134
+ this._lastError = error instanceof Error ? error.message : String(error);
135
+ this.log(`Parse file failed: ${this._lastError}`);
136
+ return NodeStatus.FAILURE;
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,439 @@
1
+ /**
2
+ * PythonScript 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 { PythonScript, type PythonScriptConfig } from "./python-script.js";
10
+
11
+ describe("PythonScript 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 PythonScript({
24
+ id: "test",
25
+ code: "bb['result'] = 42",
26
+ });
27
+
28
+ expect(node).toBeDefined();
29
+ expect(node.id).toBe("test");
30
+ });
31
+
32
+ it("should require code", () => {
33
+ expect(() => {
34
+ new PythonScript({
35
+ id: "test",
36
+ } as PythonScriptConfig);
37
+ }).toThrow(/requires code/i);
38
+ });
39
+
40
+ it("should accept optional packages and timeout", () => {
41
+ const node = new PythonScript({
42
+ id: "test",
43
+ code: "import pandas",
44
+ packages: ["pandas"],
45
+ timeout: 30000,
46
+ });
47
+
48
+ expect(node).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe("Activity requirement", () => {
53
+ it("should fail without executePythonScript activity", async () => {
54
+ const context: TemporalContext = {
55
+ blackboard,
56
+ treeRegistry: registry,
57
+ timestamp: Date.now(),
58
+ deltaTime: 0,
59
+ activities: undefined,
60
+ };
61
+
62
+ const node = new PythonScript({
63
+ id: "test",
64
+ code: "bb['result'] = 1 + 1",
65
+ });
66
+
67
+ const status = await node.tick(context);
68
+
69
+ expect(status).toBe(NodeStatus.FAILURE);
70
+ expect(node.lastError).toContain("requires activities.executePythonScript");
71
+ });
72
+
73
+ it("should fail when activities object exists but executePythonScript is missing", async () => {
74
+ const context: TemporalContext = {
75
+ blackboard,
76
+ treeRegistry: registry,
77
+ timestamp: Date.now(),
78
+ deltaTime: 0,
79
+ activities: {
80
+ executePieceAction: vi.fn(),
81
+ // executePythonScript is not provided
82
+ } as BtreeActivities,
83
+ };
84
+
85
+ const node = new PythonScript({
86
+ id: "test",
87
+ code: "bb['result'] = 1 + 1",
88
+ });
89
+
90
+ const status = await node.tick(context);
91
+
92
+ expect(status).toBe(NodeStatus.FAILURE);
93
+ expect(node.lastError).toContain("requires activities.executePythonScript");
94
+ });
95
+ });
96
+
97
+ describe("Execution with activity", () => {
98
+ it("should execute Python code via activity", async () => {
99
+ const mockPythonActivity = vi.fn().mockResolvedValue({
100
+ blackboard: { result: 42 },
101
+ stdout: "",
102
+ stderr: "",
103
+ });
104
+
105
+ const context: TemporalContext = {
106
+ blackboard,
107
+ treeRegistry: registry,
108
+ timestamp: Date.now(),
109
+ deltaTime: 0,
110
+ activities: {
111
+ executePieceAction: vi.fn(),
112
+ executePythonScript: mockPythonActivity,
113
+ },
114
+ };
115
+
116
+ const node = new PythonScript({
117
+ id: "test",
118
+ code: "bb['result'] = 21 * 2",
119
+ });
120
+
121
+ const status = await node.tick(context);
122
+
123
+ expect(status).toBe(NodeStatus.SUCCESS);
124
+ expect(mockPythonActivity).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ code: "bb['result'] = 21 * 2",
127
+ })
128
+ );
129
+ });
130
+
131
+ it("should pass blackboard state to Python", async () => {
132
+ blackboard.set("x", 10);
133
+ blackboard.set("y", 20);
134
+
135
+ const mockPythonActivity = vi.fn().mockResolvedValue({
136
+ blackboard: { sum: 30 },
137
+ });
138
+
139
+ const context: TemporalContext = {
140
+ blackboard,
141
+ treeRegistry: registry,
142
+ timestamp: Date.now(),
143
+ deltaTime: 0,
144
+ activities: {
145
+ executePieceAction: vi.fn(),
146
+ executePythonScript: mockPythonActivity,
147
+ },
148
+ };
149
+
150
+ const node = new PythonScript({
151
+ id: "test",
152
+ code: "bb['sum'] = bb['x'] + bb['y']",
153
+ });
154
+
155
+ await node.tick(context);
156
+
157
+ expect(mockPythonActivity).toHaveBeenCalledWith(
158
+ expect.objectContaining({
159
+ blackboard: { x: 10, y: 20 },
160
+ })
161
+ );
162
+ });
163
+
164
+ it("should pass workflow input to Python", async () => {
165
+ const mockPythonActivity = vi.fn().mockResolvedValue({
166
+ blackboard: {},
167
+ });
168
+
169
+ const context: TemporalContext = {
170
+ blackboard,
171
+ treeRegistry: registry,
172
+ timestamp: Date.now(),
173
+ deltaTime: 0,
174
+ input: { userId: "user-123", orderId: "order-456" },
175
+ activities: {
176
+ executePieceAction: vi.fn(),
177
+ executePythonScript: mockPythonActivity,
178
+ },
179
+ };
180
+
181
+ const node = new PythonScript({
182
+ id: "test",
183
+ code: "bb['uid'] = input['userId']",
184
+ });
185
+
186
+ await node.tick(context);
187
+
188
+ expect(mockPythonActivity).toHaveBeenCalledWith(
189
+ expect.objectContaining({
190
+ input: { userId: "user-123", orderId: "order-456" },
191
+ })
192
+ );
193
+ });
194
+
195
+ it("should merge Python blackboard changes back", async () => {
196
+ blackboard.set("existing", "value");
197
+
198
+ const mockPythonActivity = vi.fn().mockResolvedValue({
199
+ blackboard: {
200
+ newKey: "newValue",
201
+ computed: 123,
202
+ },
203
+ });
204
+
205
+ const context: TemporalContext = {
206
+ blackboard,
207
+ treeRegistry: registry,
208
+ timestamp: Date.now(),
209
+ deltaTime: 0,
210
+ activities: {
211
+ executePieceAction: vi.fn(),
212
+ executePythonScript: mockPythonActivity,
213
+ },
214
+ };
215
+
216
+ const node = new PythonScript({
217
+ id: "test",
218
+ code: "bb['newKey'] = 'newValue'\nbb['computed'] = 123",
219
+ });
220
+
221
+ await node.tick(context);
222
+
223
+ expect(blackboard.get("newKey")).toBe("newValue");
224
+ expect(blackboard.get("computed")).toBe(123);
225
+ // Original value should still be there
226
+ expect(blackboard.get("existing")).toBe("value");
227
+ });
228
+
229
+ it("should pass timeout to activity", async () => {
230
+ const mockPythonActivity = vi.fn().mockResolvedValue({
231
+ blackboard: {},
232
+ });
233
+
234
+ const context: TemporalContext = {
235
+ blackboard,
236
+ treeRegistry: registry,
237
+ timestamp: Date.now(),
238
+ deltaTime: 0,
239
+ activities: {
240
+ executePieceAction: vi.fn(),
241
+ executePythonScript: mockPythonActivity,
242
+ },
243
+ };
244
+
245
+ const node = new PythonScript({
246
+ id: "test",
247
+ code: "import time; time.sleep(1)",
248
+ timeout: 5000,
249
+ });
250
+
251
+ await node.tick(context);
252
+
253
+ expect(mockPythonActivity).toHaveBeenCalledWith(
254
+ expect.objectContaining({
255
+ timeout: 5000,
256
+ })
257
+ );
258
+ });
259
+
260
+ it("should handle activity errors", async () => {
261
+ const mockPythonActivity = vi.fn().mockRejectedValue(
262
+ new Error("Python execution failed: SyntaxError")
263
+ );
264
+
265
+ const context: TemporalContext = {
266
+ blackboard,
267
+ treeRegistry: registry,
268
+ timestamp: Date.now(),
269
+ deltaTime: 0,
270
+ activities: {
271
+ executePieceAction: vi.fn(),
272
+ executePythonScript: mockPythonActivity,
273
+ },
274
+ };
275
+
276
+ const node = new PythonScript({
277
+ id: "test",
278
+ code: "this is not valid python",
279
+ });
280
+
281
+ const status = await node.tick(context);
282
+
283
+ expect(status).toBe(NodeStatus.FAILURE);
284
+ expect(node.lastError).toContain("Python execution failed");
285
+ });
286
+ });
287
+
288
+ describe("Variable resolution in code", () => {
289
+ it("should resolve variables in code template", async () => {
290
+ const mockPythonActivity = vi.fn().mockResolvedValue({
291
+ blackboard: {},
292
+ });
293
+
294
+ const context: TemporalContext = {
295
+ blackboard,
296
+ treeRegistry: registry,
297
+ timestamp: Date.now(),
298
+ deltaTime: 0,
299
+ input: { prefix: "result" },
300
+ activities: {
301
+ executePieceAction: vi.fn(),
302
+ executePythonScript: mockPythonActivity,
303
+ },
304
+ };
305
+
306
+ const node = new PythonScript({
307
+ id: "test",
308
+ code: "bb['${input.prefix}_data'] = 42",
309
+ });
310
+
311
+ await node.tick(context);
312
+
313
+ expect(mockPythonActivity).toHaveBeenCalledWith(
314
+ expect.objectContaining({
315
+ code: "bb['result_data'] = 42",
316
+ })
317
+ );
318
+ });
319
+ });
320
+
321
+ describe("Environment variables", () => {
322
+ const originalEnv = process.env;
323
+
324
+ beforeEach(() => {
325
+ process.env = { ...originalEnv };
326
+ });
327
+
328
+ afterAll(() => {
329
+ process.env = originalEnv;
330
+ });
331
+
332
+ it("should pass allowed environment variables", async () => {
333
+ process.env.MY_API_KEY = "secret-123";
334
+ process.env.DEBUG_MODE = "true";
335
+
336
+ const mockPythonActivity = vi.fn().mockResolvedValue({
337
+ blackboard: {},
338
+ });
339
+
340
+ const context: TemporalContext = {
341
+ blackboard,
342
+ treeRegistry: registry,
343
+ timestamp: Date.now(),
344
+ deltaTime: 0,
345
+ activities: {
346
+ executePieceAction: vi.fn(),
347
+ executePythonScript: mockPythonActivity,
348
+ },
349
+ };
350
+
351
+ const node = new PythonScript({
352
+ id: "test",
353
+ code: "import os; bb['key'] = os.environ.get('MY_API_KEY')",
354
+ allowedEnvVars: ["MY_API_KEY", "DEBUG_MODE"],
355
+ });
356
+
357
+ await node.tick(context);
358
+
359
+ expect(mockPythonActivity).toHaveBeenCalledWith(
360
+ expect.objectContaining({
361
+ env: {
362
+ MY_API_KEY: "secret-123",
363
+ DEBUG_MODE: "true",
364
+ },
365
+ })
366
+ );
367
+ });
368
+
369
+ it("should not pass env vars that are not in allowedEnvVars", async () => {
370
+ process.env.SECRET_KEY = "should-not-pass";
371
+
372
+ const mockPythonActivity = vi.fn().mockResolvedValue({
373
+ blackboard: {},
374
+ });
375
+
376
+ const context: TemporalContext = {
377
+ blackboard,
378
+ treeRegistry: registry,
379
+ timestamp: Date.now(),
380
+ deltaTime: 0,
381
+ activities: {
382
+ executePieceAction: vi.fn(),
383
+ executePythonScript: mockPythonActivity,
384
+ },
385
+ };
386
+
387
+ const node = new PythonScript({
388
+ id: "test",
389
+ code: "pass",
390
+ allowedEnvVars: [], // Explicitly empty
391
+ });
392
+
393
+ await node.tick(context);
394
+
395
+ expect(mockPythonActivity).toHaveBeenCalledWith(
396
+ expect.objectContaining({
397
+ env: {},
398
+ })
399
+ );
400
+ });
401
+ });
402
+
403
+ describe("Node lifecycle", () => {
404
+ it("should clone correctly", () => {
405
+ const node = new PythonScript({
406
+ id: "original",
407
+ code: "bb['x'] = 1",
408
+ packages: ["pandas"],
409
+ timeout: 30000,
410
+ });
411
+
412
+ const cloned = node.clone() as PythonScript;
413
+
414
+ expect(cloned.id).toBe("original");
415
+ });
416
+
417
+ it("should reset status correctly", async () => {
418
+ const context: TemporalContext = {
419
+ blackboard,
420
+ treeRegistry: registry,
421
+ timestamp: Date.now(),
422
+ deltaTime: 0,
423
+ // No activities - will fail
424
+ };
425
+
426
+ const node = new PythonScript({
427
+ id: "test",
428
+ code: "pass",
429
+ });
430
+
431
+ await node.tick(context);
432
+ expect(node.status()).toBe(NodeStatus.FAILURE);
433
+
434
+ node.reset();
435
+ expect(node.status()).toBe(NodeStatus.IDLE);
436
+ expect(node.lastError).toBeUndefined();
437
+ });
438
+ });
439
+ });