@q1k-oss/btree-workflows 0.0.1 → 0.0.2

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 (206) hide show
  1. package/README.md +13 -13
  2. package/dist/index.cjs +5011 -0
  3. package/dist/index.d.cts +3320 -0
  4. package/dist/index.d.ts +3320 -0
  5. package/dist/index.js +4879 -0
  6. package/package.json +33 -3
  7. package/.claude/settings.local.json +0 -31
  8. package/CLAUDE.md +0 -181
  9. package/behaviour-tree-workflows-landing/index.html +0 -16
  10. package/behaviour-tree-workflows-landing/package-lock.json +0 -2074
  11. package/behaviour-tree-workflows-landing/package.json +0 -31
  12. package/behaviour-tree-workflows-landing/public/favicon.svg +0 -17
  13. package/behaviour-tree-workflows-landing/src/App.css +0 -103
  14. package/behaviour-tree-workflows-landing/src/App.tsx +0 -176
  15. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +0 -89
  16. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +0 -64
  17. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +0 -64
  18. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +0 -34
  19. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +0 -107
  20. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +0 -85
  21. package/behaviour-tree-workflows-landing/src/components/Header.css +0 -50
  22. package/behaviour-tree-workflows-landing/src/components/Header.tsx +0 -26
  23. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +0 -45
  24. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +0 -15
  25. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +0 -74
  26. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +0 -53
  27. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +0 -67
  28. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +0 -192
  29. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +0 -18
  30. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +0 -96
  31. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +0 -11
  32. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +0 -96
  33. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +0 -136
  34. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +0 -549
  35. package/behaviour-tree-workflows-landing/src/main.tsx +0 -9
  36. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +0 -18
  37. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +0 -24
  38. package/behaviour-tree-workflows-landing/src/stubs/path.ts +0 -16
  39. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +0 -6
  40. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +0 -22
  41. package/behaviour-tree-workflows-landing/tsconfig.json +0 -25
  42. package/behaviour-tree-workflows-landing/vite.config.ts +0 -40
  43. package/demo-google-sheets.ts +0 -181
  44. package/demo-runtime-variables.ts +0 -174
  45. package/demo-template.ts +0 -208
  46. package/docs/ARCHITECTURE_SUMMARY.md +0 -613
  47. package/docs/NODE_REFERENCE.md +0 -504
  48. package/docs/README.md +0 -53
  49. package/docs/custom-nodes-architecture.md +0 -826
  50. package/docs/observability.md +0 -175
  51. package/docs/yaml-specification.md +0 -990
  52. package/examples/temporal/README.md +0 -117
  53. package/examples/temporal/activities.ts +0 -373
  54. package/examples/temporal/client.ts +0 -115
  55. package/examples/temporal/python-worker/activities.py +0 -339
  56. package/examples/temporal/python-worker/requirements.txt +0 -12
  57. package/examples/temporal/python-worker/worker.py +0 -106
  58. package/examples/temporal/worker.ts +0 -66
  59. package/examples/temporal/workflows.ts +0 -6
  60. package/examples/temporal/yaml-workflow-loader.ts +0 -105
  61. package/examples/yaml-test.ts +0 -97
  62. package/examples/yaml-workflows/01-simple-sequence.yaml +0 -25
  63. package/examples/yaml-workflows/02-parallel-timeout.yaml +0 -45
  64. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +0 -94
  65. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +0 -346
  66. package/examples/yaml-workflows/05-order-processing.yaml +0 -146
  67. package/examples/yaml-workflows/06-activity-test.yaml +0 -71
  68. package/examples/yaml-workflows/07-activity-simple-test.yaml +0 -43
  69. package/examples/yaml-workflows/08-file-processing.yaml +0 -141
  70. package/examples/yaml-workflows/09-http-request.yaml +0 -137
  71. package/examples/yaml-workflows/README.md +0 -211
  72. package/src/actions/code-execution.schema.ts +0 -27
  73. package/src/actions/code-execution.ts +0 -218
  74. package/src/actions/generate-file.test.ts +0 -516
  75. package/src/actions/generate-file.ts +0 -166
  76. package/src/actions/http-request.test.ts +0 -784
  77. package/src/actions/http-request.ts +0 -228
  78. package/src/actions/index.ts +0 -20
  79. package/src/actions/parse-file.test.ts +0 -448
  80. package/src/actions/parse-file.ts +0 -139
  81. package/src/actions/python-script.test.ts +0 -439
  82. package/src/actions/python-script.ts +0 -154
  83. package/src/base-node.test.ts +0 -511
  84. package/src/base-node.ts +0 -605
  85. package/src/behavior-tree.test.ts +0 -431
  86. package/src/behavior-tree.ts +0 -283
  87. package/src/blackboard.test.ts +0 -222
  88. package/src/blackboard.ts +0 -192
  89. package/src/composites/conditional.schema.ts +0 -19
  90. package/src/composites/conditional.test.ts +0 -309
  91. package/src/composites/conditional.ts +0 -129
  92. package/src/composites/for-each.schema.ts +0 -23
  93. package/src/composites/for-each.test.ts +0 -254
  94. package/src/composites/for-each.ts +0 -132
  95. package/src/composites/index.ts +0 -15
  96. package/src/composites/memory-sequence.schema.ts +0 -19
  97. package/src/composites/memory-sequence.test.ts +0 -223
  98. package/src/composites/memory-sequence.ts +0 -98
  99. package/src/composites/parallel.schema.ts +0 -28
  100. package/src/composites/parallel.test.ts +0 -502
  101. package/src/composites/parallel.ts +0 -157
  102. package/src/composites/reactive-sequence.schema.ts +0 -19
  103. package/src/composites/reactive-sequence.test.ts +0 -170
  104. package/src/composites/reactive-sequence.ts +0 -85
  105. package/src/composites/recovery.schema.ts +0 -19
  106. package/src/composites/recovery.test.ts +0 -366
  107. package/src/composites/recovery.ts +0 -90
  108. package/src/composites/selector.schema.ts +0 -19
  109. package/src/composites/selector.test.ts +0 -387
  110. package/src/composites/selector.ts +0 -85
  111. package/src/composites/sequence.schema.ts +0 -19
  112. package/src/composites/sequence.test.ts +0 -337
  113. package/src/composites/sequence.ts +0 -72
  114. package/src/composites/sub-tree.schema.ts +0 -21
  115. package/src/composites/sub-tree.test.ts +0 -893
  116. package/src/composites/sub-tree.ts +0 -177
  117. package/src/composites/while.schema.ts +0 -24
  118. package/src/composites/while.test.ts +0 -381
  119. package/src/composites/while.ts +0 -149
  120. package/src/data-store/index.ts +0 -10
  121. package/src/data-store/memory-store.ts +0 -161
  122. package/src/data-store/types.ts +0 -94
  123. package/src/debug/breakpoint.test.ts +0 -47
  124. package/src/debug/breakpoint.ts +0 -30
  125. package/src/debug/index.ts +0 -17
  126. package/src/debug/resume-point.test.ts +0 -49
  127. package/src/debug/resume-point.ts +0 -29
  128. package/src/decorators/delay.schema.ts +0 -21
  129. package/src/decorators/delay.test.ts +0 -261
  130. package/src/decorators/delay.ts +0 -140
  131. package/src/decorators/force-result.schema.ts +0 -32
  132. package/src/decorators/force-result.test.ts +0 -133
  133. package/src/decorators/force-result.ts +0 -63
  134. package/src/decorators/index.ts +0 -13
  135. package/src/decorators/invert.schema.ts +0 -19
  136. package/src/decorators/invert.test.ts +0 -135
  137. package/src/decorators/invert.ts +0 -42
  138. package/src/decorators/keep-running.schema.ts +0 -20
  139. package/src/decorators/keep-running.test.ts +0 -105
  140. package/src/decorators/keep-running.ts +0 -49
  141. package/src/decorators/precondition.schema.ts +0 -19
  142. package/src/decorators/precondition.test.ts +0 -351
  143. package/src/decorators/precondition.ts +0 -139
  144. package/src/decorators/repeat.schema.ts +0 -21
  145. package/src/decorators/repeat.test.ts +0 -187
  146. package/src/decorators/repeat.ts +0 -94
  147. package/src/decorators/run-once.schema.ts +0 -19
  148. package/src/decorators/run-once.test.ts +0 -140
  149. package/src/decorators/run-once.ts +0 -61
  150. package/src/decorators/soft-assert.schema.ts +0 -19
  151. package/src/decorators/soft-assert.test.ts +0 -107
  152. package/src/decorators/soft-assert.ts +0 -68
  153. package/src/decorators/timeout.schema.ts +0 -21
  154. package/src/decorators/timeout.test.ts +0 -274
  155. package/src/decorators/timeout.ts +0 -159
  156. package/src/errors.test.ts +0 -63
  157. package/src/errors.ts +0 -34
  158. package/src/events.test.ts +0 -347
  159. package/src/events.ts +0 -183
  160. package/src/index.ts +0 -80
  161. package/src/integrations/index.ts +0 -30
  162. package/src/integrations/integration-action.test.ts +0 -571
  163. package/src/integrations/integration-action.ts +0 -233
  164. package/src/integrations/piece-executor.ts +0 -320
  165. package/src/observability/execution-tracker.ts +0 -320
  166. package/src/observability/index.ts +0 -23
  167. package/src/observability/sinks.ts +0 -138
  168. package/src/observability/types.ts +0 -130
  169. package/src/registry-utils.ts +0 -147
  170. package/src/registry.test.ts +0 -466
  171. package/src/registry.ts +0 -334
  172. package/src/schemas/base.schema.ts +0 -104
  173. package/src/schemas/index.ts +0 -223
  174. package/src/schemas/integration.test.ts +0 -238
  175. package/src/schemas/tree-definition.schema.ts +0 -170
  176. package/src/schemas/validation.test.ts +0 -146
  177. package/src/schemas/validation.ts +0 -122
  178. package/src/scripting/index.ts +0 -22
  179. package/src/templates/template-loader.test.ts +0 -281
  180. package/src/templates/template-loader.ts +0 -152
  181. package/src/temporal-integration.test.ts +0 -213
  182. package/src/test-nodes.ts +0 -259
  183. package/src/types.ts +0 -503
  184. package/src/utilities/index.ts +0 -17
  185. package/src/utilities/log-message.test.ts +0 -275
  186. package/src/utilities/log-message.ts +0 -134
  187. package/src/utilities/regex-extract.test.ts +0 -138
  188. package/src/utilities/regex-extract.ts +0 -108
  189. package/src/utilities/variable-resolver.test.ts +0 -416
  190. package/src/utilities/variable-resolver.ts +0 -318
  191. package/src/utils/error-handler.test.ts +0 -117
  192. package/src/utils/error-handler.ts +0 -48
  193. package/src/utils/signal-check.test.ts +0 -234
  194. package/src/utils/signal-check.ts +0 -140
  195. package/src/yaml/errors.ts +0 -143
  196. package/src/yaml/index.ts +0 -30
  197. package/src/yaml/loader.ts +0 -39
  198. package/src/yaml/parser.ts +0 -286
  199. package/src/yaml/validation/semantic-validator.ts +0 -196
  200. package/templates/google-sheets/insert-row.yaml +0 -76
  201. package/templates/notification-sender.yaml +0 -33
  202. package/templates/order-validation.yaml +0 -44
  203. package/tsconfig.json +0 -24
  204. package/vitest.config.ts +0 -25
  205. package/workflows/order-processor.yaml +0 -59
  206. package/workflows/process-order-workflow.yaml +0 -142
@@ -1,893 +0,0 @@
1
- /**
2
- * Tests for SubTree node
3
- */
4
-
5
- import { beforeEach, describe, expect, it } from "vitest";
6
- import { BehaviorTree } from "../behavior-tree.js";
7
- import { ScopedBlackboard } from "../blackboard.js";
8
- import { Registry } from "../registry.js";
9
- import { FailureNode, RunningNode, SuccessNode } from "../test-nodes.js";
10
- import type { TreeNode } from "../types.js";
11
- import { type TemporalContext, NodeStatus } from "../types.js";
12
- import { Sequence } from "./sequence.js";
13
- import { SubTree } from "./sub-tree.js";
14
-
15
- describe("SubTree", () => {
16
- let blackboard: ScopedBlackboard;
17
- let treeRegistry: Registry;
18
- let context: TemporalContext;
19
-
20
- // Helper to register a tree with BehaviorTree wrapper (uses test-scoped registry)
21
- const registerTree = (id: string, rootNode: TreeNode): void => {
22
- const tree = new BehaviorTree(rootNode);
23
- treeRegistry.registerTree(id, tree, "test-source");
24
- };
25
-
26
- beforeEach(() => {
27
- blackboard = new ScopedBlackboard("root");
28
- treeRegistry = new Registry();
29
- context = {
30
- blackboard,
31
- treeRegistry,
32
- timestamp: Date.now(),
33
- deltaTime: 0,
34
- };
35
- });
36
-
37
- describe("Basic Functionality", () => {
38
- it("should reference and execute a registered behavior tree", async () => {
39
- // Register a simple tree
40
- const reusableTree = new Sequence({
41
- id: "reusable",
42
- name: "Reusable Steps",
43
- });
44
- reusableTree.addChildren([
45
- new SuccessNode({ id: "child1" }),
46
- new SuccessNode({ id: "child2" }),
47
- ]);
48
- registerTree("login-steps", reusableTree);
49
-
50
- // Create SubTree that references the tree
51
- const subTree = new SubTree({
52
- id: "sg1",
53
- name: "Login",
54
- treeId: "login-steps",
55
- });
56
-
57
- const result = await subTree.tick(context);
58
- expect(result).toBe(NodeStatus.SUCCESS);
59
- });
60
-
61
- it("should fail when tree completes with failure", async () => {
62
- // Register a tree that fails
63
- const failingTree = new Sequence({
64
- id: "failing",
65
- name: "Failing Steps",
66
- });
67
- failingTree.addChildren([
68
- new SuccessNode({ id: "child1" }),
69
- new FailureNode({ id: "child2" }),
70
- ]);
71
- registerTree("failing-steps", failingTree);
72
-
73
- const subTree = new SubTree({
74
- id: "sg1",
75
- name: "Failing Group",
76
- treeId: "failing-steps",
77
- });
78
-
79
- const result = await subTree.tick(context);
80
- expect(result).toBe(NodeStatus.FAILURE);
81
- });
82
-
83
- it("should return running when tree is running", async () => {
84
- // Register a tree that stays running
85
- const runningTree = new Sequence({
86
- id: "running",
87
- name: "Running Steps",
88
- });
89
- runningTree.addChildren([
90
- new SuccessNode({ id: "child1" }),
91
- new RunningNode({ id: "child2" }),
92
- ]);
93
- registerTree("running-steps", runningTree);
94
-
95
- const subTree = new SubTree({
96
- id: "sg1",
97
- name: "Running Group",
98
- treeId: "running-steps",
99
- });
100
-
101
- const result = await subTree.tick(context);
102
- expect(result).toBe(NodeStatus.RUNNING);
103
- });
104
-
105
- it("should throw error when tree ID is not found", async () => {
106
- const subTree = new SubTree({
107
- id: "sg1",
108
- name: "Invalid Group",
109
- treeId: "nonexistent-tree",
110
- });
111
-
112
- const status = await subTree.tick(context);
113
- expect(status).toBe(NodeStatus.FAILURE);
114
- });
115
-
116
- it("should include available trees in error message", async () => {
117
- // Register some trees
118
- registerTree("tree1", new SuccessNode({ id: "t1" }));
119
- registerTree("tree2", new SuccessNode({ id: "t2" }));
120
-
121
- const subTree = new SubTree({
122
- id: "sg1",
123
- name: "Invalid Group",
124
- treeId: "nonexistent-tree",
125
- });
126
-
127
- const status = await subTree.tick(context);
128
- expect(status).toBe(NodeStatus.FAILURE);
129
- });
130
- });
131
-
132
- describe("Scoped Blackboard", () => {
133
- it("should create scoped blackboard for subTree", async () => {
134
- // Create a custom node that checks its blackboard scope
135
- let capturedScopePath: string = "";
136
- class CheckScopeNode extends SuccessNode {
137
- async tick(context: TemporalContext) {
138
- capturedScopePath = context.blackboard.getFullScopePath();
139
- return await super.tick(context);
140
- }
141
- }
142
-
143
- const tree = new Sequence({ id: "scoped", name: "Scoped Tree" });
144
- tree.addChild(new CheckScopeNode({ id: "child1" }));
145
- registerTree("scoped-steps", tree);
146
-
147
- const subTree = new SubTree({
148
- id: "sg1",
149
- name: "Scoped Group",
150
- treeId: "scoped-steps",
151
- });
152
-
153
- await subTree.tick(context);
154
-
155
- // Should have created a scope
156
- expect(capturedScopePath).toContain("subtree_sg1");
157
- });
158
-
159
- it("should isolate variables between subTrees", async () => {
160
- // First tree sets a value
161
- class SetValueNode extends SuccessNode {
162
- async tick(context: TemporalContext) {
163
- context.blackboard.set("localValue", "from-sg1");
164
- return await super.tick(context);
165
- }
166
- }
167
-
168
- // Second tree tries to read the value
169
- let capturedValue: unknown = "not-set";
170
- class ReadValueNode extends SuccessNode {
171
- async tick(context: TemporalContext) {
172
- capturedValue = context.blackboard.get("localValue");
173
- return await super.tick(context);
174
- }
175
- }
176
-
177
- const tree1 = new Sequence({ id: "tree1", name: "Tree 1" });
178
- tree1.addChild(new SetValueNode({ id: "child1" }));
179
- registerTree("steps1", tree1);
180
-
181
- const tree2 = new Sequence({ id: "tree2", name: "Tree 2" });
182
- tree2.addChild(new ReadValueNode({ id: "child2" }));
183
- registerTree("steps2", tree2);
184
-
185
- const sg1 = new SubTree({
186
- id: "sg1",
187
- name: "Group 1",
188
- treeId: "steps1",
189
- });
190
- const sg2 = new SubTree({
191
- id: "sg2",
192
- name: "Group 2",
193
- treeId: "steps2",
194
- });
195
-
196
- // Execute sg1 - sets localValue in its scope
197
- await sg1.tick(context);
198
-
199
- // Execute sg2 - should NOT see sg1's value
200
- await sg2.tick(context);
201
-
202
- // sg2 should not have access to sg1's scoped value
203
- expect(capturedValue).toBeUndefined();
204
- });
205
-
206
- it("should inherit parent blackboard values", async () => {
207
- // Create a node that reads from parent scope
208
- let parentValue: unknown = "not-set";
209
- class ReadParentNode extends SuccessNode {
210
- async tick(context: TemporalContext) {
211
- parentValue = context.blackboard.get("inheritedValue");
212
- return await super.tick(context);
213
- }
214
- }
215
-
216
- const tree = new Sequence({ id: "tree", name: "Tree" });
217
- tree.addChild(new ReadParentNode({ id: "child1" }));
218
- registerTree("read-parent", tree);
219
-
220
- // Set value in parent blackboard
221
- blackboard.set("inheritedValue", "from-parent");
222
-
223
- const subTree = new SubTree({
224
- id: "sg1",
225
- name: "Reading Group",
226
- treeId: "read-parent",
227
- });
228
-
229
- await subTree.tick(context);
230
-
231
- // Should be able to read parent value
232
- expect(parentValue).toBe("from-parent");
233
- });
234
-
235
- it("should not leak subTree-scoped values to parent", async () => {
236
- // Create a node that sets a value in its context
237
- class SetValueNode extends SuccessNode {
238
- async tick(context: TemporalContext) {
239
- context.blackboard.set("groupLocalValue", "group-value");
240
- return await super.tick(context);
241
- }
242
- }
243
-
244
- const tree = new Sequence({ id: "tree", name: "Tree" });
245
- tree.addChild(new SetValueNode({ id: "child1" }));
246
- registerTree("set-local", tree);
247
-
248
- const subTree = new SubTree({
249
- id: "sg1",
250
- name: "Setting Group",
251
- treeId: "set-local",
252
- });
253
-
254
- await subTree.tick(context);
255
-
256
- // Group-local value should NOT exist in parent blackboard
257
- expect(blackboard.has("groupLocalValue")).toBe(false);
258
- });
259
- });
260
-
261
- describe("Lazy Tree Cloning", () => {
262
- it("should clone tree only on first tick", async () => {
263
- const tree = new SuccessNode({ id: "tree" });
264
- registerTree("lazy-tree", tree);
265
-
266
- const subTree = new SubTree({
267
- id: "sg1",
268
- name: "Lazy Group",
269
- treeId: "lazy-tree",
270
- });
271
-
272
- // First tick should clone the tree
273
- await subTree.tick(context);
274
- expect(subTree.clonedTree).toBeDefined();
275
-
276
- // Store reference to cloned tree
277
- const clonedTree = subTree.clonedTree;
278
-
279
- // Second tick should reuse the same cloned tree
280
- await subTree.tick(context);
281
- expect(subTree.clonedTree).toBe(clonedTree);
282
- });
283
-
284
- it("should clone separate instances for different subTrees", async () => {
285
- const tree = new SuccessNode({ id: "tree" });
286
- registerTree("shared-tree", tree);
287
-
288
- const sg1 = new SubTree({
289
- id: "sg1",
290
- name: "Group 1",
291
- treeId: "shared-tree",
292
- });
293
- const sg2 = new SubTree({
294
- id: "sg2",
295
- name: "Group 2",
296
- treeId: "shared-tree",
297
- });
298
-
299
- await sg1.tick(context);
300
- await sg2.tick(context);
301
-
302
- // Each should have its own cloned instance
303
- expect(sg1.clonedTree).toBeDefined();
304
- expect(sg2.clonedTree).toBeDefined();
305
- expect(sg1.clonedTree).not.toBe(sg2.clonedTree);
306
- });
307
- });
308
-
309
- describe("Reset and Halt", () => {
310
- it("should reset the referenced tree", async () => {
311
- const tree = new RunningNode({ id: "tree" });
312
- registerTree("reset-tree", tree);
313
-
314
- const subTree = new SubTree({
315
- id: "sg1",
316
- name: "Reset Group",
317
- treeId: "reset-tree",
318
- });
319
-
320
- await subTree.tick(context);
321
- expect(subTree.status()).toBe(NodeStatus.RUNNING);
322
-
323
- subTree.reset();
324
- expect(subTree.status()).toBe(NodeStatus.IDLE);
325
- expect(subTree.clonedTree?.status()).toBe(NodeStatus.IDLE);
326
- });
327
-
328
- it("should halt the referenced tree", async () => {
329
- const tree = new RunningNode({ id: "tree" });
330
- registerTree("halt-tree", tree);
331
-
332
- const subTree = new SubTree({
333
- id: "sg1",
334
- name: "Halt Group",
335
- treeId: "halt-tree",
336
- });
337
-
338
- await subTree.tick(context);
339
- expect(subTree.status()).toBe(NodeStatus.RUNNING);
340
- expect(subTree.clonedTree?.status()).toBe(NodeStatus.RUNNING);
341
-
342
- subTree.halt();
343
- expect(subTree.status()).toBe(NodeStatus.IDLE);
344
- expect(subTree.clonedTree?.status()).toBe(NodeStatus.IDLE);
345
- });
346
- });
347
-
348
- describe("Clone", () => {
349
- it("should clone the subTree without cloning the cached tree", () => {
350
- const subTree = new SubTree({
351
- id: "sg1",
352
- name: "Original SubTree",
353
- treeId: "some-tree",
354
- });
355
-
356
- const cloned = subTree.clone() as SubTree;
357
-
358
- expect(cloned.id).toBe("sg1");
359
- expect(cloned.name).toBe("Original SubTree");
360
- expect(cloned.treeId).toBe("some-tree");
361
- expect(cloned.clonedTree).toBeUndefined();
362
- });
363
-
364
- it("should allow cloned subTree to lazy-load its own tree", async () => {
365
- const tree = new SuccessNode({ id: "tree" });
366
- registerTree("clone-tree", tree);
367
-
368
- const original = new SubTree({
369
- id: "sg1",
370
- name: "Original",
371
- treeId: "clone-tree",
372
- });
373
-
374
- // Tick original to trigger lazy loading
375
- await original.tick(context);
376
- expect(original.clonedTree).toBeDefined();
377
-
378
- // Clone should not have a cached tree yet
379
- const cloned = original.clone() as SubTree;
380
- expect(cloned.clonedTree).toBeUndefined();
381
-
382
- // Tick clone to trigger its own lazy loading
383
- await cloned.tick(context);
384
- expect(cloned.clonedTree).toBeDefined();
385
-
386
- // Should be different instances
387
- expect(cloned.clonedTree).not.toBe(original.clonedTree);
388
- });
389
- });
390
-
391
- describe("Parameter Passing (params)", () => {
392
- it("should pass static params to subtree blackboard", async () => {
393
- // Create a node that reads params from its blackboard
394
- let capturedOrderId: unknown = "not-set";
395
- let capturedQuantity: unknown = "not-set";
396
- class ReadParamsNode extends SuccessNode {
397
- async tick(ctx: TemporalContext) {
398
- capturedOrderId = ctx.blackboard.get("orderId");
399
- capturedQuantity = ctx.blackboard.get("quantity");
400
- return await super.tick(ctx);
401
- }
402
- }
403
-
404
- const tree = new Sequence({ id: "tree", name: "Tree" });
405
- tree.addChild(new ReadParamsNode({ id: "reader" }));
406
- registerTree("params-tree", tree);
407
-
408
- const subTree = new SubTree({
409
- id: "sg1",
410
- name: "With Params",
411
- treeId: "params-tree",
412
- params: {
413
- orderId: "ORD-123",
414
- quantity: 5,
415
- },
416
- });
417
-
418
- await subTree.tick(context);
419
-
420
- expect(capturedOrderId).toBe("ORD-123");
421
- expect(capturedQuantity).toBe(5);
422
- });
423
-
424
- it("should resolve variable references in params from parent blackboard", async () => {
425
- // Set values in parent blackboard
426
- blackboard.set("currentCustomer", "CUST-456");
427
- blackboard.set("selectedProduct", { id: "PROD-789", name: "Widget" });
428
-
429
- let capturedCustomer: unknown = "not-set";
430
- let capturedProduct: unknown = "not-set";
431
- class ReadParamsNode extends SuccessNode {
432
- async tick(ctx: TemporalContext) {
433
- capturedCustomer = ctx.blackboard.get("customer");
434
- capturedProduct = ctx.blackboard.get("product");
435
- return await super.tick(ctx);
436
- }
437
- }
438
-
439
- const tree = new Sequence({ id: "tree", name: "Tree" });
440
- tree.addChild(new ReadParamsNode({ id: "reader" }));
441
- registerTree("resolve-params-tree", tree);
442
-
443
- const subTree = new SubTree({
444
- id: "sg1",
445
- name: "With Variable Params",
446
- treeId: "resolve-params-tree",
447
- params: {
448
- customer: "${bb.currentCustomer}",
449
- product: "${bb.selectedProduct}",
450
- },
451
- });
452
-
453
- await subTree.tick(context);
454
-
455
- expect(capturedCustomer).toBe("CUST-456");
456
- expect(capturedProduct).toEqual({ id: "PROD-789", name: "Widget" });
457
- });
458
-
459
- it("should resolve params from workflow input", async () => {
460
- // Set workflow input
461
- context.input = Object.freeze({
462
- orderId: "INPUT-ORD-999",
463
- priority: "high",
464
- });
465
-
466
- let capturedOrderId: unknown = "not-set";
467
- let capturedPriority: unknown = "not-set";
468
- class ReadParamsNode extends SuccessNode {
469
- async tick(ctx: TemporalContext) {
470
- capturedOrderId = ctx.blackboard.get("orderId");
471
- capturedPriority = ctx.blackboard.get("priority");
472
- return await super.tick(ctx);
473
- }
474
- }
475
-
476
- const tree = new Sequence({ id: "tree", name: "Tree" });
477
- tree.addChild(new ReadParamsNode({ id: "reader" }));
478
- registerTree("input-params-tree", tree);
479
-
480
- const subTree = new SubTree({
481
- id: "sg1",
482
- name: "With Input Params",
483
- treeId: "input-params-tree",
484
- params: {
485
- orderId: "${input.orderId}",
486
- priority: "${input.priority}",
487
- },
488
- });
489
-
490
- await subTree.tick(context);
491
-
492
- expect(capturedOrderId).toBe("INPUT-ORD-999");
493
- expect(capturedPriority).toBe("high");
494
- });
495
-
496
- it("should resolve nested property access in params", async () => {
497
- blackboard.set("user", {
498
- profile: {
499
- name: "Alice",
500
- email: "alice@example.com",
501
- },
502
- });
503
-
504
- let capturedName: unknown = "not-set";
505
- let capturedEmail: unknown = "not-set";
506
- class ReadParamsNode extends SuccessNode {
507
- async tick(ctx: TemporalContext) {
508
- capturedName = ctx.blackboard.get("userName");
509
- capturedEmail = ctx.blackboard.get("userEmail");
510
- return await super.tick(ctx);
511
- }
512
- }
513
-
514
- const tree = new Sequence({ id: "tree", name: "Tree" });
515
- tree.addChild(new ReadParamsNode({ id: "reader" }));
516
- registerTree("nested-params-tree", tree);
517
-
518
- const subTree = new SubTree({
519
- id: "sg1",
520
- name: "With Nested Params",
521
- treeId: "nested-params-tree",
522
- params: {
523
- userName: "${bb.user.profile.name}",
524
- userEmail: "${bb.user.profile.email}",
525
- },
526
- });
527
-
528
- await subTree.tick(context);
529
-
530
- expect(capturedName).toBe("Alice");
531
- expect(capturedEmail).toBe("alice@example.com");
532
- });
533
-
534
- it("should handle complex nested params structure", async () => {
535
- blackboard.set("basePrice", 100);
536
- context.input = Object.freeze({ taxRate: 0.08 });
537
-
538
- let capturedConfig: unknown = "not-set";
539
- class ReadParamsNode extends SuccessNode {
540
- async tick(ctx: TemporalContext) {
541
- capturedConfig = ctx.blackboard.get("config");
542
- return await super.tick(ctx);
543
- }
544
- }
545
-
546
- const tree = new Sequence({ id: "tree", name: "Tree" });
547
- tree.addChild(new ReadParamsNode({ id: "reader" }));
548
- registerTree("complex-params-tree", tree);
549
-
550
- const subTree = new SubTree({
551
- id: "sg1",
552
- name: "With Complex Params",
553
- treeId: "complex-params-tree",
554
- params: {
555
- config: {
556
- price: "${bb.basePrice}",
557
- tax: "${input.taxRate}",
558
- metadata: {
559
- source: "parent",
560
- timestamp: 12345,
561
- },
562
- },
563
- },
564
- });
565
-
566
- await subTree.tick(context);
567
-
568
- expect(capturedConfig).toEqual({
569
- price: 100,
570
- tax: 0.08,
571
- metadata: {
572
- source: "parent",
573
- timestamp: 12345,
574
- },
575
- });
576
- });
577
-
578
- it("should not leak params to parent scope", async () => {
579
- const tree = new SuccessNode({ id: "tree" });
580
- registerTree("leak-test-tree", tree);
581
-
582
- const subTree = new SubTree({
583
- id: "sg1",
584
- name: "Leak Test",
585
- treeId: "leak-test-tree",
586
- params: {
587
- secretParam: "should-not-leak",
588
- },
589
- });
590
-
591
- await subTree.tick(context);
592
-
593
- // Parent blackboard should NOT have the param
594
- expect(blackboard.has("secretParam")).toBe(false);
595
- });
596
- });
597
-
598
- describe("Output Export (outputs)", () => {
599
- it("should export specified outputs to parent blackboard", async () => {
600
- // Create a node that sets values in its blackboard
601
- class SetOutputsNode extends SuccessNode {
602
- async tick(ctx: TemporalContext) {
603
- ctx.blackboard.set("result", "computation-result");
604
- ctx.blackboard.set("processingTime", 150);
605
- ctx.blackboard.set("internalState", "should-not-export");
606
- return await super.tick(ctx);
607
- }
608
- }
609
-
610
- const tree = new Sequence({ id: "tree", name: "Tree" });
611
- tree.addChild(new SetOutputsNode({ id: "setter" }));
612
- registerTree("outputs-tree", tree);
613
-
614
- const subTree = new SubTree({
615
- id: "sg1",
616
- name: "With Outputs",
617
- treeId: "outputs-tree",
618
- outputs: ["result", "processingTime"],
619
- });
620
-
621
- await subTree.tick(context);
622
-
623
- // Exported values should be in parent
624
- expect(blackboard.get("result")).toBe("computation-result");
625
- expect(blackboard.get("processingTime")).toBe(150);
626
- // Non-exported values should NOT be in parent
627
- expect(blackboard.has("internalState")).toBe(false);
628
- });
629
-
630
- it("should skip missing output keys without error", async () => {
631
- class SetPartialOutputsNode extends SuccessNode {
632
- async tick(ctx: TemporalContext) {
633
- ctx.blackboard.set("existingOutput", "value");
634
- // Note: "missingOutput" is not set
635
- return await super.tick(ctx);
636
- }
637
- }
638
-
639
- const tree = new Sequence({ id: "tree", name: "Tree" });
640
- tree.addChild(new SetPartialOutputsNode({ id: "setter" }));
641
- registerTree("partial-outputs-tree", tree);
642
-
643
- const subTree = new SubTree({
644
- id: "sg1",
645
- name: "With Partial Outputs",
646
- treeId: "partial-outputs-tree",
647
- outputs: ["existingOutput", "missingOutput"],
648
- });
649
-
650
- const status = await subTree.tick(context);
651
-
652
- expect(status).toBe(NodeStatus.SUCCESS);
653
- expect(blackboard.get("existingOutput")).toBe("value");
654
- expect(blackboard.has("missingOutput")).toBe(false);
655
- });
656
-
657
- it("should export complex objects", async () => {
658
- class SetComplexOutputNode extends SuccessNode {
659
- async tick(ctx: TemporalContext) {
660
- ctx.blackboard.set("userResult", {
661
- id: "USER-123",
662
- profile: { name: "Bob", score: 95 },
663
- tags: ["active", "premium"],
664
- });
665
- return await super.tick(ctx);
666
- }
667
- }
668
-
669
- const tree = new Sequence({ id: "tree", name: "Tree" });
670
- tree.addChild(new SetComplexOutputNode({ id: "setter" }));
671
- registerTree("complex-output-tree", tree);
672
-
673
- const subTree = new SubTree({
674
- id: "sg1",
675
- name: "With Complex Output",
676
- treeId: "complex-output-tree",
677
- outputs: ["userResult"],
678
- });
679
-
680
- await subTree.tick(context);
681
-
682
- expect(blackboard.get("userResult")).toEqual({
683
- id: "USER-123",
684
- profile: { name: "Bob", score: 95 },
685
- tags: ["active", "premium"],
686
- });
687
- });
688
-
689
- it("should not export outputs on failure", async () => {
690
- class SetThenFailNode extends FailureNode {
691
- async tick(ctx: TemporalContext) {
692
- ctx.blackboard.set("shouldNotExport", "value");
693
- return await super.tick(ctx);
694
- }
695
- }
696
-
697
- const tree = new Sequence({ id: "tree", name: "Tree" });
698
- tree.addChild(new SetThenFailNode({ id: "failer" }));
699
- registerTree("fail-output-tree", tree);
700
-
701
- const subTree = new SubTree({
702
- id: "sg1",
703
- name: "Failing SubTree",
704
- treeId: "fail-output-tree",
705
- outputs: ["shouldNotExport"],
706
- });
707
-
708
- const status = await subTree.tick(context);
709
-
710
- expect(status).toBe(NodeStatus.FAILURE);
711
- // Output should NOT be exported on failure
712
- expect(blackboard.has("shouldNotExport")).toBe(false);
713
- });
714
-
715
- it("should export outputs on running status", async () => {
716
- class SetThenRunningNode extends RunningNode {
717
- async tick(ctx: TemporalContext) {
718
- ctx.blackboard.set("partialResult", "in-progress");
719
- return await super.tick(ctx);
720
- }
721
- }
722
-
723
- const tree = new Sequence({ id: "tree", name: "Tree" });
724
- tree.addChild(new SetThenRunningNode({ id: "runner" }));
725
- registerTree("running-output-tree", tree);
726
-
727
- const subTree = new SubTree({
728
- id: "sg1",
729
- name: "Running SubTree",
730
- treeId: "running-output-tree",
731
- outputs: ["partialResult"],
732
- });
733
-
734
- const status = await subTree.tick(context);
735
-
736
- expect(status).toBe(NodeStatus.RUNNING);
737
- // Output should be exported even when running (useful for streaming results)
738
- expect(blackboard.get("partialResult")).toBe("in-progress");
739
- });
740
- });
741
-
742
- describe("Combined Params and Outputs", () => {
743
- it("should pass params and export outputs in same subtree", async () => {
744
- // Create a node that reads params, computes, and sets outputs
745
- class ComputeNode extends SuccessNode {
746
- async tick(ctx: TemporalContext) {
747
- const price = ctx.blackboard.get("price") as number;
748
- const quantity = ctx.blackboard.get("quantity") as number;
749
- const taxRate = ctx.blackboard.get("taxRate") as number;
750
-
751
- const subtotal = price * quantity;
752
- const tax = subtotal * taxRate;
753
- const total = subtotal + tax;
754
-
755
- ctx.blackboard.set("subtotal", subtotal);
756
- ctx.blackboard.set("tax", tax);
757
- ctx.blackboard.set("total", total);
758
- return await super.tick(ctx);
759
- }
760
- }
761
-
762
- const tree = new Sequence({ id: "tree", name: "Tree" });
763
- tree.addChild(new ComputeNode({ id: "compute" }));
764
- registerTree("compute-tree", tree);
765
-
766
- // Set parent values
767
- blackboard.set("productPrice", 100);
768
- blackboard.set("orderQuantity", 3);
769
-
770
- const subTree = new SubTree({
771
- id: "calculate-order",
772
- name: "Calculate Order",
773
- treeId: "compute-tree",
774
- params: {
775
- price: "${bb.productPrice}",
776
- quantity: "${bb.orderQuantity}",
777
- taxRate: 0.08,
778
- },
779
- outputs: ["subtotal", "tax", "total"],
780
- });
781
-
782
- await subTree.tick(context);
783
-
784
- // Outputs should be in parent blackboard
785
- expect(blackboard.get("subtotal")).toBe(300);
786
- expect(blackboard.get("tax")).toBe(24);
787
- expect(blackboard.get("total")).toBe(324);
788
-
789
- // Params should NOT be in parent blackboard
790
- expect(blackboard.has("price")).toBe(false);
791
- expect(blackboard.has("quantity")).toBe(false);
792
- expect(blackboard.has("taxRate")).toBe(false);
793
- });
794
-
795
- it("should handle real-world order processing scenario", async () => {
796
- // Simulate order processing subtree
797
- class ProcessOrderNode extends SuccessNode {
798
- async tick(ctx: TemporalContext) {
799
- const orderId = ctx.blackboard.get("orderId") as string;
800
- const items = ctx.blackboard.get("items") as Array<{ price: number; qty: number }>;
801
-
802
- // Simulate processing
803
- const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
804
-
805
- ctx.blackboard.set("orderResult", {
806
- orderId,
807
- total,
808
- status: "processed",
809
- timestamp: Date.now(),
810
- });
811
- ctx.blackboard.set("orderStatus", "SUCCESS");
812
-
813
- return await super.tick(ctx);
814
- }
815
- }
816
-
817
- const tree = new Sequence({ id: "tree", name: "Tree" });
818
- tree.addChild(new ProcessOrderNode({ id: "process" }));
819
- registerTree("order-processor", tree);
820
-
821
- // Set up workflow input and parent state
822
- context.input = Object.freeze({
823
- orderId: "ORD-2024-001",
824
- });
825
- blackboard.set("cartItems", [
826
- { price: 25, qty: 2 },
827
- { price: 50, qty: 1 },
828
- { price: 10, qty: 3 },
829
- ]);
830
-
831
- const subTree = new SubTree({
832
- id: "process-order",
833
- name: "Process Order",
834
- treeId: "order-processor",
835
- params: {
836
- orderId: "${input.orderId}",
837
- items: "${bb.cartItems}",
838
- },
839
- outputs: ["orderResult", "orderStatus"],
840
- });
841
-
842
- await subTree.tick(context);
843
-
844
- // Check exported outputs
845
- expect(blackboard.get("orderStatus")).toBe("SUCCESS");
846
- const orderResult = blackboard.get("orderResult") as Record<string, unknown>;
847
- expect(orderResult.orderId).toBe("ORD-2024-001");
848
- expect(orderResult.total).toBe(130); // 25*2 + 50*1 + 10*3
849
- expect(orderResult.status).toBe("processed");
850
- });
851
-
852
- it("should work with multiple subtree calls sharing outputs", async () => {
853
- class IncrementNode extends SuccessNode {
854
- async tick(ctx: TemporalContext) {
855
- const current = ctx.blackboard.get("counter") as number;
856
- ctx.blackboard.set("counter", current + 1);
857
- return await super.tick(ctx);
858
- }
859
- }
860
-
861
- const tree = new Sequence({ id: "tree", name: "Tree" });
862
- tree.addChild(new IncrementNode({ id: "increment" }));
863
- registerTree("incrementer", tree);
864
-
865
- blackboard.set("counter", 0);
866
-
867
- const subTree1 = new SubTree({
868
- id: "inc1",
869
- name: "Increment 1",
870
- treeId: "incrementer",
871
- params: { counter: "${bb.counter}" },
872
- outputs: ["counter"],
873
- });
874
-
875
- const subTree2 = new SubTree({
876
- id: "inc2",
877
- name: "Increment 2",
878
- treeId: "incrementer",
879
- params: { counter: "${bb.counter}" },
880
- outputs: ["counter"],
881
- });
882
-
883
- await subTree1.tick(context);
884
- expect(blackboard.get("counter")).toBe(1);
885
-
886
- await subTree2.tick(context);
887
- expect(blackboard.get("counter")).toBe(2);
888
-
889
- await subTree1.tick(context);
890
- expect(blackboard.get("counter")).toBe(3);
891
- });
892
- });
893
- });