@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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader Tests
|
|
3
|
+
* TDD: Write tests first, then implement to make them pass
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import { Registry } from "../registry.js";
|
|
11
|
+
import { registerStandardNodes } from "../registry-utils.js";
|
|
12
|
+
import {
|
|
13
|
+
loadTemplatesFromDirectory,
|
|
14
|
+
loadTemplate,
|
|
15
|
+
} from "./template-loader.js";
|
|
16
|
+
|
|
17
|
+
describe("Template Loader", () => {
|
|
18
|
+
let registry: Registry;
|
|
19
|
+
let tempDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Create fresh registry with standard nodes
|
|
23
|
+
registry = new Registry();
|
|
24
|
+
registerStandardNodes(registry);
|
|
25
|
+
|
|
26
|
+
// Create temp directory for test templates
|
|
27
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "btree-templates-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
// Clean up temp directory
|
|
32
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("loadTemplatesFromDirectory", () => {
|
|
36
|
+
it("should load all YAML files from directory", async () => {
|
|
37
|
+
// Create test template files
|
|
38
|
+
const template1 = `
|
|
39
|
+
type: Sequence
|
|
40
|
+
id: template-one
|
|
41
|
+
children:
|
|
42
|
+
- type: LogMessage
|
|
43
|
+
id: log1
|
|
44
|
+
props:
|
|
45
|
+
message: "Template 1"
|
|
46
|
+
`;
|
|
47
|
+
const template2 = `
|
|
48
|
+
type: Sequence
|
|
49
|
+
id: template-two
|
|
50
|
+
children:
|
|
51
|
+
- type: LogMessage
|
|
52
|
+
id: log2
|
|
53
|
+
props:
|
|
54
|
+
message: "Template 2"
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(path.join(tempDir, "template-one.yaml"), template1);
|
|
58
|
+
fs.writeFileSync(path.join(tempDir, "template-two.yaml"), template2);
|
|
59
|
+
|
|
60
|
+
const loadedIds = await loadTemplatesFromDirectory(registry, {
|
|
61
|
+
templatesDir: tempDir,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(loadedIds).toHaveLength(2);
|
|
65
|
+
expect(loadedIds).toContain("template-one");
|
|
66
|
+
expect(loadedIds).toContain("template-two");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should register templates in the registry", async () => {
|
|
70
|
+
const template = `
|
|
71
|
+
type: Sequence
|
|
72
|
+
id: my-template
|
|
73
|
+
children:
|
|
74
|
+
- type: LogMessage
|
|
75
|
+
id: log
|
|
76
|
+
props:
|
|
77
|
+
message: "Hello"
|
|
78
|
+
`;
|
|
79
|
+
fs.writeFileSync(path.join(tempDir, "my-template.yaml"), template);
|
|
80
|
+
|
|
81
|
+
await loadTemplatesFromDirectory(registry, { templatesDir: tempDir });
|
|
82
|
+
|
|
83
|
+
expect(registry.hasTree("my-template")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should use filename as template ID", async () => {
|
|
87
|
+
const template = `
|
|
88
|
+
type: Sequence
|
|
89
|
+
id: internal-id
|
|
90
|
+
children:
|
|
91
|
+
- type: LogMessage
|
|
92
|
+
id: log
|
|
93
|
+
props:
|
|
94
|
+
message: "Test"
|
|
95
|
+
`;
|
|
96
|
+
fs.writeFileSync(path.join(tempDir, "external-name.yaml"), template);
|
|
97
|
+
|
|
98
|
+
await loadTemplatesFromDirectory(registry, { templatesDir: tempDir });
|
|
99
|
+
|
|
100
|
+
// Template ID should be filename, not internal id
|
|
101
|
+
expect(registry.hasTree("external-name")).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should support .yml extension", async () => {
|
|
105
|
+
const template = `
|
|
106
|
+
type: Sequence
|
|
107
|
+
id: yml-template
|
|
108
|
+
children:
|
|
109
|
+
- type: LogMessage
|
|
110
|
+
id: log
|
|
111
|
+
props:
|
|
112
|
+
message: "YML extension"
|
|
113
|
+
`;
|
|
114
|
+
fs.writeFileSync(path.join(tempDir, "yml-template.yml"), template);
|
|
115
|
+
|
|
116
|
+
const loadedIds = await loadTemplatesFromDirectory(registry, {
|
|
117
|
+
templatesDir: tempDir,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(loadedIds).toContain("yml-template");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should apply ID prefix when provided", async () => {
|
|
124
|
+
const template = `
|
|
125
|
+
type: Sequence
|
|
126
|
+
id: test
|
|
127
|
+
children:
|
|
128
|
+
- type: LogMessage
|
|
129
|
+
id: log
|
|
130
|
+
props:
|
|
131
|
+
message: "Prefixed"
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(path.join(tempDir, "my-template.yaml"), template);
|
|
134
|
+
|
|
135
|
+
await loadTemplatesFromDirectory(registry, {
|
|
136
|
+
templatesDir: tempDir,
|
|
137
|
+
idPrefix: "tpl:",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(registry.hasTree("tpl:my-template")).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should return empty array for empty directory", async () => {
|
|
144
|
+
const loadedIds = await loadTemplatesFromDirectory(registry, {
|
|
145
|
+
templatesDir: tempDir,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(loadedIds).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should ignore non-YAML files", async () => {
|
|
152
|
+
fs.writeFileSync(path.join(tempDir, "readme.md"), "# README");
|
|
153
|
+
fs.writeFileSync(path.join(tempDir, "config.json"), "{}");
|
|
154
|
+
|
|
155
|
+
const template = `
|
|
156
|
+
type: Sequence
|
|
157
|
+
id: only-yaml
|
|
158
|
+
children:
|
|
159
|
+
- type: LogMessage
|
|
160
|
+
id: log
|
|
161
|
+
props:
|
|
162
|
+
message: "Only YAML"
|
|
163
|
+
`;
|
|
164
|
+
fs.writeFileSync(path.join(tempDir, "valid.yaml"), template);
|
|
165
|
+
|
|
166
|
+
const loadedIds = await loadTemplatesFromDirectory(registry, {
|
|
167
|
+
templatesDir: tempDir,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(loadedIds).toHaveLength(1);
|
|
171
|
+
expect(loadedIds).toContain("valid");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should throw error for invalid YAML syntax", async () => {
|
|
175
|
+
fs.writeFileSync(path.join(tempDir, "invalid.yaml"), "invalid: yaml: content:");
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
loadTemplatesFromDirectory(registry, { templatesDir: tempDir })
|
|
179
|
+
).rejects.toThrow();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should throw error for invalid tree structure", async () => {
|
|
183
|
+
const invalidTemplate = `
|
|
184
|
+
type: UnknownNodeType
|
|
185
|
+
id: invalid
|
|
186
|
+
`;
|
|
187
|
+
fs.writeFileSync(path.join(tempDir, "invalid.yaml"), invalidTemplate);
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
loadTemplatesFromDirectory(registry, { templatesDir: tempDir })
|
|
191
|
+
).rejects.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("loadTemplate", () => {
|
|
196
|
+
it("should load a single template file", () => {
|
|
197
|
+
const template = `
|
|
198
|
+
type: Sequence
|
|
199
|
+
id: single
|
|
200
|
+
children:
|
|
201
|
+
- type: LogMessage
|
|
202
|
+
id: log
|
|
203
|
+
props:
|
|
204
|
+
message: "Single template"
|
|
205
|
+
`;
|
|
206
|
+
const filePath = path.join(tempDir, "single.yaml");
|
|
207
|
+
fs.writeFileSync(filePath, template);
|
|
208
|
+
|
|
209
|
+
const id = loadTemplate(registry, filePath);
|
|
210
|
+
|
|
211
|
+
expect(id).toBe("single");
|
|
212
|
+
expect(registry.hasTree("single")).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should use custom ID when provided", () => {
|
|
216
|
+
const template = `
|
|
217
|
+
type: Sequence
|
|
218
|
+
id: default
|
|
219
|
+
children:
|
|
220
|
+
- type: LogMessage
|
|
221
|
+
id: log
|
|
222
|
+
props:
|
|
223
|
+
message: "Custom ID"
|
|
224
|
+
`;
|
|
225
|
+
const filePath = path.join(tempDir, "default.yaml");
|
|
226
|
+
fs.writeFileSync(filePath, template);
|
|
227
|
+
|
|
228
|
+
const id = loadTemplate(registry, filePath, "custom-id");
|
|
229
|
+
|
|
230
|
+
expect(id).toBe("custom-id");
|
|
231
|
+
expect(registry.hasTree("custom-id")).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should throw error for non-existent file", () => {
|
|
235
|
+
expect(() => {
|
|
236
|
+
loadTemplate(registry, "/non/existent/path.yaml");
|
|
237
|
+
}).toThrow();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("Template execution via SubTree", () => {
|
|
242
|
+
it("should allow SubTree to reference loaded template", async () => {
|
|
243
|
+
// Create and load a template
|
|
244
|
+
const template = `
|
|
245
|
+
type: Sequence
|
|
246
|
+
id: reusable
|
|
247
|
+
children:
|
|
248
|
+
- type: LogMessage
|
|
249
|
+
id: log
|
|
250
|
+
props:
|
|
251
|
+
message: "Reusable template executed"
|
|
252
|
+
`;
|
|
253
|
+
const filePath = path.join(tempDir, "reusable.yaml");
|
|
254
|
+
fs.writeFileSync(filePath, template);
|
|
255
|
+
loadTemplate(registry, filePath);
|
|
256
|
+
|
|
257
|
+
// Verify template can be cloned (as SubTree would do)
|
|
258
|
+
const clonedTree = registry.cloneTree("reusable");
|
|
259
|
+
expect(clonedTree).toBeDefined();
|
|
260
|
+
expect(clonedTree.getRoot()).toBeDefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should track source file for templates", () => {
|
|
264
|
+
const template = `
|
|
265
|
+
type: Sequence
|
|
266
|
+
id: tracked
|
|
267
|
+
children:
|
|
268
|
+
- type: LogMessage
|
|
269
|
+
id: log
|
|
270
|
+
props:
|
|
271
|
+
message: "Tracked"
|
|
272
|
+
`;
|
|
273
|
+
const filePath = path.join(tempDir, "tracked.yaml");
|
|
274
|
+
fs.writeFileSync(filePath, template);
|
|
275
|
+
loadTemplate(registry, filePath);
|
|
276
|
+
|
|
277
|
+
const sourceFile = registry.getTreeSourceFile("tracked");
|
|
278
|
+
expect(sourceFile).toBe(filePath);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader
|
|
3
|
+
* Load YAML template files from a directory and register them in the btree Registry.
|
|
4
|
+
* Templates can then be referenced by SubTree nodes using their template ID.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { Registry } from "../registry.js";
|
|
10
|
+
import { loadTreeFromYaml } from "../yaml/index.js";
|
|
11
|
+
import { BehaviorTree } from "../behavior-tree.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for loading templates from a directory
|
|
15
|
+
*/
|
|
16
|
+
export interface TemplateLoaderOptions {
|
|
17
|
+
/** Path to the templates directory */
|
|
18
|
+
templatesDir: string;
|
|
19
|
+
/** Optional prefix to add to all template IDs (e.g., "tpl:") */
|
|
20
|
+
idPrefix?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load all template YAML files from a directory and register them in the registry.
|
|
25
|
+
*
|
|
26
|
+
* Each YAML file becomes a registered BehaviorTree that can be referenced
|
|
27
|
+
* by SubTree nodes using the filename (without extension) as the tree ID.
|
|
28
|
+
*
|
|
29
|
+
* @param registry - The Registry instance to register templates in
|
|
30
|
+
* @param options - Configuration options
|
|
31
|
+
* @returns Array of template IDs that were loaded
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const registry = new Registry();
|
|
36
|
+
* registerStandardNodes(registry);
|
|
37
|
+
*
|
|
38
|
+
* const loadedIds = await loadTemplatesFromDirectory(registry, {
|
|
39
|
+
* templatesDir: './templates',
|
|
40
|
+
* idPrefix: 'tpl:'
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Templates can now be used via SubTree:
|
|
44
|
+
* // type: SubTree
|
|
45
|
+
* // props:
|
|
46
|
+
* // treeId: "tpl:order-validation"
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export async function loadTemplatesFromDirectory(
|
|
50
|
+
registry: Registry,
|
|
51
|
+
options: TemplateLoaderOptions
|
|
52
|
+
): Promise<string[]> {
|
|
53
|
+
const { templatesDir, idPrefix = "" } = options;
|
|
54
|
+
const loadedIds: string[] = [];
|
|
55
|
+
|
|
56
|
+
// Ensure directory exists
|
|
57
|
+
if (!fs.existsSync(templatesDir)) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`[TemplateLoader] Templates directory not found: ${templatesDir}`
|
|
60
|
+
);
|
|
61
|
+
return loadedIds;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Find all YAML files in directory
|
|
65
|
+
const files = fs
|
|
66
|
+
.readdirSync(templatesDir)
|
|
67
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
68
|
+
|
|
69
|
+
// Load each file
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const filePath = path.join(templatesDir, file);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Read and parse YAML content
|
|
75
|
+
const yamlContent = fs.readFileSync(filePath, "utf-8");
|
|
76
|
+
const rootNode = loadTreeFromYaml(yamlContent, registry);
|
|
77
|
+
const tree = new BehaviorTree(rootNode);
|
|
78
|
+
|
|
79
|
+
// Derive template ID from filename (without extension)
|
|
80
|
+
const baseName = path.basename(file, path.extname(file));
|
|
81
|
+
const templateId = idPrefix + baseName;
|
|
82
|
+
|
|
83
|
+
// Register in registry
|
|
84
|
+
registry.registerTree(templateId, tree, filePath);
|
|
85
|
+
loadedIds.push(templateId);
|
|
86
|
+
|
|
87
|
+
console.log(`[TemplateLoader] Registered template: ${templateId}`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const errorMessage =
|
|
90
|
+
error instanceof Error ? error.message : String(error);
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Failed to load template '${file}': ${errorMessage}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(
|
|
98
|
+
`[TemplateLoader] Loaded ${loadedIds.length} template(s) from ${templatesDir}`
|
|
99
|
+
);
|
|
100
|
+
return loadedIds;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load a single template file and register it in the registry.
|
|
105
|
+
*
|
|
106
|
+
* @param registry - The Registry instance to register the template in
|
|
107
|
+
* @param filePath - Path to the YAML template file
|
|
108
|
+
* @param templateId - Optional custom ID for the template (defaults to filename)
|
|
109
|
+
* @returns The template ID that was registered
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const id = loadTemplate(registry, './templates/order-validation.yaml');
|
|
114
|
+
* // Template is now available as "order-validation"
|
|
115
|
+
*
|
|
116
|
+
* const customId = loadTemplate(registry, './special.yaml', 'my-custom-id');
|
|
117
|
+
* // Template is now available as "my-custom-id"
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function loadTemplate(
|
|
121
|
+
registry: Registry,
|
|
122
|
+
filePath: string,
|
|
123
|
+
templateId?: string
|
|
124
|
+
): string {
|
|
125
|
+
// Read and parse YAML content
|
|
126
|
+
const yamlContent = fs.readFileSync(filePath, "utf-8");
|
|
127
|
+
const rootNode = loadTreeFromYaml(yamlContent, registry);
|
|
128
|
+
const tree = new BehaviorTree(rootNode);
|
|
129
|
+
|
|
130
|
+
// Derive template ID from filename if not provided
|
|
131
|
+
const id = templateId || path.basename(filePath, path.extname(filePath));
|
|
132
|
+
|
|
133
|
+
// Register in registry
|
|
134
|
+
registry.registerTree(id, tree, filePath);
|
|
135
|
+
|
|
136
|
+
console.log(`[TemplateLoader] Registered template: ${id} from ${filePath}`);
|
|
137
|
+
return id;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a template exists in the registry
|
|
142
|
+
*/
|
|
143
|
+
export function hasTemplate(registry: Registry, templateId: string): boolean {
|
|
144
|
+
return registry.hasTree(templateId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get all registered template IDs
|
|
149
|
+
*/
|
|
150
|
+
export function getTemplateIds(registry: Registry): string[] {
|
|
151
|
+
return registry.getAllTreeIds();
|
|
152
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal Integration Tests
|
|
3
|
+
* Validates that behavior trees work correctly as Temporal workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
7
|
+
import { TestWorkflowEnvironment } from "@temporalio/testing";
|
|
8
|
+
import { BehaviorTree } from "./behavior-tree.js";
|
|
9
|
+
import { Sequence } from "./composites/sequence.js";
|
|
10
|
+
import { Selector } from "./composites/selector.js";
|
|
11
|
+
import { Parallel, ParallelStrategy } from "./composites/parallel.js";
|
|
12
|
+
import { PrintAction, MockAction, CounterAction } from "./test-nodes.js";
|
|
13
|
+
import { Timeout } from "./decorators/timeout.js";
|
|
14
|
+
import { Delay } from "./decorators/delay.js";
|
|
15
|
+
import { NodeStatus } from "./types.js";
|
|
16
|
+
import { Registry } from "./registry.js";
|
|
17
|
+
|
|
18
|
+
describe.skip("Temporal Integration", () => {
|
|
19
|
+
let testEnv: TestWorkflowEnvironment;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
testEnv = await TestWorkflowEnvironment.createLocal();
|
|
23
|
+
}, 60000); // 60 second timeout for creating test environment
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await testEnv?.teardown();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should execute simple sequence workflow", async () => {
|
|
30
|
+
const root = new Sequence({ id: "root" });
|
|
31
|
+
root.addChild(new PrintAction({ id: "step1", message: "Hello" }));
|
|
32
|
+
root.addChild(new PrintAction({ id: "step2", message: "World" }));
|
|
33
|
+
|
|
34
|
+
const tree = new BehaviorTree(root);
|
|
35
|
+
const workflow = tree.toWorkflow();
|
|
36
|
+
|
|
37
|
+
const treeRegistry = new Registry();
|
|
38
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
39
|
+
|
|
40
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
41
|
+
expect(result.output).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should handle selector (fallback) logic", async () => {
|
|
45
|
+
const root = new Selector({ id: "root" });
|
|
46
|
+
root.addChild(new MockAction({ id: "fail1", returnStatus: NodeStatus.FAILURE }));
|
|
47
|
+
root.addChild(new MockAction({ id: "fail2", returnStatus: NodeStatus.FAILURE }));
|
|
48
|
+
root.addChild(new MockAction({ id: "success", returnStatus: NodeStatus.SUCCESS }));
|
|
49
|
+
|
|
50
|
+
const tree = new BehaviorTree(root);
|
|
51
|
+
const workflow = tree.toWorkflow();
|
|
52
|
+
|
|
53
|
+
const treeRegistry = new Registry();
|
|
54
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
55
|
+
|
|
56
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should execute parallel nodes concurrently", async () => {
|
|
60
|
+
const root = new Parallel({
|
|
61
|
+
id: "root",
|
|
62
|
+
strategy: ParallelStrategy.JOIN_ALL,
|
|
63
|
+
threshold: 3,
|
|
64
|
+
});
|
|
65
|
+
root.addChild(new MockAction({ id: "task1", returnStatus: NodeStatus.SUCCESS }));
|
|
66
|
+
root.addChild(new MockAction({ id: "task2", returnStatus: NodeStatus.SUCCESS }));
|
|
67
|
+
root.addChild(new MockAction({ id: "task3", returnStatus: NodeStatus.SUCCESS }));
|
|
68
|
+
|
|
69
|
+
const tree = new BehaviorTree(root);
|
|
70
|
+
const workflow = tree.toWorkflow();
|
|
71
|
+
|
|
72
|
+
const treeRegistry = new Registry();
|
|
73
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
74
|
+
|
|
75
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle blackboard input and output", async () => {
|
|
79
|
+
const root = new Sequence({ id: "root" });
|
|
80
|
+
root.addChild(new CounterAction({ id: "increment1", counterKey: "count", increment: 5 }));
|
|
81
|
+
root.addChild(new CounterAction({ id: "increment2", counterKey: "count", increment: 3 }));
|
|
82
|
+
|
|
83
|
+
const tree = new BehaviorTree(root);
|
|
84
|
+
const workflow = tree.toWorkflow();
|
|
85
|
+
|
|
86
|
+
const treeRegistry = new Registry();
|
|
87
|
+
const result = await workflow({
|
|
88
|
+
input: { count: 10 },
|
|
89
|
+
treeRegistry
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
93
|
+
expect(result.output.count).toBe(18); // 10 + 5 + 3
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// NOTE: Retry decorator removed - use Temporal's native RetryPolicy instead
|
|
97
|
+
|
|
98
|
+
it("should handle timeout decorator", async () => {
|
|
99
|
+
const timeout = new Timeout({
|
|
100
|
+
id: "timeout",
|
|
101
|
+
timeoutMs: 100,
|
|
102
|
+
});
|
|
103
|
+
const fastAction = new MockAction({
|
|
104
|
+
id: "fast",
|
|
105
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
106
|
+
ticksBeforeComplete: 1,
|
|
107
|
+
});
|
|
108
|
+
timeout.setChild(fastAction);
|
|
109
|
+
|
|
110
|
+
const tree = new BehaviorTree(timeout);
|
|
111
|
+
const workflow = tree.toWorkflow();
|
|
112
|
+
|
|
113
|
+
const treeRegistry = new Registry();
|
|
114
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
115
|
+
|
|
116
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle delay decorator", async () => {
|
|
120
|
+
const delay = new Delay({
|
|
121
|
+
id: "delay",
|
|
122
|
+
delayMs: 50,
|
|
123
|
+
});
|
|
124
|
+
const action = new PrintAction({
|
|
125
|
+
id: "action",
|
|
126
|
+
message: "Delayed action",
|
|
127
|
+
});
|
|
128
|
+
delay.setChild(action);
|
|
129
|
+
|
|
130
|
+
const tree = new BehaviorTree(delay);
|
|
131
|
+
const workflow = tree.toWorkflow();
|
|
132
|
+
|
|
133
|
+
const treeRegistry = new Registry();
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
136
|
+
const duration = Date.now() - startTime;
|
|
137
|
+
|
|
138
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
139
|
+
expect(duration).toBeGreaterThanOrEqual(50);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle complex nested trees", async () => {
|
|
143
|
+
// Create a complex tree: Sequence -> (Selector, Parallel, Action)
|
|
144
|
+
const root = new Sequence({ id: "root" });
|
|
145
|
+
|
|
146
|
+
const selector = new Selector({ id: "selector" });
|
|
147
|
+
selector.addChild(new MockAction({ id: "fail", returnStatus: NodeStatus.FAILURE }));
|
|
148
|
+
selector.addChild(new MockAction({ id: "success", returnStatus: NodeStatus.SUCCESS }));
|
|
149
|
+
|
|
150
|
+
const parallel = new Parallel({
|
|
151
|
+
id: "parallel",
|
|
152
|
+
strategy: ParallelStrategy.JOIN_ALL,
|
|
153
|
+
threshold: 2,
|
|
154
|
+
});
|
|
155
|
+
parallel.addChild(new PrintAction({ id: "p1", message: "Parallel 1" }));
|
|
156
|
+
parallel.addChild(new PrintAction({ id: "p2", message: "Parallel 2" }));
|
|
157
|
+
|
|
158
|
+
const action = new MockAction({ id: "finalAction", returnStatus: NodeStatus.SUCCESS });
|
|
159
|
+
|
|
160
|
+
root.addChildren([selector, parallel, action]);
|
|
161
|
+
|
|
162
|
+
const tree = new BehaviorTree(root);
|
|
163
|
+
const workflow = tree.toWorkflow();
|
|
164
|
+
|
|
165
|
+
const treeRegistry = new Registry();
|
|
166
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
167
|
+
|
|
168
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should preserve workflow info in context", async () => {
|
|
172
|
+
let capturedWorkflowInfo: any = null;
|
|
173
|
+
|
|
174
|
+
const action = new MockAction({
|
|
175
|
+
id: "capture",
|
|
176
|
+
returnStatus: NodeStatus.SUCCESS,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Override tick to capture workflow info
|
|
180
|
+
const originalTick = action.tick.bind(action);
|
|
181
|
+
action.tick = async (context) => {
|
|
182
|
+
capturedWorkflowInfo = context.workflowInfo;
|
|
183
|
+
return originalTick(context);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const tree = new BehaviorTree(action);
|
|
187
|
+
const workflow = tree.toWorkflow();
|
|
188
|
+
|
|
189
|
+
const treeRegistry = new Registry();
|
|
190
|
+
const result = await workflow({ input: {}, treeRegistry });
|
|
191
|
+
|
|
192
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
193
|
+
// In test environment, workflowInfo might not be available
|
|
194
|
+
// This test just validates the structure doesn't break
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should handle session ID properly", async () => {
|
|
198
|
+
const root = new PrintAction({ id: "root", message: "Session test" });
|
|
199
|
+
|
|
200
|
+
const tree = new BehaviorTree(root);
|
|
201
|
+
const workflow = tree.toWorkflow();
|
|
202
|
+
|
|
203
|
+
const treeRegistry = new Registry();
|
|
204
|
+
const customSessionId = "test-session-123";
|
|
205
|
+
const result = await workflow({
|
|
206
|
+
input: {},
|
|
207
|
+
treeRegistry,
|
|
208
|
+
sessionId: customSessionId,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(result.status).toBe(NodeStatus.SUCCESS);
|
|
212
|
+
});
|
|
213
|
+
});
|