@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PythonScript Node
|
|
3
|
+
*
|
|
4
|
+
* Executes Python code via a cross-language Temporal activity.
|
|
5
|
+
* This node requires the `executePythonScript` activity to be configured
|
|
6
|
+
* in the context - it does not support standalone/inline execution because
|
|
7
|
+
* Python execution requires a separate Python worker process.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Access to blackboard state via `bb` dict in Python
|
|
11
|
+
* - Access to workflow input via `input` dict in Python
|
|
12
|
+
* - Modifications to `bb` dict are merged back to blackboard
|
|
13
|
+
* - Configurable timeout and environment variables
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ActionNode } from "../base-node.js";
|
|
17
|
+
import { ConfigurationError } from "../errors.js";
|
|
18
|
+
import {
|
|
19
|
+
type TemporalContext,
|
|
20
|
+
type NodeConfiguration,
|
|
21
|
+
type PythonScriptRequest,
|
|
22
|
+
NodeStatus,
|
|
23
|
+
} from "../types.js";
|
|
24
|
+
import { resolveString, type VariableContext } from "../utilities/variable-resolver.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for PythonScript node
|
|
28
|
+
*/
|
|
29
|
+
export interface PythonScriptConfig extends NodeConfiguration {
|
|
30
|
+
/** Python code to execute */
|
|
31
|
+
code: string;
|
|
32
|
+
/** Required packages (for documentation/validation) */
|
|
33
|
+
packages?: string[];
|
|
34
|
+
/** Execution timeout in ms (default: 60000) */
|
|
35
|
+
timeout?: number;
|
|
36
|
+
/** Allowed environment variables to pass to Python */
|
|
37
|
+
allowedEnvVars?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* PythonScript Node
|
|
42
|
+
*
|
|
43
|
+
* Executes Python code via a cross-language Temporal activity.
|
|
44
|
+
* The Python code has access to:
|
|
45
|
+
* - `bb`: dict - Blackboard state (read/write)
|
|
46
|
+
* - `input`: dict - Workflow input (read-only)
|
|
47
|
+
*
|
|
48
|
+
* @example YAML
|
|
49
|
+
* ```yaml
|
|
50
|
+
* type: PythonScript
|
|
51
|
+
* id: transform-data
|
|
52
|
+
* props:
|
|
53
|
+
* code: |
|
|
54
|
+
* import pandas as pd
|
|
55
|
+
* df = pd.DataFrame(bb['orders'])
|
|
56
|
+
* bb['total'] = df['amount'].sum()
|
|
57
|
+
* bb['count'] = len(df)
|
|
58
|
+
* packages:
|
|
59
|
+
* - pandas
|
|
60
|
+
* timeout: 30000
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class PythonScript extends ActionNode {
|
|
64
|
+
private code: string;
|
|
65
|
+
private packages: string[];
|
|
66
|
+
private timeout: number;
|
|
67
|
+
private allowedEnvVars: string[];
|
|
68
|
+
|
|
69
|
+
constructor(config: PythonScriptConfig) {
|
|
70
|
+
super(config);
|
|
71
|
+
|
|
72
|
+
if (!config.code) {
|
|
73
|
+
throw new ConfigurationError("PythonScript requires code");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.code = config.code;
|
|
77
|
+
this.packages = config.packages || [];
|
|
78
|
+
this.timeout = config.timeout || 60000;
|
|
79
|
+
this.allowedEnvVars = config.allowedEnvVars || [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
83
|
+
// Validate activity is available (Python requires cross-language activity)
|
|
84
|
+
if (!context.activities?.executePythonScript) {
|
|
85
|
+
this._lastError =
|
|
86
|
+
"PythonScript requires activities.executePythonScript to be configured. " +
|
|
87
|
+
"This activity executes Python code in a separate worker process.";
|
|
88
|
+
this.log(`Error: ${this._lastError}`);
|
|
89
|
+
return NodeStatus.FAILURE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const request: PythonScriptRequest = {
|
|
94
|
+
code: this.resolveCode(context),
|
|
95
|
+
blackboard: context.blackboard.toJSON(),
|
|
96
|
+
input: context.input ? { ...context.input } : undefined,
|
|
97
|
+
env: this.getAllowedEnv(),
|
|
98
|
+
timeout: this.timeout,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.log(`Executing Python script (${this.code.length} chars, timeout: ${this.timeout}ms)`);
|
|
102
|
+
const result = await context.activities.executePythonScript(request);
|
|
103
|
+
|
|
104
|
+
// Merge Python changes back to blackboard
|
|
105
|
+
for (const [key, value] of Object.entries(result.blackboard)) {
|
|
106
|
+
context.blackboard.set(key, value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.log(`Python script completed, ${Object.keys(result.blackboard).length} blackboard keys updated`);
|
|
110
|
+
|
|
111
|
+
// Log stdout/stderr if present
|
|
112
|
+
if (result.stdout) {
|
|
113
|
+
this.log(`Python stdout: ${result.stdout}`);
|
|
114
|
+
}
|
|
115
|
+
if (result.stderr) {
|
|
116
|
+
this.log(`Python stderr: ${result.stderr}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return NodeStatus.SUCCESS;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this._lastError = error instanceof Error ? error.message : String(error);
|
|
122
|
+
this.log(`Python script failed: ${this._lastError}`);
|
|
123
|
+
return NodeStatus.FAILURE;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve variable references in the Python code
|
|
129
|
+
* Allows dynamic code templates like:
|
|
130
|
+
* code: "bb['output_key'] = '${input.prefix}_result'"
|
|
131
|
+
*/
|
|
132
|
+
private resolveCode(context: TemporalContext): string {
|
|
133
|
+
const varCtx: VariableContext = {
|
|
134
|
+
blackboard: context.blackboard,
|
|
135
|
+
input: context.input,
|
|
136
|
+
testData: context.testData,
|
|
137
|
+
};
|
|
138
|
+
return resolveString(this.code, varCtx) as string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get allowed environment variables to pass to Python
|
|
143
|
+
*/
|
|
144
|
+
private getAllowedEnv(): Record<string, string> {
|
|
145
|
+
const env: Record<string, string> = {};
|
|
146
|
+
for (const varName of this.allowedEnvVars) {
|
|
147
|
+
const value = process.env[varName];
|
|
148
|
+
if (value !== undefined) {
|
|
149
|
+
env[varName] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return env;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ActionNode, CompositeNode, DecoratorNode } from "./base-node.js";
|
|
3
|
+
import { ScopedBlackboard } from "./blackboard.js";
|
|
4
|
+
import { ConfigurationError } from "./errors.js";
|
|
5
|
+
import { NodeEventEmitter, NodeEventType } from "./events.js";
|
|
6
|
+
import {
|
|
7
|
+
type TemporalContext,
|
|
8
|
+
type NodeConfiguration,
|
|
9
|
+
NodeStatus,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import { OperationCancelledError } from "./utils/signal-check.js";
|
|
12
|
+
|
|
13
|
+
// Mock implementations for testing
|
|
14
|
+
class MockActionNode extends ActionNode {
|
|
15
|
+
async executeTick(_context: TemporalContext) {
|
|
16
|
+
this._status = NodeStatus.SUCCESS;
|
|
17
|
+
return NodeStatus.SUCCESS;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class MockDecoratorNode extends DecoratorNode {
|
|
22
|
+
async executeTick(context: TemporalContext) {
|
|
23
|
+
if (!this.child) {
|
|
24
|
+
throw new Error("No child");
|
|
25
|
+
}
|
|
26
|
+
return await this.child.tick(context);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MockCompositeNode extends CompositeNode {
|
|
31
|
+
async executeTick(context: TemporalContext) {
|
|
32
|
+
for (const child of this._children) {
|
|
33
|
+
const status = await child.tick(context);
|
|
34
|
+
if (status !== NodeStatus.SUCCESS) {
|
|
35
|
+
return status;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return NodeStatus.SUCCESS;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("BaseNode", () => {
|
|
43
|
+
let context: TemporalContext;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
context = {
|
|
47
|
+
blackboard: new ScopedBlackboard(),
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
deltaTime: 0,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("Node initialization", () => {
|
|
54
|
+
it("should initialize with correct properties", () => {
|
|
55
|
+
const config: NodeConfiguration = {
|
|
56
|
+
id: "test-node",
|
|
57
|
+
name: "Test Node",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const node = new MockActionNode(config);
|
|
61
|
+
|
|
62
|
+
expect(node.id).toBe("test-node");
|
|
63
|
+
expect(node.name).toBe("Test Node");
|
|
64
|
+
expect(node.type).toBe("MockActionNode");
|
|
65
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should use id as name if name not provided", () => {
|
|
69
|
+
const config: NodeConfiguration = {
|
|
70
|
+
id: "test-node",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const node = new MockActionNode(config);
|
|
74
|
+
expect(node.name).toBe("test-node");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("Status management", () => {
|
|
79
|
+
it("should track node status", async () => {
|
|
80
|
+
const node = new MockActionNode({ id: "test" });
|
|
81
|
+
|
|
82
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
83
|
+
|
|
84
|
+
await node.tick(context);
|
|
85
|
+
expect(node.status()).toBe(NodeStatus.SUCCESS);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Halt and Reset", () => {
|
|
90
|
+
it("should halt running nodes", () => {
|
|
91
|
+
const node = new MockActionNode({ id: "test" });
|
|
92
|
+
(node as any)._status = NodeStatus.RUNNING;
|
|
93
|
+
|
|
94
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
95
|
+
node.halt();
|
|
96
|
+
|
|
97
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
98
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining("Halting..."),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should not halt non-running nodes", () => {
|
|
104
|
+
const node = new MockActionNode({ id: "test" });
|
|
105
|
+
(node as any)._status = NodeStatus.SUCCESS;
|
|
106
|
+
|
|
107
|
+
node.halt();
|
|
108
|
+
expect(node.status()).toBe(NodeStatus.SUCCESS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should reset node state", () => {
|
|
112
|
+
const node = new MockActionNode({ id: "test" });
|
|
113
|
+
(node as any)._status = NodeStatus.SUCCESS;
|
|
114
|
+
|
|
115
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
116
|
+
node.reset();
|
|
117
|
+
|
|
118
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
119
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
120
|
+
expect.stringContaining("Resetting..."),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("Input/Output helpers", () => {
|
|
126
|
+
it("should get input from blackboard", () => {
|
|
127
|
+
context.blackboard.set("testKey", "testValue");
|
|
128
|
+
|
|
129
|
+
const node = new MockActionNode({ id: "test" });
|
|
130
|
+
const value = (node as any).getInput(context, "testKey");
|
|
131
|
+
|
|
132
|
+
expect(value).toBe("testValue");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should use config mapping for input keys", () => {
|
|
136
|
+
context.blackboard.set("actualKey", "value");
|
|
137
|
+
|
|
138
|
+
const node = new MockActionNode({
|
|
139
|
+
id: "test",
|
|
140
|
+
testKey: "actualKey", // Map testKey to actualKey
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const value = (node as any).getInput(context, "testKey");
|
|
144
|
+
expect(value).toBe("value");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should return default value for missing input", () => {
|
|
148
|
+
const node = new MockActionNode({ id: "test" });
|
|
149
|
+
const value = (node as any).getInput(context, "missing", "default");
|
|
150
|
+
|
|
151
|
+
expect(value).toBe("default");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should set output to blackboard", () => {
|
|
155
|
+
const node = new MockActionNode({ id: "test" });
|
|
156
|
+
(node as any).setOutput(context, "outputKey", "outputValue");
|
|
157
|
+
|
|
158
|
+
expect(context.blackboard.get("outputKey")).toBe("outputValue");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should use config mapping for output keys", () => {
|
|
162
|
+
const node = new MockActionNode({
|
|
163
|
+
id: "test",
|
|
164
|
+
outputKey: "actualOutputKey",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
(node as any).setOutput(context, "outputKey", "value");
|
|
168
|
+
expect(context.blackboard.get("actualOutputKey")).toBe("value");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("DecoratorNode", () => {
|
|
174
|
+
let _context: TemporalContext;
|
|
175
|
+
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
_context = {
|
|
178
|
+
blackboard: new ScopedBlackboard(),
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
deltaTime: 0,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should manage single child", () => {
|
|
185
|
+
const decorator = new MockDecoratorNode({ id: "decorator" });
|
|
186
|
+
const child = new MockActionNode({ id: "child" });
|
|
187
|
+
|
|
188
|
+
decorator.setChild(child);
|
|
189
|
+
|
|
190
|
+
expect((decorator as any).child).toBe(child);
|
|
191
|
+
expect(decorator.children).toEqual([child]);
|
|
192
|
+
expect(child.parent).toBe(decorator);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should halt child when halted", () => {
|
|
196
|
+
const decorator = new MockDecoratorNode({ id: "decorator" });
|
|
197
|
+
const child = new MockActionNode({ id: "child" });
|
|
198
|
+
(child as any)._status = NodeStatus.RUNNING;
|
|
199
|
+
|
|
200
|
+
decorator.setChild(child);
|
|
201
|
+
|
|
202
|
+
const childHaltSpy = vi.spyOn(child, "halt");
|
|
203
|
+
decorator.halt();
|
|
204
|
+
|
|
205
|
+
expect(childHaltSpy).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should reset child when reset", () => {
|
|
209
|
+
const decorator = new MockDecoratorNode({ id: "decorator" });
|
|
210
|
+
const child = new MockActionNode({ id: "child" });
|
|
211
|
+
|
|
212
|
+
decorator.setChild(child);
|
|
213
|
+
|
|
214
|
+
const childResetSpy = vi.spyOn(child, "reset");
|
|
215
|
+
decorator.reset();
|
|
216
|
+
|
|
217
|
+
expect(childResetSpy).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("CompositeNode", () => {
|
|
222
|
+
let _context: TemporalContext;
|
|
223
|
+
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
_context = {
|
|
226
|
+
blackboard: new ScopedBlackboard(),
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
deltaTime: 0,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should manage multiple children", () => {
|
|
233
|
+
const composite = new MockCompositeNode({ id: "composite" });
|
|
234
|
+
const child1 = new MockActionNode({ id: "child1" });
|
|
235
|
+
const child2 = new MockActionNode({ id: "child2" });
|
|
236
|
+
|
|
237
|
+
composite.addChild(child1);
|
|
238
|
+
composite.addChild(child2);
|
|
239
|
+
|
|
240
|
+
expect(composite.children).toEqual([child1, child2]);
|
|
241
|
+
expect(child1.parent).toBe(composite);
|
|
242
|
+
expect(child2.parent).toBe(composite);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should add multiple children at once", () => {
|
|
246
|
+
const composite = new MockCompositeNode({ id: "composite" });
|
|
247
|
+
const children = [
|
|
248
|
+
new MockActionNode({ id: "child1" }),
|
|
249
|
+
new MockActionNode({ id: "child2" }),
|
|
250
|
+
new MockActionNode({ id: "child3" }),
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
composite.addChildren(children);
|
|
254
|
+
|
|
255
|
+
expect(composite.children).toEqual(children);
|
|
256
|
+
children.forEach((child) => {
|
|
257
|
+
expect(child.parent).toBe(composite);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should halt all running children", () => {
|
|
262
|
+
const composite = new MockCompositeNode({ id: "composite" });
|
|
263
|
+
const runningChild = new MockActionNode({ id: "running" });
|
|
264
|
+
const idleChild = new MockActionNode({ id: "idle" });
|
|
265
|
+
|
|
266
|
+
(runningChild as any)._status = NodeStatus.RUNNING;
|
|
267
|
+
(idleChild as any)._status = NodeStatus.IDLE;
|
|
268
|
+
|
|
269
|
+
composite.addChildren([runningChild, idleChild]);
|
|
270
|
+
|
|
271
|
+
const runningHaltSpy = vi.spyOn(runningChild, "halt");
|
|
272
|
+
const idleHaltSpy = vi.spyOn(idleChild, "halt");
|
|
273
|
+
|
|
274
|
+
composite.halt();
|
|
275
|
+
|
|
276
|
+
expect(runningHaltSpy).toHaveBeenCalled();
|
|
277
|
+
expect(idleHaltSpy).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should reset all children", () => {
|
|
281
|
+
const composite = new MockCompositeNode({ id: "composite" });
|
|
282
|
+
const child1 = new MockActionNode({ id: "child1" });
|
|
283
|
+
const child2 = new MockActionNode({ id: "child2" });
|
|
284
|
+
|
|
285
|
+
composite.addChildren([child1, child2]);
|
|
286
|
+
|
|
287
|
+
const reset1Spy = vi.spyOn(child1, "reset");
|
|
288
|
+
const reset2Spy = vi.spyOn(child2, "reset");
|
|
289
|
+
|
|
290
|
+
composite.reset();
|
|
291
|
+
|
|
292
|
+
expect(reset1Spy).toHaveBeenCalled();
|
|
293
|
+
expect(reset2Spy).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should halt children from specific index", () => {
|
|
297
|
+
const composite = new MockCompositeNode({ id: "composite" });
|
|
298
|
+
const children = [
|
|
299
|
+
new MockActionNode({ id: "child0" }),
|
|
300
|
+
new MockActionNode({ id: "child1" }),
|
|
301
|
+
new MockActionNode({ id: "child2" }),
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
children.forEach((child) => {
|
|
305
|
+
(child as any)._status = NodeStatus.RUNNING;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
composite.addChildren(children);
|
|
309
|
+
|
|
310
|
+
const haltSpies = children.map((child) => vi.spyOn(child, "halt"));
|
|
311
|
+
|
|
312
|
+
(composite as any).haltChildren(1); // Start from index 1
|
|
313
|
+
|
|
314
|
+
expect(haltSpies[0]).not.toHaveBeenCalled();
|
|
315
|
+
expect(haltSpies[1]).toHaveBeenCalled();
|
|
316
|
+
expect(haltSpies[2]).toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("lastError property", () => {
|
|
321
|
+
it("should initially be undefined", () => {
|
|
322
|
+
const node = new MockActionNode({ id: "test" });
|
|
323
|
+
expect(node.lastError).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should store error when set", () => {
|
|
327
|
+
const node = new MockActionNode({ id: "test" });
|
|
328
|
+
(node as any)._lastError = "Test error message";
|
|
329
|
+
expect(node.lastError).toBe("Test error message");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should clear error on reset", () => {
|
|
333
|
+
const node = new MockActionNode({ id: "test" });
|
|
334
|
+
(node as any)._lastError = "Test error message";
|
|
335
|
+
node.reset();
|
|
336
|
+
expect(node.lastError).toBeUndefined();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe("Error handling in tick()", () => {
|
|
341
|
+
let context: TemporalContext;
|
|
342
|
+
let eventEmitter: NodeEventEmitter;
|
|
343
|
+
let errorEvents: any[];
|
|
344
|
+
let tickEndEvents: any[];
|
|
345
|
+
|
|
346
|
+
beforeEach(() => {
|
|
347
|
+
errorEvents = [];
|
|
348
|
+
tickEndEvents = [];
|
|
349
|
+
eventEmitter = new NodeEventEmitter();
|
|
350
|
+
eventEmitter.on(NodeEventType.ERROR, (event) => errorEvents.push(event));
|
|
351
|
+
eventEmitter.on(NodeEventType.TICK_END, (event) =>
|
|
352
|
+
tickEndEvents.push(event),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
context = {
|
|
356
|
+
blackboard: new ScopedBlackboard(),
|
|
357
|
+
timestamp: Date.now(),
|
|
358
|
+
deltaTime: 0,
|
|
359
|
+
eventEmitter,
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should catch thrown errors and convert to FAILURE status", async () => {
|
|
364
|
+
class FailingNode extends ActionNode {
|
|
365
|
+
async executeTick(_context: TemporalContext) {
|
|
366
|
+
throw new Error("Test error from Effect.fail");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const node = new FailingNode({ id: "test" });
|
|
371
|
+
const status = await node.tick(context);
|
|
372
|
+
|
|
373
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
374
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
375
|
+
expect(node.lastError).toBe("Test error from Effect.fail");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should catch JavaScript throw and convert to FAILURE status", async () => {
|
|
379
|
+
class ThrowingNode extends ActionNode {
|
|
380
|
+
async executeTick(_context: TemporalContext): Promise<NodeStatus> {
|
|
381
|
+
throw new Error("Test error from throw");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const node = new ThrowingNode({ id: "test" });
|
|
386
|
+
const status = await node.tick(context);
|
|
387
|
+
|
|
388
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
389
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
390
|
+
expect(node.lastError).toBe("Test error from throw");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should emit ERROR event when error occurs", async () => {
|
|
394
|
+
class FailingNode extends ActionNode {
|
|
395
|
+
async executeTick(_context: TemporalContext) {
|
|
396
|
+
throw new Error("Test error");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const node = new FailingNode({ id: "test-id", name: "test-name" });
|
|
401
|
+
await node.tick(context);
|
|
402
|
+
|
|
403
|
+
expect(errorEvents.length).toBe(1);
|
|
404
|
+
expect(errorEvents[0].nodeId).toBe("test-id");
|
|
405
|
+
expect(errorEvents[0].nodeName).toBe("test-name");
|
|
406
|
+
expect(errorEvents[0].data.error).toBe("Test error");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should emit TICK_END with FAILURE status on error", async () => {
|
|
410
|
+
class FailingNode extends ActionNode {
|
|
411
|
+
async executeTick(_context: TemporalContext) {
|
|
412
|
+
throw new Error("Test error");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const node = new FailingNode({ id: "test" });
|
|
417
|
+
await node.tick(context);
|
|
418
|
+
|
|
419
|
+
const failureEvents = tickEndEvents.filter(
|
|
420
|
+
(e) => e.data.status === NodeStatus.FAILURE,
|
|
421
|
+
);
|
|
422
|
+
expect(failureEvents.length).toBe(1);
|
|
423
|
+
expect(failureEvents[0].nodeId).toBe("test");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should re-propagate OperationCancelledError", async () => {
|
|
427
|
+
class CancellingNode extends ActionNode {
|
|
428
|
+
async executeTick(_context: TemporalContext) {
|
|
429
|
+
throw new OperationCancelledError("Test cancellation");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const node = new CancellingNode({ id: "test" });
|
|
434
|
+
|
|
435
|
+
// OperationCancelledError should propagate as rejection
|
|
436
|
+
try {
|
|
437
|
+
await node.tick(context);
|
|
438
|
+
expect.fail("Should have thrown");
|
|
439
|
+
} catch (error) {
|
|
440
|
+
expect(error).toBeInstanceOf(OperationCancelledError);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// But node status should still be FAILURE
|
|
444
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
445
|
+
expect(node.lastError).toBe("Test cancellation");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("should emit ERROR event even for OperationCancelledError", async () => {
|
|
449
|
+
class CancellingNode extends ActionNode {
|
|
450
|
+
async executeTick(_context: TemporalContext): Promise<NodeStatus> {
|
|
451
|
+
throw new OperationCancelledError("Test cancellation");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const node = new CancellingNode({ id: "test" });
|
|
456
|
+
|
|
457
|
+
// Catch the re-propagated error
|
|
458
|
+
try {
|
|
459
|
+
await node.tick(context);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
// Expected to throw
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
expect(errorEvents.length).toBe(1);
|
|
465
|
+
expect(errorEvents[0].data.error).toBe("Test cancellation");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("should re-propagate ConfigurationError", async () => {
|
|
469
|
+
class MisconfiguredNode extends ActionNode {
|
|
470
|
+
async executeTick(_context: TemporalContext) {
|
|
471
|
+
throw new ConfigurationError("Test is broken - missing element");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const node = new MisconfiguredNode({ id: "test" });
|
|
476
|
+
|
|
477
|
+
// ConfigurationError should propagate as rejection
|
|
478
|
+
try {
|
|
479
|
+
await node.tick(context);
|
|
480
|
+
expect.fail("Should have thrown");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
expect(error).toBeInstanceOf(ConfigurationError);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// But node status should still be FAILURE
|
|
486
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
487
|
+
expect(node.lastError).toBe("Test is broken - missing element");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should emit ERROR event even for ConfigurationError", async () => {
|
|
491
|
+
class MisconfiguredNode extends ActionNode {
|
|
492
|
+
async executeTick(_context: TemporalContext): Promise<NodeStatus> {
|
|
493
|
+
throw new ConfigurationError("Test is broken - missing element");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const node = new MisconfiguredNode({ id: "test" });
|
|
498
|
+
|
|
499
|
+
// Catch the re-propagated error
|
|
500
|
+
try {
|
|
501
|
+
await node.tick(context);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// Expected to throw
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
expect(errorEvents.length).toBe(1);
|
|
507
|
+
expect(errorEvents[0].data.error).toBe(
|
|
508
|
+
"Test is broken - missing element",
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
});
|