@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,416 @@
1
+ /**
2
+ * Tests for Variable Resolver Utility
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import {
8
+ resolveString,
9
+ resolveValue,
10
+ hasVariables,
11
+ extractVariables,
12
+ type VariableContext,
13
+ } from "./variable-resolver.js";
14
+
15
+ describe("Variable Resolver", () => {
16
+ // Helper to create a context
17
+ function createContext(overrides: Partial<VariableContext> = {}): VariableContext {
18
+ return {
19
+ blackboard: new ScopedBlackboard(),
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe("resolveString", () => {
25
+ describe("blackboard variables (${bb.key} and ${key})", () => {
26
+ it("should resolve ${bb.key} from blackboard", () => {
27
+ const ctx = createContext();
28
+ ctx.blackboard.set("username", "john");
29
+
30
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
31
+ const result = resolveString("${bb.username}", ctx);
32
+ expect(result).toBe("john");
33
+ });
34
+
35
+ it("should resolve ${key} shorthand from blackboard", () => {
36
+ const ctx = createContext();
37
+ ctx.blackboard.set("username", "john");
38
+
39
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
40
+ const result = resolveString("${username}", ctx);
41
+ expect(result).toBe("john");
42
+ });
43
+
44
+ it("should resolve nested blackboard values", () => {
45
+ const ctx = createContext();
46
+ ctx.blackboard.set("user", { profile: { name: "Alice" } });
47
+
48
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
49
+ const result = resolveString("${bb.user.profile.name}", ctx);
50
+ expect(result).toBe("Alice");
51
+ });
52
+
53
+ it("should preserve type for full match", () => {
54
+ const ctx = createContext();
55
+ const user = { name: "Bob", age: 30 };
56
+ ctx.blackboard.set("user", user);
57
+
58
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
59
+ const result = resolveString("${bb.user}", ctx);
60
+ expect(result).toEqual(user);
61
+ expect(typeof result).toBe("object");
62
+ });
63
+
64
+ it("should preserve array type for full match", () => {
65
+ const ctx = createContext();
66
+ const items = ["a", "b", "c"];
67
+ ctx.blackboard.set("items", items);
68
+
69
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
70
+ const result = resolveString("${bb.items}", ctx);
71
+ expect(result).toEqual(items);
72
+ expect(Array.isArray(result)).toBe(true);
73
+ });
74
+
75
+ it("should interpolate multiple values in string", () => {
76
+ const ctx = createContext();
77
+ ctx.blackboard.set("name", "Alice");
78
+ ctx.blackboard.set("age", 25);
79
+
80
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
81
+ const result = resolveString("${name} is ${age} years old", ctx);
82
+ expect(result).toBe("Alice is 25 years old");
83
+ });
84
+
85
+ it("should JSON stringify objects in interpolation", () => {
86
+ const ctx = createContext();
87
+ ctx.blackboard.set("data", { key: "value" });
88
+
89
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
90
+ const result = resolveString("Data: ${data}", ctx);
91
+ expect(result).toBe('Data: {"key":"value"}');
92
+ });
93
+ });
94
+
95
+ describe("input variables (${input.key})", () => {
96
+ it("should resolve ${input.key} from input", () => {
97
+ const ctx = createContext({
98
+ input: { orderId: "ORD-123" },
99
+ });
100
+
101
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
102
+ const result = resolveString("${input.orderId}", ctx);
103
+ expect(result).toBe("ORD-123");
104
+ });
105
+
106
+ it("should resolve nested input values", () => {
107
+ const ctx = createContext({
108
+ input: { order: { id: "ORD-456", customer: "Jane" } },
109
+ });
110
+
111
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
112
+ const result = resolveString("${input.order.id}", ctx);
113
+ expect(result).toBe("ORD-456");
114
+ });
115
+
116
+ it("should preserve type for full input match", () => {
117
+ const order = { id: "ORD-789", items: ["a", "b"] };
118
+ const ctx = createContext({
119
+ input: { order },
120
+ });
121
+
122
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
123
+ const result = resolveString("${input.order}", ctx);
124
+ expect(result).toEqual(order);
125
+ });
126
+
127
+ it("should return undefined for missing input", () => {
128
+ const ctx = createContext({
129
+ input: { existing: "value" },
130
+ });
131
+
132
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
133
+ const result = resolveString("${input.missing}", ctx, { preserveUndefined: false });
134
+ expect(result).toBeUndefined();
135
+ });
136
+ });
137
+
138
+ describe("environment variables (${env.KEY})", () => {
139
+ it("should resolve ${env.KEY} from process.env", () => {
140
+ const ctx = createContext();
141
+
142
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
143
+ const result = resolveString("${env.NODE_ENV}", ctx, {
144
+ envSource: { NODE_ENV: "test" },
145
+ });
146
+ expect(result).toBe("test");
147
+ });
148
+
149
+ it("should use custom env source", () => {
150
+ const ctx = createContext();
151
+
152
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
153
+ const result = resolveString("${env.CUSTOM_VAR}", ctx, {
154
+ envSource: { CUSTOM_VAR: "custom-value" },
155
+ });
156
+ expect(result).toBe("custom-value");
157
+ });
158
+
159
+ it("should preserve placeholder for missing env var", () => {
160
+ const ctx = createContext();
161
+
162
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
163
+ const result = resolveString("${env.MISSING_VAR}", ctx, {
164
+ envSource: {},
165
+ });
166
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
167
+ expect(result).toBe("${env.MISSING_VAR}");
168
+ });
169
+ });
170
+
171
+ describe("test data variables (${param.key})", () => {
172
+ it("should resolve ${param.key} from testData", () => {
173
+ const testData = new Map<string, unknown>();
174
+ testData.set("testId", "TEST-001");
175
+
176
+ const ctx = createContext({ testData });
177
+
178
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
179
+ const result = resolveString("${param.testId}", ctx);
180
+ expect(result).toBe("TEST-001");
181
+ });
182
+
183
+ it("should resolve nested param values", () => {
184
+ const testData = new Map<string, unknown>();
185
+ testData.set("test", { data: { value: 42 } });
186
+
187
+ const ctx = createContext({ testData });
188
+
189
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
190
+ const result = resolveString("${param.test.data.value}", ctx);
191
+ expect(result).toBe(42);
192
+ });
193
+ });
194
+
195
+ describe("undefined value handling", () => {
196
+ it("should preserve placeholder when preserveUndefined is true (default)", () => {
197
+ const ctx = createContext();
198
+
199
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
200
+ const result = resolveString("${bb.missing}", ctx);
201
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
202
+ expect(result).toBe("${bb.missing}");
203
+ });
204
+
205
+ it("should return undefined when preserveUndefined is false (full match)", () => {
206
+ const ctx = createContext();
207
+
208
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
209
+ const result = resolveString("${bb.missing}", ctx, { preserveUndefined: false });
210
+ expect(result).toBeUndefined();
211
+ });
212
+
213
+ it("should return empty string when preserveUndefined is false (interpolation)", () => {
214
+ const ctx = createContext();
215
+
216
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
217
+ const result = resolveString("Value: ${bb.missing}", ctx, { preserveUndefined: false });
218
+ expect(result).toBe("Value: ");
219
+ });
220
+ });
221
+
222
+ describe("edge cases", () => {
223
+ it("should handle null values", () => {
224
+ const ctx = createContext();
225
+ ctx.blackboard.set("nullVal", null);
226
+
227
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
228
+ const result = resolveString("Value: ${nullVal}", ctx);
229
+ expect(result).toBe("Value: null");
230
+ });
231
+
232
+ it("should handle number values", () => {
233
+ const ctx = createContext();
234
+ ctx.blackboard.set("count", 42);
235
+
236
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
237
+ const result = resolveString("Count: ${count}", ctx);
238
+ expect(result).toBe("Count: 42");
239
+ });
240
+
241
+ it("should handle boolean values", () => {
242
+ const ctx = createContext();
243
+ ctx.blackboard.set("active", true);
244
+
245
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
246
+ const result = resolveString("Active: ${active}", ctx);
247
+ expect(result).toBe("Active: true");
248
+ });
249
+
250
+ it("should return string as-is when no variables", () => {
251
+ const ctx = createContext();
252
+ const result = resolveString("Hello World", ctx);
253
+ expect(result).toBe("Hello World");
254
+ });
255
+
256
+ it("should handle empty string", () => {
257
+ const ctx = createContext();
258
+ const result = resolveString("", ctx);
259
+ expect(result).toBe("");
260
+ });
261
+ });
262
+ });
263
+
264
+ describe("resolveValue", () => {
265
+ it("should resolve string values", () => {
266
+ const ctx = createContext();
267
+ ctx.blackboard.set("name", "Alice");
268
+
269
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
270
+ const result = resolveValue("Hello ${name}", ctx);
271
+ expect(result).toBe("Hello Alice");
272
+ });
273
+
274
+ it("should resolve values in objects recursively", () => {
275
+ const ctx = createContext();
276
+ ctx.blackboard.set("orderId", "ORD-123");
277
+ ctx.blackboard.set("customer", "Bob");
278
+
279
+ const input = {
280
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
281
+ id: "${orderId}",
282
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
283
+ name: "${customer}",
284
+ nested: {
285
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
286
+ value: "${orderId}",
287
+ },
288
+ };
289
+
290
+ const result = resolveValue(input, ctx);
291
+ expect(result).toEqual({
292
+ id: "ORD-123",
293
+ name: "Bob",
294
+ nested: {
295
+ value: "ORD-123",
296
+ },
297
+ });
298
+ });
299
+
300
+ it("should resolve values in arrays", () => {
301
+ const ctx = createContext();
302
+ ctx.blackboard.set("a", "first");
303
+ ctx.blackboard.set("b", "second");
304
+
305
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
306
+ const result = resolveValue(["${a}", "${b}", "static"], ctx);
307
+ expect(result).toEqual(["first", "second", "static"]);
308
+ });
309
+
310
+ it("should pass through primitives unchanged", () => {
311
+ const ctx = createContext();
312
+
313
+ expect(resolveValue(42, ctx)).toBe(42);
314
+ expect(resolveValue(true, ctx)).toBe(true);
315
+ expect(resolveValue(null, ctx)).toBe(null);
316
+ expect(resolveValue(undefined, ctx)).toBe(undefined);
317
+ });
318
+
319
+ it("should handle mixed objects and arrays", () => {
320
+ const ctx = createContext();
321
+ ctx.blackboard.set("val", "resolved");
322
+
323
+ const input = {
324
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
325
+ items: [{ key: "${val}" }, { key: "static" }],
326
+ count: 5,
327
+ };
328
+
329
+ const result = resolveValue(input, ctx);
330
+ expect(result).toEqual({
331
+ items: [{ key: "resolved" }, { key: "static" }],
332
+ count: 5,
333
+ });
334
+ });
335
+ });
336
+
337
+ describe("hasVariables", () => {
338
+ it("should return true for strings with variables", () => {
339
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
340
+ expect(hasVariables("${bb.key}")).toBe(true);
341
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
342
+ expect(hasVariables("${key}")).toBe(true);
343
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
344
+ expect(hasVariables("${input.val}")).toBe(true);
345
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
346
+ expect(hasVariables("Hello ${name}")).toBe(true);
347
+ });
348
+
349
+ it("should return false for strings without variables", () => {
350
+ expect(hasVariables("Hello World")).toBe(false);
351
+ expect(hasVariables("")).toBe(false);
352
+ expect(hasVariables("$notvar")).toBe(false);
353
+ expect(hasVariables("{notvar}")).toBe(false);
354
+ });
355
+ });
356
+
357
+ describe("extractVariables", () => {
358
+ it("should extract single variable", () => {
359
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
360
+ const result = extractVariables("${bb.username}");
361
+ expect(result).toEqual([{ namespace: "bb", key: "username" }]);
362
+ });
363
+
364
+ it("should extract shorthand variable as blackboard", () => {
365
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
366
+ const result = extractVariables("${username}");
367
+ expect(result).toEqual([{ namespace: "bb", key: "username" }]);
368
+ });
369
+
370
+ it("should extract multiple variables", () => {
371
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
372
+ const result = extractVariables("${input.a} and ${bb.b} and ${c}");
373
+ expect(result).toEqual([
374
+ { namespace: "input", key: "a" },
375
+ { namespace: "bb", key: "b" },
376
+ { namespace: "bb", key: "c" },
377
+ ]);
378
+ });
379
+
380
+ it("should extract variables with nested keys", () => {
381
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
382
+ const result = extractVariables("${bb.user.profile.name}");
383
+ expect(result).toEqual([{ namespace: "bb", key: "user.profile.name" }]);
384
+ });
385
+
386
+ it("should return empty array for no variables", () => {
387
+ const result = extractVariables("Hello World");
388
+ expect(result).toEqual([]);
389
+ });
390
+ });
391
+
392
+ describe("backward compatibility", () => {
393
+ it("should work with LogMessage-style ${key} syntax", () => {
394
+ const ctx = createContext();
395
+ ctx.blackboard.set("username", "alice");
396
+ ctx.blackboard.set("count", 10);
397
+
398
+ // This is how LogMessage currently works
399
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
400
+ const result = resolveString("User ${username} has ${count} items", ctx);
401
+ expect(result).toBe("User alice has 10 items");
402
+ });
403
+
404
+ it("should work with IntegrationAction-style ${bb.key} syntax", () => {
405
+ const ctx = createContext();
406
+ ctx.blackboard.set("spreadsheetId", "sheet-123");
407
+ ctx.blackboard.set("values", ["a", "b", "c"]);
408
+
409
+ // This is how IntegrationAction currently works
410
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
411
+ expect(resolveString("${bb.spreadsheetId}", ctx)).toBe("sheet-123");
412
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable syntax
413
+ expect(resolveString("${bb.values}", ctx)).toEqual(["a", "b", "c"]);
414
+ });
415
+ });
416
+ });