@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,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for LogMessage Node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
afterEach,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
vi,
|
|
12
|
+
} from "vitest";
|
|
13
|
+
import { NodeEventEmitter, NodeEventType } from "../events.js";
|
|
14
|
+
import {
|
|
15
|
+
type TemporalContext,
|
|
16
|
+
NodeStatus,
|
|
17
|
+
ScopedBlackboard,
|
|
18
|
+
} from "../index.js";
|
|
19
|
+
import { LogMessage } from "./log-message.js";
|
|
20
|
+
|
|
21
|
+
describe("LogMessage", () => {
|
|
22
|
+
let blackboard: ScopedBlackboard;
|
|
23
|
+
let context: TemporalContext;
|
|
24
|
+
let consoleLogSpy: unknown;
|
|
25
|
+
let consoleWarnSpy: unknown;
|
|
26
|
+
let consoleErrorSpy: unknown;
|
|
27
|
+
let consoleDebugSpy: unknown;
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
blackboard = new ScopedBlackboard();
|
|
35
|
+
context = {
|
|
36
|
+
blackboard: blackboard,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
deltaTime: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Spy on console methods
|
|
42
|
+
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
43
|
+
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
44
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
45
|
+
consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should log a simple message", async () => {
|
|
49
|
+
const node = new LogMessage({
|
|
50
|
+
id: "log-1",
|
|
51
|
+
message: "Test message",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await node.tick(context);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
57
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
58
|
+
expect.stringContaining("[LogMessage:log-1] Test message"),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should log message with blackboard value placeholder", async () => {
|
|
63
|
+
blackboard.set("username", "testuser");
|
|
64
|
+
blackboard.set("count", 42);
|
|
65
|
+
|
|
66
|
+
const node = new LogMessage({
|
|
67
|
+
id: "log-2",
|
|
68
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
69
|
+
message: "User: ${username}, Count: ${count}",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await node.tick(context);
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
75
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
76
|
+
expect.stringContaining("[LogMessage:log-2] User: testuser, Count: 42"),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should handle missing blackboard values", async () => {
|
|
81
|
+
const node = new LogMessage({
|
|
82
|
+
id: "log-3",
|
|
83
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
84
|
+
message: "Value: ${missingKey}",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await node.tick(context);
|
|
88
|
+
|
|
89
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
90
|
+
// Should keep the placeholder if value is missing
|
|
91
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
92
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - testing placeholder syntax
|
|
93
|
+
expect.stringContaining("[LogMessage:log-3] Value: ${missingKey}"),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle null values", async () => {
|
|
98
|
+
blackboard.set("nullValue", null);
|
|
99
|
+
|
|
100
|
+
const node = new LogMessage({
|
|
101
|
+
id: "log-4",
|
|
102
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
103
|
+
message: "Null value: ${nullValue}",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await node.tick(context);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
109
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
110
|
+
expect.stringContaining("[LogMessage:log-4] Null value: null"),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should handle object values", async () => {
|
|
115
|
+
blackboard.set("user", { name: "John", age: 30 });
|
|
116
|
+
|
|
117
|
+
const node = new LogMessage({
|
|
118
|
+
id: "log-5",
|
|
119
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
120
|
+
message: "User: ${user}",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await node.tick(context);
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
126
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
127
|
+
expect.stringContaining(
|
|
128
|
+
'[LogMessage:log-5] User: {"name":"John","age":30}',
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should log with warn level", async () => {
|
|
134
|
+
const node = new LogMessage({
|
|
135
|
+
id: "log-6",
|
|
136
|
+
message: "Warning message",
|
|
137
|
+
level: "warn",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await node.tick(context);
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
143
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
144
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should log with error level", async () => {
|
|
148
|
+
const node = new LogMessage({
|
|
149
|
+
id: "log-7",
|
|
150
|
+
message: "Error message",
|
|
151
|
+
level: "error",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await node.tick(context);
|
|
155
|
+
|
|
156
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
157
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
158
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should log with debug level", async () => {
|
|
162
|
+
const node = new LogMessage({
|
|
163
|
+
id: "log-8",
|
|
164
|
+
message: "Debug message",
|
|
165
|
+
level: "debug",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await node.tick(context);
|
|
169
|
+
|
|
170
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
171
|
+
expect(consoleDebugSpy).toHaveBeenCalled();
|
|
172
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should default to info level", async () => {
|
|
176
|
+
const node = new LogMessage({
|
|
177
|
+
id: "log-9",
|
|
178
|
+
message: "Info message",
|
|
179
|
+
// level not specified
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await node.tick(context);
|
|
183
|
+
|
|
184
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
185
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should handle multiple placeholders", async () => {
|
|
189
|
+
blackboard.set("name", "Alice");
|
|
190
|
+
blackboard.set("age", 25);
|
|
191
|
+
blackboard.set("city", "New York");
|
|
192
|
+
|
|
193
|
+
const node = new LogMessage({
|
|
194
|
+
id: "log-10",
|
|
195
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
196
|
+
message: "${name} is ${age} years old and lives in ${city}",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = await node.tick(context);
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
202
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
203
|
+
expect.stringContaining(
|
|
204
|
+
"[LogMessage:log-10] Alice is 25 years old and lives in New York",
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should handle array values", async () => {
|
|
210
|
+
blackboard.set("items", ["apple", "banana", "cherry"]);
|
|
211
|
+
|
|
212
|
+
const node = new LogMessage({
|
|
213
|
+
id: "log-11",
|
|
214
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: This is intentional - LogMessage processes ${} syntax
|
|
215
|
+
message: "Items: ${items}",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const result = await node.tick(context);
|
|
219
|
+
|
|
220
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
221
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
222
|
+
expect.stringContaining(
|
|
223
|
+
'[LogMessage:log-11] Items: ["apple","banana","cherry"]',
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("LOG event emission", () => {
|
|
229
|
+
it("should emit LOG event when executed with event emitter", async () => {
|
|
230
|
+
const eventEmitter = new NodeEventEmitter();
|
|
231
|
+
const receivedEvents: unknown[] = [];
|
|
232
|
+
|
|
233
|
+
eventEmitter.on(NodeEventType.LOG, (event) => {
|
|
234
|
+
receivedEvents.push(event);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const contextWithEmitter: TemporalContext = {
|
|
238
|
+
...context,
|
|
239
|
+
eventEmitter,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const node = new LogMessage({
|
|
243
|
+
id: "log-event",
|
|
244
|
+
name: "EventTest",
|
|
245
|
+
message: "Event test message",
|
|
246
|
+
level: "warn",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await node.tick(contextWithEmitter);
|
|
250
|
+
|
|
251
|
+
expect(receivedEvents).toHaveLength(1);
|
|
252
|
+
const event = receivedEvents[0] as {
|
|
253
|
+
type: string;
|
|
254
|
+
nodeId: string;
|
|
255
|
+
data: { level: string; message: string };
|
|
256
|
+
};
|
|
257
|
+
expect(event.type).toBe(NodeEventType.LOG);
|
|
258
|
+
expect(event.nodeId).toBe("log-event");
|
|
259
|
+
expect(event.data.level).toBe("warn");
|
|
260
|
+
expect(event.data.message).toBe("Event test message");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should not fail when no event emitter present", async () => {
|
|
264
|
+
const node = new LogMessage({
|
|
265
|
+
id: "log-no-emitter",
|
|
266
|
+
message: "No emitter test",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Context without eventEmitter
|
|
270
|
+
const result = await node.tick(context);
|
|
271
|
+
|
|
272
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogMessage Node - Log messages for debugging and test visibility
|
|
3
|
+
*
|
|
4
|
+
* Utility node that logs messages to console with optional variable resolution.
|
|
5
|
+
* Useful for debugging test flows and tracking execution progress.
|
|
6
|
+
*
|
|
7
|
+
* Supports variable resolution:
|
|
8
|
+
* - ${key} - Shorthand for blackboard (backward compatible)
|
|
9
|
+
* - ${input.key} - Workflow input parameters
|
|
10
|
+
* - ${bb.key} - Blackboard values
|
|
11
|
+
* - ${env.KEY} - Environment variables
|
|
12
|
+
* - ${param.key} - Test data parameters
|
|
13
|
+
*
|
|
14
|
+
* Use Cases:
|
|
15
|
+
* - Log intermediate values during test execution
|
|
16
|
+
* - Debug blackboard state at specific points
|
|
17
|
+
* - Add visibility to test steps
|
|
18
|
+
* - Track execution flow
|
|
19
|
+
*
|
|
20
|
+
* Examples:
|
|
21
|
+
* - Log static message: <LogMessage message="Starting form submission" />
|
|
22
|
+
* - Log blackboard value: <LogMessage message="Current URL: ${currentUrl}" />
|
|
23
|
+
* - Log input value: <LogMessage message="Order ID: ${input.orderId}" />
|
|
24
|
+
* - Log with level: <LogMessage message="Error occurred" level="error" />
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { ActionNode } from "../base-node.js";
|
|
28
|
+
import { NodeEventType } from "../events.js";
|
|
29
|
+
import {
|
|
30
|
+
type TemporalContext,
|
|
31
|
+
type NodeConfiguration,
|
|
32
|
+
NodeStatus,
|
|
33
|
+
} from "../types.js";
|
|
34
|
+
import { resolveString, type VariableContext } from "./variable-resolver.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration for LogMessage node
|
|
38
|
+
*/
|
|
39
|
+
export interface LogMessageConfig extends NodeConfiguration {
|
|
40
|
+
message: string; // Message to log (supports ${key}, ${input.key}, ${bb.key}, ${env.KEY})
|
|
41
|
+
level?: "info" | "warn" | "error" | "debug"; // Log level (default: 'info')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* LogMessage Node - Logs messages with optional variable resolution
|
|
46
|
+
*/
|
|
47
|
+
export class LogMessage extends ActionNode {
|
|
48
|
+
private message: string;
|
|
49
|
+
private level: "info" | "warn" | "error" | "debug";
|
|
50
|
+
|
|
51
|
+
constructor(config: LogMessageConfig) {
|
|
52
|
+
super(config);
|
|
53
|
+
this.message = config.message;
|
|
54
|
+
this.level = config.level || "info";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
58
|
+
try {
|
|
59
|
+
// Resolve variable references in message
|
|
60
|
+
const resolvedMessage = this.resolveMessage(this.message, context);
|
|
61
|
+
|
|
62
|
+
// Log based on level
|
|
63
|
+
switch (this.level) {
|
|
64
|
+
case "warn":
|
|
65
|
+
console.warn(`[LogMessage:${this.name}] ${resolvedMessage}`);
|
|
66
|
+
break;
|
|
67
|
+
case "error":
|
|
68
|
+
console.error(`[LogMessage:${this.name}] ${resolvedMessage}`);
|
|
69
|
+
break;
|
|
70
|
+
case "debug":
|
|
71
|
+
console.debug(`[LogMessage:${this.name}] ${resolvedMessage}`);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
console.log(`[LogMessage:${this.name}] ${resolvedMessage}`);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Emit LOG event for collection
|
|
79
|
+
context.eventEmitter?.emit({
|
|
80
|
+
type: NodeEventType.LOG,
|
|
81
|
+
nodeId: this.id,
|
|
82
|
+
nodeName: this.name,
|
|
83
|
+
nodeType: this.type,
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
data: { level: this.level, message: resolvedMessage },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this._status = NodeStatus.SUCCESS;
|
|
89
|
+
return NodeStatus.SUCCESS;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const errorMessage =
|
|
92
|
+
error instanceof Error ? error.message : String(error);
|
|
93
|
+
console.error(
|
|
94
|
+
`[LogMessage:${this.name}] Failed to log message: ${errorMessage}`,
|
|
95
|
+
);
|
|
96
|
+
this._status = NodeStatus.FAILURE;
|
|
97
|
+
return NodeStatus.FAILURE;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve variable references in message string
|
|
103
|
+
* Supports ${key}, ${input.key}, ${bb.key}, ${env.KEY}, ${param.key}
|
|
104
|
+
*/
|
|
105
|
+
private resolveMessage(message: string, context: TemporalContext): string {
|
|
106
|
+
const varCtx: VariableContext = {
|
|
107
|
+
blackboard: context.blackboard,
|
|
108
|
+
input: context.input,
|
|
109
|
+
testData: context.testData,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const resolved = resolveString(message, varCtx);
|
|
113
|
+
|
|
114
|
+
// Always return a string for logging
|
|
115
|
+
if (typeof resolved === "string") {
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If resolved to non-string (shouldn't happen for message templates), stringify
|
|
120
|
+
if (resolved === null) {
|
|
121
|
+
return "null";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof resolved === "object") {
|
|
125
|
+
try {
|
|
126
|
+
return JSON.stringify(resolved);
|
|
127
|
+
} catch {
|
|
128
|
+
return String(resolved);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return String(resolved);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RegexExtract Node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
type TemporalContext,
|
|
8
|
+
NodeStatus,
|
|
9
|
+
ScopedBlackboard,
|
|
10
|
+
} from "../index.js";
|
|
11
|
+
import { RegexExtract } from "./regex-extract.js";
|
|
12
|
+
|
|
13
|
+
describe("RegexExtract", () => {
|
|
14
|
+
let blackboard: ScopedBlackboard;
|
|
15
|
+
let context: TemporalContext;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
blackboard = new ScopedBlackboard();
|
|
19
|
+
context = {
|
|
20
|
+
blackboard: blackboard,
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
deltaTime: 0,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should extract all matches when matchIndex is not specified", async () => {
|
|
27
|
+
blackboard.set("text", "Contact: support@example.com, sales@example.com");
|
|
28
|
+
|
|
29
|
+
const node = new RegexExtract({
|
|
30
|
+
id: "extract-emails",
|
|
31
|
+
input: "text",
|
|
32
|
+
pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
|
33
|
+
outputKey: "emails",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await node.tick(context);
|
|
37
|
+
|
|
38
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
39
|
+
const emails = blackboard.get("emails");
|
|
40
|
+
expect(emails).toEqual(["support@example.com", "sales@example.com"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should extract specific match when matchIndex is specified", async () => {
|
|
44
|
+
blackboard.set("text", "Price: $99.99 and $149.99");
|
|
45
|
+
|
|
46
|
+
const node = new RegexExtract({
|
|
47
|
+
id: "extract-price",
|
|
48
|
+
input: "text",
|
|
49
|
+
pattern: "\\$\\d+\\.\\d{2}",
|
|
50
|
+
outputKey: "firstPrice",
|
|
51
|
+
matchIndex: 0,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await node.tick(context);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
57
|
+
const price = blackboard.get("firstPrice");
|
|
58
|
+
expect(price).toBe("$99.99");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should return null when matchIndex is out of bounds", async () => {
|
|
62
|
+
blackboard.set("text", "No numbers here");
|
|
63
|
+
|
|
64
|
+
const node = new RegexExtract({
|
|
65
|
+
id: "extract-number",
|
|
66
|
+
input: "text",
|
|
67
|
+
pattern: "\\d+",
|
|
68
|
+
outputKey: "number",
|
|
69
|
+
matchIndex: 0,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await node.tick(context);
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
75
|
+
const number = blackboard.get("number");
|
|
76
|
+
expect(number).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return empty array when no matches found", async () => {
|
|
80
|
+
blackboard.set("text", "No emails here");
|
|
81
|
+
|
|
82
|
+
const node = new RegexExtract({
|
|
83
|
+
id: "extract-emails",
|
|
84
|
+
input: "text",
|
|
85
|
+
pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
|
86
|
+
outputKey: "emails",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await node.tick(context);
|
|
90
|
+
|
|
91
|
+
expect(result).toBe(NodeStatus.SUCCESS);
|
|
92
|
+
const emails = blackboard.get("emails");
|
|
93
|
+
expect(emails).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should fail when input is not a string", async () => {
|
|
97
|
+
blackboard.set("text", 123);
|
|
98
|
+
|
|
99
|
+
const node = new RegexExtract({
|
|
100
|
+
id: "extract",
|
|
101
|
+
input: "text",
|
|
102
|
+
pattern: "\\d+",
|
|
103
|
+
outputKey: "result",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await node.tick(context);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should fail when input is not found in blackboard", async () => {
|
|
112
|
+
const node = new RegexExtract({
|
|
113
|
+
id: "extract",
|
|
114
|
+
input: "missing",
|
|
115
|
+
pattern: "\\d+",
|
|
116
|
+
outputKey: "result",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await node.tick(context);
|
|
120
|
+
|
|
121
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should fail when regex pattern is invalid", async () => {
|
|
125
|
+
blackboard.set("text", "test");
|
|
126
|
+
|
|
127
|
+
const node = new RegexExtract({
|
|
128
|
+
id: "extract",
|
|
129
|
+
input: "text",
|
|
130
|
+
pattern: "[invalid",
|
|
131
|
+
outputKey: "result",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await node.tick(context);
|
|
135
|
+
|
|
136
|
+
expect(result).toBe(NodeStatus.FAILURE);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegexExtract Node - Extract text using regular expressions
|
|
3
|
+
*
|
|
4
|
+
* Data manipulation utility node that operates on blackboard values.
|
|
5
|
+
* Extracts matches from a text string using a regex pattern.
|
|
6
|
+
*
|
|
7
|
+
* Use Cases:
|
|
8
|
+
* - Extract email addresses from text
|
|
9
|
+
* - Extract numbers/IDs from strings
|
|
10
|
+
* - Parse structured data from unstructured text
|
|
11
|
+
* - Extract URLs from content
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ActionNode } from "../base-node.js";
|
|
15
|
+
import { ConfigurationError } from "../errors.js";
|
|
16
|
+
import {
|
|
17
|
+
type TemporalContext,
|
|
18
|
+
type NodeConfiguration,
|
|
19
|
+
NodeStatus,
|
|
20
|
+
} from "../types.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for RegexExtract node
|
|
24
|
+
*/
|
|
25
|
+
export interface RegexExtractConfig extends NodeConfiguration {
|
|
26
|
+
input: string; // Blackboard key containing text to extract from
|
|
27
|
+
pattern: string; // Regular expression pattern
|
|
28
|
+
outputKey: string; // Blackboard key to store extracted matches
|
|
29
|
+
flags?: string; // Regex flags (default: "g" for global)
|
|
30
|
+
matchIndex?: number; // Which match to return (0 = first, undefined = all matches array)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* RegexExtract Node - Extracts text using regular expressions
|
|
35
|
+
*/
|
|
36
|
+
export class RegexExtract extends ActionNode {
|
|
37
|
+
private input: string;
|
|
38
|
+
private pattern: string;
|
|
39
|
+
private outputKey: string;
|
|
40
|
+
private flags: string;
|
|
41
|
+
private matchIndex?: number;
|
|
42
|
+
|
|
43
|
+
constructor(config: RegexExtractConfig) {
|
|
44
|
+
super(config);
|
|
45
|
+
this.input = config.input;
|
|
46
|
+
this.pattern = config.pattern;
|
|
47
|
+
this.outputKey = config.outputKey;
|
|
48
|
+
this.flags = config.flags || "g";
|
|
49
|
+
this.matchIndex = config.matchIndex;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
53
|
+
try {
|
|
54
|
+
// Get input text from blackboard
|
|
55
|
+
const text = context.blackboard.get(this.input);
|
|
56
|
+
|
|
57
|
+
// Validate input
|
|
58
|
+
if (text === undefined || text === null) {
|
|
59
|
+
throw new ConfigurationError(
|
|
60
|
+
`Input '${this.input}' not found in blackboard`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof text !== "string") {
|
|
65
|
+
throw new ConfigurationError(
|
|
66
|
+
`Input '${this.input}' must be a string, got ${typeof text}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create regex from pattern and flags
|
|
71
|
+
let regex: RegExp;
|
|
72
|
+
try {
|
|
73
|
+
regex = new RegExp(this.pattern, this.flags);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const errorMessage =
|
|
76
|
+
error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new Error(`Invalid regex pattern: ${errorMessage}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract matches
|
|
81
|
+
const matches = text.match(regex) || [];
|
|
82
|
+
|
|
83
|
+
// Determine output value
|
|
84
|
+
let result: string | string[] | null;
|
|
85
|
+
if (this.matchIndex !== undefined && this.matchIndex >= 0) {
|
|
86
|
+
// Return specific match
|
|
87
|
+
result = matches[this.matchIndex] || null;
|
|
88
|
+
this.log(`Extracted match at index ${this.matchIndex}: ${result}`);
|
|
89
|
+
} else {
|
|
90
|
+
// Return all matches as array
|
|
91
|
+
result = matches;
|
|
92
|
+
this.log(`Extracted ${matches.length} match(es) from input`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Store result in blackboard
|
|
96
|
+
this.setOutput(context, this.outputKey, result);
|
|
97
|
+
|
|
98
|
+
this._status = NodeStatus.SUCCESS;
|
|
99
|
+
return NodeStatus.SUCCESS;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const errorMessage =
|
|
102
|
+
error instanceof Error ? error.message : String(error);
|
|
103
|
+
this.log(`RegexExtract failed: ${errorMessage}`);
|
|
104
|
+
this._status = NodeStatus.FAILURE;
|
|
105
|
+
return NodeStatus.FAILURE;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|