@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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Conditional node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { FailureNode, RunningNode, SuccessNode } from "../test-nodes.js";
|
|
8
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
9
|
+
import { Conditional } from "./conditional.js";
|
|
10
|
+
|
|
11
|
+
describe("Conditional", () => {
|
|
12
|
+
let blackboard: ScopedBlackboard;
|
|
13
|
+
let context: TemporalContext;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
blackboard = new ScopedBlackboard("root");
|
|
17
|
+
context = {
|
|
18
|
+
blackboard,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
deltaTime: 0,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("If-Then Logic", () => {
|
|
25
|
+
it("should execute then branch when condition succeeds", async () => {
|
|
26
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
27
|
+
const condition = new SuccessNode({ id: "condition" });
|
|
28
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
29
|
+
|
|
30
|
+
conditional.addChild(condition);
|
|
31
|
+
conditional.addChild(thenBranch);
|
|
32
|
+
|
|
33
|
+
const result = await conditional.tick(context);
|
|
34
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
35
|
+
expect(thenBranch.status()).toBe(NodeStatus.SUCCESS);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return then branch status", async () => {
|
|
39
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
40
|
+
const condition = new SuccessNode({ id: "condition" });
|
|
41
|
+
const thenBranch = new FailureNode({ id: "then" });
|
|
42
|
+
|
|
43
|
+
conditional.addChild(condition);
|
|
44
|
+
conditional.addChild(thenBranch);
|
|
45
|
+
|
|
46
|
+
const result = await conditional.tick(context);
|
|
47
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("If-Else Logic", () => {
|
|
52
|
+
it("should execute else branch when condition fails", async () => {
|
|
53
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
54
|
+
const condition = new FailureNode({ id: "condition" });
|
|
55
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
56
|
+
const elseBranch = new SuccessNode({ id: "else" });
|
|
57
|
+
|
|
58
|
+
conditional.addChild(condition);
|
|
59
|
+
conditional.addChild(thenBranch);
|
|
60
|
+
conditional.addChild(elseBranch);
|
|
61
|
+
|
|
62
|
+
const result = await conditional.tick(context);
|
|
63
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
64
|
+
expect(thenBranch.status()).toBe(NodeStatus.IDLE); // Not executed
|
|
65
|
+
expect(elseBranch.status()).toBe(NodeStatus.SUCCESS);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return else branch status", async () => {
|
|
69
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
70
|
+
const condition = new FailureNode({ id: "condition" });
|
|
71
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
72
|
+
const elseBranch = new FailureNode({ id: "else" });
|
|
73
|
+
|
|
74
|
+
conditional.addChild(condition);
|
|
75
|
+
conditional.addChild(thenBranch);
|
|
76
|
+
conditional.addChild(elseBranch);
|
|
77
|
+
|
|
78
|
+
const result = await conditional.tick(context);
|
|
79
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it(
|
|
83
|
+
"should return FAILURE when condition fails and no else branch",
|
|
84
|
+
async () => {
|
|
85
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
86
|
+
const condition = new FailureNode({ id: "condition" });
|
|
87
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
88
|
+
|
|
89
|
+
conditional.addChild(condition);
|
|
90
|
+
conditional.addChild(thenBranch);
|
|
91
|
+
|
|
92
|
+
const result = await conditional.tick(context);
|
|
93
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
94
|
+
expect(thenBranch.status()).toBe(NodeStatus.IDLE);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("RUNNING State", () => {
|
|
100
|
+
it("should return RUNNING when condition is running", async () => {
|
|
101
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
102
|
+
const condition = new RunningNode({ id: "condition" });
|
|
103
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
104
|
+
|
|
105
|
+
conditional.addChild(condition);
|
|
106
|
+
conditional.addChild(thenBranch);
|
|
107
|
+
|
|
108
|
+
const result = await conditional.tick(context);
|
|
109
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
110
|
+
expect(thenBranch.status()).toBe(NodeStatus.IDLE); // Not executed
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should return RUNNING when then branch is running", async () => {
|
|
114
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
115
|
+
const condition = new SuccessNode({ id: "condition" });
|
|
116
|
+
const thenBranch = new RunningNode({ id: "then" });
|
|
117
|
+
|
|
118
|
+
conditional.addChild(condition);
|
|
119
|
+
conditional.addChild(thenBranch);
|
|
120
|
+
|
|
121
|
+
const result = await conditional.tick(context);
|
|
122
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should return RUNNING when else branch is running", async () => {
|
|
126
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
127
|
+
const condition = new FailureNode({ id: "condition" });
|
|
128
|
+
const thenBranch = new SuccessNode({ id: "then" });
|
|
129
|
+
const elseBranch = new RunningNode({ id: "else" });
|
|
130
|
+
|
|
131
|
+
conditional.addChild(condition);
|
|
132
|
+
conditional.addChild(thenBranch);
|
|
133
|
+
conditional.addChild(elseBranch);
|
|
134
|
+
|
|
135
|
+
const result = await conditional.tick(context);
|
|
136
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Edge Cases", () => {
|
|
141
|
+
it("should enforce maximum 3 children", () => {
|
|
142
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
143
|
+
conditional.addChild(new SuccessNode({ id: "child1" }));
|
|
144
|
+
conditional.addChild(new SuccessNode({ id: "child2" }));
|
|
145
|
+
conditional.addChild(new SuccessNode({ id: "child3" }));
|
|
146
|
+
|
|
147
|
+
expect(() => {
|
|
148
|
+
conditional.addChild(new SuccessNode({ id: "child4" }));
|
|
149
|
+
}).toThrow("Conditional can have maximum 3 children");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should return FAILURE without condition", async () => {
|
|
153
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
154
|
+
|
|
155
|
+
const status = await conditional.tick(context);
|
|
156
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return FAILURE without then branch", async () => {
|
|
160
|
+
const conditional = new Conditional({ id: "cond1" });
|
|
161
|
+
conditional.addChild(new SuccessNode({ id: "condition" }));
|
|
162
|
+
|
|
163
|
+
const status = await conditional.tick(context);
|
|
164
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("Multi-tick branch execution - FIXED BEHAVIOR", () => {
|
|
169
|
+
it(
|
|
170
|
+
"should check condition ONCE and not re-check while branch is RUNNING",
|
|
171
|
+
async () => {
|
|
172
|
+
let conditionTickCount = 0;
|
|
173
|
+
let branchTickCount = 0;
|
|
174
|
+
|
|
175
|
+
// Condition that counts how many times it's checked
|
|
176
|
+
class CountingCondition extends SuccessNode {
|
|
177
|
+
async tick(_context: TemporalContext): Promise<NodeStatus> {
|
|
178
|
+
conditionTickCount++;
|
|
179
|
+
console.log(`[CountingCondition] Tick #${conditionTickCount}`);
|
|
180
|
+
this._status = NodeStatus.SUCCESS;
|
|
181
|
+
return NodeStatus.SUCCESS;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Branch that returns RUNNING for first 2 ticks, then SUCCESS
|
|
186
|
+
class MultiTickBranch extends SuccessNode {
|
|
187
|
+
async tick(context: TemporalContext): Promise<NodeStatus> {
|
|
188
|
+
const superTick = super.tick.bind(this);
|
|
189
|
+
branchTickCount++;
|
|
190
|
+
console.log(`[MultiTickBranch] Tick #${branchTickCount}`);
|
|
191
|
+
|
|
192
|
+
if (branchTickCount < 3) {
|
|
193
|
+
this._status = NodeStatus.RUNNING;
|
|
194
|
+
return NodeStatus.RUNNING;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return await superTick(context); // SUCCESS on tick 3
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const condition = new CountingCondition({ id: "counting-condition" });
|
|
202
|
+
const thenBranch = new MultiTickBranch({ id: "multi-tick-branch" });
|
|
203
|
+
const conditional = new Conditional({
|
|
204
|
+
id: "test-conditional",
|
|
205
|
+
name: "test-conditional",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
conditional.addChild(condition);
|
|
209
|
+
conditional.addChild(thenBranch);
|
|
210
|
+
|
|
211
|
+
// Tick 1: Should check condition, then execute branch (returns RUNNING)
|
|
212
|
+
const status1 = await conditional.tick(context);
|
|
213
|
+
expect(status1).toBe(NodeStatus.RUNNING);
|
|
214
|
+
expect(conditionTickCount).toBe(1); // Condition checked once
|
|
215
|
+
expect(branchTickCount).toBe(1); // Branch executed once
|
|
216
|
+
|
|
217
|
+
// Tick 2: FIXED: Should NOT re-check condition, just execute branch (returns RUNNING)
|
|
218
|
+
const status2 = await conditional.tick(context);
|
|
219
|
+
expect(status2).toBe(NodeStatus.RUNNING);
|
|
220
|
+
expect(conditionTickCount).toBe(1); // Still 1! Not re-checked
|
|
221
|
+
expect(branchTickCount).toBe(2); // Branch executed again
|
|
222
|
+
|
|
223
|
+
// Tick 3: FIXED: Should NOT re-check condition, just execute branch (returns SUCCESS)
|
|
224
|
+
const status3 = await conditional.tick(context);
|
|
225
|
+
expect(status3).toBe(NodeStatus.SUCCESS);
|
|
226
|
+
expect(conditionTickCount).toBe(1); // Still 1! Not re-checked
|
|
227
|
+
expect(branchTickCount).toBe(3); // Branch completes
|
|
228
|
+
|
|
229
|
+
// Summary: Condition was checked only 1 time (on first tick)
|
|
230
|
+
// This confirms the condition is NOT re-evaluated on subsequent ticks
|
|
231
|
+
console.log(
|
|
232
|
+
"\n✅ FIXED: Condition checked only 1 time, not re-checked during branch execution",
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
it(
|
|
238
|
+
"should NOT be affected if condition changes while branch is RUNNING",
|
|
239
|
+
async () => {
|
|
240
|
+
let branchTickCount = 0;
|
|
241
|
+
let conditionShouldSucceed = true;
|
|
242
|
+
|
|
243
|
+
// Condition that succeeds initially, then fails on subsequent ticks
|
|
244
|
+
class ChangeableCondition extends SuccessNode {
|
|
245
|
+
async tick(_context: TemporalContext): Promise<NodeStatus> {
|
|
246
|
+
const result = conditionShouldSucceed
|
|
247
|
+
? NodeStatus.SUCCESS
|
|
248
|
+
: NodeStatus.FAILURE;
|
|
249
|
+
console.log(`[ChangeableCondition] Returning: ${result}`);
|
|
250
|
+
this._status = result;
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Branch that takes 3 ticks to complete
|
|
256
|
+
class SlowBranch extends SuccessNode {
|
|
257
|
+
async tick(context: TemporalContext): Promise<NodeStatus> {
|
|
258
|
+
const superTick = super.tick.bind(this);
|
|
259
|
+
branchTickCount++;
|
|
260
|
+
console.log(`[SlowBranch] Tick #${branchTickCount}`);
|
|
261
|
+
|
|
262
|
+
if (branchTickCount < 3) {
|
|
263
|
+
this._status = NodeStatus.RUNNING;
|
|
264
|
+
return NodeStatus.RUNNING;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return await superTick(context); // SUCCESS on tick 3
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const condition = new ChangeableCondition({
|
|
272
|
+
id: "changeable-condition",
|
|
273
|
+
});
|
|
274
|
+
const thenBranch = new SlowBranch({ id: "slow-branch" });
|
|
275
|
+
const conditional = new Conditional({
|
|
276
|
+
id: "test-conditional",
|
|
277
|
+
name: "test-conditional",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
conditional.addChild(condition);
|
|
281
|
+
conditional.addChild(thenBranch);
|
|
282
|
+
|
|
283
|
+
// Tick 1: Condition passes, branch returns RUNNING
|
|
284
|
+
const status1 = await conditional.tick(context);
|
|
285
|
+
expect(status1).toBe(NodeStatus.RUNNING);
|
|
286
|
+
expect(branchTickCount).toBe(1);
|
|
287
|
+
|
|
288
|
+
// Change condition to fail
|
|
289
|
+
conditionShouldSucceed = false;
|
|
290
|
+
|
|
291
|
+
// Tick 2: FIXED: Condition is NOT re-checked, branch continues
|
|
292
|
+
const status2 = await conditional.tick(context);
|
|
293
|
+
expect(status2).toBe(NodeStatus.RUNNING); // Still RUNNING!
|
|
294
|
+
expect(branchTickCount).toBe(2); // Branch continues executing
|
|
295
|
+
|
|
296
|
+
// Tick 3: FIXED: Branch completes successfully despite condition now failing
|
|
297
|
+
const status3 = await conditional.tick(context);
|
|
298
|
+
expect(status3).toBe(NodeStatus.SUCCESS); // Branch completes!
|
|
299
|
+
expect(branchTickCount).toBe(3); // Branch executed all 3 ticks
|
|
300
|
+
|
|
301
|
+
// This demonstrates the fix: branch execution is NOT interrupted
|
|
302
|
+
// even though the condition would now fail if re-checked
|
|
303
|
+
console.log(
|
|
304
|
+
"\n✅ SAFE: Branch execution continues uninterrupted despite condition change",
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional node - If-then-else logic for behavior trees
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CompositeNode } from "../base-node.js";
|
|
6
|
+
import { ConfigurationError } from "../errors.js";
|
|
7
|
+
import { type TemporalContext, NodeStatus, type TreeNode } from "../types.js";
|
|
8
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Conditional implements if-then-else logic.
|
|
12
|
+
* Structure:
|
|
13
|
+
* - First child = condition
|
|
14
|
+
* - Second child = then branch
|
|
15
|
+
* - Third child (optional) = else branch
|
|
16
|
+
*/
|
|
17
|
+
export class Conditional extends CompositeNode {
|
|
18
|
+
private condition?: TreeNode;
|
|
19
|
+
private thenBranch?: TreeNode;
|
|
20
|
+
private elseBranch?: TreeNode;
|
|
21
|
+
private conditionEvaluated: boolean = false;
|
|
22
|
+
private selectedBranch?: TreeNode;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override addChild to enforce conditional structure
|
|
26
|
+
*/
|
|
27
|
+
addChild(child: TreeNode): void {
|
|
28
|
+
if (!this.condition) {
|
|
29
|
+
this.condition = child;
|
|
30
|
+
this._children.push(child);
|
|
31
|
+
child.parent = this;
|
|
32
|
+
} else if (!this.thenBranch) {
|
|
33
|
+
this.thenBranch = child;
|
|
34
|
+
this._children.push(child);
|
|
35
|
+
child.parent = this;
|
|
36
|
+
} else if (!this.elseBranch) {
|
|
37
|
+
this.elseBranch = child;
|
|
38
|
+
this._children.push(child);
|
|
39
|
+
child.parent = this;
|
|
40
|
+
} else {
|
|
41
|
+
throw new ConfigurationError(
|
|
42
|
+
"Conditional can have maximum 3 children (condition, then, else)",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
48
|
+
// Check for cancellation before processing conditional
|
|
49
|
+
checkSignal(context.signal);
|
|
50
|
+
|
|
51
|
+
if (!this.condition) {
|
|
52
|
+
throw new Error("Conditional requires at least a condition child");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!this.thenBranch) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Conditional requires at least condition and then branch",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Only evaluate condition if not already evaluated
|
|
62
|
+
if (!this.conditionEvaluated) {
|
|
63
|
+
this.log("Evaluating condition");
|
|
64
|
+
const conditionStatus = await this.condition.tick(context);
|
|
65
|
+
|
|
66
|
+
switch (conditionStatus) {
|
|
67
|
+
case NodeStatus.SUCCESS:
|
|
68
|
+
this.log("Condition succeeded - will execute then branch");
|
|
69
|
+
this.selectedBranch = this.thenBranch;
|
|
70
|
+
this.conditionEvaluated = true;
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case NodeStatus.FAILURE:
|
|
74
|
+
if (this.elseBranch) {
|
|
75
|
+
this.log("Condition failed - will execute else branch");
|
|
76
|
+
this.selectedBranch = this.elseBranch;
|
|
77
|
+
this.conditionEvaluated = true;
|
|
78
|
+
} else {
|
|
79
|
+
this.log("Condition failed - no else branch, returning FAILURE");
|
|
80
|
+
this._status = NodeStatus.FAILURE;
|
|
81
|
+
return NodeStatus.FAILURE;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case NodeStatus.RUNNING:
|
|
86
|
+
this.log("Condition is running");
|
|
87
|
+
this._status = NodeStatus.RUNNING;
|
|
88
|
+
return NodeStatus.RUNNING;
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Unexpected status from condition: ${conditionStatus}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
this.log("Condition already evaluated - continuing branch execution");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Execute selected branch
|
|
100
|
+
if (!this.selectedBranch) {
|
|
101
|
+
throw new Error("No branch selected for execution");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const branchStatus = await this.selectedBranch.tick(context);
|
|
105
|
+
this._status = branchStatus;
|
|
106
|
+
|
|
107
|
+
// Reset flag when branch completes
|
|
108
|
+
if (branchStatus !== NodeStatus.RUNNING) {
|
|
109
|
+
this.log("Branch completed - resetting condition check flag");
|
|
110
|
+
this.conditionEvaluated = false;
|
|
111
|
+
this.selectedBranch = undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return branchStatus;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected onHalt(): void {
|
|
118
|
+
this.log("Halting - resetting condition check flag");
|
|
119
|
+
this.conditionEvaluated = false;
|
|
120
|
+
this.selectedBranch = undefined;
|
|
121
|
+
super.onHalt();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected onReset(): void {
|
|
125
|
+
this.log("Resetting - clearing condition check flag");
|
|
126
|
+
this.conditionEvaluated = false;
|
|
127
|
+
this.selectedBranch = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForEach composite configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createNodeSchema, validations } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for ForEach composite configuration
|
|
10
|
+
* Validates collection and item keys
|
|
11
|
+
*/
|
|
12
|
+
export const forEachConfigurationSchema = createNodeSchema("ForEach", {
|
|
13
|
+
collectionKey: validations.blackboardKey,
|
|
14
|
+
itemKey: validations.blackboardKey,
|
|
15
|
+
indexKey: validations.blackboardKey.optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validated ForEach configuration type
|
|
20
|
+
*/
|
|
21
|
+
export type ValidatedForEachConfiguration = z.infer<
|
|
22
|
+
typeof forEachConfigurationSchema
|
|
23
|
+
>;
|