@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.
Files changed (203) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/CLAUDE.md +181 -0
  3. package/LICENSE +21 -0
  4. package/README.md +920 -0
  5. package/behaviour-tree-workflows-landing/index.html +16 -0
  6. package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
  7. package/behaviour-tree-workflows-landing/package.json +31 -0
  8. package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
  9. package/behaviour-tree-workflows-landing/src/App.css +103 -0
  10. package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
  11. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
  12. package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
  13. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
  14. package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
  15. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
  16. package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
  17. package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
  18. package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
  19. package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
  20. package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
  21. package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
  22. package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
  23. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
  24. package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
  25. package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
  26. package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
  27. package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
  28. package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
  29. package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
  30. package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
  31. package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
  32. package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
  33. package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
  34. package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
  35. package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
  36. package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
  37. package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
  38. package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
  39. package/demo-google-sheets.ts +181 -0
  40. package/demo-runtime-variables.ts +174 -0
  41. package/demo-template.ts +208 -0
  42. package/docs/ARCHITECTURE_SUMMARY.md +613 -0
  43. package/docs/NODE_REFERENCE.md +504 -0
  44. package/docs/README.md +53 -0
  45. package/docs/custom-nodes-architecture.md +826 -0
  46. package/docs/observability.md +175 -0
  47. package/docs/yaml-specification.md +990 -0
  48. package/examples/temporal/README.md +117 -0
  49. package/examples/temporal/activities.ts +373 -0
  50. package/examples/temporal/client.ts +115 -0
  51. package/examples/temporal/python-worker/activities.py +339 -0
  52. package/examples/temporal/python-worker/requirements.txt +12 -0
  53. package/examples/temporal/python-worker/worker.py +106 -0
  54. package/examples/temporal/worker.ts +66 -0
  55. package/examples/temporal/workflows.ts +6 -0
  56. package/examples/temporal/yaml-workflow-loader.ts +105 -0
  57. package/examples/yaml-test.ts +97 -0
  58. package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
  59. package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
  60. package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
  61. package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
  62. package/examples/yaml-workflows/05-order-processing.yaml +146 -0
  63. package/examples/yaml-workflows/06-activity-test.yaml +71 -0
  64. package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
  65. package/examples/yaml-workflows/08-file-processing.yaml +141 -0
  66. package/examples/yaml-workflows/09-http-request.yaml +137 -0
  67. package/examples/yaml-workflows/README.md +211 -0
  68. package/package.json +38 -0
  69. package/src/actions/code-execution.schema.ts +27 -0
  70. package/src/actions/code-execution.ts +218 -0
  71. package/src/actions/generate-file.test.ts +516 -0
  72. package/src/actions/generate-file.ts +166 -0
  73. package/src/actions/http-request.test.ts +784 -0
  74. package/src/actions/http-request.ts +228 -0
  75. package/src/actions/index.ts +20 -0
  76. package/src/actions/parse-file.test.ts +448 -0
  77. package/src/actions/parse-file.ts +139 -0
  78. package/src/actions/python-script.test.ts +439 -0
  79. package/src/actions/python-script.ts +154 -0
  80. package/src/base-node.test.ts +511 -0
  81. package/src/base-node.ts +605 -0
  82. package/src/behavior-tree.test.ts +431 -0
  83. package/src/behavior-tree.ts +283 -0
  84. package/src/blackboard.test.ts +222 -0
  85. package/src/blackboard.ts +192 -0
  86. package/src/composites/conditional.schema.ts +19 -0
  87. package/src/composites/conditional.test.ts +309 -0
  88. package/src/composites/conditional.ts +129 -0
  89. package/src/composites/for-each.schema.ts +23 -0
  90. package/src/composites/for-each.test.ts +254 -0
  91. package/src/composites/for-each.ts +132 -0
  92. package/src/composites/index.ts +15 -0
  93. package/src/composites/memory-sequence.schema.ts +19 -0
  94. package/src/composites/memory-sequence.test.ts +223 -0
  95. package/src/composites/memory-sequence.ts +98 -0
  96. package/src/composites/parallel.schema.ts +28 -0
  97. package/src/composites/parallel.test.ts +502 -0
  98. package/src/composites/parallel.ts +157 -0
  99. package/src/composites/reactive-sequence.schema.ts +19 -0
  100. package/src/composites/reactive-sequence.test.ts +170 -0
  101. package/src/composites/reactive-sequence.ts +85 -0
  102. package/src/composites/recovery.schema.ts +19 -0
  103. package/src/composites/recovery.test.ts +366 -0
  104. package/src/composites/recovery.ts +90 -0
  105. package/src/composites/selector.schema.ts +19 -0
  106. package/src/composites/selector.test.ts +387 -0
  107. package/src/composites/selector.ts +85 -0
  108. package/src/composites/sequence.schema.ts +19 -0
  109. package/src/composites/sequence.test.ts +337 -0
  110. package/src/composites/sequence.ts +72 -0
  111. package/src/composites/sub-tree.schema.ts +21 -0
  112. package/src/composites/sub-tree.test.ts +893 -0
  113. package/src/composites/sub-tree.ts +177 -0
  114. package/src/composites/while.schema.ts +24 -0
  115. package/src/composites/while.test.ts +381 -0
  116. package/src/composites/while.ts +149 -0
  117. package/src/data-store/index.ts +10 -0
  118. package/src/data-store/memory-store.ts +161 -0
  119. package/src/data-store/types.ts +94 -0
  120. package/src/debug/breakpoint.test.ts +47 -0
  121. package/src/debug/breakpoint.ts +30 -0
  122. package/src/debug/index.ts +17 -0
  123. package/src/debug/resume-point.test.ts +49 -0
  124. package/src/debug/resume-point.ts +29 -0
  125. package/src/decorators/delay.schema.ts +21 -0
  126. package/src/decorators/delay.test.ts +261 -0
  127. package/src/decorators/delay.ts +140 -0
  128. package/src/decorators/force-result.schema.ts +32 -0
  129. package/src/decorators/force-result.test.ts +133 -0
  130. package/src/decorators/force-result.ts +63 -0
  131. package/src/decorators/index.ts +13 -0
  132. package/src/decorators/invert.schema.ts +19 -0
  133. package/src/decorators/invert.test.ts +135 -0
  134. package/src/decorators/invert.ts +42 -0
  135. package/src/decorators/keep-running.schema.ts +20 -0
  136. package/src/decorators/keep-running.test.ts +105 -0
  137. package/src/decorators/keep-running.ts +49 -0
  138. package/src/decorators/precondition.schema.ts +19 -0
  139. package/src/decorators/precondition.test.ts +351 -0
  140. package/src/decorators/precondition.ts +139 -0
  141. package/src/decorators/repeat.schema.ts +21 -0
  142. package/src/decorators/repeat.test.ts +187 -0
  143. package/src/decorators/repeat.ts +94 -0
  144. package/src/decorators/run-once.schema.ts +19 -0
  145. package/src/decorators/run-once.test.ts +140 -0
  146. package/src/decorators/run-once.ts +61 -0
  147. package/src/decorators/soft-assert.schema.ts +19 -0
  148. package/src/decorators/soft-assert.test.ts +107 -0
  149. package/src/decorators/soft-assert.ts +68 -0
  150. package/src/decorators/timeout.schema.ts +21 -0
  151. package/src/decorators/timeout.test.ts +274 -0
  152. package/src/decorators/timeout.ts +159 -0
  153. package/src/errors.test.ts +63 -0
  154. package/src/errors.ts +34 -0
  155. package/src/events.test.ts +347 -0
  156. package/src/events.ts +183 -0
  157. package/src/index.ts +80 -0
  158. package/src/integrations/index.ts +30 -0
  159. package/src/integrations/integration-action.test.ts +571 -0
  160. package/src/integrations/integration-action.ts +233 -0
  161. package/src/integrations/piece-executor.ts +320 -0
  162. package/src/observability/execution-tracker.ts +320 -0
  163. package/src/observability/index.ts +23 -0
  164. package/src/observability/sinks.ts +138 -0
  165. package/src/observability/types.ts +130 -0
  166. package/src/registry-utils.ts +147 -0
  167. package/src/registry.test.ts +466 -0
  168. package/src/registry.ts +334 -0
  169. package/src/schemas/base.schema.ts +104 -0
  170. package/src/schemas/index.ts +223 -0
  171. package/src/schemas/integration.test.ts +238 -0
  172. package/src/schemas/tree-definition.schema.ts +170 -0
  173. package/src/schemas/validation.test.ts +146 -0
  174. package/src/schemas/validation.ts +122 -0
  175. package/src/scripting/index.ts +22 -0
  176. package/src/templates/template-loader.test.ts +281 -0
  177. package/src/templates/template-loader.ts +152 -0
  178. package/src/temporal-integration.test.ts +213 -0
  179. package/src/test-nodes.ts +259 -0
  180. package/src/types.ts +503 -0
  181. package/src/utilities/index.ts +17 -0
  182. package/src/utilities/log-message.test.ts +275 -0
  183. package/src/utilities/log-message.ts +134 -0
  184. package/src/utilities/regex-extract.test.ts +138 -0
  185. package/src/utilities/regex-extract.ts +108 -0
  186. package/src/utilities/variable-resolver.test.ts +416 -0
  187. package/src/utilities/variable-resolver.ts +318 -0
  188. package/src/utils/error-handler.test.ts +117 -0
  189. package/src/utils/error-handler.ts +48 -0
  190. package/src/utils/signal-check.test.ts +234 -0
  191. package/src/utils/signal-check.ts +140 -0
  192. package/src/yaml/errors.ts +143 -0
  193. package/src/yaml/index.ts +30 -0
  194. package/src/yaml/loader.ts +39 -0
  195. package/src/yaml/parser.ts +286 -0
  196. package/src/yaml/validation/semantic-validator.ts +196 -0
  197. package/templates/google-sheets/insert-row.yaml +76 -0
  198. package/templates/notification-sender.yaml +33 -0
  199. package/templates/order-validation.yaml +44 -0
  200. package/tsconfig.json +24 -0
  201. package/vitest.config.ts +25 -0
  202. package/workflows/order-processor.yaml +59 -0
  203. package/workflows/process-order-workflow.yaml +142 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Unified Variable Resolver
3
+ *
4
+ * Resolves variable references in strings and objects with support for multiple namespaces:
5
+ * - ${input.key} - Workflow input parameters (immutable)
6
+ * - ${bb.key} - Blackboard values (mutable runtime state)
7
+ * - ${env.KEY} - Environment variables
8
+ * - ${param.key} - Test data parameters
9
+ * - ${key} - Shorthand for ${bb.key} (backward compatibility)
10
+ *
11
+ * Features:
12
+ * - Nested property access: ${bb.user.profile.name}
13
+ * - Type preservation for full matches: "${bb.user}" returns the user object
14
+ * - String interpolation for partial matches: "Hello ${bb.name}!" returns string
15
+ */
16
+
17
+ import type { IScopedBlackboard } from "../types.js";
18
+
19
+ /**
20
+ * Context for variable resolution
21
+ */
22
+ export interface VariableContext {
23
+ /** Blackboard for runtime state */
24
+ blackboard: IScopedBlackboard;
25
+ /** Immutable workflow input parameters */
26
+ input?: Readonly<Record<string, unknown>>;
27
+ /** Test data parameters (from CSV, data tables, etc.) */
28
+ testData?: Map<string, unknown>;
29
+ }
30
+
31
+ /**
32
+ * Options for variable resolution
33
+ */
34
+ export interface ResolveOptions {
35
+ /** Whether to keep undefined placeholders in output (default: true) */
36
+ preserveUndefined?: boolean;
37
+ /** Custom environment source (default: process.env) */
38
+ envSource?: Record<string, string | undefined>;
39
+ }
40
+
41
+ // Pattern for variable references: ${namespace.key} or ${key}
42
+ // Matches: ${input.orderId}, ${bb.user.name}, ${env.API_KEY}, ${param.testId}, ${simpleKey}
43
+ const VARIABLE_PATTERN = /\$\{(input|bb|env|param)\.([a-zA-Z0-9_.]+)\}|\$\{([a-zA-Z0-9_.]+)\}/g;
44
+
45
+ // Same pattern without global flag for testing (avoids lastIndex issues)
46
+ const HAS_VARIABLE_PATTERN = /\$\{(input|bb|env|param)\.([a-zA-Z0-9_.]+)\}|\$\{([a-zA-Z0-9_.]+)\}/;
47
+
48
+ // Pattern for checking if entire string is a single variable reference
49
+ const FULL_MATCH_PATTERN = /^\$\{(input|bb|env|param)\.([a-zA-Z0-9_.]+)\}$|^\$\{([a-zA-Z0-9_.]+)\}$/;
50
+
51
+ /**
52
+ * Resolve a string containing variable references
53
+ *
54
+ * @param str - String potentially containing ${...} references
55
+ * @param ctx - Variable context with blackboard, input, testData
56
+ * @param opts - Resolution options
57
+ * @returns Resolved value (original type if full match, string if interpolation)
58
+ *
59
+ * @example
60
+ * // Full match - returns original type
61
+ * resolveString("${bb.user}", ctx) // returns user object
62
+ *
63
+ * // Interpolation - returns string
64
+ * resolveString("Hello ${bb.name}!", ctx) // returns "Hello John!"
65
+ */
66
+ // Safe reference to process.env that works in both Node.js and browser/sandbox environments
67
+ const safeProcessEnv = (): Record<string, string | undefined> => {
68
+ try {
69
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
70
+ return typeof process !== "undefined" && process?.env ? process.env : {};
71
+ } catch {
72
+ return {};
73
+ }
74
+ };
75
+
76
+ export function resolveString(
77
+ str: string,
78
+ ctx: VariableContext,
79
+ opts: ResolveOptions = {}
80
+ ): unknown {
81
+ const { preserveUndefined = true, envSource = safeProcessEnv() } = opts;
82
+
83
+ // Check if entire string is a single variable reference (for type preservation)
84
+ const fullMatch = str.match(FULL_MATCH_PATTERN);
85
+ if (fullMatch) {
86
+ // Groups: [full, namespace, key] or [full, undefined, undefined, key]
87
+ const namespace = fullMatch[1]; // 'input', 'bb', 'env', 'param', or undefined
88
+ const namespacedKey = fullMatch[2]; // key if namespaced
89
+ const simpleKey = fullMatch[3]; // key if no namespace (shorthand)
90
+
91
+ const key = namespacedKey || simpleKey;
92
+ const ns = namespace || "bb"; // Default to blackboard
93
+
94
+ if (key) {
95
+ const value = resolveVariable(ns, key, ctx, envSource);
96
+ if (value !== undefined) {
97
+ return value;
98
+ }
99
+ // Return original placeholder if preserveUndefined and value is undefined
100
+ return preserveUndefined ? str : undefined;
101
+ }
102
+ }
103
+
104
+ // Template interpolation - replace all variable references in string
105
+ return str.replace(VARIABLE_PATTERN, (match, namespace, namespacedKey, simpleKey) => {
106
+ const key = namespacedKey || simpleKey;
107
+ const ns = namespace || "bb"; // Default to blackboard
108
+
109
+ const value = resolveVariable(ns, key, ctx, envSource);
110
+
111
+ if (value === undefined) {
112
+ return preserveUndefined ? match : "";
113
+ }
114
+
115
+ // Convert to string for interpolation
116
+ if (value === null) {
117
+ return "null";
118
+ }
119
+
120
+ if (typeof value === "object") {
121
+ try {
122
+ return JSON.stringify(value);
123
+ } catch {
124
+ return String(value);
125
+ }
126
+ }
127
+
128
+ return String(value);
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Resolve variables in any value (string, object, array, or primitive)
134
+ *
135
+ * @param value - Value to resolve
136
+ * @param ctx - Variable context
137
+ * @param opts - Resolution options
138
+ * @returns Resolved value with all variable references replaced
139
+ */
140
+ export function resolveValue(
141
+ value: unknown,
142
+ ctx: VariableContext,
143
+ opts: ResolveOptions = {}
144
+ ): unknown {
145
+ if (typeof value === "string") {
146
+ return resolveString(value, ctx, opts);
147
+ }
148
+
149
+ if (Array.isArray(value)) {
150
+ return value.map((item) => resolveValue(item, ctx, opts));
151
+ }
152
+
153
+ if (value !== null && typeof value === "object") {
154
+ const resolved: Record<string, unknown> = {};
155
+ for (const [key, val] of Object.entries(value)) {
156
+ resolved[key] = resolveValue(val, ctx, opts);
157
+ }
158
+ return resolved;
159
+ }
160
+
161
+ // Primitives (number, boolean, null, undefined) pass through unchanged
162
+ return value;
163
+ }
164
+
165
+ /**
166
+ * Resolve a single variable reference
167
+ *
168
+ * @param namespace - Variable namespace (input, bb, env, param)
169
+ * @param key - Variable key, potentially with dots for nested access
170
+ * @param ctx - Variable context
171
+ * @param envSource - Environment variable source
172
+ * @returns Resolved value or undefined
173
+ */
174
+ function resolveVariable(
175
+ namespace: string,
176
+ key: string,
177
+ ctx: VariableContext,
178
+ envSource: Record<string, string | undefined>
179
+ ): unknown {
180
+ switch (namespace) {
181
+ case "input":
182
+ return getNestedValue(ctx.input, key);
183
+
184
+ case "bb":
185
+ return getNestedBlackboardValue(ctx.blackboard, key);
186
+
187
+ case "env":
188
+ // Environment variables don't support nested access
189
+ return envSource[key];
190
+
191
+ case "param":
192
+ // Test data uses Map, check for nested access
193
+ if (ctx.testData) {
194
+ const parts = key.split(".");
195
+ const firstPart = parts[0];
196
+ if (firstPart) {
197
+ let value = ctx.testData.get(firstPart);
198
+ for (let i = 1; i < parts.length && value !== undefined; i++) {
199
+ const part = parts[i];
200
+ if (part && typeof value === "object" && value !== null) {
201
+ value = (value as Record<string, unknown>)[part];
202
+ } else {
203
+ return undefined;
204
+ }
205
+ }
206
+ return value;
207
+ }
208
+ }
209
+ return undefined;
210
+
211
+ default:
212
+ // Unknown namespace - treat as blackboard
213
+ return getNestedBlackboardValue(ctx.blackboard, key);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get a nested value from a plain object using dot notation
219
+ *
220
+ * @param obj - Object to traverse
221
+ * @param path - Dot-separated path (e.g., "user.profile.name")
222
+ * @returns Value at path or undefined
223
+ */
224
+ function getNestedValue(obj: unknown, path: string): unknown {
225
+ if (obj === undefined || obj === null) {
226
+ return undefined;
227
+ }
228
+
229
+ if (typeof obj !== "object") {
230
+ return undefined;
231
+ }
232
+
233
+ const parts = path.split(".");
234
+ let value: unknown = obj;
235
+
236
+ for (const part of parts) {
237
+ if (value === undefined || value === null) {
238
+ return undefined;
239
+ }
240
+
241
+ if (typeof value !== "object") {
242
+ return undefined;
243
+ }
244
+
245
+ value = (value as Record<string, unknown>)[part];
246
+ }
247
+
248
+ return value;
249
+ }
250
+
251
+ /**
252
+ * Get a nested value from blackboard using dot notation
253
+ * First part is the blackboard key, rest is object traversal
254
+ *
255
+ * @param blackboard - Scoped blackboard
256
+ * @param path - Dot-separated path (e.g., "user.profile.name")
257
+ * @returns Value at path or undefined
258
+ */
259
+ function getNestedBlackboardValue(
260
+ blackboard: IScopedBlackboard,
261
+ path: string
262
+ ): unknown {
263
+ const parts = path.split(".");
264
+ const firstPart = parts[0];
265
+
266
+ if (!firstPart) {
267
+ return undefined;
268
+ }
269
+
270
+ // First part is the blackboard key
271
+ let value: unknown = blackboard.get(firstPart);
272
+
273
+ // Navigate through nested properties
274
+ for (let i = 1; i < parts.length && value !== undefined; i++) {
275
+ const part = parts[i];
276
+ if (part && typeof value === "object" && value !== null) {
277
+ value = (value as Record<string, unknown>)[part];
278
+ } else {
279
+ return undefined;
280
+ }
281
+ }
282
+
283
+ return value;
284
+ }
285
+
286
+ /**
287
+ * Check if a string contains any variable references
288
+ *
289
+ * @param str - String to check
290
+ * @returns True if string contains ${...} patterns
291
+ */
292
+ export function hasVariables(str: string): boolean {
293
+ return HAS_VARIABLE_PATTERN.test(str);
294
+ }
295
+
296
+ /**
297
+ * Extract all variable references from a string
298
+ *
299
+ * @param str - String to analyze
300
+ * @returns Array of {namespace, key} objects
301
+ */
302
+ export function extractVariables(
303
+ str: string
304
+ ): Array<{ namespace: string; key: string }> {
305
+ const variables: Array<{ namespace: string; key: string }> = [];
306
+ const pattern = /\$\{(input|bb|env|param)\.([a-zA-Z0-9_.]+)\}|\$\{([a-zA-Z0-9_.]+)\}/g;
307
+
308
+ let match;
309
+ while ((match = pattern.exec(str)) !== null) {
310
+ const namespace = match[1] || "bb";
311
+ const key = match[2] || match[3];
312
+ if (key) {
313
+ variables.push({ namespace, key });
314
+ }
315
+ }
316
+
317
+ return variables;
318
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tests for error handler utility
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { ConfigurationError } from "../errors.js";
7
+ import { NodeStatus } from "../types.js";
8
+ import { handleNodeError } from "./error-handler.js";
9
+ import { OperationCancelledError } from "./signal-check.js";
10
+
11
+ describe("handleNodeError", () => {
12
+ describe("ConfigurationError handling", () => {
13
+ it("should re-propagate ConfigurationError", async () => {
14
+ const error = new ConfigurationError("Test config error");
15
+
16
+ try {
17
+ await handleNodeError(error);
18
+ expect.fail("Should have thrown an error");
19
+ } catch (err) {
20
+ expect(err).toBeInstanceOf(ConfigurationError);
21
+ expect((err as ConfigurationError).message).toBe("Test config error");
22
+ }
23
+ });
24
+
25
+ it("should preserve ConfigurationError hint when re-propagating", async () => {
26
+ const error = new ConfigurationError(
27
+ "Test config error",
28
+ "Check your test file",
29
+ );
30
+
31
+ try {
32
+ await handleNodeError(error);
33
+ expect.fail("Should have thrown an error");
34
+ } catch (err) {
35
+ expect(err).toBeInstanceOf(ConfigurationError);
36
+ expect((err as ConfigurationError).hint).toBe("Check your test file");
37
+ }
38
+ });
39
+ });
40
+
41
+ describe("OperationCancelledError handling", () => {
42
+ it("should re-propagate OperationCancelledError", async () => {
43
+ const error = new OperationCancelledError("Test cancelled");
44
+
45
+ try {
46
+ await handleNodeError(error);
47
+ expect.fail("Should have thrown an error");
48
+ } catch (err) {
49
+ expect(err).toBeInstanceOf(OperationCancelledError);
50
+ expect((err as OperationCancelledError).message).toBe("Test cancelled");
51
+ }
52
+ });
53
+ });
54
+
55
+ describe("Generic error handling", () => {
56
+ it("should convert generic Error to FAILURE status", async () => {
57
+ const error = new Error("Generic error");
58
+ const result = await handleNodeError(error);
59
+
60
+ expect(result).toBe(NodeStatus.FAILURE);
61
+ });
62
+
63
+ it("should convert string error to FAILURE status", async () => {
64
+ const error = "String error";
65
+ const result = await handleNodeError(error);
66
+
67
+ expect(result).toBe(NodeStatus.FAILURE);
68
+ });
69
+
70
+ it("should convert unknown error to FAILURE status", async () => {
71
+ const error = { custom: "error object" };
72
+ const result = await handleNodeError(error);
73
+
74
+ expect(result).toBe(NodeStatus.FAILURE);
75
+ });
76
+
77
+ it("should convert null error to FAILURE status", async () => {
78
+ const error = null;
79
+ const result = await handleNodeError(error);
80
+
81
+ expect(result).toBe(NodeStatus.FAILURE);
82
+ });
83
+
84
+ it("should convert undefined error to FAILURE status", async () => {
85
+ const error = undefined;
86
+ const result = await handleNodeError(error);
87
+
88
+ expect(result).toBe(NodeStatus.FAILURE);
89
+ });
90
+ });
91
+
92
+ describe("Error type priority", () => {
93
+ it("should prioritize ConfigurationError over other error types", async () => {
94
+ // Even if ConfigurationError extends Error, it should be caught first
95
+ const error = new ConfigurationError("Config error");
96
+
97
+ try {
98
+ await handleNodeError(error);
99
+ expect.fail("Should have thrown an error");
100
+ } catch (err) {
101
+ expect(err).toBeInstanceOf(ConfigurationError);
102
+ }
103
+ });
104
+
105
+ it("should prioritize OperationCancelledError over generic errors", async () => {
106
+ // Even if OperationCancelledError extends Error, it should be caught first
107
+ const error = new OperationCancelledError("Cancelled");
108
+
109
+ try {
110
+ await handleNodeError(error);
111
+ expect.fail("Should have thrown an error");
112
+ } catch (err) {
113
+ expect(err).toBeInstanceOf(OperationCancelledError);
114
+ }
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Centralized error handling for behavior tree nodes
3
+ */
4
+
5
+ import { ConfigurationError } from "../errors.js";
6
+ import { OperationCancelledError } from "./signal-check.js";
7
+ import { NodeStatus } from "../types.js";
8
+
9
+ /**
10
+ * Standard error handler for behavior tree nodes in Temporal workflows.
11
+ * Re-throws special errors (ConfigurationError, OperationCancelledError)
12
+ * and converts all other errors to FAILURE status.
13
+ *
14
+ * This function is used by all base node classes to ensure consistent error handling:
15
+ * - ConfigurationError: Test authoring error, propagates immediately
16
+ * - OperationCancelledError: Execution cancelled, propagates for cleanup
17
+ * - All other errors: Converted to NodeStatus.FAILURE
18
+ *
19
+ * Usage in base classes:
20
+ * ```typescript
21
+ * try {
22
+ * const status = await this.executeTick(context);
23
+ * return status;
24
+ * } catch (error) {
25
+ * return handleNodeError(error);
26
+ * }
27
+ * ```
28
+ *
29
+ * @param error - The error to handle
30
+ * @returns NodeStatus.FAILURE for normal errors, or throws special errors
31
+ */
32
+ export function handleNodeError(error: unknown): NodeStatus {
33
+ // Re-throw ConfigurationError - test is broken, don't mask it
34
+ // These errors indicate test authoring bugs, not operational failures
35
+ if (error instanceof ConfigurationError) {
36
+ throw error;
37
+ }
38
+
39
+ // Re-throw OperationCancelledError - execution was cancelled
40
+ // This allows cancellation to be detected at higher levels while still
41
+ // setting the node status to FAILURE
42
+ if (error instanceof OperationCancelledError) {
43
+ throw error;
44
+ }
45
+
46
+ // All other errors convert to FAILURE status
47
+ return NodeStatus.FAILURE;
48
+ }