@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.
- package/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { ActionNode } from "../base-node.js";
|
|
3
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
4
|
+
import { ConfigurationError } from "../errors.js";
|
|
5
|
+
import { MockAction } from "../test-nodes.js";
|
|
6
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
7
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
8
|
+
import { Fallback, Selector } from "./selector.js";
|
|
9
|
+
|
|
10
|
+
describe("Selector", () => {
|
|
11
|
+
let context: TemporalContext;
|
|
12
|
+
let selector: Selector;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
context = {
|
|
16
|
+
blackboard: new ScopedBlackboard(),
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
deltaTime: 0,
|
|
19
|
+
};
|
|
20
|
+
selector = new Selector({ id: "test-selector" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return FAILURE when empty", async () => {
|
|
24
|
+
const status = await selector.tick(context);
|
|
25
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return SUCCESS on first successful child", async () => {
|
|
29
|
+
const executionOrder: string[] = [];
|
|
30
|
+
|
|
31
|
+
const child1 = new MockAction({
|
|
32
|
+
id: "child1",
|
|
33
|
+
returnStatus: NodeStatus.FAILURE,
|
|
34
|
+
});
|
|
35
|
+
const child2 = new MockAction({
|
|
36
|
+
id: "child2",
|
|
37
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
38
|
+
});
|
|
39
|
+
const child3 = new MockAction({
|
|
40
|
+
id: "child3",
|
|
41
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Track execution order
|
|
45
|
+
const originalTick1 = child1.tick.bind(child1);
|
|
46
|
+
const originalTick2 = child2.tick.bind(child2);
|
|
47
|
+
const originalTick3 = child3.tick.bind(child3);
|
|
48
|
+
|
|
49
|
+
child1.tick = (ctx) => {
|
|
50
|
+
executionOrder.push("child1");
|
|
51
|
+
return originalTick1(ctx);
|
|
52
|
+
};
|
|
53
|
+
child2.tick = (ctx) => {
|
|
54
|
+
executionOrder.push("child2");
|
|
55
|
+
return originalTick2(ctx);
|
|
56
|
+
};
|
|
57
|
+
child3.tick = (ctx) => {
|
|
58
|
+
executionOrder.push("child3");
|
|
59
|
+
return originalTick3(ctx);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
selector.addChildren([child1, child2, child3]);
|
|
63
|
+
|
|
64
|
+
const status = await selector.tick(context);
|
|
65
|
+
|
|
66
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
67
|
+
expect(executionOrder).toEqual(["child1", "child2"]); // child3 should not execute
|
|
68
|
+
expect(selector.status()).toBe(NodeStatus.SUCCESS);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return FAILURE when all children fail", async () => {
|
|
72
|
+
const child1 = new MockAction({
|
|
73
|
+
id: "child1",
|
|
74
|
+
returnStatus: NodeStatus.FAILURE,
|
|
75
|
+
});
|
|
76
|
+
const child2 = new MockAction({
|
|
77
|
+
id: "child2",
|
|
78
|
+
returnStatus: NodeStatus.FAILURE,
|
|
79
|
+
});
|
|
80
|
+
const child3 = new MockAction({
|
|
81
|
+
id: "child3",
|
|
82
|
+
returnStatus: NodeStatus.FAILURE,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
selector.addChildren([child1, child2, child3]);
|
|
86
|
+
|
|
87
|
+
const status = await selector.tick(context);
|
|
88
|
+
|
|
89
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
90
|
+
expect(selector.status()).toBe(NodeStatus.FAILURE);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle RUNNING status correctly", async () => {
|
|
94
|
+
const child1 = new MockAction({
|
|
95
|
+
id: "child1",
|
|
96
|
+
returnStatus: NodeStatus.FAILURE,
|
|
97
|
+
});
|
|
98
|
+
let child2 = new MockAction({
|
|
99
|
+
id: "child2",
|
|
100
|
+
returnStatus: NodeStatus.RUNNING,
|
|
101
|
+
ticksBeforeComplete: 2,
|
|
102
|
+
});
|
|
103
|
+
const child3 = new MockAction({
|
|
104
|
+
id: "child3",
|
|
105
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
selector.addChildren([child1, child2, child3]);
|
|
109
|
+
|
|
110
|
+
// First tick - child2 returns RUNNING
|
|
111
|
+
let status = await selector.tick(context);
|
|
112
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
113
|
+
expect(child1.status()).toBe(NodeStatus.FAILURE);
|
|
114
|
+
expect(child2.status()).toBe(NodeStatus.RUNNING);
|
|
115
|
+
|
|
116
|
+
// Second tick - child2 still RUNNING
|
|
117
|
+
status = await selector.tick(context);
|
|
118
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
119
|
+
|
|
120
|
+
// Replace child2 to simulate completion
|
|
121
|
+
child2 = new MockAction({
|
|
122
|
+
id: "child2",
|
|
123
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
124
|
+
});
|
|
125
|
+
selector._children[1] = child2;
|
|
126
|
+
|
|
127
|
+
status = await selector.tick(context);
|
|
128
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
129
|
+
expect(child3.status()).toBe(NodeStatus.IDLE); // Should not have been executed
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should continue after RUNNING child fails", async () => {
|
|
133
|
+
let tickCount = 0;
|
|
134
|
+
const child1 = new MockAction({
|
|
135
|
+
id: "child1",
|
|
136
|
+
returnStatus: NodeStatus.FAILURE,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Custom child that returns RUNNING first, then FAILURE
|
|
140
|
+
const child2 = new MockAction({ id: "child2" });
|
|
141
|
+
child2.tick = async (_ctx) => {
|
|
142
|
+
tickCount++;
|
|
143
|
+
if (tickCount === 1) {
|
|
144
|
+
(child2 as unknown)._status = NodeStatus.RUNNING;
|
|
145
|
+
return NodeStatus.RUNNING;
|
|
146
|
+
}
|
|
147
|
+
(child2 as unknown)._status = NodeStatus.FAILURE;
|
|
148
|
+
return NodeStatus.FAILURE;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const child3 = new MockAction({
|
|
152
|
+
id: "child3",
|
|
153
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
selector.addChildren([child1, child2, child3]);
|
|
157
|
+
|
|
158
|
+
// First tick - child2 returns RUNNING
|
|
159
|
+
let status = await selector.tick(context);
|
|
160
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
161
|
+
|
|
162
|
+
// Second tick - child2 fails, moves to child3
|
|
163
|
+
status = await selector.tick(context);
|
|
164
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
165
|
+
expect(child3.status()).toBe(NodeStatus.SUCCESS);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should reset child index on completion", async () => {
|
|
169
|
+
const child1 = new MockAction({
|
|
170
|
+
id: "child1",
|
|
171
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
172
|
+
});
|
|
173
|
+
const child2 = new MockAction({
|
|
174
|
+
id: "child2",
|
|
175
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
selector.addChildren([child1, child2]);
|
|
179
|
+
|
|
180
|
+
// First execution
|
|
181
|
+
await selector.tick(context);
|
|
182
|
+
expect((selector as unknown).currentChildIndex).toBe(0);
|
|
183
|
+
|
|
184
|
+
// Reset children status for second execution
|
|
185
|
+
child1.reset();
|
|
186
|
+
|
|
187
|
+
// Second execution should start from beginning
|
|
188
|
+
await selector.tick(context);
|
|
189
|
+
expect(child1.status()).toBe(NodeStatus.SUCCESS);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should halt running children when halted", async () => {
|
|
193
|
+
const child1 = new MockAction({
|
|
194
|
+
id: "child1",
|
|
195
|
+
returnStatus: NodeStatus.FAILURE,
|
|
196
|
+
});
|
|
197
|
+
const child2 = new MockAction({
|
|
198
|
+
id: "child2",
|
|
199
|
+
returnStatus: NodeStatus.RUNNING,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
selector.addChildren([child1, child2]);
|
|
203
|
+
|
|
204
|
+
// Start execution
|
|
205
|
+
await selector.tick(context);
|
|
206
|
+
expect(selector.status()).toBe(NodeStatus.RUNNING);
|
|
207
|
+
|
|
208
|
+
// Halt the selector
|
|
209
|
+
selector.halt();
|
|
210
|
+
|
|
211
|
+
expect(selector.status()).toBe(NodeStatus.IDLE);
|
|
212
|
+
expect((selector as unknown).currentChildIndex).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should throw error if child is undefined", () => {
|
|
216
|
+
expect(() => selector.addChild(undefined as unknown)).toThrow(
|
|
217
|
+
"Cannot add undefined child to composite node",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("Signal-based cancellation", () => {
|
|
222
|
+
it("should stop executing children when signal is aborted", async () => {
|
|
223
|
+
const executionOrder: string[] = [];
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
|
|
226
|
+
context.signal = controller.signal;
|
|
227
|
+
|
|
228
|
+
const child1 = new MockAction({
|
|
229
|
+
id: "child1",
|
|
230
|
+
returnStatus: NodeStatus.FAILURE,
|
|
231
|
+
});
|
|
232
|
+
const child2 = new MockAction({
|
|
233
|
+
id: "child2",
|
|
234
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
235
|
+
});
|
|
236
|
+
const child3 = new MockAction({
|
|
237
|
+
id: "child3",
|
|
238
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const originalTick1 = child1.tick.bind(child1);
|
|
242
|
+
const originalTick2 = child2.tick.bind(child2);
|
|
243
|
+
const originalTick3 = child3.tick.bind(child3);
|
|
244
|
+
|
|
245
|
+
child1.tick = (ctx: TemporalContext) => {
|
|
246
|
+
executionOrder.push("child1");
|
|
247
|
+
return originalTick1(ctx);
|
|
248
|
+
};
|
|
249
|
+
child2.tick = (ctx: TemporalContext) => {
|
|
250
|
+
executionOrder.push("child2");
|
|
251
|
+
return originalTick2(ctx);
|
|
252
|
+
};
|
|
253
|
+
child3.tick = (ctx: TemporalContext) => {
|
|
254
|
+
executionOrder.push("child3");
|
|
255
|
+
return originalTick3(ctx);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
selector.addChildren([child1, child2, child3]);
|
|
259
|
+
|
|
260
|
+
// Abort signal before ticking
|
|
261
|
+
controller.abort();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await selector.tick(context);
|
|
265
|
+
expect.fail("Should have thrown an error");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
expect(error).toBeInstanceOf(Error);
|
|
268
|
+
expect((error as Error).name).toBe("OperationCancelledError");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
expect(executionOrder.length).toBe(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should respect abort signal in child iteration loop", async () => {
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
const childrenExecuted: string[] = [];
|
|
277
|
+
|
|
278
|
+
context.signal = controller.signal;
|
|
279
|
+
|
|
280
|
+
const children = Array.from({ length: 5 }, (_, i) => {
|
|
281
|
+
const child = new MockAction({
|
|
282
|
+
id: `child${i}`,
|
|
283
|
+
returnStatus: NodeStatus.FAILURE,
|
|
284
|
+
});
|
|
285
|
+
child.tick = async (ctx: TemporalContext) => {
|
|
286
|
+
await checkSignal(ctx.signal);
|
|
287
|
+
childrenExecuted.push(`child${i}`);
|
|
288
|
+
return NodeStatus.FAILURE;
|
|
289
|
+
};
|
|
290
|
+
return child;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
selector.addChildren(children);
|
|
294
|
+
|
|
295
|
+
// Abort after 2 children
|
|
296
|
+
let execCount = 0;
|
|
297
|
+
children.forEach((child) => {
|
|
298
|
+
const orig = child.tick;
|
|
299
|
+
child.tick = async (ctx: TemporalContext) => {
|
|
300
|
+
execCount++;
|
|
301
|
+
if (execCount === 2) {
|
|
302
|
+
controller.abort();
|
|
303
|
+
}
|
|
304
|
+
return await orig(ctx);
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
await selector.tick(context);
|
|
310
|
+
expect.fail("Should have thrown an error");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
expect(error).toBeInstanceOf(Error);
|
|
313
|
+
expect((error as Error).name).toBe("OperationCancelledError");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
expect(childrenExecuted.length).toBeLessThanOrEqual(2);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("ConfigurationError handling", () => {
|
|
321
|
+
it(
|
|
322
|
+
"should NOT catch ConfigurationError from child - it propagates up",
|
|
323
|
+
async () => {
|
|
324
|
+
class MisconfiguredNode extends ActionNode {
|
|
325
|
+
executeTick(_context: TemporalContext) {
|
|
326
|
+
throw new ConfigurationError("Element not found in blackboard");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const misconfiguredChild = new MisconfiguredNode({ id: "broken" });
|
|
331
|
+
const validChild = new MockAction({
|
|
332
|
+
id: "valid",
|
|
333
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
selector.addChildren([misconfiguredChild, validChild]);
|
|
337
|
+
|
|
338
|
+
// ConfigurationError should propagate as error
|
|
339
|
+
// Selector should NOT try the next child
|
|
340
|
+
try {
|
|
341
|
+
await selector.tick(context);
|
|
342
|
+
expect.fail("Should have thrown ConfigurationError");
|
|
343
|
+
} catch (error) {
|
|
344
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
345
|
+
expect((error as ConfigurationError).message).toContain(
|
|
346
|
+
"Element not found in blackboard",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Verify second child was NOT executed
|
|
351
|
+
expect(validChild.status()).toBe(NodeStatus.IDLE);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("Fallback", () => {
|
|
358
|
+
it("should be an alias for Selector", () => {
|
|
359
|
+
const fallback = new Fallback({ id: "test-fallback" });
|
|
360
|
+
expect(fallback).toBeInstanceOf(Selector);
|
|
361
|
+
expect(fallback.type).toBe("Fallback");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should behave like Selector", async () => {
|
|
365
|
+
const context: TemporalContext = {
|
|
366
|
+
blackboard: new ScopedBlackboard(),
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
deltaTime: 0,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const fallback = new Fallback({ id: "test-fallback" });
|
|
372
|
+
|
|
373
|
+
const child1 = new MockAction({
|
|
374
|
+
id: "child1",
|
|
375
|
+
returnStatus: NodeStatus.FAILURE,
|
|
376
|
+
});
|
|
377
|
+
const child2 = new MockAction({
|
|
378
|
+
id: "child2",
|
|
379
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
fallback.addChildren([child1, child2]);
|
|
383
|
+
|
|
384
|
+
const status = await fallback.tick(context);
|
|
385
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector (Fallback) composite node
|
|
3
|
+
* Executes children in order until one succeeds or all fail
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CompositeNode } from "../base-node.js";
|
|
7
|
+
import {
|
|
8
|
+
type TemporalContext,
|
|
9
|
+
type NodeConfiguration,
|
|
10
|
+
NodeStatus,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
13
|
+
|
|
14
|
+
export class Selector extends CompositeNode {
|
|
15
|
+
private currentChildIndex: number = 0;
|
|
16
|
+
|
|
17
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
18
|
+
this.log("Ticking with", this._children.length, "children");
|
|
19
|
+
|
|
20
|
+
if (this._children.length === 0) {
|
|
21
|
+
return NodeStatus.FAILURE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Continue from where we left off if RUNNING
|
|
25
|
+
while (this.currentChildIndex < this._children.length) {
|
|
26
|
+
// Check for cancellation before ticking each child
|
|
27
|
+
checkSignal(context.signal);
|
|
28
|
+
|
|
29
|
+
const child = this._children[this.currentChildIndex];
|
|
30
|
+
if (!child) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Child at index ${this.currentChildIndex} is undefined`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.log(`Ticking child ${this.currentChildIndex}: ${child.name}`);
|
|
37
|
+
const childStatus = await child.tick(context);
|
|
38
|
+
|
|
39
|
+
switch (childStatus) {
|
|
40
|
+
case NodeStatus.SUCCESS:
|
|
41
|
+
this.log(`Child ${child.name} succeeded - selector succeeds`);
|
|
42
|
+
this._status = NodeStatus.SUCCESS;
|
|
43
|
+
this.currentChildIndex = 0;
|
|
44
|
+
return NodeStatus.SUCCESS;
|
|
45
|
+
|
|
46
|
+
case NodeStatus.FAILURE:
|
|
47
|
+
this.log(`Child ${child.name} failed`);
|
|
48
|
+
this.currentChildIndex++;
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case NodeStatus.RUNNING:
|
|
52
|
+
this.log(`Child ${child.name} is running`);
|
|
53
|
+
this._status = NodeStatus.RUNNING;
|
|
54
|
+
return NodeStatus.RUNNING;
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unexpected status from child: ${childStatus}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// All children failed
|
|
62
|
+
this.log("All children failed");
|
|
63
|
+
this._status = NodeStatus.FAILURE;
|
|
64
|
+
this.currentChildIndex = 0;
|
|
65
|
+
return NodeStatus.FAILURE;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected onHalt(): void {
|
|
69
|
+
this.haltChildren(this.currentChildIndex);
|
|
70
|
+
this.currentChildIndex = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected onReset(): void {
|
|
74
|
+
this.currentChildIndex = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fallback is an alias for Selector (BehaviorTree.CPP compatibility)
|
|
80
|
+
*/
|
|
81
|
+
export class Fallback extends Selector {
|
|
82
|
+
constructor(config: NodeConfiguration) {
|
|
83
|
+
super({ ...config, type: "Fallback" });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequence composite configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for Sequence composite configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const sequenceConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated Sequence configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedSequenceConfiguration = z.infer<
|
|
18
|
+
typeof sequenceConfigurationSchema
|
|
19
|
+
>;
|