@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,784 @@
1
+ /**
2
+ * HttpRequest Node Tests
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { ScopedBlackboard } from "../blackboard.js";
7
+ import { Registry } from "../registry.js";
8
+ import { type TemporalContext, type BtreeActivities, NodeStatus } from "../types.js";
9
+ import { HttpRequest, type HttpRequestConfig } from "./http-request.js";
10
+
11
+ describe("HttpRequest Node", () => {
12
+ let blackboard: ScopedBlackboard;
13
+ let registry: Registry;
14
+
15
+ beforeEach(() => {
16
+ blackboard = new ScopedBlackboard();
17
+ registry = new Registry();
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ describe("Construction and validation", () => {
22
+ it("should create node with valid config", () => {
23
+ const node = new HttpRequest({
24
+ id: "test",
25
+ url: "https://api.example.com/data",
26
+ outputKey: "response",
27
+ });
28
+
29
+ expect(node).toBeDefined();
30
+ expect(node.id).toBe("test");
31
+ });
32
+
33
+ it("should require url", () => {
34
+ expect(() => {
35
+ new HttpRequest({
36
+ id: "test",
37
+ outputKey: "response",
38
+ } as HttpRequestConfig);
39
+ }).toThrow(/requires url/i);
40
+ });
41
+
42
+ it("should require outputKey", () => {
43
+ expect(() => {
44
+ new HttpRequest({
45
+ id: "test",
46
+ url: "https://api.example.com/data",
47
+ } as HttpRequestConfig);
48
+ }).toThrow(/requires outputKey/i);
49
+ });
50
+
51
+ it("should accept optional method, headers, body, timeout, and retry", () => {
52
+ const node = new HttpRequest({
53
+ id: "test",
54
+ url: "https://api.example.com/data",
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: { key: "value" },
58
+ timeout: 5000,
59
+ retry: { maxAttempts: 3, backoffMs: 1000 },
60
+ outputKey: "response",
61
+ });
62
+
63
+ expect(node).toBeDefined();
64
+ });
65
+
66
+ it("should default method to GET", () => {
67
+ const node = new HttpRequest({
68
+ id: "test",
69
+ url: "https://api.example.com/data",
70
+ outputKey: "response",
71
+ });
72
+
73
+ expect(node).toBeDefined();
74
+ });
75
+ });
76
+
77
+ describe("Activity requirement", () => {
78
+ it("should fail without fetchUrl activity", async () => {
79
+ const context: TemporalContext = {
80
+ blackboard,
81
+ treeRegistry: registry,
82
+ timestamp: Date.now(),
83
+ deltaTime: 0,
84
+ activities: undefined,
85
+ };
86
+
87
+ const node = new HttpRequest({
88
+ id: "test",
89
+ url: "https://api.example.com/data",
90
+ outputKey: "response",
91
+ });
92
+
93
+ const status = await node.tick(context);
94
+
95
+ expect(status).toBe(NodeStatus.FAILURE);
96
+ expect(node.lastError).toContain("requires activities.fetchUrl");
97
+ });
98
+
99
+ it("should fail when activities object exists but fetchUrl is missing", async () => {
100
+ const context: TemporalContext = {
101
+ blackboard,
102
+ treeRegistry: registry,
103
+ timestamp: Date.now(),
104
+ deltaTime: 0,
105
+ activities: {
106
+ executePieceAction: vi.fn(),
107
+ // fetchUrl is not provided
108
+ } as BtreeActivities,
109
+ };
110
+
111
+ const node = new HttpRequest({
112
+ id: "test",
113
+ url: "https://api.example.com/data",
114
+ outputKey: "response",
115
+ });
116
+
117
+ const status = await node.tick(context);
118
+
119
+ expect(status).toBe(NodeStatus.FAILURE);
120
+ expect(node.lastError).toContain("requires activities.fetchUrl");
121
+ });
122
+ });
123
+
124
+ describe("Execution with activity", () => {
125
+ it("should make GET request via activity", async () => {
126
+ const mockFetchActivity = vi.fn().mockResolvedValue({
127
+ status: 200,
128
+ headers: { "content-type": "application/json" },
129
+ data: { id: 1, name: "Test" },
130
+ });
131
+
132
+ const context: TemporalContext = {
133
+ blackboard,
134
+ treeRegistry: registry,
135
+ timestamp: Date.now(),
136
+ deltaTime: 0,
137
+ activities: {
138
+ executePieceAction: vi.fn(),
139
+ fetchUrl: mockFetchActivity,
140
+ },
141
+ };
142
+
143
+ const node = new HttpRequest({
144
+ id: "test",
145
+ url: "https://api.example.com/users/1",
146
+ method: "GET",
147
+ outputKey: "userData",
148
+ });
149
+
150
+ const status = await node.tick(context);
151
+
152
+ expect(status).toBe(NodeStatus.SUCCESS);
153
+ expect(mockFetchActivity).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ url: "https://api.example.com/users/1",
156
+ method: "GET",
157
+ })
158
+ );
159
+ });
160
+
161
+ it("should make POST request with body", async () => {
162
+ const mockFetchActivity = vi.fn().mockResolvedValue({
163
+ status: 201,
164
+ headers: { "content-type": "application/json" },
165
+ data: { id: 123, success: true },
166
+ });
167
+
168
+ const context: TemporalContext = {
169
+ blackboard,
170
+ treeRegistry: registry,
171
+ timestamp: Date.now(),
172
+ deltaTime: 0,
173
+ activities: {
174
+ executePieceAction: vi.fn(),
175
+ fetchUrl: mockFetchActivity,
176
+ },
177
+ };
178
+
179
+ const node = new HttpRequest({
180
+ id: "test",
181
+ url: "https://api.example.com/orders",
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: { product: "Widget", quantity: 5 },
185
+ outputKey: "orderResponse",
186
+ });
187
+
188
+ const status = await node.tick(context);
189
+
190
+ expect(status).toBe(NodeStatus.SUCCESS);
191
+ expect(mockFetchActivity).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ url: "https://api.example.com/orders",
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json" },
196
+ body: { product: "Widget", quantity: 5 },
197
+ })
198
+ );
199
+ });
200
+
201
+ it("should store response in blackboard", async () => {
202
+ const responseData = { users: [{ id: 1 }, { id: 2 }] };
203
+ const mockFetchActivity = vi.fn().mockResolvedValue({
204
+ status: 200,
205
+ headers: { "content-type": "application/json" },
206
+ data: responseData,
207
+ });
208
+
209
+ const context: TemporalContext = {
210
+ blackboard,
211
+ treeRegistry: registry,
212
+ timestamp: Date.now(),
213
+ deltaTime: 0,
214
+ activities: {
215
+ executePieceAction: vi.fn(),
216
+ fetchUrl: mockFetchActivity,
217
+ },
218
+ };
219
+
220
+ const node = new HttpRequest({
221
+ id: "test",
222
+ url: "https://api.example.com/users",
223
+ outputKey: "apiResponse",
224
+ });
225
+
226
+ await node.tick(context);
227
+
228
+ const stored = blackboard.get("apiResponse") as {
229
+ status: number;
230
+ headers: Record<string, string>;
231
+ data: unknown;
232
+ };
233
+ expect(stored.status).toBe(200);
234
+ expect(stored.headers).toEqual({ "content-type": "application/json" });
235
+ expect(stored.data).toEqual(responseData);
236
+ });
237
+
238
+ it("should resolve URL from input", async () => {
239
+ const mockFetchActivity = vi.fn().mockResolvedValue({
240
+ status: 200,
241
+ headers: {},
242
+ data: {},
243
+ });
244
+
245
+ const context: TemporalContext = {
246
+ blackboard,
247
+ treeRegistry: registry,
248
+ timestamp: Date.now(),
249
+ deltaTime: 0,
250
+ input: { userId: "abc123" },
251
+ activities: {
252
+ executePieceAction: vi.fn(),
253
+ fetchUrl: mockFetchActivity,
254
+ },
255
+ };
256
+
257
+ const node = new HttpRequest({
258
+ id: "test",
259
+ url: "https://api.example.com/users/${input.userId}",
260
+ outputKey: "response",
261
+ });
262
+
263
+ await node.tick(context);
264
+
265
+ expect(mockFetchActivity).toHaveBeenCalledWith(
266
+ expect.objectContaining({
267
+ url: "https://api.example.com/users/abc123",
268
+ })
269
+ );
270
+ });
271
+
272
+ it("should resolve URL from blackboard", async () => {
273
+ blackboard.set("apiEndpoint", "https://custom-api.example.com");
274
+
275
+ const mockFetchActivity = vi.fn().mockResolvedValue({
276
+ status: 200,
277
+ headers: {},
278
+ data: {},
279
+ });
280
+
281
+ const context: TemporalContext = {
282
+ blackboard,
283
+ treeRegistry: registry,
284
+ timestamp: Date.now(),
285
+ deltaTime: 0,
286
+ activities: {
287
+ executePieceAction: vi.fn(),
288
+ fetchUrl: mockFetchActivity,
289
+ },
290
+ };
291
+
292
+ const node = new HttpRequest({
293
+ id: "test",
294
+ url: "${bb.apiEndpoint}/data",
295
+ outputKey: "response",
296
+ });
297
+
298
+ await node.tick(context);
299
+
300
+ expect(mockFetchActivity).toHaveBeenCalledWith(
301
+ expect.objectContaining({
302
+ url: "https://custom-api.example.com/data",
303
+ })
304
+ );
305
+ });
306
+
307
+ it("should resolve headers from blackboard", async () => {
308
+ blackboard.set("accessToken", "secret-token-123");
309
+
310
+ const mockFetchActivity = vi.fn().mockResolvedValue({
311
+ status: 200,
312
+ headers: {},
313
+ data: {},
314
+ });
315
+
316
+ const context: TemporalContext = {
317
+ blackboard,
318
+ treeRegistry: registry,
319
+ timestamp: Date.now(),
320
+ deltaTime: 0,
321
+ activities: {
322
+ executePieceAction: vi.fn(),
323
+ fetchUrl: mockFetchActivity,
324
+ },
325
+ };
326
+
327
+ const node = new HttpRequest({
328
+ id: "test",
329
+ url: "https://api.example.com/protected",
330
+ headers: {
331
+ Authorization: "Bearer ${bb.accessToken}",
332
+ },
333
+ outputKey: "response",
334
+ });
335
+
336
+ await node.tick(context);
337
+
338
+ expect(mockFetchActivity).toHaveBeenCalledWith(
339
+ expect.objectContaining({
340
+ headers: {
341
+ Authorization: "Bearer secret-token-123",
342
+ },
343
+ })
344
+ );
345
+ });
346
+
347
+ it("should resolve body from blackboard and input", async () => {
348
+ blackboard.set("cartItems", [{ productId: 1 }, { productId: 2 }]);
349
+
350
+ const mockFetchActivity = vi.fn().mockResolvedValue({
351
+ status: 201,
352
+ headers: {},
353
+ data: { orderId: "ORD-001" },
354
+ });
355
+
356
+ const context: TemporalContext = {
357
+ blackboard,
358
+ treeRegistry: registry,
359
+ timestamp: Date.now(),
360
+ deltaTime: 0,
361
+ input: { customerId: "CUST-123" },
362
+ activities: {
363
+ executePieceAction: vi.fn(),
364
+ fetchUrl: mockFetchActivity,
365
+ },
366
+ };
367
+
368
+ const node = new HttpRequest({
369
+ id: "test",
370
+ url: "https://api.example.com/orders",
371
+ method: "POST",
372
+ body: {
373
+ customerId: "${input.customerId}",
374
+ items: "${bb.cartItems}",
375
+ },
376
+ outputKey: "response",
377
+ });
378
+
379
+ await node.tick(context);
380
+
381
+ expect(mockFetchActivity).toHaveBeenCalledWith(
382
+ expect.objectContaining({
383
+ body: {
384
+ customerId: "CUST-123",
385
+ items: [{ productId: 1 }, { productId: 2 }],
386
+ },
387
+ })
388
+ );
389
+ });
390
+
391
+ it("should pass timeout to activity", async () => {
392
+ const mockFetchActivity = vi.fn().mockResolvedValue({
393
+ status: 200,
394
+ headers: {},
395
+ data: {},
396
+ });
397
+
398
+ const context: TemporalContext = {
399
+ blackboard,
400
+ treeRegistry: registry,
401
+ timestamp: Date.now(),
402
+ deltaTime: 0,
403
+ activities: {
404
+ executePieceAction: vi.fn(),
405
+ fetchUrl: mockFetchActivity,
406
+ },
407
+ };
408
+
409
+ const node = new HttpRequest({
410
+ id: "test",
411
+ url: "https://api.example.com/data",
412
+ timeout: 5000,
413
+ outputKey: "response",
414
+ });
415
+
416
+ await node.tick(context);
417
+
418
+ expect(mockFetchActivity).toHaveBeenCalledWith(
419
+ expect.objectContaining({
420
+ timeout: 5000,
421
+ })
422
+ );
423
+ });
424
+
425
+ it("should parse JSON string response when responseType is json", async () => {
426
+ const mockFetchActivity = vi.fn().mockResolvedValue({
427
+ status: 200,
428
+ headers: { "content-type": "application/json" },
429
+ data: '{"key": "value"}',
430
+ });
431
+
432
+ const context: TemporalContext = {
433
+ blackboard,
434
+ treeRegistry: registry,
435
+ timestamp: Date.now(),
436
+ deltaTime: 0,
437
+ activities: {
438
+ executePieceAction: vi.fn(),
439
+ fetchUrl: mockFetchActivity,
440
+ },
441
+ };
442
+
443
+ const node = new HttpRequest({
444
+ id: "test",
445
+ url: "https://api.example.com/data",
446
+ responseType: "json",
447
+ outputKey: "response",
448
+ });
449
+
450
+ await node.tick(context);
451
+
452
+ const stored = blackboard.get("response") as { data: unknown };
453
+ expect(stored.data).toEqual({ key: "value" });
454
+ });
455
+
456
+ it("should keep text response as-is when responseType is text", async () => {
457
+ const mockFetchActivity = vi.fn().mockResolvedValue({
458
+ status: 200,
459
+ headers: { "content-type": "text/plain" },
460
+ data: "Plain text response",
461
+ });
462
+
463
+ const context: TemporalContext = {
464
+ blackboard,
465
+ treeRegistry: registry,
466
+ timestamp: Date.now(),
467
+ deltaTime: 0,
468
+ activities: {
469
+ executePieceAction: vi.fn(),
470
+ fetchUrl: mockFetchActivity,
471
+ },
472
+ };
473
+
474
+ const node = new HttpRequest({
475
+ id: "test",
476
+ url: "https://api.example.com/data",
477
+ responseType: "text",
478
+ outputKey: "response",
479
+ });
480
+
481
+ await node.tick(context);
482
+
483
+ const stored = blackboard.get("response") as { data: unknown };
484
+ expect(stored.data).toBe("Plain text response");
485
+ });
486
+
487
+ it("should handle activity errors", async () => {
488
+ const mockFetchActivity = vi.fn().mockRejectedValue(
489
+ new Error("Network error: Connection refused")
490
+ );
491
+
492
+ const context: TemporalContext = {
493
+ blackboard,
494
+ treeRegistry: registry,
495
+ timestamp: Date.now(),
496
+ deltaTime: 0,
497
+ activities: {
498
+ executePieceAction: vi.fn(),
499
+ fetchUrl: mockFetchActivity,
500
+ },
501
+ };
502
+
503
+ const node = new HttpRequest({
504
+ id: "test",
505
+ url: "https://api.example.com/data",
506
+ outputKey: "response",
507
+ });
508
+
509
+ const status = await node.tick(context);
510
+
511
+ expect(status).toBe(NodeStatus.FAILURE);
512
+ expect(node.lastError).toContain("Network error");
513
+ });
514
+
515
+ it("should return FAILURE for 4xx status codes", async () => {
516
+ const mockFetchActivity = vi.fn().mockResolvedValue({
517
+ status: 404,
518
+ headers: {},
519
+ data: { error: "Not found" },
520
+ });
521
+
522
+ const context: TemporalContext = {
523
+ blackboard,
524
+ treeRegistry: registry,
525
+ timestamp: Date.now(),
526
+ deltaTime: 0,
527
+ activities: {
528
+ executePieceAction: vi.fn(),
529
+ fetchUrl: mockFetchActivity,
530
+ },
531
+ };
532
+
533
+ const node = new HttpRequest({
534
+ id: "test",
535
+ url: "https://api.example.com/missing",
536
+ outputKey: "response",
537
+ });
538
+
539
+ const status = await node.tick(context);
540
+
541
+ expect(status).toBe(NodeStatus.FAILURE);
542
+ // Response should still be stored
543
+ const stored = blackboard.get("response") as { status: number };
544
+ expect(stored.status).toBe(404);
545
+ });
546
+
547
+ it("should return FAILURE for 5xx status codes", async () => {
548
+ const mockFetchActivity = vi.fn().mockResolvedValue({
549
+ status: 500,
550
+ headers: {},
551
+ data: { error: "Internal server error" },
552
+ });
553
+
554
+ const context: TemporalContext = {
555
+ blackboard,
556
+ treeRegistry: registry,
557
+ timestamp: Date.now(),
558
+ deltaTime: 0,
559
+ activities: {
560
+ executePieceAction: vi.fn(),
561
+ fetchUrl: mockFetchActivity,
562
+ },
563
+ };
564
+
565
+ const node = new HttpRequest({
566
+ id: "test",
567
+ url: "https://api.example.com/error",
568
+ outputKey: "response",
569
+ });
570
+
571
+ const status = await node.tick(context);
572
+
573
+ expect(status).toBe(NodeStatus.FAILURE);
574
+ });
575
+
576
+ it("should return SUCCESS for 2xx status codes", async () => {
577
+ for (const statusCode of [200, 201, 204]) {
578
+ const mockFetchActivity = vi.fn().mockResolvedValue({
579
+ status: statusCode,
580
+ headers: {},
581
+ data: {},
582
+ });
583
+
584
+ const context: TemporalContext = {
585
+ blackboard,
586
+ treeRegistry: registry,
587
+ timestamp: Date.now(),
588
+ deltaTime: 0,
589
+ activities: {
590
+ executePieceAction: vi.fn(),
591
+ fetchUrl: mockFetchActivity,
592
+ },
593
+ };
594
+
595
+ const node = new HttpRequest({
596
+ id: `test-${statusCode}`,
597
+ url: "https://api.example.com/data",
598
+ outputKey: `response-${statusCode}`,
599
+ });
600
+
601
+ const status = await node.tick(context);
602
+ expect(status).toBe(NodeStatus.SUCCESS);
603
+ }
604
+ });
605
+ });
606
+
607
+ describe("Retry functionality", () => {
608
+ it("should retry on failure up to maxAttempts", async () => {
609
+ const mockFetchActivity = vi
610
+ .fn()
611
+ .mockRejectedValueOnce(new Error("Temporary failure 1"))
612
+ .mockRejectedValueOnce(new Error("Temporary failure 2"))
613
+ .mockResolvedValueOnce({
614
+ status: 200,
615
+ headers: {},
616
+ data: { success: true },
617
+ });
618
+
619
+ const context: TemporalContext = {
620
+ blackboard,
621
+ treeRegistry: registry,
622
+ timestamp: Date.now(),
623
+ deltaTime: 0,
624
+ activities: {
625
+ executePieceAction: vi.fn(),
626
+ fetchUrl: mockFetchActivity,
627
+ },
628
+ };
629
+
630
+ const node = new HttpRequest({
631
+ id: "test",
632
+ url: "https://api.example.com/flaky",
633
+ retry: {
634
+ maxAttempts: 3,
635
+ backoffMs: 10, // Use small backoff for testing
636
+ },
637
+ outputKey: "response",
638
+ });
639
+
640
+ const status = await node.tick(context);
641
+
642
+ expect(status).toBe(NodeStatus.SUCCESS);
643
+ expect(mockFetchActivity).toHaveBeenCalledTimes(3);
644
+ });
645
+
646
+ it("should fail after exhausting all retry attempts", async () => {
647
+ const mockFetchActivity = vi.fn().mockRejectedValue(
648
+ new Error("Persistent failure")
649
+ );
650
+
651
+ const context: TemporalContext = {
652
+ blackboard,
653
+ treeRegistry: registry,
654
+ timestamp: Date.now(),
655
+ deltaTime: 0,
656
+ activities: {
657
+ executePieceAction: vi.fn(),
658
+ fetchUrl: mockFetchActivity,
659
+ },
660
+ };
661
+
662
+ const node = new HttpRequest({
663
+ id: "test",
664
+ url: "https://api.example.com/broken",
665
+ retry: {
666
+ maxAttempts: 3,
667
+ backoffMs: 10,
668
+ },
669
+ outputKey: "response",
670
+ });
671
+
672
+ const status = await node.tick(context);
673
+
674
+ expect(status).toBe(NodeStatus.FAILURE);
675
+ expect(mockFetchActivity).toHaveBeenCalledTimes(3);
676
+ expect(node.lastError).toContain("Persistent failure");
677
+ });
678
+
679
+ it("should not retry when retry config is not provided", async () => {
680
+ const mockFetchActivity = vi.fn().mockRejectedValue(
681
+ new Error("Single failure")
682
+ );
683
+
684
+ const context: TemporalContext = {
685
+ blackboard,
686
+ treeRegistry: registry,
687
+ timestamp: Date.now(),
688
+ deltaTime: 0,
689
+ activities: {
690
+ executePieceAction: vi.fn(),
691
+ fetchUrl: mockFetchActivity,
692
+ },
693
+ };
694
+
695
+ const node = new HttpRequest({
696
+ id: "test",
697
+ url: "https://api.example.com/data",
698
+ outputKey: "response",
699
+ });
700
+
701
+ const status = await node.tick(context);
702
+
703
+ expect(status).toBe(NodeStatus.FAILURE);
704
+ expect(mockFetchActivity).toHaveBeenCalledTimes(1);
705
+ });
706
+ });
707
+
708
+ describe("Node lifecycle", () => {
709
+ it("should clone correctly", () => {
710
+ const node = new HttpRequest({
711
+ id: "original",
712
+ url: "https://api.example.com/data",
713
+ method: "POST",
714
+ headers: { "X-Custom": "header" },
715
+ outputKey: "response",
716
+ });
717
+
718
+ const cloned = node.clone() as HttpRequest;
719
+
720
+ expect(cloned.id).toBe("original");
721
+ });
722
+
723
+ it("should reset status correctly", async () => {
724
+ const context: TemporalContext = {
725
+ blackboard,
726
+ treeRegistry: registry,
727
+ timestamp: Date.now(),
728
+ deltaTime: 0,
729
+ // No activities - will fail
730
+ };
731
+
732
+ const node = new HttpRequest({
733
+ id: "test",
734
+ url: "https://api.example.com/data",
735
+ outputKey: "response",
736
+ });
737
+
738
+ await node.tick(context);
739
+ expect(node.status()).toBe(NodeStatus.FAILURE);
740
+
741
+ node.reset();
742
+ expect(node.status()).toBe(NodeStatus.IDLE);
743
+ expect(node.lastError).toBeUndefined();
744
+ });
745
+ });
746
+
747
+ describe("HTTP methods", () => {
748
+ const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
749
+
750
+ for (const method of methods) {
751
+ it(`should support ${method} method`, async () => {
752
+ const mockFetchActivity = vi.fn().mockResolvedValue({
753
+ status: 200,
754
+ headers: {},
755
+ data: {},
756
+ });
757
+
758
+ const context: TemporalContext = {
759
+ blackboard,
760
+ treeRegistry: registry,
761
+ timestamp: Date.now(),
762
+ deltaTime: 0,
763
+ activities: {
764
+ executePieceAction: vi.fn(),
765
+ fetchUrl: mockFetchActivity,
766
+ },
767
+ };
768
+
769
+ const node = new HttpRequest({
770
+ id: `test-${method}`,
771
+ url: "https://api.example.com/data",
772
+ method,
773
+ outputKey: "response",
774
+ });
775
+
776
+ await node.tick(context);
777
+
778
+ expect(mockFetchActivity).toHaveBeenCalledWith(
779
+ expect.objectContaining({ method })
780
+ );
781
+ });
782
+ }
783
+ });
784
+ });