@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry utilities for registering standard nodes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Registry } from "./registry.js";
|
|
6
|
+
|
|
7
|
+
// Composites
|
|
8
|
+
import { Sequence } from "./composites/sequence.js";
|
|
9
|
+
import { Selector } from "./composites/selector.js";
|
|
10
|
+
import { Parallel } from "./composites/parallel.js";
|
|
11
|
+
import { Conditional } from "./composites/conditional.js";
|
|
12
|
+
import { ForEach } from "./composites/for-each.js";
|
|
13
|
+
import { While } from "./composites/while.js";
|
|
14
|
+
import { Recovery } from "./composites/recovery.js";
|
|
15
|
+
import { ReactiveSequence } from "./composites/reactive-sequence.js";
|
|
16
|
+
import { MemorySequence } from "./composites/memory-sequence.js";
|
|
17
|
+
import { SubTree } from "./composites/sub-tree.js";
|
|
18
|
+
|
|
19
|
+
// Decorators
|
|
20
|
+
import { Timeout } from "./decorators/timeout.js";
|
|
21
|
+
import { Delay } from "./decorators/delay.js";
|
|
22
|
+
import { Repeat } from "./decorators/repeat.js";
|
|
23
|
+
import { Invert } from "./decorators/invert.js";
|
|
24
|
+
import { ForceSuccess, ForceFailure } from "./decorators/force-result.js";
|
|
25
|
+
import { RunOnce } from "./decorators/run-once.js";
|
|
26
|
+
import { KeepRunningUntilFailure } from "./decorators/keep-running.js";
|
|
27
|
+
import { Precondition } from "./decorators/precondition.js";
|
|
28
|
+
import { SoftAssert } from "./decorators/soft-assert.js";
|
|
29
|
+
|
|
30
|
+
// Test nodes (commonly used in examples and testing)
|
|
31
|
+
import {
|
|
32
|
+
PrintAction,
|
|
33
|
+
MockAction,
|
|
34
|
+
SuccessNode,
|
|
35
|
+
FailureNode,
|
|
36
|
+
RunningNode,
|
|
37
|
+
CounterAction,
|
|
38
|
+
CheckCondition,
|
|
39
|
+
AlwaysCondition,
|
|
40
|
+
WaitAction,
|
|
41
|
+
} from "./test-nodes.js";
|
|
42
|
+
|
|
43
|
+
// Scripting - Script node removed, use CodeExecution instead
|
|
44
|
+
|
|
45
|
+
// Utilities
|
|
46
|
+
import { LogMessage } from "./utilities/log-message.js";
|
|
47
|
+
import { RegexExtract } from "./utilities/regex-extract.js";
|
|
48
|
+
|
|
49
|
+
// Integrations
|
|
50
|
+
import { IntegrationAction } from "./integrations/integration-action.js";
|
|
51
|
+
|
|
52
|
+
// Activity-based action nodes
|
|
53
|
+
import { PythonScript } from "./actions/python-script.js";
|
|
54
|
+
import { ParseFile } from "./actions/parse-file.js";
|
|
55
|
+
import { GenerateFile } from "./actions/generate-file.js";
|
|
56
|
+
import { HttpRequest } from "./actions/http-request.js";
|
|
57
|
+
import { CodeExecution } from "./actions/code-execution.js";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register all standard built-in nodes to a registry
|
|
61
|
+
* This includes composites, decorators, actions, conditions, and utilities
|
|
62
|
+
*
|
|
63
|
+
* @param registry - Registry to register nodes to
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { Registry, registerStandardNodes } from '@wayfarer-ai/btree';
|
|
68
|
+
*
|
|
69
|
+
* const registry = new Registry();
|
|
70
|
+
* registerStandardNodes(registry);
|
|
71
|
+
*
|
|
72
|
+
* // Now register your custom nodes
|
|
73
|
+
* registry.register('MyCustomAction', MyCustomAction, { category: 'action' });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function registerStandardNodes(registry: Registry): void {
|
|
77
|
+
// Composites
|
|
78
|
+
registry.register("Sequence", Sequence as any, { category: "composite" });
|
|
79
|
+
registry.register("Selector", Selector as any, { category: "composite" });
|
|
80
|
+
registry.register("Parallel", Parallel as any, { category: "composite" });
|
|
81
|
+
registry.register("Conditional", Conditional as any, {
|
|
82
|
+
category: "composite",
|
|
83
|
+
});
|
|
84
|
+
registry.register("ForEach", ForEach as any, { category: "composite" });
|
|
85
|
+
registry.register("While", While as any, { category: "composite" });
|
|
86
|
+
registry.register("Recovery", Recovery as any, { category: "composite" });
|
|
87
|
+
registry.register("ReactiveSequence", ReactiveSequence as any, {
|
|
88
|
+
category: "composite",
|
|
89
|
+
});
|
|
90
|
+
registry.register("MemorySequence", MemorySequence as any, {
|
|
91
|
+
category: "composite",
|
|
92
|
+
});
|
|
93
|
+
registry.register("SubTree", SubTree as any, { category: "composite" });
|
|
94
|
+
|
|
95
|
+
// Decorators
|
|
96
|
+
registry.register("Timeout", Timeout as any, { category: "decorator" });
|
|
97
|
+
registry.register("Delay", Delay as any, { category: "decorator" });
|
|
98
|
+
registry.register("Repeat", Repeat as any, { category: "decorator" });
|
|
99
|
+
registry.register("Invert", Invert as any, { category: "decorator" });
|
|
100
|
+
registry.register("ForceSuccess", ForceSuccess as any, {
|
|
101
|
+
category: "decorator",
|
|
102
|
+
});
|
|
103
|
+
registry.register("ForceFailure", ForceFailure as any, {
|
|
104
|
+
category: "decorator",
|
|
105
|
+
});
|
|
106
|
+
registry.register("RunOnce", RunOnce as any, { category: "decorator" });
|
|
107
|
+
registry.register("KeepRunningUntilFailure", KeepRunningUntilFailure as any, {
|
|
108
|
+
category: "decorator",
|
|
109
|
+
});
|
|
110
|
+
registry.register("Precondition", Precondition as any, {
|
|
111
|
+
category: "decorator",
|
|
112
|
+
});
|
|
113
|
+
registry.register("SoftAssert", SoftAssert as any, { category: "decorator" });
|
|
114
|
+
|
|
115
|
+
// Test/Example nodes
|
|
116
|
+
registry.register("PrintAction", PrintAction as any, { category: "action" });
|
|
117
|
+
registry.register("MockAction", MockAction as any, { category: "action" });
|
|
118
|
+
registry.register("SuccessNode", SuccessNode as any, { category: "action" });
|
|
119
|
+
registry.register("FailureNode", FailureNode as any, { category: "action" });
|
|
120
|
+
registry.register("RunningNode", RunningNode as any, { category: "action" });
|
|
121
|
+
registry.register("CounterAction", CounterAction as any, {
|
|
122
|
+
category: "action",
|
|
123
|
+
});
|
|
124
|
+
registry.register("CheckCondition", CheckCondition as any, {
|
|
125
|
+
category: "condition",
|
|
126
|
+
});
|
|
127
|
+
registry.register("AlwaysCondition", AlwaysCondition as any, {
|
|
128
|
+
category: "condition",
|
|
129
|
+
});
|
|
130
|
+
registry.register("WaitAction", WaitAction as any, { category: "action" });
|
|
131
|
+
|
|
132
|
+
// Scripting - Script node removed, use CodeExecution instead
|
|
133
|
+
|
|
134
|
+
// Utilities
|
|
135
|
+
registry.register("LogMessage", LogMessage as any, { category: "action" });
|
|
136
|
+
registry.register("RegexExtract", RegexExtract as any, { category: "action" });
|
|
137
|
+
|
|
138
|
+
// Integrations
|
|
139
|
+
registry.register("IntegrationAction", IntegrationAction as any, { category: "action" });
|
|
140
|
+
|
|
141
|
+
// Activity-based action nodes (require activities in context)
|
|
142
|
+
registry.register("PythonScript", PythonScript as any, { category: "action" });
|
|
143
|
+
registry.register("ParseFile", ParseFile as any, { category: "action" });
|
|
144
|
+
registry.register("GenerateFile", GenerateFile as any, { category: "action" });
|
|
145
|
+
registry.register("HttpRequest", HttpRequest as any, { category: "action" });
|
|
146
|
+
registry.register("CodeExecution", CodeExecution as any, { category: "action" });
|
|
147
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ActionNode } from "./base-node.js";
|
|
3
|
+
import { BehaviorTree } from "./behavior-tree.js";
|
|
4
|
+
import { Sequence } from "./composites/sequence.js";
|
|
5
|
+
import { Invert } from "./decorators/invert.js";
|
|
6
|
+
import { Registry } from "./registry.js";
|
|
7
|
+
import {
|
|
8
|
+
type TemporalContext,
|
|
9
|
+
type NodeConfiguration,
|
|
10
|
+
NodeStatus,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
// Mock node for testing
|
|
14
|
+
class TestActionNode extends ActionNode {
|
|
15
|
+
async tick(_context: TemporalContext) {
|
|
16
|
+
this._status = NodeStatus.SUCCESS;
|
|
17
|
+
return NodeStatus.SUCCESS;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Registry", () => {
|
|
22
|
+
let registry: Registry;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
registry = new Registry();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("Node registration", () => {
|
|
29
|
+
it("should register node types", () => {
|
|
30
|
+
registry.register("TestAction", TestActionNode, {
|
|
31
|
+
category: "action",
|
|
32
|
+
description: "Test action node",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(registry.has("TestAction")).toBe(true);
|
|
36
|
+
expect(registry.getRegisteredTypes()).toContain("TestAction");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should throw error when registering duplicate type", () => {
|
|
40
|
+
registry.register("TestAction", TestActionNode);
|
|
41
|
+
|
|
42
|
+
expect(() => {
|
|
43
|
+
registry.register("TestAction", TestActionNode);
|
|
44
|
+
}).toThrow("Node type 'TestAction' is already registered");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should store metadata for registered nodes", () => {
|
|
48
|
+
registry.register("TestAction", TestActionNode, {
|
|
49
|
+
category: "action",
|
|
50
|
+
description: "Test action node",
|
|
51
|
+
ports: [
|
|
52
|
+
{ name: "input1", type: "input" },
|
|
53
|
+
{ name: "output1", type: "output" },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const metadata = registry.getMetadata("TestAction");
|
|
58
|
+
expect(metadata).toBeDefined();
|
|
59
|
+
expect(metadata?.type).toBe("TestAction");
|
|
60
|
+
expect(metadata?.category).toBe("action");
|
|
61
|
+
expect(metadata?.description).toBe("Test action node");
|
|
62
|
+
expect(metadata?.ports).toHaveLength(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should use default metadata values", () => {
|
|
66
|
+
registry.register("TestAction", TestActionNode);
|
|
67
|
+
|
|
68
|
+
const metadata = registry.getMetadata("TestAction");
|
|
69
|
+
expect(metadata?.category).toBe("action");
|
|
70
|
+
expect(metadata?.ports).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("Node creation", () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
registry.register("TestAction", TestActionNode);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should create nodes by type", () => {
|
|
80
|
+
const config: NodeConfiguration = {
|
|
81
|
+
id: "test-node",
|
|
82
|
+
name: "Test Node",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const node = registry.create("TestAction", config);
|
|
86
|
+
|
|
87
|
+
expect(node).toBeInstanceOf(TestActionNode);
|
|
88
|
+
expect(node.id).toBe("test-node");
|
|
89
|
+
expect(node.name).toBe("Test Node");
|
|
90
|
+
expect(node.type).toBe("TestActionNode");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should throw error for unknown node type", () => {
|
|
94
|
+
expect(() => {
|
|
95
|
+
registry.create("UnknownType", { id: "test" });
|
|
96
|
+
}).toThrow(/Unknown node type: 'UnknownType'/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should list available types in error message", () => {
|
|
100
|
+
registry.register("Type1", TestActionNode);
|
|
101
|
+
registry.register("Type2", TestActionNode);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
registry.create("UnknownType", { id: "test" });
|
|
105
|
+
} catch (error: unknown) {
|
|
106
|
+
expect(error.message).toContain("Type1");
|
|
107
|
+
expect(error.message).toContain("Type2");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("Type queries", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
registry.register("Action1", TestActionNode, { category: "action" });
|
|
115
|
+
registry.register("Action2", TestActionNode, { category: "action" });
|
|
116
|
+
registry.register("Sequence", Sequence, { category: "composite" });
|
|
117
|
+
registry.register("Invert", Invert, { category: "decorator" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should get types by category", () => {
|
|
121
|
+
const actions = registry.getTypesByCategory("action");
|
|
122
|
+
expect(actions).toEqual(["Action1", "Action2"]);
|
|
123
|
+
|
|
124
|
+
const composites = registry.getTypesByCategory("composite");
|
|
125
|
+
expect(composites).toEqual(["Sequence"]);
|
|
126
|
+
|
|
127
|
+
const decorators = registry.getTypesByCategory("decorator");
|
|
128
|
+
expect(decorators).toEqual(["Invert"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should return empty array for unknown category", () => {
|
|
132
|
+
const types = registry.getTypesByCategory("unknown" as unknown);
|
|
133
|
+
expect(types).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should clear all registrations", () => {
|
|
137
|
+
expect(registry.getRegisteredTypes()).toHaveLength(4);
|
|
138
|
+
|
|
139
|
+
registry.clear();
|
|
140
|
+
|
|
141
|
+
expect(registry.getRegisteredTypes()).toHaveLength(0);
|
|
142
|
+
expect(registry.has("Action1")).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("Tree creation from JSON", () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
registry.register("TestAction", TestActionNode);
|
|
149
|
+
registry.register("Sequence", Sequence);
|
|
150
|
+
registry.register("Invert", Invert);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should create single node from definition", () => {
|
|
154
|
+
const definition = {
|
|
155
|
+
type: "TestAction",
|
|
156
|
+
id: "action1",
|
|
157
|
+
name: "Test Action",
|
|
158
|
+
props: {
|
|
159
|
+
customProp: "value",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const node = registry.createTree(definition);
|
|
164
|
+
|
|
165
|
+
expect(node).toBeInstanceOf(TestActionNode);
|
|
166
|
+
expect(node.id).toBe("action1");
|
|
167
|
+
expect(node.name).toBe("Test Action");
|
|
168
|
+
expect((node as unknown).config.customProp).toBe("value");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should throw error if type is missing", () => {
|
|
172
|
+
const definition = {
|
|
173
|
+
id: "node1",
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
expect(() => {
|
|
177
|
+
registry.createTree(definition);
|
|
178
|
+
}).toThrow(/type/); // Zod validation error mentions "type" field
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should generate id if not provided", () => {
|
|
182
|
+
const definition = {
|
|
183
|
+
type: "TestAction",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const node = registry.createTree(definition);
|
|
187
|
+
|
|
188
|
+
expect(node.id).toMatch(/^TestAction_\d+$/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should create composite with children", () => {
|
|
192
|
+
const definition = {
|
|
193
|
+
type: "Sequence",
|
|
194
|
+
id: "seq1",
|
|
195
|
+
children: [
|
|
196
|
+
{ type: "TestAction", id: "child1" },
|
|
197
|
+
{ type: "TestAction", id: "child2" },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const node = registry.createTree(definition) as Sequence;
|
|
202
|
+
|
|
203
|
+
expect(node).toBeInstanceOf(Sequence);
|
|
204
|
+
expect(node.children).toHaveLength(2);
|
|
205
|
+
expect(node.children?.[0]?.id).toBe("child1");
|
|
206
|
+
expect(node.children?.[1]?.id).toBe("child2");
|
|
207
|
+
expect(node.children?.[0]?.parent).toBe(node);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should create decorator with single child", () => {
|
|
211
|
+
const definition = {
|
|
212
|
+
type: "Invert",
|
|
213
|
+
id: "inv1",
|
|
214
|
+
children: [{ type: "TestAction", id: "child1" }],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const node = registry.createTree(definition) as Invert;
|
|
218
|
+
|
|
219
|
+
expect(node).toBeInstanceOf(Invert);
|
|
220
|
+
expect(node.children).toHaveLength(1);
|
|
221
|
+
expect(node.children?.[0]?.id).toBe("child1");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should throw error if decorator has wrong number of children", () => {
|
|
225
|
+
const definition = {
|
|
226
|
+
type: "Invert",
|
|
227
|
+
id: "inv1",
|
|
228
|
+
children: [
|
|
229
|
+
{ type: "TestAction", id: "child1" },
|
|
230
|
+
{ type: "TestAction", id: "child2" },
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
expect(() => {
|
|
235
|
+
registry.createTree(definition);
|
|
236
|
+
}).toThrow("Decorator Invert must have exactly one child");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should create nested tree structure", () => {
|
|
240
|
+
const definition = {
|
|
241
|
+
type: "Sequence",
|
|
242
|
+
id: "root",
|
|
243
|
+
children: [
|
|
244
|
+
{
|
|
245
|
+
type: "Invert",
|
|
246
|
+
id: "inv1",
|
|
247
|
+
children: [{ type: "TestAction", id: "action1" }],
|
|
248
|
+
},
|
|
249
|
+
{ type: "TestAction", id: "action2" },
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const root = registry.createTree(definition) as Sequence;
|
|
254
|
+
|
|
255
|
+
expect(root.children).toHaveLength(2);
|
|
256
|
+
expect(root.children?.[0]).toBeInstanceOf(Invert);
|
|
257
|
+
expect(root.children?.[0]?.children).toHaveLength(1);
|
|
258
|
+
expect(root.children?.[0]?.children?.[0]?.id).toBe("action1");
|
|
259
|
+
expect(root.children?.[1]?.id).toBe("action2");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("Logging", () => {
|
|
264
|
+
it("should log registry operations", () => {
|
|
265
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
266
|
+
|
|
267
|
+
new Registry();
|
|
268
|
+
expect(consoleSpy).toHaveBeenCalledWith("[Registry] Registry created");
|
|
269
|
+
|
|
270
|
+
registry.register("TestNode", TestActionNode);
|
|
271
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
272
|
+
expect.stringContaining("Registered node type: TestNode"),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
registry.create("TestNode", { id: "test" });
|
|
276
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
277
|
+
expect.stringContaining("Creating node of type: TestNode"),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
registry.clear();
|
|
281
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
282
|
+
"[Registry] Registry cleared (nodes and trees)",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("Behavior Tree Registration with Metadata", () => {
|
|
289
|
+
let registry: Registry;
|
|
290
|
+
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
registry = new Registry();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should register a BehaviorTree with source file", () => {
|
|
296
|
+
const node = new Sequence({ id: "seq1", name: "Test Sequence" });
|
|
297
|
+
const tree = new BehaviorTree(node);
|
|
298
|
+
const sourceFile = "/path/to/test.sigma";
|
|
299
|
+
|
|
300
|
+
registry.registerTree("TestTree", tree, sourceFile);
|
|
301
|
+
|
|
302
|
+
expect(registry.hasTree("TestTree")).toBe(true);
|
|
303
|
+
expect(registry.getTree("TestTree")).toBe(tree);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should return the source file for a registered tree", () => {
|
|
307
|
+
const node = new Sequence({ id: "seq1", name: "Test Sequence" });
|
|
308
|
+
const tree = new BehaviorTree(node);
|
|
309
|
+
const sourceFile = "/path/to/test.sigma";
|
|
310
|
+
|
|
311
|
+
registry.registerTree("TestTree", tree, sourceFile);
|
|
312
|
+
|
|
313
|
+
expect(registry.getTreeSourceFile("TestTree")).toBe(sourceFile);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should return undefined for unknown tree source file", () => {
|
|
317
|
+
expect(registry.getTreeSourceFile("UnknownTree")).toBeUndefined();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should get all trees for a specific source file", () => {
|
|
321
|
+
const node1 = new Sequence({ id: "seq1" });
|
|
322
|
+
const tree1 = new BehaviorTree(node1);
|
|
323
|
+
const node2 = new Sequence({ id: "seq2" });
|
|
324
|
+
const tree2 = new BehaviorTree(node2);
|
|
325
|
+
const node3 = new Sequence({ id: "seq3" });
|
|
326
|
+
const tree3 = new BehaviorTree(node3);
|
|
327
|
+
|
|
328
|
+
const file1 = "/path/to/file1.sigma";
|
|
329
|
+
const file2 = "/path/to/file2.sigma";
|
|
330
|
+
|
|
331
|
+
registry.registerTree("Tree1", tree1, file1);
|
|
332
|
+
registry.registerTree("Tree2", tree2, file1); // Same file as Tree1
|
|
333
|
+
registry.registerTree("Tree3", tree3, file2); // Different file
|
|
334
|
+
|
|
335
|
+
const treesInFile1 = registry.getTreesForFile(file1);
|
|
336
|
+
|
|
337
|
+
expect(treesInFile1.size).toBe(2);
|
|
338
|
+
expect(treesInFile1.has("Tree1")).toBe(true);
|
|
339
|
+
expect(treesInFile1.has("Tree2")).toBe(true);
|
|
340
|
+
expect(treesInFile1.has("Tree3")).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should return empty map for unknown file", () => {
|
|
344
|
+
const treesInFile = registry.getTreesForFile("/unknown/file.sigma");
|
|
345
|
+
expect(treesInFile.size).toBe(0);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should clone a registered BehaviorTree", () => {
|
|
349
|
+
const node = new Sequence({ id: "seq1", name: "Test Sequence" });
|
|
350
|
+
const tree = new BehaviorTree(node);
|
|
351
|
+
const sourceFile = "/path/to/test.sigma";
|
|
352
|
+
|
|
353
|
+
registry.registerTree("TestTree", tree, sourceFile);
|
|
354
|
+
|
|
355
|
+
const clonedTree = registry.cloneTree("TestTree");
|
|
356
|
+
|
|
357
|
+
expect(clonedTree).toBeInstanceOf(BehaviorTree);
|
|
358
|
+
expect(clonedTree).not.toBe(tree); // Different instance
|
|
359
|
+
expect(clonedTree.getRoot().id).toBe(node.id);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should throw when registering duplicate tree ID", () => {
|
|
363
|
+
const node = new Sequence({ id: "seq1" });
|
|
364
|
+
const tree = new BehaviorTree(node);
|
|
365
|
+
|
|
366
|
+
registry.registerTree("TestTree", tree, "/path/to/file.sigma");
|
|
367
|
+
|
|
368
|
+
expect(() => {
|
|
369
|
+
registry.registerTree("TestTree", tree, "/path/to/file.sigma");
|
|
370
|
+
}).toThrow("Behavior tree 'TestTree' is already registered");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should throw when cloning unknown tree", () => {
|
|
374
|
+
expect(() => {
|
|
375
|
+
registry.cloneTree("UnknownTree");
|
|
376
|
+
}).toThrow("Behavior tree 'UnknownTree' not found");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should clear all registered trees", () => {
|
|
380
|
+
const node = new Sequence({ id: "seq1" });
|
|
381
|
+
const tree = new BehaviorTree(node);
|
|
382
|
+
|
|
383
|
+
registry.registerTree("TestTree", tree, "/path/to/file.sigma");
|
|
384
|
+
expect(registry.hasTree("TestTree")).toBe(true);
|
|
385
|
+
|
|
386
|
+
registry.clearTrees();
|
|
387
|
+
expect(registry.hasTree("TestTree")).toBe(false);
|
|
388
|
+
expect(registry.getAllTreeIds()).toHaveLength(0);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should get all registered tree IDs", () => {
|
|
392
|
+
const node1 = new Sequence({ id: "seq1" });
|
|
393
|
+
const tree1 = new BehaviorTree(node1);
|
|
394
|
+
const node2 = new Sequence({ id: "seq2" });
|
|
395
|
+
const tree2 = new BehaviorTree(node2);
|
|
396
|
+
|
|
397
|
+
registry.registerTree("Tree1", tree1, "/path/to/file1.sigma");
|
|
398
|
+
registry.registerTree("Tree2", tree2, "/path/to/file2.sigma");
|
|
399
|
+
|
|
400
|
+
const allIds = registry.getAllTreeIds();
|
|
401
|
+
|
|
402
|
+
expect(allIds).toContain("Tree1");
|
|
403
|
+
expect(allIds).toContain("Tree2");
|
|
404
|
+
expect(allIds).toHaveLength(2);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("should unregister a tree and return true", () => {
|
|
408
|
+
const node = new Sequence({ id: "seq1" });
|
|
409
|
+
const tree = new BehaviorTree(node);
|
|
410
|
+
|
|
411
|
+
registry.registerTree("TestTree", tree, "/path/to/file.sigma");
|
|
412
|
+
expect(registry.hasTree("TestTree")).toBe(true);
|
|
413
|
+
|
|
414
|
+
const result = registry.unregisterTree("TestTree");
|
|
415
|
+
|
|
416
|
+
expect(result).toBe(true);
|
|
417
|
+
expect(registry.hasTree("TestTree")).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should return false when unregistering non-existent tree", () => {
|
|
421
|
+
const result = registry.unregisterTree("NonExistent");
|
|
422
|
+
expect(result).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should replace an existing tree", () => {
|
|
426
|
+
const node1 = new Sequence({ id: "seq1" });
|
|
427
|
+
const tree1 = new BehaviorTree(node1);
|
|
428
|
+
const node2 = new Sequence({ id: "seq2" });
|
|
429
|
+
const tree2 = new BehaviorTree(node2);
|
|
430
|
+
|
|
431
|
+
registry.registerTree("TestTree", tree1, "/path/to/old.sigma");
|
|
432
|
+
expect(registry.getTree("TestTree")).toBe(tree1);
|
|
433
|
+
expect(registry.getTreeSourceFile("TestTree")).toBe("/path/to/old.sigma");
|
|
434
|
+
|
|
435
|
+
registry.replaceTree("TestTree", tree2, "/path/to/new.sigma");
|
|
436
|
+
|
|
437
|
+
expect(registry.getTree("TestTree")).toBe(tree2);
|
|
438
|
+
expect(registry.getTreeSourceFile("TestTree")).toBe("/path/to/new.sigma");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("should register new tree when replacing non-existent tree", () => {
|
|
442
|
+
const node = new Sequence({ id: "seq1" });
|
|
443
|
+
const tree = new BehaviorTree(node);
|
|
444
|
+
|
|
445
|
+
registry.replaceTree("NewTree", tree, "/path/to/file.sigma");
|
|
446
|
+
|
|
447
|
+
expect(registry.hasTree("NewTree")).toBe(true);
|
|
448
|
+
expect(registry.getTree("NewTree")).toBe(tree);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("should clear trees when calling clear()", () => {
|
|
452
|
+
const node = new Sequence({ id: "seq1" });
|
|
453
|
+
const tree = new BehaviorTree(node);
|
|
454
|
+
|
|
455
|
+
registry.register("TestNode", Sequence);
|
|
456
|
+
registry.registerTree("TestTree", tree, "/path/to/file.sigma");
|
|
457
|
+
|
|
458
|
+
expect(registry.has("TestNode")).toBe(true);
|
|
459
|
+
expect(registry.hasTree("TestTree")).toBe(true);
|
|
460
|
+
|
|
461
|
+
registry.clear();
|
|
462
|
+
|
|
463
|
+
expect(registry.has("TestNode")).toBe(false);
|
|
464
|
+
expect(registry.hasTree("TestTree")).toBe(false);
|
|
465
|
+
});
|
|
466
|
+
});
|