@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,431 @@
1
+ /**
2
+ * Tests for BehaviorTree with path-based indexing
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { BehaviorTree } from "./behavior-tree.js";
7
+ import { ScopedBlackboard } from "./blackboard.js";
8
+ import { Selector } from "./composites/selector.js";
9
+ import { Sequence } from "./composites/sequence.js";
10
+ import { ActionNode, type NodeConfiguration } from "./index.js";
11
+ import { Registry } from "./registry.js";
12
+ import { FailureNode, SuccessNode } from "./test-nodes.js";
13
+ import { type TemporalContext, NodeStatus } from "./types.js";
14
+
15
+ // Simple print action for testing
16
+ class PrintAction extends ActionNode {
17
+ private message: string;
18
+
19
+ constructor(config: NodeConfiguration & { message: string }) {
20
+ super(config);
21
+ this.message = config.message;
22
+ }
23
+
24
+ async executeTick(_context: TemporalContext): Promise<NodeStatus> {
25
+ this.log(`Message: ${this.message}`);
26
+ this._status = NodeStatus.SUCCESS;
27
+ return NodeStatus.SUCCESS;
28
+ }
29
+ }
30
+
31
+ describe("BehaviorTree", () => {
32
+ describe("Construction and Path-based Indexing", () => {
33
+ it("should index simple tree with paths", () => {
34
+ const seq = new Sequence({ id: "seq" });
35
+ const a1 = new PrintAction({ id: "a1", message: "First" });
36
+ const a2 = new PrintAction({ id: "a2", message: "Second" });
37
+ const a3 = new PrintAction({ id: "a3", message: "Third" });
38
+ seq.addChildren([a1, a2, a3]);
39
+
40
+ const tree = new BehaviorTree(seq);
41
+
42
+ // Root at /
43
+ expect(tree.findNodeByPath("/")).toBe(seq);
44
+
45
+ // Children at /0, /1, /2
46
+ expect(tree.findNodeByPath("/0")).toBe(a1);
47
+ expect(tree.findNodeByPath("/1")).toBe(a2);
48
+ expect(tree.findNodeByPath("/2")).toBe(a3);
49
+ });
50
+
51
+ it("should index nested tree structure", () => {
52
+ const root = new Sequence({ id: "root" });
53
+ const step = new Sequence({ id: "step", name: "step" });
54
+ const innerSeq = new Sequence({ id: "inner" });
55
+ const action = new PrintAction({ id: "action", message: "Deep" });
56
+
57
+ innerSeq.addChild(action);
58
+ step.addChild(innerSeq);
59
+ root.addChild(step);
60
+
61
+ const tree = new BehaviorTree(root);
62
+
63
+ expect(tree.findNodeByPath("/")).toBe(root); // root
64
+ expect(tree.findNodeByPath("/0")).toBe(step); // root → step
65
+ expect(tree.findNodeByPath("/0/0")).toBe(innerSeq); // root → step → seq
66
+ expect(tree.findNodeByPath("/0/0/0")).toBe(action); // root → step → seq → action
67
+ });
68
+
69
+ it("should return root node", () => {
70
+ const seq = new Sequence({ id: "seq" });
71
+ const tree = new BehaviorTree(seq);
72
+
73
+ expect(tree.getRoot()).toBe(seq);
74
+ });
75
+
76
+ it("should handle empty tree (single root node)", () => {
77
+ const root = new Sequence({ id: "root" });
78
+ const tree = new BehaviorTree(root);
79
+
80
+ expect(tree.findNodeByPath("/")).toBe(root);
81
+ expect(tree.findNodeByPath("/0")).toBeNull();
82
+ });
83
+ });
84
+
85
+ describe("Node Lookup by Path", () => {
86
+ it("should find nodes by path", () => {
87
+ const root = new Sequence({ id: "root" });
88
+ const child1 = new PrintAction({ id: "c1", message: "Child 1" });
89
+ const child2 = new PrintAction({ id: "c2", message: "Child 2" });
90
+ root.addChildren([child1, child2]);
91
+
92
+ const tree = new BehaviorTree(root);
93
+
94
+ expect(tree.findNodeByPath("/")).toBe(root);
95
+ expect(tree.findNodeByPath("/0")).toBe(child1);
96
+ expect(tree.findNodeByPath("/1")).toBe(child2);
97
+ });
98
+
99
+ it("should return null for non-existent path", () => {
100
+ const root = new Sequence({ id: "root" });
101
+ const tree = new BehaviorTree(root);
102
+
103
+ expect(tree.findNodeByPath("/0")).toBeNull();
104
+ expect(tree.findNodeByPath("/99")).toBeNull();
105
+ expect(tree.findNodeByPath("/invalid")).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe("Node Lookup by ID", () => {
110
+ it("should allow lookup by ID when IDs are present", () => {
111
+ const seq = new Sequence({ id: "my-seq" });
112
+ const action = new PrintAction({ id: "my-action", message: "Test" });
113
+ seq.addChild(action);
114
+
115
+ const tree = new BehaviorTree(seq);
116
+
117
+ expect(tree.findNodeById("my-seq")).toBe(seq);
118
+ expect(tree.findNodeById("my-action")).toBe(action);
119
+ expect(tree.findNodeById("nonexistent")).toBeNull();
120
+ });
121
+
122
+ it("should handle nodes without IDs", () => {
123
+ const seq = new Sequence({ id: "seq" });
124
+ const action1 = new PrintAction({ id: "a1", message: "Test 1" });
125
+ const action2 = new PrintAction({ id: "a2", message: "Test 2" });
126
+ seq.addChildren([action1, action2]);
127
+
128
+ const tree = new BehaviorTree(seq);
129
+
130
+ // Can still find by path even without IDs
131
+ expect(tree.findNodeByPath("/0")).toBe(action1);
132
+ expect(tree.findNodeByPath("/1")).toBe(action2);
133
+ });
134
+ });
135
+
136
+ describe("getNodePath", () => {
137
+ it("should return correct path for a node", () => {
138
+ const root = new Sequence({ id: "root" });
139
+ const step = new Sequence({ id: "step", name: "step" });
140
+ const action = new PrintAction({ id: "action", message: "Test" });
141
+
142
+ step.addChild(action);
143
+ root.addChild(step);
144
+
145
+ const tree = new BehaviorTree(root);
146
+
147
+ expect(tree.getNodePath(root)).toBe("/");
148
+ expect(tree.getNodePath(step)).toBe("/0");
149
+ expect(tree.getNodePath(action)).toBe("/0/0");
150
+ });
151
+
152
+ it("should return null for node not in tree", () => {
153
+ const root = new Sequence({ id: "root" });
154
+ const tree = new BehaviorTree(root);
155
+
156
+ const orphan = new PrintAction({ id: "orphan", message: "Not in tree" });
157
+ expect(tree.getNodePath(orphan)).toBeNull();
158
+ });
159
+ });
160
+
161
+ describe("getNodePathById", () => {
162
+ it("should return path by node ID", () => {
163
+ const root = new Sequence({ id: "root" });
164
+ const step = new Sequence({ id: "step-1", name: "step" });
165
+ const action = new PrintAction({ id: "action-1", message: "Test" });
166
+
167
+ step.addChild(action);
168
+ root.addChild(step);
169
+
170
+ const tree = new BehaviorTree(root);
171
+
172
+ expect(tree.getNodePathById("root")).toBe("/");
173
+ expect(tree.getNodePathById("step-1")).toBe("/0");
174
+ expect(tree.getNodePathById("action-1")).toBe("/0/0");
175
+ });
176
+
177
+ it("should return null for nonexistent node ID", () => {
178
+ const root = new Sequence({ id: "root" });
179
+ const tree = new BehaviorTree(root);
180
+
181
+ expect(tree.getNodePathById("nonexistent")).toBeNull();
182
+ });
183
+
184
+ it("should handle multiple children with nested structure", () => {
185
+ const root = new Sequence({ id: "root" });
186
+ const child1 = new PrintAction({ id: "child-1", message: "First" });
187
+ const child2 = new Sequence({ id: "child-2" });
188
+ const grandchild = new PrintAction({
189
+ id: "grandchild",
190
+ message: "Nested",
191
+ });
192
+
193
+ child2.addChild(grandchild);
194
+ root.addChildren([child1, child2]);
195
+
196
+ const tree = new BehaviorTree(root);
197
+
198
+ expect(tree.getNodePathById("root")).toBe("/");
199
+ expect(tree.getNodePathById("child-1")).toBe("/0");
200
+ expect(tree.getNodePathById("child-2")).toBe("/1");
201
+ expect(tree.getNodePathById("grandchild")).toBe("/1/0");
202
+ });
203
+ });
204
+
205
+ describe("replaceNodeAtPath - Basic Cases", () => {
206
+ it("should replace node at path even without ID", () => {
207
+ const seq = new Sequence({ id: "seq" });
208
+ const a1 = new PrintAction({ id: "a1", message: "First" });
209
+ const a2 = new PrintAction({ id: "a2", message: "Second" });
210
+ seq.addChildren([a1, a2]);
211
+
212
+ const tree = new BehaviorTree(seq);
213
+
214
+ // Replace middle node using path
215
+ const newAction = new PrintAction({ id: "new", message: "Replaced" });
216
+ tree.replaceNodeAtPath("/1", newAction);
217
+
218
+ // Verify replacement
219
+ expect(tree.findNodeByPath("/1")).toBe(newAction);
220
+ expect(seq.children?.[1]).toBe(newAction);
221
+ expect(newAction.parent).toBe(seq);
222
+ });
223
+
224
+ it("should preserve sibling nodes when replacing", () => {
225
+ const seq = new Sequence({ id: "seq" });
226
+ const a1 = new PrintAction({ id: "a1", message: "First" });
227
+ const a2 = new PrintAction({ id: "a2", message: "Second" });
228
+ const a3 = new PrintAction({ id: "a3", message: "Third" });
229
+ seq.addChildren([a1, a2, a3]);
230
+
231
+ const tree = new BehaviorTree(seq);
232
+
233
+ // Replace middle node
234
+ const newAction = new PrintAction({ id: "new", message: "New" });
235
+ tree.replaceNodeAtPath("/1", newAction);
236
+
237
+ // Siblings unchanged
238
+ expect(tree.findNodeByPath("/0")).toBe(a1);
239
+ expect(tree.findNodeByPath("/2")).toBe(a3);
240
+ expect(seq.children?.length).toBe(3);
241
+ });
242
+
243
+ it("should replace composite and reindex new children", () => {
244
+ const root = new Sequence({ id: "root" });
245
+ const step1 = new Sequence({ id: "step1", name: "step1" });
246
+ const step2 = new Sequence({ id: "step2", name: "step2" });
247
+
248
+ step1.addChild(new PrintAction({ id: "old-action", message: "Old" }));
249
+ step2.addChild(new PrintAction({ id: "other-action", message: "Other" }));
250
+ root.addChildren([step1, step2]);
251
+
252
+ const tree = new BehaviorTree(root);
253
+
254
+ // Replace step1 with new step
255
+ const newStep = new Sequence({ id: "step1-new", name: "step1-new" });
256
+ newStep.addChild(new PrintAction({ id: "new-action", message: "New" }));
257
+ tree.replaceNodeAtPath("/0", newStep);
258
+
259
+ // New children indexed
260
+ expect(tree.findNodeByPath("/0/0")?.id).toBe("new-action");
261
+ expect(tree.findNodeById("new-action")).toBeDefined();
262
+
263
+ // Old children gone
264
+ expect(tree.findNodeById("old-action")).toBeNull();
265
+
266
+ // Sibling preserved
267
+ expect(tree.findNodeByPath("/1")).toBe(step2);
268
+ expect(tree.findNodeById("other-action")).toBeDefined();
269
+ });
270
+
271
+ it("should handle root replacement", () => {
272
+ const oldRoot = new Sequence({ id: "old-root" });
273
+ oldRoot.addChild(new PrintAction({ id: "child", message: "Test" }));
274
+
275
+ const tree = new BehaviorTree(oldRoot);
276
+
277
+ const newRoot = new Selector({ id: "new-root" });
278
+ newRoot.addChild(new PrintAction({ id: "new-child", message: "New" }));
279
+
280
+ tree.replaceNodeAtPath("/", newRoot);
281
+
282
+ expect(tree.getRoot()).toBe(newRoot);
283
+ expect(tree.findNodeByPath("/")).toBe(newRoot);
284
+ expect(tree.findNodeByPath("/0")?.id).toBe("new-child");
285
+ expect(newRoot.parent).toBeUndefined();
286
+ });
287
+
288
+ it("should replace leaf node in deeply nested tree", () => {
289
+ const root = new Sequence({ id: "root" });
290
+ const step1 = new Sequence({ id: "step1", name: "step1" });
291
+ const step2 = new Sequence({ id: "step2", name: "step2" });
292
+ const innerSeq = new Sequence({ id: "inner" });
293
+ const action = new PrintAction({ id: "deep-action", message: "Deep" });
294
+
295
+ innerSeq.addChild(action);
296
+ step2.addChild(innerSeq);
297
+ root.addChildren([step1, step2]);
298
+
299
+ const tree = new BehaviorTree(root);
300
+
301
+ // Replace deeply nested action
302
+ const newAction = new PrintAction({
303
+ id: "new-deep",
304
+ message: "New Deep",
305
+ });
306
+ tree.replaceNodeAtPath("/1/0/0", newAction);
307
+
308
+ expect(tree.findNodeByPath("/1/0/0")).toBe(newAction);
309
+ expect(tree.findNodeById("new-deep")).toBe(newAction);
310
+ expect(tree.findNodeById("deep-action")).toBeNull();
311
+ });
312
+ });
313
+
314
+ describe("replaceNodeAtPath - Edge Cases", () => {
315
+ it("should throw error for invalid path", () => {
316
+ const tree = new BehaviorTree(new Sequence({ id: "root" }));
317
+
318
+ expect(() => {
319
+ tree.replaceNodeAtPath(
320
+ "/99",
321
+ new PrintAction({ id: "test", message: "Test" }),
322
+ );
323
+ }).toThrow("Node not found at path");
324
+ });
325
+
326
+ it("should throw error when trying to replace non-existent node", () => {
327
+ const root = new Sequence({ id: "root" });
328
+ root.addChild(new PrintAction({ id: "c1", message: "Child" }));
329
+ const tree = new BehaviorTree(root);
330
+
331
+ expect(() => {
332
+ tree.replaceNodeAtPath(
333
+ "/5",
334
+ new PrintAction({ id: "test", message: "Test" }),
335
+ );
336
+ }).toThrow("Node not found at path");
337
+ });
338
+ });
339
+
340
+ describe("Multiple Replacements", () => {
341
+ it("should handle multiple sequential replacements", () => {
342
+ const seq = new Sequence({ id: "seq" });
343
+ const a1 = new PrintAction({ id: "a1", message: "First" });
344
+ const a2 = new PrintAction({ id: "a2", message: "Second" });
345
+ const a3 = new PrintAction({ id: "a3", message: "Third" });
346
+ seq.addChildren([a1, a2, a3]);
347
+
348
+ const tree = new BehaviorTree(seq);
349
+
350
+ // First replacement
351
+ tree.replaceNodeAtPath(
352
+ "/0",
353
+ new PrintAction({ id: "new1", message: "New First" }),
354
+ );
355
+ expect(tree.findNodeById("new1")).toBeDefined();
356
+ expect(tree.findNodeById("a1")).toBeNull();
357
+
358
+ // Second replacement
359
+ tree.replaceNodeAtPath(
360
+ "/2",
361
+ new PrintAction({ id: "new3", message: "New Third" }),
362
+ );
363
+ expect(tree.findNodeById("new3")).toBeDefined();
364
+ expect(tree.findNodeById("a3")).toBeNull();
365
+
366
+ // Middle node unchanged
367
+ expect(tree.findNodeByPath("/1")).toBe(a2);
368
+ });
369
+ });
370
+
371
+ describe("parsePathWithTreeId", () => {
372
+ it("should parse path with tree ID prefix (#TreeID/path)", () => {
373
+ const result = BehaviorTree.parsePathWithTreeId("#SimpleTest/0/1");
374
+
375
+ expect(result.treeId).toBe("SimpleTest");
376
+ expect(result.nodePath).toBe("/0/1");
377
+ });
378
+
379
+ it("should parse path with tree ID and root path", () => {
380
+ const result = BehaviorTree.parsePathWithTreeId("#MyTree/");
381
+
382
+ expect(result.treeId).toBe("MyTree");
383
+ expect(result.nodePath).toBe("/");
384
+ });
385
+
386
+ it("should parse path with tree ID only (no slash)", () => {
387
+ const result = BehaviorTree.parsePathWithTreeId("#OnlyTreeId");
388
+
389
+ expect(result.treeId).toBe("OnlyTreeId");
390
+ expect(result.nodePath).toBe("/");
391
+ });
392
+
393
+ it("should throw error for path without tree ID prefix", () => {
394
+ expect(() => BehaviorTree.parsePathWithTreeId("/0/1/2")).toThrow(
395
+ "Invalid path format",
396
+ );
397
+ });
398
+
399
+ it("should throw error for empty tree ID (#/0/1)", () => {
400
+ expect(() => BehaviorTree.parsePathWithTreeId("#/0/1")).toThrow(
401
+ "tree ID cannot be empty",
402
+ );
403
+ });
404
+
405
+ it("should throw error for # alone", () => {
406
+ expect(() => BehaviorTree.parsePathWithTreeId("#")).toThrow(
407
+ "tree ID cannot be empty",
408
+ );
409
+ });
410
+
411
+ it("should throw error for whitespace-only tree ID", () => {
412
+ expect(() => BehaviorTree.parsePathWithTreeId("# /0")).toThrow(
413
+ "tree ID cannot be empty",
414
+ );
415
+ });
416
+
417
+ it("should handle tree ID with special characters", () => {
418
+ const result = BehaviorTree.parsePathWithTreeId("#My_Tree-123/0");
419
+
420
+ expect(result.treeId).toBe("My_Tree-123");
421
+ expect(result.nodePath).toBe("/0");
422
+ });
423
+
424
+ it("should handle deeply nested paths", () => {
425
+ const result = BehaviorTree.parsePathWithTreeId("#Root/0/1/2/3/4");
426
+
427
+ expect(result.treeId).toBe("Root");
428
+ expect(result.nodePath).toBe("/0/1/2/3/4");
429
+ });
430
+ });
431
+ });