@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,274 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
3
|
+
import { ConfigurationError } from "../errors.js";
|
|
4
|
+
import { MockAction, WaitAction } from "../test-nodes.js";
|
|
5
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
6
|
+
import { Timeout } from "./timeout.js";
|
|
7
|
+
|
|
8
|
+
describe("Timeout", () => {
|
|
9
|
+
let context: TemporalContext;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
context = {
|
|
13
|
+
blackboard: new ScopedBlackboard(),
|
|
14
|
+
timestamp: Date.now(),
|
|
15
|
+
deltaTime: 0,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should throw error if timeoutMs is not positive", () => {
|
|
20
|
+
expect(() => new Timeout({ id: "test", timeoutMs: 0 })).toThrow(
|
|
21
|
+
"test: Timeout must be positive (got 0)",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(() => new Timeout({ id: "test", timeoutMs: -100 })).toThrow(
|
|
25
|
+
"test: Timeout must be positive (got -100)",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should propagate ConfigurationError if no child is set", async () => {
|
|
30
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
31
|
+
try {
|
|
32
|
+
await timeout.tick(context);
|
|
33
|
+
throw new Error("Expected tick to throw");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
36
|
+
expect((error as ConfigurationError).message).toContain(
|
|
37
|
+
"Decorator must have a child",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should pass through immediate SUCCESS", async () => {
|
|
43
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
44
|
+
const child = new MockAction({
|
|
45
|
+
id: "child",
|
|
46
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
timeout.setChild(child);
|
|
50
|
+
|
|
51
|
+
const status = await timeout.tick(context);
|
|
52
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
53
|
+
expect(timeout.status()).toBe(NodeStatus.SUCCESS);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should pass through immediate FAILURE", async () => {
|
|
57
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
58
|
+
const child = new MockAction({
|
|
59
|
+
id: "child",
|
|
60
|
+
returnStatus: NodeStatus.FAILURE,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
timeout.setChild(child);
|
|
64
|
+
|
|
65
|
+
const status = await timeout.tick(context);
|
|
66
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
67
|
+
expect(timeout.status()).toBe(NodeStatus.FAILURE);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should fail if child takes too long", async () => {
|
|
71
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 50 });
|
|
72
|
+
const child = new WaitAction({
|
|
73
|
+
id: "child",
|
|
74
|
+
waitMs: 200, // Will take longer than timeout
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
timeout.setChild(child);
|
|
78
|
+
|
|
79
|
+
// First tick - child starts running
|
|
80
|
+
let status = await timeout.tick(context);
|
|
81
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
82
|
+
|
|
83
|
+
// Wait for timeout to elapse
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
85
|
+
|
|
86
|
+
// Next tick should fail due to timeout
|
|
87
|
+
status = await timeout.tick(context);
|
|
88
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
89
|
+
expect(child.status()).toBe(NodeStatus.IDLE); // Child should be halted
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should succeed if child completes before timeout", async () => {
|
|
93
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 100 });
|
|
94
|
+
const child = new WaitAction({
|
|
95
|
+
id: "child",
|
|
96
|
+
waitMs: 30, // Will complete before timeout
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
timeout.setChild(child);
|
|
100
|
+
|
|
101
|
+
// First tick - child starts running
|
|
102
|
+
let status = await timeout.tick(context);
|
|
103
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
104
|
+
|
|
105
|
+
// Wait for child to complete
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
107
|
+
|
|
108
|
+
// Next tick should succeed
|
|
109
|
+
status = await timeout.tick(context);
|
|
110
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
111
|
+
expect(timeout.status()).toBe(NodeStatus.SUCCESS);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should halt child when timeout is reached", async () => {
|
|
115
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 50 });
|
|
116
|
+
const child = new WaitAction({
|
|
117
|
+
id: "child",
|
|
118
|
+
waitMs: 200,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const haltSpy = vi.spyOn(child, "halt");
|
|
122
|
+
|
|
123
|
+
timeout.setChild(child);
|
|
124
|
+
|
|
125
|
+
// Start execution
|
|
126
|
+
await timeout.tick(context);
|
|
127
|
+
|
|
128
|
+
// Wait for timeout
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
130
|
+
|
|
131
|
+
// Next tick should halt the child
|
|
132
|
+
await timeout.tick(context);
|
|
133
|
+
|
|
134
|
+
expect(haltSpy).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should cleanup timeout on completion", async () => {
|
|
138
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
139
|
+
const child = new MockAction({
|
|
140
|
+
id: "child",
|
|
141
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
timeout.setChild(child);
|
|
145
|
+
|
|
146
|
+
await timeout.tick(context);
|
|
147
|
+
|
|
148
|
+
// Check internal state is cleaned up
|
|
149
|
+
expect((timeout as unknown).startTime).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should handle halt correctly", async () => {
|
|
153
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
154
|
+
const child = new WaitAction({
|
|
155
|
+
id: "child",
|
|
156
|
+
waitMs: 100,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
timeout.setChild(child);
|
|
160
|
+
|
|
161
|
+
// Start execution but don't await
|
|
162
|
+
const tickPromise = timeout.tick(context);
|
|
163
|
+
|
|
164
|
+
// Give it a moment to start
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
166
|
+
|
|
167
|
+
// Halt while running
|
|
168
|
+
timeout.halt();
|
|
169
|
+
|
|
170
|
+
// Status should be IDLE immediately after halt
|
|
171
|
+
expect(timeout.status()).toBe(NodeStatus.IDLE);
|
|
172
|
+
expect((timeout as unknown).startTime).toBeNull();
|
|
173
|
+
|
|
174
|
+
// Wait for tick to complete or fail (may complete with error)
|
|
175
|
+
try {
|
|
176
|
+
await tickPromise;
|
|
177
|
+
} catch (_e) {
|
|
178
|
+
// Expected if halt interrupts execution
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should handle reset correctly", async () => {
|
|
183
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 1000 });
|
|
184
|
+
const child = new MockAction({
|
|
185
|
+
id: "child",
|
|
186
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
timeout.setChild(child);
|
|
190
|
+
|
|
191
|
+
// Execute
|
|
192
|
+
await timeout.tick(context);
|
|
193
|
+
|
|
194
|
+
// Reset
|
|
195
|
+
timeout.reset();
|
|
196
|
+
|
|
197
|
+
expect(timeout.status()).toBe(NodeStatus.IDLE);
|
|
198
|
+
expect((timeout as unknown).startTime).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should handle multiple ticks while waiting", async () => {
|
|
202
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 100 });
|
|
203
|
+
const child = new WaitAction({
|
|
204
|
+
id: "child",
|
|
205
|
+
waitMs: 50,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
timeout.setChild(child);
|
|
209
|
+
|
|
210
|
+
// Multiple ticks while child is running
|
|
211
|
+
let status = await timeout.tick(context);
|
|
212
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
213
|
+
|
|
214
|
+
status = await timeout.tick(context);
|
|
215
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
216
|
+
|
|
217
|
+
// Wait for child to complete
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
219
|
+
|
|
220
|
+
status = await timeout.tick(context);
|
|
221
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should fail immediately if already timed out", async () => {
|
|
225
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 50 });
|
|
226
|
+
const child = new WaitAction({
|
|
227
|
+
id: "child",
|
|
228
|
+
waitMs: 200,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
timeout.setChild(child);
|
|
232
|
+
|
|
233
|
+
// Start and wait for timeout
|
|
234
|
+
await timeout.tick(context);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
236
|
+
|
|
237
|
+
// Second tick after timeout elapsed - should fail immediately
|
|
238
|
+
const status = await timeout.tick(context);
|
|
239
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should handle timeout during child execution", async () => {
|
|
243
|
+
const timeout = new Timeout({ id: "test-timeout", timeoutMs: 20 });
|
|
244
|
+
|
|
245
|
+
// Child that will definitely take longer than timeout
|
|
246
|
+
const child = new WaitAction({
|
|
247
|
+
id: "child",
|
|
248
|
+
waitMs: 100, // Much longer than timeout
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
timeout.setChild(child);
|
|
252
|
+
|
|
253
|
+
// First tick: should start timeout and return RUNNING
|
|
254
|
+
const startTime = Date.now();
|
|
255
|
+
let status = await timeout.tick(context);
|
|
256
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
257
|
+
expect(timeout.status()).toBe(NodeStatus.RUNNING);
|
|
258
|
+
|
|
259
|
+
// Wait for timeout to occur
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
261
|
+
|
|
262
|
+
// Second tick: should detect timeout and return FAILURE
|
|
263
|
+
status = await timeout.tick(context);
|
|
264
|
+
const elapsed = Date.now() - startTime;
|
|
265
|
+
|
|
266
|
+
// Should timeout after ~20ms, not wait for full 100ms
|
|
267
|
+
expect(elapsed).toBeLessThan(50); // Give some buffer for test environment
|
|
268
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
269
|
+
expect(timeout.status()).toBe(NodeStatus.FAILURE);
|
|
270
|
+
|
|
271
|
+
// Child should have been halted
|
|
272
|
+
expect(child.status()).toBe(NodeStatus.IDLE);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout decorator node
|
|
3
|
+
* Fails if the child doesn't complete within a specified time
|
|
4
|
+
*
|
|
5
|
+
* In Temporal workflows: Uses CancellationScope for deterministic timeouts
|
|
6
|
+
* In standalone mode: Uses Date.now() polling for multi-tick behavior
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CancellationScope, isCancellation } from "@temporalio/workflow";
|
|
10
|
+
import { DecoratorNode } from "../base-node.js";
|
|
11
|
+
import { ConfigurationError } from "../errors.js";
|
|
12
|
+
import {
|
|
13
|
+
type TemporalContext,
|
|
14
|
+
type NodeConfiguration,
|
|
15
|
+
NodeStatus,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
18
|
+
|
|
19
|
+
export interface TimeoutConfiguration extends NodeConfiguration {
|
|
20
|
+
/**
|
|
21
|
+
* Timeout duration in milliseconds
|
|
22
|
+
*/
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Timeout extends DecoratorNode {
|
|
27
|
+
private timeoutMs: number;
|
|
28
|
+
private startTime: number | null = null;
|
|
29
|
+
private useTemporalAPI: boolean | null = null; // Cached detection result
|
|
30
|
+
|
|
31
|
+
constructor(config: TimeoutConfiguration) {
|
|
32
|
+
super(config);
|
|
33
|
+
this.timeoutMs = config.timeoutMs;
|
|
34
|
+
|
|
35
|
+
if (this.timeoutMs <= 0) {
|
|
36
|
+
throw new ConfigurationError(
|
|
37
|
+
`${this.name}: Timeout must be positive (got ${this.timeoutMs})`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
43
|
+
checkSignal(context.signal);
|
|
44
|
+
|
|
45
|
+
if (!this.child) {
|
|
46
|
+
throw new ConfigurationError(`${this.name}: Decorator must have a child`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Try Temporal API on first execution only
|
|
50
|
+
if (this.useTemporalAPI === null) {
|
|
51
|
+
try {
|
|
52
|
+
this.log(`Starting timeout for ${this.timeoutMs}ms`);
|
|
53
|
+
const childStatus = await CancellationScope.withTimeout(
|
|
54
|
+
this.timeoutMs,
|
|
55
|
+
async () => {
|
|
56
|
+
return await this.child!.tick(context);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Success - we're in a Temporal workflow
|
|
61
|
+
this.useTemporalAPI = true;
|
|
62
|
+
this._status = childStatus;
|
|
63
|
+
this.log(`Child completed with ${childStatus}`);
|
|
64
|
+
return childStatus;
|
|
65
|
+
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Handle Temporal timeout cancellation
|
|
68
|
+
if (isCancellation(err)) {
|
|
69
|
+
this.useTemporalAPI = true;
|
|
70
|
+
this.log(`Timeout after ${this.timeoutMs}ms`);
|
|
71
|
+
|
|
72
|
+
if (this.child.status() === NodeStatus.RUNNING) {
|
|
73
|
+
this.child.halt();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this._status = NodeStatus.FAILURE;
|
|
77
|
+
return NodeStatus.FAILURE;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Not in Temporal workflow - use standalone polling
|
|
81
|
+
this.useTemporalAPI = false;
|
|
82
|
+
// Fall through to polling implementation below
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Use Temporal API (we know we're in a workflow)
|
|
87
|
+
if (this.useTemporalAPI === true) {
|
|
88
|
+
try {
|
|
89
|
+
this.log(`Starting timeout for ${this.timeoutMs}ms`);
|
|
90
|
+
const childStatus = await CancellationScope.withTimeout(
|
|
91
|
+
this.timeoutMs,
|
|
92
|
+
async () => {
|
|
93
|
+
return await this.child!.tick(context);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this._status = childStatus;
|
|
98
|
+
this.log(`Child completed with ${childStatus}`);
|
|
99
|
+
return childStatus;
|
|
100
|
+
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (isCancellation(err)) {
|
|
103
|
+
this.log(`Timeout after ${this.timeoutMs}ms`);
|
|
104
|
+
|
|
105
|
+
if (this.child.status() === NodeStatus.RUNNING) {
|
|
106
|
+
this.child.halt();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this._status = NodeStatus.FAILURE;
|
|
110
|
+
return NodeStatus.FAILURE;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Standalone polling implementation (multi-tick)
|
|
117
|
+
if (this.startTime === null) {
|
|
118
|
+
this.startTime = Date.now();
|
|
119
|
+
this.log(`Starting timeout for ${this.timeoutMs}ms`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const elapsed = Date.now() - this.startTime;
|
|
123
|
+
|
|
124
|
+
if (elapsed >= this.timeoutMs) {
|
|
125
|
+
this.log(`Timeout after ${this.timeoutMs}ms`);
|
|
126
|
+
this.startTime = null;
|
|
127
|
+
|
|
128
|
+
if (this.child.status() === NodeStatus.RUNNING) {
|
|
129
|
+
this.child.halt();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this._status = NodeStatus.FAILURE;
|
|
133
|
+
return NodeStatus.FAILURE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Execute child
|
|
137
|
+
const childStatus = await this.child.tick(context);
|
|
138
|
+
|
|
139
|
+
if (childStatus !== NodeStatus.RUNNING) {
|
|
140
|
+
// Child completed - cleanup
|
|
141
|
+
this.startTime = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this._status = childStatus;
|
|
145
|
+
return childStatus;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected onHalt(): void {
|
|
149
|
+
this.startTime = null;
|
|
150
|
+
if (this.child && this.child.status() === NodeStatus.RUNNING) {
|
|
151
|
+
this.child.halt();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
protected onReset(): void {
|
|
156
|
+
this.startTime = null;
|
|
157
|
+
this.useTemporalAPI = null; // Re-detect on next execution
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ConfigurationError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
describe("ConfigurationError", () => {
|
|
5
|
+
it("should create error with message", () => {
|
|
6
|
+
const error = new ConfigurationError("Element not found");
|
|
7
|
+
|
|
8
|
+
expect(error).toBeInstanceOf(Error);
|
|
9
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
10
|
+
expect(error.message).toBe("Element not found");
|
|
11
|
+
expect(error.name).toBe("ConfigurationError");
|
|
12
|
+
expect(error.isConfigurationError).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should create error with message and hint", () => {
|
|
16
|
+
const error = new ConfigurationError(
|
|
17
|
+
"Element not found",
|
|
18
|
+
"Use LocateElement first",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(error.message).toBe("Element not found");
|
|
22
|
+
expect(error.hint).toBe("Use LocateElement first");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should create error without hint", () => {
|
|
26
|
+
const error = new ConfigurationError("Something went wrong");
|
|
27
|
+
|
|
28
|
+
expect(error.message).toBe("Something went wrong");
|
|
29
|
+
expect(error.hint).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should have proper stack trace", () => {
|
|
33
|
+
const error = new ConfigurationError("Test error");
|
|
34
|
+
|
|
35
|
+
expect(error.stack).toBeDefined();
|
|
36
|
+
expect(error.stack).toContain("ConfigurationError");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should be distinguishable from regular Error", () => {
|
|
40
|
+
const configError = new ConfigurationError("Config error");
|
|
41
|
+
const regularError = new Error("Regular error");
|
|
42
|
+
|
|
43
|
+
expect(configError).toBeInstanceOf(ConfigurationError);
|
|
44
|
+
expect(regularError).not.toBeInstanceOf(ConfigurationError);
|
|
45
|
+
|
|
46
|
+
// ConfigurationError is also an Error
|
|
47
|
+
expect(configError).toBeInstanceOf(Error);
|
|
48
|
+
expect(regularError).toBeInstanceOf(Error);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should preserve error properties when caught and rethrown", () => {
|
|
52
|
+
try {
|
|
53
|
+
throw new ConfigurationError("Original message", "Original hint");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
56
|
+
if (error instanceof ConfigurationError) {
|
|
57
|
+
expect(error.message).toBe("Original message");
|
|
58
|
+
expect(error.hint).toBe("Original hint");
|
|
59
|
+
expect(error.isConfigurationError).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error types for behavior tree execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error for test configuration/authoring issues.
|
|
7
|
+
* These errors are NOT caught by Selector/Sequence - they propagate up
|
|
8
|
+
* to signal that the test case itself is broken.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* - Element reference not found in blackboard
|
|
12
|
+
* - Missing required adapter
|
|
13
|
+
* - Invalid tree structure (decorator without child)
|
|
14
|
+
* - Invalid attribute values (negative timeout)
|
|
15
|
+
*
|
|
16
|
+
* These differ from operational failures (element not visible, timeout exceeded)
|
|
17
|
+
* which should return NodeStatus.FAILURE and can be handled by Selector.
|
|
18
|
+
*/
|
|
19
|
+
export class ConfigurationError extends Error {
|
|
20
|
+
readonly isConfigurationError = true;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
message: string,
|
|
24
|
+
public readonly hint?: string,
|
|
25
|
+
) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ConfigurationError";
|
|
28
|
+
|
|
29
|
+
// Maintains proper stack trace in V8 environments (Chrome, Node.js)
|
|
30
|
+
if (Error.captureStackTrace) {
|
|
31
|
+
Error.captureStackTrace(this, ConfigurationError);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|