@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for schema validation with Registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
+
import { Registry } from "../registry.js";
|
|
7
|
+
import { Timeout } from "../decorators/timeout.js";
|
|
8
|
+
import { Delay } from "../decorators/delay.js";
|
|
9
|
+
import { Parallel } from "../composites/parallel.js";
|
|
10
|
+
import { Sequence } from "../composites/sequence.js";
|
|
11
|
+
import { ConfigurationError } from "../errors.js";
|
|
12
|
+
|
|
13
|
+
describe("Schema Validation Integration", () => {
|
|
14
|
+
let registry: Registry;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
registry = new Registry();
|
|
18
|
+
registry.register("Timeout", Timeout, { category: "decorator" });
|
|
19
|
+
registry.register("Delay", Delay, { category: "decorator" });
|
|
20
|
+
registry.register("Parallel", Parallel, { category: "composite" });
|
|
21
|
+
registry.register("Sequence", Sequence, { category: "composite" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Timeout validation", () => {
|
|
25
|
+
it("should accept valid timeoutMs", () => {
|
|
26
|
+
const node = registry.create("Timeout", {
|
|
27
|
+
id: "test",
|
|
28
|
+
timeoutMs: 1000,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(node).toBeInstanceOf(Timeout);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should reject negative timeoutMs", () => {
|
|
35
|
+
expect(() => {
|
|
36
|
+
registry.create("Timeout", {
|
|
37
|
+
id: "test",
|
|
38
|
+
timeoutMs: -100,
|
|
39
|
+
});
|
|
40
|
+
}).toThrow(ConfigurationError);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject zero timeoutMs", () => {
|
|
44
|
+
expect(() => {
|
|
45
|
+
registry.create("Timeout", {
|
|
46
|
+
id: "test",
|
|
47
|
+
timeoutMs: 0,
|
|
48
|
+
});
|
|
49
|
+
}).toThrow(ConfigurationError);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should provide helpful error message", () => {
|
|
53
|
+
try {
|
|
54
|
+
registry.create("Timeout", {
|
|
55
|
+
id: "test-timeout",
|
|
56
|
+
timeoutMs: -100,
|
|
57
|
+
});
|
|
58
|
+
expect.fail("Should have thrown");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
61
|
+
const message = (error as ConfigurationError).message;
|
|
62
|
+
expect(message).toContain("Timeout:test-timeout");
|
|
63
|
+
expect(message).toContain("timeoutMs");
|
|
64
|
+
expect(message).toContain("positive");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Delay validation", () => {
|
|
70
|
+
it("should accept positive delayMs", () => {
|
|
71
|
+
const node = registry.create("Delay", {
|
|
72
|
+
id: "test",
|
|
73
|
+
delayMs: 500,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(node).toBeInstanceOf(Delay);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should accept zero delayMs", () => {
|
|
80
|
+
const node = registry.create("Delay", {
|
|
81
|
+
id: "test",
|
|
82
|
+
delayMs: 0,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(node).toBeInstanceOf(Delay);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should reject negative delayMs", () => {
|
|
89
|
+
expect(() => {
|
|
90
|
+
registry.create("Delay", {
|
|
91
|
+
id: "test",
|
|
92
|
+
delayMs: -100,
|
|
93
|
+
});
|
|
94
|
+
}).toThrow(ConfigurationError);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Parallel validation", () => {
|
|
99
|
+
it("should accept valid strategy", () => {
|
|
100
|
+
const node = registry.create("Parallel", {
|
|
101
|
+
id: "test",
|
|
102
|
+
strategy: "strict",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(node).toBeInstanceOf(Parallel);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should accept 'any' strategy", () => {
|
|
109
|
+
const node = registry.create("Parallel", {
|
|
110
|
+
id: "test",
|
|
111
|
+
strategy: "any",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(node).toBeInstanceOf(Parallel);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should use default strategy if not provided", () => {
|
|
118
|
+
const node = registry.create("Parallel", {
|
|
119
|
+
id: "test",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(node).toBeInstanceOf(Parallel);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should reject invalid strategy", () => {
|
|
126
|
+
expect(() => {
|
|
127
|
+
registry.create("Parallel", {
|
|
128
|
+
id: "test",
|
|
129
|
+
strategy: "invalid" as never,
|
|
130
|
+
});
|
|
131
|
+
}).toThrow(ConfigurationError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should accept positive thresholds", () => {
|
|
135
|
+
const node = registry.create("Parallel", {
|
|
136
|
+
id: "test",
|
|
137
|
+
successThreshold: 2,
|
|
138
|
+
failureThreshold: 1,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(node).toBeInstanceOf(Parallel);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("Tree creation validation", () => {
|
|
146
|
+
it("should validate entire tree structure", () => {
|
|
147
|
+
const definition = {
|
|
148
|
+
type: "Sequence",
|
|
149
|
+
id: "root",
|
|
150
|
+
children: [
|
|
151
|
+
{
|
|
152
|
+
type: "Timeout",
|
|
153
|
+
id: "timeout1",
|
|
154
|
+
props: {
|
|
155
|
+
timeoutMs: 1000,
|
|
156
|
+
},
|
|
157
|
+
children: [
|
|
158
|
+
{
|
|
159
|
+
type: "Delay",
|
|
160
|
+
id: "delay1",
|
|
161
|
+
props: {
|
|
162
|
+
delayMs: 500,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const tree = registry.createTree(definition);
|
|
171
|
+
expect(tree).toBeInstanceOf(Sequence);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should catch validation errors in nested nodes", () => {
|
|
175
|
+
const definition = {
|
|
176
|
+
type: "Sequence",
|
|
177
|
+
id: "root",
|
|
178
|
+
children: [
|
|
179
|
+
{
|
|
180
|
+
type: "Timeout",
|
|
181
|
+
id: "timeout1",
|
|
182
|
+
props: {
|
|
183
|
+
timeoutMs: -100, // Invalid!
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
expect(() => {
|
|
190
|
+
registry.createTree(definition);
|
|
191
|
+
}).toThrow(ConfigurationError);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should validate tree structure schema", () => {
|
|
195
|
+
const definition = {
|
|
196
|
+
id: "missing-type",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
expect(() => {
|
|
200
|
+
registry.createTree(definition);
|
|
201
|
+
}).toThrow(/type/);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("safeCreateTree", () => {
|
|
206
|
+
it("should return success for valid tree", () => {
|
|
207
|
+
const definition = {
|
|
208
|
+
type: "Sequence",
|
|
209
|
+
id: "root",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = registry.safeCreateTree(definition);
|
|
213
|
+
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
if (result.success) {
|
|
216
|
+
expect(result.tree).toBeInstanceOf(Sequence);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should return error for invalid tree", () => {
|
|
221
|
+
const definition = {
|
|
222
|
+
type: "Timeout",
|
|
223
|
+
id: "invalid",
|
|
224
|
+
props: {
|
|
225
|
+
timeoutMs: -100,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = registry.safeCreateTree(definition);
|
|
230
|
+
|
|
231
|
+
expect(result.success).toBe(false);
|
|
232
|
+
if (!result.success) {
|
|
233
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
234
|
+
expect(result.error.message).toContain("timeoutMs");
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree definition schemas
|
|
3
|
+
* Validates the structure of tree definitions before node creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tree definition type
|
|
10
|
+
* Matches the format expected by Registry.createTree()
|
|
11
|
+
*/
|
|
12
|
+
export interface TreeDefinition {
|
|
13
|
+
type: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
props?: Record<string, unknown>;
|
|
17
|
+
children?: TreeDefinition[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Recursive schema for tree definitions
|
|
22
|
+
* Supports nested children and arbitrary props
|
|
23
|
+
*
|
|
24
|
+
* Validates:
|
|
25
|
+
* - type is a non-empty string
|
|
26
|
+
* - id is optional string
|
|
27
|
+
* - name is optional string
|
|
28
|
+
* - props is optional record of unknown values
|
|
29
|
+
* - children is optional array of tree definitions
|
|
30
|
+
*/
|
|
31
|
+
const treeDefSchemaObject = z.object({
|
|
32
|
+
type: z.string().min(1, "Node type is required"),
|
|
33
|
+
id: z.string().optional(),
|
|
34
|
+
name: z.string().optional(),
|
|
35
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
36
|
+
children: z
|
|
37
|
+
.array(
|
|
38
|
+
z.lazy(() => treeDefinitionSchema) as z.ZodType<TreeDefinition>,
|
|
39
|
+
)
|
|
40
|
+
.optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const treeDefinitionSchema: z.ZodType<TreeDefinition> =
|
|
44
|
+
treeDefSchemaObject;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate tree definition structure (without type-specific validation)
|
|
48
|
+
* Type-specific validation happens in Registry.create()
|
|
49
|
+
*
|
|
50
|
+
* @param definition - Tree definition to validate
|
|
51
|
+
* @returns Validated tree definition
|
|
52
|
+
* @throws ZodError if structure is invalid
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const definition = validateTreeDefinition({
|
|
57
|
+
* type: 'Sequence',
|
|
58
|
+
* id: 'root',
|
|
59
|
+
* children: [
|
|
60
|
+
* { type: 'PrintAction', id: 'action1' }
|
|
61
|
+
* ]
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function validateTreeDefinition(definition: unknown): TreeDefinition {
|
|
66
|
+
return treeDefinitionSchema.parse(definition) as TreeDefinition;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate decorator has exactly one child
|
|
71
|
+
*
|
|
72
|
+
* @param nodeType - Type of the decorator node
|
|
73
|
+
* @param children - Children array to validate
|
|
74
|
+
* @throws Error if child count is not exactly 1
|
|
75
|
+
*/
|
|
76
|
+
export function validateDecoratorChildren(
|
|
77
|
+
nodeType: string,
|
|
78
|
+
children?: TreeDefinition[],
|
|
79
|
+
): void {
|
|
80
|
+
const childCount = children?.length || 0;
|
|
81
|
+
if (childCount !== 1) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Decorator ${nodeType} must have exactly one child (got ${childCount})`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate composite has at least minimum children
|
|
90
|
+
*
|
|
91
|
+
* @param nodeType - Type of the composite node
|
|
92
|
+
* @param children - Children array to validate
|
|
93
|
+
* @param minChildren - Minimum required children (default: 0)
|
|
94
|
+
* @throws Error if child count is less than minimum
|
|
95
|
+
*/
|
|
96
|
+
export function validateCompositeChildren(
|
|
97
|
+
nodeType: string,
|
|
98
|
+
children?: TreeDefinition[],
|
|
99
|
+
minChildren: number = 0,
|
|
100
|
+
): void {
|
|
101
|
+
const count = children?.length || 0;
|
|
102
|
+
if (count < minChildren) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Composite ${nodeType} requires at least ${minChildren} children (got ${count})`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate specific child count for composites with fixed requirements
|
|
111
|
+
*
|
|
112
|
+
* @param nodeType - Type of the composite node
|
|
113
|
+
* @param children - Children array to validate
|
|
114
|
+
* @param expectedCount - Exact number of expected children
|
|
115
|
+
* @throws Error if child count doesn't match expected
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // While node requires exactly 2 children (condition, body)
|
|
120
|
+
* validateChildCount('While', children, 2);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function validateChildCount(
|
|
124
|
+
nodeType: string,
|
|
125
|
+
children?: TreeDefinition[],
|
|
126
|
+
expectedCount: number = 0,
|
|
127
|
+
): void {
|
|
128
|
+
const count = children?.length || 0;
|
|
129
|
+
if (count !== expectedCount) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`${nodeType} requires exactly ${expectedCount} children (got ${count})`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate child count range for composites with flexible requirements
|
|
138
|
+
*
|
|
139
|
+
* @param nodeType - Type of the composite node
|
|
140
|
+
* @param children - Children array to validate
|
|
141
|
+
* @param minChildren - Minimum required children
|
|
142
|
+
* @param maxChildren - Maximum allowed children
|
|
143
|
+
* @throws Error if child count is outside range
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* // Conditional node requires 2-3 children (condition, then, optional else)
|
|
148
|
+
* validateChildCountRange('Conditional', children, 2, 3);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function validateChildCountRange(
|
|
152
|
+
nodeType: string,
|
|
153
|
+
children?: TreeDefinition[],
|
|
154
|
+
minChildren: number = 0,
|
|
155
|
+
maxChildren?: number,
|
|
156
|
+
): void {
|
|
157
|
+
const count = children?.length || 0;
|
|
158
|
+
|
|
159
|
+
if (count < minChildren) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`${nodeType} requires at least ${minChildren} children (got ${count})`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (maxChildren !== undefined && count > maxChildren) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`${nodeType} allows at most ${maxChildren} children (got ${count})`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for schema validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
zodErrorToConfigurationError,
|
|
9
|
+
validateConfiguration,
|
|
10
|
+
safeValidateConfiguration,
|
|
11
|
+
} from "./validation.js";
|
|
12
|
+
import { ConfigurationError } from "../errors.js";
|
|
13
|
+
|
|
14
|
+
describe("Schema Validation Utilities", () => {
|
|
15
|
+
describe("zodErrorToConfigurationError", () => {
|
|
16
|
+
it("should convert Zod error to ConfigurationError", () => {
|
|
17
|
+
const schema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
value: z.number().positive(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
schema.parse({ id: "test", value: -5 });
|
|
24
|
+
expect.fail("Should have thrown");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof z.ZodError) {
|
|
27
|
+
const configError = zodErrorToConfigurationError(
|
|
28
|
+
error,
|
|
29
|
+
"TestNode",
|
|
30
|
+
"test-id",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(configError).toBeInstanceOf(ConfigurationError);
|
|
34
|
+
expect(configError.message).toContain("TestNode:test-id");
|
|
35
|
+
expect(configError.message).toContain("value");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should include field path in error message", () => {
|
|
41
|
+
const schema = z.object({
|
|
42
|
+
nested: z.object({
|
|
43
|
+
value: z.number(),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
schema.parse({ nested: { value: "not a number" } });
|
|
49
|
+
expect.fail("Should have thrown");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof z.ZodError) {
|
|
52
|
+
const configError = zodErrorToConfigurationError(error, "TestNode");
|
|
53
|
+
|
|
54
|
+
expect(configError.message).toContain("nested.value");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("validateConfiguration", () => {
|
|
61
|
+
const testSchema = z.object({
|
|
62
|
+
id: z.string(),
|
|
63
|
+
count: z.number().int().positive(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should validate and return valid configuration", () => {
|
|
67
|
+
const config = { id: "test", count: 5 };
|
|
68
|
+
const result = validateConfiguration(
|
|
69
|
+
testSchema,
|
|
70
|
+
config,
|
|
71
|
+
"TestNode",
|
|
72
|
+
"test-id",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual(config);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should throw ConfigurationError for invalid configuration", () => {
|
|
79
|
+
const config = { id: "test", count: -5 };
|
|
80
|
+
|
|
81
|
+
expect(() => {
|
|
82
|
+
validateConfiguration(testSchema, config, "TestNode", "test-id");
|
|
83
|
+
}).toThrow(ConfigurationError);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should include node type and ID in error message", () => {
|
|
87
|
+
const config = { id: "test", count: 0 };
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
validateConfiguration(testSchema, config, "TestNode", "test-id");
|
|
91
|
+
expect.fail("Should have thrown");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
94
|
+
expect((error as ConfigurationError).message).toContain(
|
|
95
|
+
"TestNode:test-id",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("safeValidateConfiguration", () => {
|
|
102
|
+
const testSchema = z.object({
|
|
103
|
+
id: z.string(),
|
|
104
|
+
value: z.number(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should return success result for valid configuration", () => {
|
|
108
|
+
const config = { id: "test", value: 42 };
|
|
109
|
+
const result = safeValidateConfiguration(
|
|
110
|
+
testSchema,
|
|
111
|
+
config,
|
|
112
|
+
"TestNode",
|
|
113
|
+
"test-id",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result.success).toBe(true);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
expect(result.data).toEqual(config);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return error result for invalid configuration", () => {
|
|
123
|
+
const config = { id: "test", value: "not a number" };
|
|
124
|
+
const result = safeValidateConfiguration(
|
|
125
|
+
testSchema,
|
|
126
|
+
config,
|
|
127
|
+
"TestNode",
|
|
128
|
+
"test-id",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
expect(result.error).toBeInstanceOf(ConfigurationError);
|
|
134
|
+
expect(result.error.message).toContain("value");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should not throw errors", () => {
|
|
139
|
+
const config = { id: 123, value: "invalid" }; // Multiple errors
|
|
140
|
+
|
|
141
|
+
expect(() => {
|
|
142
|
+
safeValidateConfiguration(testSchema, config, "TestNode");
|
|
143
|
+
}).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities and error conversion
|
|
3
|
+
* Converts Zod validation errors to ConfigurationError
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ConfigurationError } from "../errors.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert Zod validation errors to ConfigurationError
|
|
11
|
+
* Preserves detailed error context and provides helpful hints
|
|
12
|
+
*
|
|
13
|
+
* @param error - Zod validation error
|
|
14
|
+
* @param nodeType - Type of node being validated
|
|
15
|
+
* @param nodeId - Optional node ID for context
|
|
16
|
+
* @returns ConfigurationError with formatted message
|
|
17
|
+
*/
|
|
18
|
+
export function zodErrorToConfigurationError(
|
|
19
|
+
error: z.ZodError<unknown>,
|
|
20
|
+
nodeType: string,
|
|
21
|
+
nodeId?: string,
|
|
22
|
+
): ConfigurationError {
|
|
23
|
+
const nodeIdentifier = nodeId ? `${nodeType}:${nodeId}` : nodeType;
|
|
24
|
+
|
|
25
|
+
// Format Zod errors into readable message
|
|
26
|
+
const issues = error.issues
|
|
27
|
+
.map((issue: z.ZodIssue) => {
|
|
28
|
+
const path = issue.path.join(".");
|
|
29
|
+
return ` - ${path ? path + ": " : ""}${issue.message}`;
|
|
30
|
+
})
|
|
31
|
+
.join("\n");
|
|
32
|
+
|
|
33
|
+
const message = `Invalid configuration for ${nodeIdentifier}:\n${issues}`;
|
|
34
|
+
|
|
35
|
+
// Include hint for common fixes
|
|
36
|
+
const hint =
|
|
37
|
+
"Check the node configuration and ensure all required fields are provided with valid values.";
|
|
38
|
+
|
|
39
|
+
return new ConfigurationError(message, hint);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate and parse configuration with ConfigurationError conversion
|
|
44
|
+
* Throws ConfigurationError if validation fails
|
|
45
|
+
*
|
|
46
|
+
* @param schema - Zod schema to validate against
|
|
47
|
+
* @param config - Configuration object to validate
|
|
48
|
+
* @param nodeType - Type of node being validated
|
|
49
|
+
* @param nodeId - Optional node ID for error context
|
|
50
|
+
* @returns Validated and parsed configuration
|
|
51
|
+
* @throws ConfigurationError if validation fails
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const validatedConfig = validateConfiguration(
|
|
56
|
+
* timeoutSchema,
|
|
57
|
+
* { id: 'test', timeoutMs: 1000 },
|
|
58
|
+
* 'Timeout',
|
|
59
|
+
* 'test'
|
|
60
|
+
* );
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function validateConfiguration<T = unknown>(
|
|
64
|
+
schema: z.ZodSchema<T>,
|
|
65
|
+
config: unknown,
|
|
66
|
+
nodeType: string,
|
|
67
|
+
nodeId?: string,
|
|
68
|
+
): T {
|
|
69
|
+
try {
|
|
70
|
+
return schema.parse(config);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof z.ZodError) {
|
|
73
|
+
throw zodErrorToConfigurationError(error, nodeType, nodeId);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Safe validation that returns result instead of throwing
|
|
81
|
+
* Useful for user-facing tools that need graceful error handling
|
|
82
|
+
*
|
|
83
|
+
* @param schema - Zod schema to validate against
|
|
84
|
+
* @param config - Configuration object to validate
|
|
85
|
+
* @param nodeType - Type of node being validated (for error messages)
|
|
86
|
+
* @param nodeId - Optional node ID for error context
|
|
87
|
+
* @returns Success result with data or failure result with error
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const result = safeValidateConfiguration(
|
|
92
|
+
* timeoutSchema,
|
|
93
|
+
* { id: 'test', timeoutMs: -100 },
|
|
94
|
+
* 'Timeout'
|
|
95
|
+
* );
|
|
96
|
+
*
|
|
97
|
+
* if (result.success) {
|
|
98
|
+
* console.log(result.data);
|
|
99
|
+
* } else {
|
|
100
|
+
* console.error(result.error.message);
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function safeValidateConfiguration<T>(
|
|
105
|
+
schema: z.ZodSchema<T>,
|
|
106
|
+
config: unknown,
|
|
107
|
+
nodeType: string,
|
|
108
|
+
nodeId?: string,
|
|
109
|
+
):
|
|
110
|
+
| { success: true; data: T }
|
|
111
|
+
| { success: false; error: ConfigurationError } {
|
|
112
|
+
const result = schema.safeParse(config);
|
|
113
|
+
|
|
114
|
+
if (result.success) {
|
|
115
|
+
return { success: true, data: result.data };
|
|
116
|
+
} else {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: zodErrorToConfigurationError(result.error, nodeType, nodeId),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripting module (deprecated)
|
|
3
|
+
*
|
|
4
|
+
* The Script node has been replaced by CodeExecution which uses
|
|
5
|
+
* Microsandbox for secure, isolated code execution.
|
|
6
|
+
*
|
|
7
|
+
* Use CodeExecution instead:
|
|
8
|
+
* @example
|
|
9
|
+
* ```yaml
|
|
10
|
+
* type: CodeExecution
|
|
11
|
+
* id: transform-data
|
|
12
|
+
* props:
|
|
13
|
+
* language: javascript # or 'python'
|
|
14
|
+
* code: |
|
|
15
|
+
* const users = getBB('users');
|
|
16
|
+
* setBB('count', users.length);
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @see CodeExecution in src/actions/code-execution.ts
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// No exports - use CodeExecution instead
|