@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,261 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
3
|
+
import { MockAction } from "../test-nodes.js";
|
|
4
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
5
|
+
import { Delay } from "./delay.js";
|
|
6
|
+
|
|
7
|
+
describe("Delay", () => {
|
|
8
|
+
let context: TemporalContext;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
context = {
|
|
12
|
+
blackboard: new ScopedBlackboard(),
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
deltaTime: 0,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should throw error if delayMs is negative", () => {
|
|
19
|
+
expect(() => new Delay({ id: "test", delayMs: -100 })).toThrow(
|
|
20
|
+
"test: Delay must be non-negative (got -100)",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should throw ConfigurationError if no child is set", async () => {
|
|
25
|
+
const delay = new Delay({ id: "test-delay", delayMs: 100 });
|
|
26
|
+
await expect(delay.tick(context)).rejects.toThrow("test-delay: Decorator must have a child");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should execute child immediately when delayMs is 0", async () => {
|
|
30
|
+
const delay = new Delay({ id: "test-delay", delayMs: 0 });
|
|
31
|
+
const child = new MockAction({
|
|
32
|
+
id: "child",
|
|
33
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
delay.setChild(child);
|
|
37
|
+
|
|
38
|
+
const status = await delay.tick(context);
|
|
39
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
40
|
+
expect(child.status()).toBe(NodeStatus.SUCCESS);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should delay child execution", async () => {
|
|
44
|
+
const delayMs = 50;
|
|
45
|
+
const delay = new Delay({ id: "test-delay", delayMs });
|
|
46
|
+
const child = new MockAction({
|
|
47
|
+
id: "child",
|
|
48
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
delay.setChild(child);
|
|
52
|
+
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
|
|
55
|
+
// First tick - should be delaying
|
|
56
|
+
let status = await delay.tick(context);
|
|
57
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
58
|
+
expect(child.status()).toBe(NodeStatus.IDLE); // Child not executed yet
|
|
59
|
+
|
|
60
|
+
// Tick while still delaying
|
|
61
|
+
status = await delay.tick(context);
|
|
62
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
63
|
+
expect(Date.now() - startTime).toBeLessThan(delayMs);
|
|
64
|
+
|
|
65
|
+
// Wait for delay to complete
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs + 10));
|
|
67
|
+
|
|
68
|
+
// Next tick should execute child
|
|
69
|
+
status = await delay.tick(context);
|
|
70
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
71
|
+
expect(child.status()).toBe(NodeStatus.SUCCESS);
|
|
72
|
+
expect(Date.now() - startTime).toBeGreaterThanOrEqual(delayMs);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should pass through child status after delay", async () => {
|
|
76
|
+
const delay = new Delay({ id: "test-delay", delayMs: 30 });
|
|
77
|
+
const child = new MockAction({
|
|
78
|
+
id: "child",
|
|
79
|
+
returnStatus: NodeStatus.FAILURE,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
delay.setChild(child);
|
|
83
|
+
|
|
84
|
+
// Wait through delay
|
|
85
|
+
await delay.tick(context);
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
87
|
+
|
|
88
|
+
const status = await delay.tick(context);
|
|
89
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
90
|
+
expect(delay.status()).toBe(NodeStatus.FAILURE);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle RUNNING child after delay", async () => {
|
|
94
|
+
const delay = new Delay({ id: "test-delay", delayMs: 30 });
|
|
95
|
+
let tickCount = 0;
|
|
96
|
+
const child = new MockAction({ id: "child" });
|
|
97
|
+
child.tick = async (_ctx) => {
|
|
98
|
+
tickCount++;
|
|
99
|
+
if (tickCount < 3) {
|
|
100
|
+
(child as any)._status = NodeStatus.RUNNING;
|
|
101
|
+
return NodeStatus.RUNNING;
|
|
102
|
+
}
|
|
103
|
+
(child as any)._status = NodeStatus.SUCCESS;
|
|
104
|
+
return NodeStatus.SUCCESS;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
delay.setChild(child);
|
|
108
|
+
|
|
109
|
+
// Wait through delay
|
|
110
|
+
await delay.tick(context);
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
112
|
+
|
|
113
|
+
// Child starts executing
|
|
114
|
+
let status = await delay.tick(context);
|
|
115
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
116
|
+
// Child has been ticked (verified by child status check)
|
|
117
|
+
|
|
118
|
+
// Child still running
|
|
119
|
+
status = await delay.tick(context);
|
|
120
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
121
|
+
|
|
122
|
+
// Child completes
|
|
123
|
+
status = await delay.tick(context);
|
|
124
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should reset delay state on halt", async () => {
|
|
128
|
+
const delay = new Delay({ id: "test-delay", delayMs: 100 });
|
|
129
|
+
const child = new MockAction({
|
|
130
|
+
id: "child",
|
|
131
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
delay.setChild(child);
|
|
135
|
+
|
|
136
|
+
// Start delay
|
|
137
|
+
await delay.tick(context);
|
|
138
|
+
expect((delay as any).delayStartTime).not.toBeNull();
|
|
139
|
+
|
|
140
|
+
// Halt
|
|
141
|
+
delay.halt();
|
|
142
|
+
|
|
143
|
+
expect(delay.status()).toBe(NodeStatus.IDLE);
|
|
144
|
+
expect((delay as any).delayStartTime).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should reset delay state on reset", async () => {
|
|
148
|
+
const delay = new Delay({ id: "test-delay", delayMs: 30 });
|
|
149
|
+
const child = new MockAction({
|
|
150
|
+
id: "child",
|
|
151
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
delay.setChild(child);
|
|
155
|
+
|
|
156
|
+
// Complete execution
|
|
157
|
+
await delay.tick(context);
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
159
|
+
await delay.tick(context);
|
|
160
|
+
|
|
161
|
+
// Child has been ticked (verified by child status check)
|
|
162
|
+
|
|
163
|
+
// Reset
|
|
164
|
+
delay.reset();
|
|
165
|
+
|
|
166
|
+
expect(delay.status()).toBe(NodeStatus.IDLE);
|
|
167
|
+
expect((delay as any).delayStartTime).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should track remaining delay time correctly", async () => {
|
|
171
|
+
const delayMs = 100;
|
|
172
|
+
const delay = new Delay({ id: "test-delay", delayMs });
|
|
173
|
+
const child = new MockAction({
|
|
174
|
+
id: "child",
|
|
175
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
delay.setChild(child);
|
|
179
|
+
|
|
180
|
+
// Start delay
|
|
181
|
+
await delay.tick(context);
|
|
182
|
+
|
|
183
|
+
// Check multiple times during delay
|
|
184
|
+
for (let i = 0; i < 3; i++) {
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
186
|
+
const status = await delay.tick(context);
|
|
187
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
188
|
+
|
|
189
|
+
// Verify delay is being tracked (can check logs)
|
|
190
|
+
const delayStartTime = (delay as any).delayStartTime;
|
|
191
|
+
if (!delayStartTime) {
|
|
192
|
+
throw new Error("delayStartTime not found");
|
|
193
|
+
}
|
|
194
|
+
const elapsed = Date.now() - delayStartTime;
|
|
195
|
+
expect(elapsed).toBeLessThan(delayMs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Wait for completion
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
200
|
+
const finalStatus = await delay.tick(context);
|
|
201
|
+
expect(finalStatus).toBe(NodeStatus.SUCCESS);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should only start child execution once", async () => {
|
|
205
|
+
const delay = new Delay({ id: "test-delay", delayMs: 30 });
|
|
206
|
+
let tickCount = 0;
|
|
207
|
+
|
|
208
|
+
const child = new MockAction({ id: "child" });
|
|
209
|
+
const originalTick = child.tick.bind(child);
|
|
210
|
+
child.tick = (ctx) => {
|
|
211
|
+
tickCount++;
|
|
212
|
+
return originalTick(ctx);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
delay.setChild(child);
|
|
216
|
+
|
|
217
|
+
// Wait through delay
|
|
218
|
+
await delay.tick(context);
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
220
|
+
|
|
221
|
+
// First tick after delay
|
|
222
|
+
await delay.tick(context);
|
|
223
|
+
expect(tickCount).toBe(1);
|
|
224
|
+
// Child has been ticked (verified by child status check)
|
|
225
|
+
|
|
226
|
+
// Subsequent ticks start new delay cycles (return RUNNING without ticking child)
|
|
227
|
+
await delay.tick(context);
|
|
228
|
+
await delay.tick(context);
|
|
229
|
+
expect(tickCount).toBe(1); // Child only ticked once (subsequent ticks are delaying)
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should handle multiple executions correctly", async () => {
|
|
233
|
+
const delay = new Delay({ id: "test-delay", delayMs: 30 });
|
|
234
|
+
const child = new MockAction({
|
|
235
|
+
id: "child",
|
|
236
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
delay.setChild(child);
|
|
240
|
+
|
|
241
|
+
// First execution
|
|
242
|
+
await delay.tick(context);
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
244
|
+
let status = await delay.tick(context);
|
|
245
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
246
|
+
|
|
247
|
+
// Reset for second execution
|
|
248
|
+
delay.reset();
|
|
249
|
+
child.reset();
|
|
250
|
+
|
|
251
|
+
// Second execution should also delay
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
status = await delay.tick(context);
|
|
254
|
+
expect(status).toBe(NodeStatus.RUNNING);
|
|
255
|
+
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
257
|
+
status = await delay.tick(context);
|
|
258
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
259
|
+
expect(Date.now() - startTime).toBeGreaterThanOrEqual(30);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delay decorator node
|
|
3
|
+
* Waits for a specified duration before executing the child
|
|
4
|
+
*
|
|
5
|
+
* In Temporal workflows: Uses sleep() for deterministic delays
|
|
6
|
+
* In standalone mode: Uses Date.now() polling for multi-tick behavior
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sleep } 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 DelayConfiguration extends NodeConfiguration {
|
|
20
|
+
/**
|
|
21
|
+
* Delay duration in milliseconds
|
|
22
|
+
*/
|
|
23
|
+
delayMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Delay extends DecoratorNode {
|
|
27
|
+
private delayMs: number;
|
|
28
|
+
private delayStartTime: number | null = null;
|
|
29
|
+
private useTemporalAPI: boolean | null = null; // Cached detection result
|
|
30
|
+
|
|
31
|
+
constructor(config: DelayConfiguration) {
|
|
32
|
+
super(config);
|
|
33
|
+
this.delayMs = config.delayMs;
|
|
34
|
+
|
|
35
|
+
if (this.delayMs < 0) {
|
|
36
|
+
throw new ConfigurationError(
|
|
37
|
+
`${this.name}: Delay must be non-negative (got ${this.delayMs})`,
|
|
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
|
+
// If delay is 0, just execute the child immediately
|
|
50
|
+
if (this.delayMs === 0) {
|
|
51
|
+
return await this.child.tick(context);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Try Temporal API on first execution only
|
|
55
|
+
if (this.useTemporalAPI === null) {
|
|
56
|
+
try {
|
|
57
|
+
this.log(`Starting delay of ${this.delayMs}ms`);
|
|
58
|
+
await sleep(this.delayMs);
|
|
59
|
+
|
|
60
|
+
// Success - we're in a Temporal workflow
|
|
61
|
+
this.useTemporalAPI = true;
|
|
62
|
+
this.log("Delay completed, executing child");
|
|
63
|
+
|
|
64
|
+
checkSignal(context.signal);
|
|
65
|
+
const childStatus = await this.child.tick(context);
|
|
66
|
+
this._status = childStatus;
|
|
67
|
+
return childStatus;
|
|
68
|
+
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Not in Temporal workflow - use standalone polling
|
|
71
|
+
this.useTemporalAPI = false;
|
|
72
|
+
// Fall through to polling implementation below
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Use Temporal API (we know we're in a workflow)
|
|
77
|
+
if (this.useTemporalAPI === true) {
|
|
78
|
+
this.log(`Starting delay of ${this.delayMs}ms`);
|
|
79
|
+
await sleep(this.delayMs);
|
|
80
|
+
this.log("Delay completed, executing child");
|
|
81
|
+
|
|
82
|
+
checkSignal(context.signal);
|
|
83
|
+
const childStatus = await this.child.tick(context);
|
|
84
|
+
this._status = childStatus;
|
|
85
|
+
return childStatus;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Standalone polling implementation (multi-tick)
|
|
89
|
+
// If child is already running from a previous tick, execute it immediately
|
|
90
|
+
if (this.child.status() === NodeStatus.RUNNING) {
|
|
91
|
+
checkSignal(context.signal);
|
|
92
|
+
const childStatus = await this.child.tick(context);
|
|
93
|
+
|
|
94
|
+
if (childStatus !== NodeStatus.RUNNING) {
|
|
95
|
+
// Child completed - reset delay timer for next cycle
|
|
96
|
+
this.delayStartTime = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._status = childStatus;
|
|
100
|
+
return childStatus;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Start delay if not started
|
|
104
|
+
if (this.delayStartTime === null) {
|
|
105
|
+
this.delayStartTime = Date.now();
|
|
106
|
+
this.log(`Starting delay of ${this.delayMs}ms`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const elapsed = Date.now() - this.delayStartTime;
|
|
110
|
+
|
|
111
|
+
if (elapsed < this.delayMs) {
|
|
112
|
+
// Still delaying
|
|
113
|
+
this._status = NodeStatus.RUNNING;
|
|
114
|
+
return NodeStatus.RUNNING;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Delay completed - execute child
|
|
118
|
+
this.log("Delay completed, executing child");
|
|
119
|
+
|
|
120
|
+
checkSignal(context.signal);
|
|
121
|
+
const childStatus = await this.child.tick(context);
|
|
122
|
+
|
|
123
|
+
if (childStatus !== NodeStatus.RUNNING) {
|
|
124
|
+
// Child completed - reset delay timer for next cycle
|
|
125
|
+
this.delayStartTime = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this._status = childStatus;
|
|
129
|
+
return childStatus;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected onHalt(): void {
|
|
133
|
+
this.delayStartTime = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected onReset(): void {
|
|
137
|
+
this.delayStartTime = null;
|
|
138
|
+
this.useTemporalAPI = null; // Re-detect on next execution
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForceSuccess and ForceFailure decorator configuration schemas
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for ForceSuccess decorator configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const forceSuccessConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Schema for ForceFailure decorator configuration
|
|
16
|
+
* Uses base schema only (no additional properties)
|
|
17
|
+
*/
|
|
18
|
+
export const forceFailureConfigurationSchema = nodeConfigurationSchema;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validated ForceSuccess configuration type
|
|
22
|
+
*/
|
|
23
|
+
export type ValidatedForceSuccessConfiguration = z.infer<
|
|
24
|
+
typeof forceSuccessConfigurationSchema
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validated ForceFailure configuration type
|
|
29
|
+
*/
|
|
30
|
+
export type ValidatedForceFailureConfiguration = z.infer<
|
|
31
|
+
typeof forceFailureConfigurationSchema
|
|
32
|
+
>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ForceSuccess and ForceFailure decorators
|
|
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 { ForceFailure, ForceSuccess } from "./force-result.js";
|
|
10
|
+
|
|
11
|
+
describe("ForceSuccess", () => {
|
|
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
|
+
it("should return SUCCESS when child succeeds", async () => {
|
|
25
|
+
const force = new ForceSuccess({ id: "force1" });
|
|
26
|
+
force.setChild(new SuccessNode({ id: "child" }));
|
|
27
|
+
|
|
28
|
+
const result = await force.tick(context);
|
|
29
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return SUCCESS when child fails", async () => {
|
|
33
|
+
const force = new ForceSuccess({ id: "force1" });
|
|
34
|
+
force.setChild(new FailureNode({ id: "child" }));
|
|
35
|
+
|
|
36
|
+
const result = await force.tick(context);
|
|
37
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return RUNNING when child is running", async () => {
|
|
41
|
+
const force = new ForceSuccess({ id: "force1" });
|
|
42
|
+
force.setChild(new RunningNode({ id: "child" }));
|
|
43
|
+
|
|
44
|
+
const result = await force.tick(context);
|
|
45
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
46
|
+
expect(force.status()).toBe(NodeStatus.RUNNING);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should still tick child for side effects", async () => {
|
|
50
|
+
const force = new ForceSuccess({ id: "force1" });
|
|
51
|
+
|
|
52
|
+
let childTicked = false;
|
|
53
|
+
class SideEffectNode extends FailureNode {
|
|
54
|
+
tick(context: TemporalContext) {
|
|
55
|
+
childTicked = true;
|
|
56
|
+
return super.tick(context);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
force.setChild(new SideEffectNode({ id: "child" }));
|
|
61
|
+
|
|
62
|
+
await force.tick(context);
|
|
63
|
+
expect(childTicked).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should throw ConfigurationError if no child", async () => {
|
|
67
|
+
const force = new ForceSuccess({ id: "force1" });
|
|
68
|
+
|
|
69
|
+
await expect(force.tick(context)).rejects.toThrow("ForceSuccess requires a child");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("ForceFailure", () => {
|
|
74
|
+
let blackboard: ScopedBlackboard;
|
|
75
|
+
let context: TemporalContext;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
blackboard = new ScopedBlackboard("root");
|
|
79
|
+
context = {
|
|
80
|
+
blackboard,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
deltaTime: 0,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should return FAILURE when child succeeds", async () => {
|
|
87
|
+
const force = new ForceFailure({ id: "force1" });
|
|
88
|
+
force.setChild(new SuccessNode({ id: "child" }));
|
|
89
|
+
|
|
90
|
+
const result = await force.tick(context);
|
|
91
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return FAILURE when child fails", async () => {
|
|
95
|
+
const force = new ForceFailure({ id: "force1" });
|
|
96
|
+
force.setChild(new FailureNode({ id: "child" }));
|
|
97
|
+
|
|
98
|
+
const result = await force.tick(context);
|
|
99
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should return RUNNING when child is running", async () => {
|
|
103
|
+
const force = new ForceFailure({ id: "force1" });
|
|
104
|
+
force.setChild(new RunningNode({ id: "child" }));
|
|
105
|
+
|
|
106
|
+
const result = await force.tick(context);
|
|
107
|
+
expect(result).toBe(NodeStatus.RUNNING);
|
|
108
|
+
expect(force.status()).toBe(NodeStatus.RUNNING);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should still tick child for side effects", async () => {
|
|
112
|
+
const force = new ForceFailure({ id: "force1" });
|
|
113
|
+
|
|
114
|
+
let childTicked = false;
|
|
115
|
+
class SideEffectNode extends SuccessNode {
|
|
116
|
+
tick(context: TemporalContext) {
|
|
117
|
+
childTicked = true;
|
|
118
|
+
return super.tick(context);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
force.setChild(new SideEffectNode({ id: "child" }));
|
|
123
|
+
|
|
124
|
+
await force.tick(context);
|
|
125
|
+
expect(childTicked).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should throw ConfigurationError if no child", async () => {
|
|
129
|
+
const force = new ForceFailure({ id: "force1" });
|
|
130
|
+
|
|
131
|
+
await expect(force.tick(context)).rejects.toThrow("ForceFailure requires a child");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForceSuccess and ForceFailure decorators
|
|
3
|
+
* Always return specific result regardless of child
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DecoratorNode } from "../base-node.js";
|
|
7
|
+
import { ConfigurationError } from "../errors.js";
|
|
8
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
9
|
+
import { checkSignal } from "../utils/signal-check.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ForceSuccess always returns SUCCESS regardless of child result.
|
|
13
|
+
* Useful for ensuring a branch always succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export class ForceSuccess extends DecoratorNode {
|
|
16
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
17
|
+
checkSignal(context.signal);
|
|
18
|
+
|
|
19
|
+
if (!this.child) {
|
|
20
|
+
throw new ConfigurationError("ForceSuccess requires a child");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Tick child and check status
|
|
24
|
+
const childStatus = await this.child.tick(context);
|
|
25
|
+
|
|
26
|
+
// Propagate RUNNING status - only force result when child completes
|
|
27
|
+
if (childStatus === NodeStatus.RUNNING) {
|
|
28
|
+
this._status = NodeStatus.RUNNING;
|
|
29
|
+
return NodeStatus.RUNNING;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Force SUCCESS regardless of child result (SUCCESS or FAILURE)
|
|
33
|
+
this._status = NodeStatus.SUCCESS;
|
|
34
|
+
return NodeStatus.SUCCESS;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ForceFailure always returns FAILURE regardless of child result.
|
|
40
|
+
* Useful for negation or testing.
|
|
41
|
+
*/
|
|
42
|
+
export class ForceFailure extends DecoratorNode {
|
|
43
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
44
|
+
checkSignal(context.signal);
|
|
45
|
+
|
|
46
|
+
if (!this.child) {
|
|
47
|
+
throw new ConfigurationError("ForceFailure requires a child");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Tick child and check status
|
|
51
|
+
const childStatus = await this.child.tick(context);
|
|
52
|
+
|
|
53
|
+
// Propagate RUNNING status - only force result when child completes
|
|
54
|
+
if (childStatus === NodeStatus.RUNNING) {
|
|
55
|
+
this._status = NodeStatus.RUNNING;
|
|
56
|
+
return NodeStatus.RUNNING;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Force FAILURE regardless of child result (SUCCESS or FAILURE)
|
|
60
|
+
this._status = NodeStatus.FAILURE;
|
|
61
|
+
return NodeStatus.FAILURE;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export all decorator nodes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { Delay } from "./delay.js";
|
|
6
|
+
export { ForceFailure, ForceSuccess } from "./force-result.js";
|
|
7
|
+
export { Invert } from "./invert.js";
|
|
8
|
+
export { KeepRunningUntilFailure } from "./keep-running.js";
|
|
9
|
+
export { Precondition } from "./precondition.js";
|
|
10
|
+
export { Repeat } from "./repeat.js";
|
|
11
|
+
export { RunOnce } from "./run-once.js";
|
|
12
|
+
export { SoftAssert } from "./soft-assert.js";
|
|
13
|
+
export { Timeout } from "./timeout.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invert decorator configuration schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { nodeConfigurationSchema } from "../schemas/base.schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema for Invert decorator configuration
|
|
10
|
+
* Uses base schema only (no additional properties)
|
|
11
|
+
*/
|
|
12
|
+
export const invertConfigurationSchema = nodeConfigurationSchema;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validated Invert configuration type
|
|
16
|
+
*/
|
|
17
|
+
export type ValidatedInvertConfiguration = z.infer<
|
|
18
|
+
typeof invertConfigurationSchema
|
|
19
|
+
>;
|