@griffin-app/griffin-executor 0.1.0

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 (153) hide show
  1. package/README.md +152 -0
  2. package/dist/adapters/axios.d.ts +5 -0
  3. package/dist/adapters/axios.d.ts.map +1 -0
  4. package/dist/adapters/axios.js +36 -0
  5. package/dist/adapters/axios.js.map +1 -0
  6. package/dist/adapters/index.d.ts +3 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +3 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/stub.d.ts +22 -0
  11. package/dist/adapters/stub.d.ts.map +1 -0
  12. package/dist/adapters/stub.js +36 -0
  13. package/dist/adapters/stub.js.map +1 -0
  14. package/dist/events/adapters/in-memory.d.ts +52 -0
  15. package/dist/events/adapters/in-memory.d.ts.map +1 -0
  16. package/dist/events/adapters/in-memory.js +70 -0
  17. package/dist/events/adapters/in-memory.js.map +1 -0
  18. package/dist/events/adapters/in-memory.test.d.ts +2 -0
  19. package/dist/events/adapters/in-memory.test.d.ts.map +1 -0
  20. package/dist/events/adapters/in-memory.test.js +109 -0
  21. package/dist/events/adapters/in-memory.test.js.map +1 -0
  22. package/dist/events/adapters/index.d.ts +9 -0
  23. package/dist/events/adapters/index.d.ts.map +1 -0
  24. package/dist/events/adapters/index.js +9 -0
  25. package/dist/events/adapters/index.js.map +1 -0
  26. package/dist/events/adapters/kinesis.d.ts +91 -0
  27. package/dist/events/adapters/kinesis.d.ts.map +1 -0
  28. package/dist/events/adapters/kinesis.js +136 -0
  29. package/dist/events/adapters/kinesis.js.map +1 -0
  30. package/dist/events/adapters/kinesis.test.d.ts +2 -0
  31. package/dist/events/adapters/kinesis.test.d.ts.map +1 -0
  32. package/dist/events/adapters/kinesis.test.js +249 -0
  33. package/dist/events/adapters/kinesis.test.js.map +1 -0
  34. package/dist/events/emitter.d.ts +68 -0
  35. package/dist/events/emitter.d.ts.map +1 -0
  36. package/dist/events/emitter.js +83 -0
  37. package/dist/events/emitter.js.map +1 -0
  38. package/dist/events/emitter.test.d.ts +2 -0
  39. package/dist/events/emitter.test.d.ts.map +1 -0
  40. package/dist/events/emitter.test.js +262 -0
  41. package/dist/events/emitter.test.js.map +1 -0
  42. package/dist/events/index.d.ts +4 -0
  43. package/dist/events/index.d.ts.map +1 -0
  44. package/dist/events/index.js +4 -0
  45. package/dist/events/index.js.map +1 -0
  46. package/dist/events/types.d.ts +112 -0
  47. package/dist/events/types.d.ts.map +1 -0
  48. package/dist/events/types.js +9 -0
  49. package/dist/events/types.js.map +1 -0
  50. package/dist/executor.d.ts +4 -0
  51. package/dist/executor.d.ts.map +1 -0
  52. package/dist/executor.js +799 -0
  53. package/dist/executor.js.map +1 -0
  54. package/dist/executor.test.d.ts +2 -0
  55. package/dist/executor.test.d.ts.map +1 -0
  56. package/dist/executor.test.js +1584 -0
  57. package/dist/executor.test.js.map +1 -0
  58. package/dist/index.d.ts +9 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +15 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/secrets/factory.d.ts +121 -0
  63. package/dist/secrets/factory.d.ts.map +1 -0
  64. package/dist/secrets/factory.js +137 -0
  65. package/dist/secrets/factory.js.map +1 -0
  66. package/dist/secrets/index.d.ts +14 -0
  67. package/dist/secrets/index.d.ts.map +1 -0
  68. package/dist/secrets/index.js +18 -0
  69. package/dist/secrets/index.js.map +1 -0
  70. package/dist/secrets/providers/aws.d.ts +63 -0
  71. package/dist/secrets/providers/aws.d.ts.map +1 -0
  72. package/dist/secrets/providers/aws.js +110 -0
  73. package/dist/secrets/providers/aws.js.map +1 -0
  74. package/dist/secrets/providers/env.d.ts +36 -0
  75. package/dist/secrets/providers/env.d.ts.map +1 -0
  76. package/dist/secrets/providers/env.js +37 -0
  77. package/dist/secrets/providers/env.js.map +1 -0
  78. package/dist/secrets/providers/index.d.ts +7 -0
  79. package/dist/secrets/providers/index.d.ts.map +1 -0
  80. package/dist/secrets/providers/index.js +7 -0
  81. package/dist/secrets/providers/index.js.map +1 -0
  82. package/dist/secrets/providers/vault.d.ts +75 -0
  83. package/dist/secrets/providers/vault.d.ts.map +1 -0
  84. package/dist/secrets/providers/vault.js +143 -0
  85. package/dist/secrets/providers/vault.js.map +1 -0
  86. package/dist/secrets/registry.d.ts +39 -0
  87. package/dist/secrets/registry.d.ts.map +1 -0
  88. package/dist/secrets/registry.js +134 -0
  89. package/dist/secrets/registry.js.map +1 -0
  90. package/dist/secrets/resolver.d.ts +45 -0
  91. package/dist/secrets/resolver.d.ts.map +1 -0
  92. package/dist/secrets/resolver.js +188 -0
  93. package/dist/secrets/resolver.js.map +1 -0
  94. package/dist/secrets/secrets.test.d.ts +2 -0
  95. package/dist/secrets/secrets.test.d.ts.map +1 -0
  96. package/dist/secrets/secrets.test.js +317 -0
  97. package/dist/secrets/secrets.test.js.map +1 -0
  98. package/dist/secrets/types.d.ts +70 -0
  99. package/dist/secrets/types.d.ts.map +1 -0
  100. package/dist/secrets/types.js +42 -0
  101. package/dist/secrets/types.js.map +1 -0
  102. package/dist/shared.d.ts +8 -0
  103. package/dist/shared.d.ts.map +1 -0
  104. package/dist/shared.js +30 -0
  105. package/dist/shared.js.map +1 -0
  106. package/dist/test-monitor-types.d.ts +43 -0
  107. package/dist/test-monitor-types.d.ts.map +1 -0
  108. package/dist/test-monitor-types.js +2 -0
  109. package/dist/test-monitor-types.js.map +1 -0
  110. package/dist/test-plan-types.d.ts +43 -0
  111. package/dist/test-plan-types.d.ts.map +1 -0
  112. package/dist/test-plan-types.js +2 -0
  113. package/dist/test-plan-types.js.map +1 -0
  114. package/dist/types.d.ts +93 -0
  115. package/dist/types.d.ts.map +1 -0
  116. package/dist/types.js +3 -0
  117. package/dist/types.js.map +1 -0
  118. package/dist/utils/dates.d.ts +11 -0
  119. package/dist/utils/dates.d.ts.map +1 -0
  120. package/dist/utils/dates.js +13 -0
  121. package/dist/utils/dates.js.map +1 -0
  122. package/package.json +39 -0
  123. package/src/adapters/axios.ts +39 -0
  124. package/src/adapters/index.ts +2 -0
  125. package/src/adapters/stub.ts +47 -0
  126. package/src/events/adapters/README.md +144 -0
  127. package/src/events/adapters/in-memory.test.ts +146 -0
  128. package/src/events/adapters/in-memory.ts +93 -0
  129. package/src/events/adapters/index.ts +9 -0
  130. package/src/events/adapters/kinesis.test.ts +323 -0
  131. package/src/events/adapters/kinesis.ts +211 -0
  132. package/src/events/emitter.test.ts +327 -0
  133. package/src/events/emitter.ts +133 -0
  134. package/src/events/index.ts +3 -0
  135. package/src/events/types.ts +136 -0
  136. package/src/executor.test.ts +1732 -0
  137. package/src/executor.ts +1075 -0
  138. package/src/index.ts +81 -0
  139. package/src/secrets/factory.ts +248 -0
  140. package/src/secrets/index.ts +48 -0
  141. package/src/secrets/providers/aws.ts +178 -0
  142. package/src/secrets/providers/env.ts +66 -0
  143. package/src/secrets/providers/index.ts +15 -0
  144. package/src/secrets/providers/vault.ts +257 -0
  145. package/src/secrets/resolver.ts +269 -0
  146. package/src/secrets/secrets.test.ts +402 -0
  147. package/src/secrets/types.ts +106 -0
  148. package/src/shared.ts +46 -0
  149. package/src/test-monitor-types.ts +49 -0
  150. package/src/types.ts +114 -0
  151. package/src/utils/dates.ts +13 -0
  152. package/tsconfig.json +20 -0
  153. package/vitest.config.ts +14 -0
@@ -0,0 +1,1732 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { executeMonitorV1 } from "./executor.js";
3
+ import { StubAdapter } from "./adapters/stub.js";
4
+ import { MonitorV1 } from "@griffin-app/griffin-hub-sdk";
5
+ import { START, END, type ExecutionOptions } from "./types.js";
6
+ import { LocalEventEmitter, type ExecutionEvent } from "./events";
7
+ import { EnvSecretProvider } from "./secrets/providers/env.js";
8
+
9
+ describe("executeMonitorV1", () => {
10
+ let stubClient: StubAdapter;
11
+ let options: ExecutionOptions;
12
+ let organizationId: string = "test-org";
13
+ beforeEach(() => {
14
+ stubClient = new StubAdapter();
15
+ options = {
16
+ mode: "local",
17
+ httpClient: stubClient,
18
+ timeout: 5000,
19
+ secretProvider: new EnvSecretProvider(),
20
+ };
21
+ });
22
+
23
+ describe("Single HttpRequest Execution", () => {
24
+ it("should execute a simple GET request successfully", async () => {
25
+ const monitor: MonitorV1 = {
26
+ id: "test-monitor-1",
27
+ project: "test-project",
28
+ frequency: { every: 1, unit: "MINUTE" },
29
+ name: "Simple GET Test",
30
+ environment: "default",
31
+ version: "1.0",
32
+ nodes: [
33
+ {
34
+ id: "get-users",
35
+ type: "HTTP_REQUEST",
36
+ method: "GET",
37
+ path: "/users",
38
+ base: "https://api.example.com",
39
+ response_format: "JSON",
40
+ },
41
+ ],
42
+ edges: [
43
+ {
44
+ from: START,
45
+ to: "get-users",
46
+ },
47
+ {
48
+ from: "get-users",
49
+ to: END,
50
+ },
51
+ ],
52
+ };
53
+
54
+ stubClient.addStub({
55
+ match: "https://api.example.com/users",
56
+ response: {
57
+ status: 200,
58
+ statusText: "OK",
59
+ data: { users: [{ id: 1, name: "Alice" }] },
60
+ },
61
+ });
62
+
63
+ const result = await executeMonitorV1(monitor, organizationId, options);
64
+
65
+ expect(result.success).toBe(true);
66
+ expect(result.errors).toHaveLength(0);
67
+ expect(result.results).toHaveLength(1);
68
+ expect(result.results[0].nodeId).toBe("get-users");
69
+ expect(result.results[0].success).toBe(true);
70
+ expect(result.results[0].response).toEqual({
71
+ users: [{ id: 1, name: "Alice" }],
72
+ });
73
+ expect(result.results[0].duration_ms).toBeGreaterThanOrEqual(0);
74
+ });
75
+
76
+ it("should execute a POST request with body and headers", async () => {
77
+ const monitor: MonitorV1 = {
78
+ id: "test-monitor-2",
79
+ name: "POST Test",
80
+ version: "1.0",
81
+ environment: "default",
82
+ project: "test-project",
83
+ frequency: { every: 1, unit: "MINUTE" },
84
+ nodes: [
85
+ {
86
+ id: "create-user",
87
+ type: "HTTP_REQUEST",
88
+ method: "POST",
89
+ base: "https://api.example.com",
90
+ path: "/users",
91
+ headers: {
92
+ "Content-Type": { $literal: "application/json" },
93
+ Authorization: { $literal: "Bearer token123" },
94
+ },
95
+ body: { name: "Bob", email: "bob@example.com" },
96
+ response_format: "JSON",
97
+ },
98
+ ],
99
+ edges: [
100
+ {
101
+ from: START,
102
+ to: "create-user",
103
+ },
104
+ {
105
+ from: "create-user",
106
+ to: END,
107
+ },
108
+ ],
109
+ };
110
+
111
+ stubClient.addStub({
112
+ match: (req) =>
113
+ req.url === "https://api.example.com/users" && req.method === "POST",
114
+ response: {
115
+ status: 201,
116
+ statusText: "Created",
117
+ data: { id: 2, name: "Bob", email: "bob@example.com" },
118
+ },
119
+ });
120
+
121
+ const result = await executeMonitorV1(monitor, organizationId, options);
122
+
123
+ expect(result.success).toBe(true);
124
+ expect(result.results[0].response).toEqual({
125
+ id: 2,
126
+ name: "Bob",
127
+ email: "bob@example.com",
128
+ });
129
+ });
130
+
131
+ it("should handle JSON string responses by parsing them", async () => {
132
+ const monitor: MonitorV1 = {
133
+ id: "test-monitor-3",
134
+ name: "JSON String Response Test",
135
+ version: "1.0",
136
+ environment: "default",
137
+ project: "test-project",
138
+ frequency: { every: 1, unit: "MINUTE" },
139
+ nodes: [
140
+ {
141
+ id: "get-data",
142
+ type: "HTTP_REQUEST",
143
+ method: "GET",
144
+ path: "/data",
145
+ base: "https://api.example.com",
146
+ response_format: "JSON",
147
+ },
148
+ ],
149
+ edges: [
150
+ {
151
+ from: START,
152
+ to: "get-data",
153
+ },
154
+ {
155
+ from: "get-data",
156
+ to: END,
157
+ },
158
+ ],
159
+ };
160
+
161
+ stubClient.addStub({
162
+ match: "https://api.example.com/data",
163
+ response: {
164
+ status: 200,
165
+ statusText: "OK",
166
+ data: '{"message":"hello"}',
167
+ },
168
+ });
169
+
170
+ const result = await executeMonitorV1(monitor, organizationId, options);
171
+
172
+ expect(result.success).toBe(true);
173
+ expect(result.results[0].response).toEqual({ message: "hello" });
174
+ });
175
+
176
+ it("should override endpoint_host with baseUrl option", async () => {
177
+ const monitor: MonitorV1 = {
178
+ id: "test-monitor-4",
179
+ name: "BaseUrl Override Test",
180
+ version: "1.0",
181
+ environment: "default",
182
+ project: "test-project",
183
+ frequency: { every: 1, unit: "MINUTE" },
184
+ nodes: [
185
+ {
186
+ id: "get-users",
187
+ type: "HTTP_REQUEST",
188
+ method: "GET",
189
+ base: "https://api.example.com",
190
+ path: "/users",
191
+ response_format: "JSON",
192
+ },
193
+ ],
194
+ edges: [
195
+ {
196
+ from: START,
197
+ to: "get-users",
198
+ },
199
+ {
200
+ from: "get-users",
201
+ to: END,
202
+ },
203
+ ],
204
+ };
205
+
206
+ stubClient.addStub({
207
+ match: "https://api.example.com/users",
208
+ response: {
209
+ status: 200,
210
+ statusText: "OK",
211
+ data: { users: [] },
212
+ },
213
+ });
214
+
215
+ const result = await executeMonitorV1(monitor, organizationId, options);
216
+
217
+ expect(result.success).toBe(true);
218
+ });
219
+ });
220
+
221
+ describe("Multiple HTTP Methods", () => {
222
+ it("should handle PUT requests", async () => {
223
+ const monitor: MonitorV1 = {
224
+ id: "test-monitor-5",
225
+ project: "test-project",
226
+ frequency: { every: 1, unit: "MINUTE" },
227
+ name: "PUT Test",
228
+ version: "1.0",
229
+ environment: "default",
230
+ nodes: [
231
+ {
232
+ id: "update-user",
233
+ type: "HTTP_REQUEST",
234
+ method: "PUT",
235
+ path: "/users/1",
236
+ base: "https://api.example.com",
237
+ body: { name: "Updated Name" },
238
+ response_format: "JSON",
239
+ },
240
+ ],
241
+ edges: [
242
+ {
243
+ from: START,
244
+ to: "update-user",
245
+ },
246
+ {
247
+ from: "update-user",
248
+ to: END,
249
+ },
250
+ ],
251
+ };
252
+
253
+ stubClient.addStub({
254
+ match: "https://api.example.com/users/1",
255
+ response: {
256
+ status: 200,
257
+ statusText: "OK",
258
+ data: { id: 1, name: "Updated Name" },
259
+ },
260
+ });
261
+
262
+ const result = await executeMonitorV1(monitor, organizationId, options);
263
+
264
+ expect(result.success).toBe(true);
265
+ expect(result.results[0].response).toEqual({
266
+ id: 1,
267
+ name: "Updated Name",
268
+ });
269
+ });
270
+
271
+ it("should handle DELETE requests", async () => {
272
+ const monitor: MonitorV1 = {
273
+ id: "test-monitor-6",
274
+ project: "test-project",
275
+ frequency: { every: 1, unit: "MINUTE" },
276
+ name: "DELETE Test",
277
+ version: "1.0",
278
+ environment: "default",
279
+ nodes: [
280
+ {
281
+ id: "delete-user",
282
+ type: "HTTP_REQUEST",
283
+ method: "DELETE",
284
+ path: "/users/1",
285
+ base: "https://api.example.com",
286
+ response_format: "JSON",
287
+ },
288
+ ],
289
+ edges: [
290
+ {
291
+ from: START,
292
+ to: "delete-user",
293
+ },
294
+ {
295
+ from: "delete-user",
296
+ to: END,
297
+ },
298
+ ],
299
+ };
300
+
301
+ stubClient.addStub({
302
+ match: "https://api.example.com/users/1",
303
+ response: {
304
+ status: 204,
305
+ statusText: "No Content",
306
+ data: null,
307
+ },
308
+ });
309
+
310
+ const result = await executeMonitorV1(monitor, organizationId, options);
311
+
312
+ expect(result.success).toBe(true);
313
+ expect(result.results[0].response).toBeNull();
314
+ });
315
+
316
+ it("should handle PATCH requests", async () => {
317
+ const monitor: MonitorV1 = {
318
+ id: "test-monitor-7",
319
+ project: "test-project",
320
+ frequency: { every: 1, unit: "MINUTE" },
321
+ name: "PATCH Test",
322
+ version: "1.0",
323
+ environment: "default",
324
+ nodes: [
325
+ {
326
+ id: "patch-user",
327
+ type: "HTTP_REQUEST",
328
+ method: "PATCH",
329
+ path: "/users/1",
330
+ base: "https://api.example.com",
331
+ body: { email: "newemail@example.com" },
332
+ response_format: "JSON",
333
+ },
334
+ ],
335
+ edges: [
336
+ {
337
+ from: START,
338
+ to: "patch-user",
339
+ },
340
+ {
341
+ from: "patch-user",
342
+ to: END,
343
+ },
344
+ ],
345
+ };
346
+
347
+ stubClient.addStub({
348
+ match: "https://api.example.com/users/1",
349
+ response: {
350
+ status: 200,
351
+ statusText: "OK",
352
+ data: { id: 1, email: "newemail@example.com" },
353
+ },
354
+ });
355
+
356
+ const result = await executeMonitorV1(monitor, organizationId, options);
357
+
358
+ expect(result.success).toBe(true);
359
+ });
360
+ });
361
+
362
+ describe("Sequential Execution", () => {
363
+ it("should execute two endpoints in sequence", async () => {
364
+ const monitor: MonitorV1 = {
365
+ id: "test-monitor-8",
366
+ project: "test-project",
367
+ frequency: { every: 1, unit: "MINUTE" },
368
+ name: "Sequential Test",
369
+ version: "1.0",
370
+ environment: "default",
371
+ nodes: [
372
+ {
373
+ id: "first",
374
+ type: "HTTP_REQUEST",
375
+ method: "GET",
376
+ path: "/first",
377
+ base: "https://api.example.com",
378
+ response_format: "JSON",
379
+ },
380
+ {
381
+ id: "second",
382
+ type: "HTTP_REQUEST",
383
+ method: "GET",
384
+ path: "/second",
385
+ base: "https://api.example.com",
386
+ response_format: "JSON",
387
+ },
388
+ ],
389
+ edges: [
390
+ {
391
+ from: START,
392
+ to: "first",
393
+ },
394
+ {
395
+ from: "first",
396
+ to: "second",
397
+ },
398
+ {
399
+ from: "second",
400
+ to: END,
401
+ },
402
+ ],
403
+ };
404
+
405
+ stubClient
406
+ .addStub({
407
+ match: "https://api.example.com/first",
408
+ response: {
409
+ status: 200,
410
+ statusText: "OK",
411
+ data: { step: 1 },
412
+ },
413
+ })
414
+ .addStub({
415
+ match: "https://api.example.com/second",
416
+ response: {
417
+ status: 200,
418
+ statusText: "OK",
419
+ data: { step: 2 },
420
+ },
421
+ });
422
+
423
+ const result = await executeMonitorV1(monitor, organizationId, options);
424
+
425
+ expect(result.success).toBe(true);
426
+ expect(result.results).toHaveLength(2);
427
+ expect(result.results[0].nodeId).toBe("first");
428
+ expect(result.results[1].nodeId).toBe("second");
429
+ expect(result.results[0].response).toEqual({ step: 1 });
430
+ expect(result.results[1].response).toEqual({ step: 2 });
431
+ });
432
+
433
+ it("should execute a linear chain of multiple endpoints", async () => {
434
+ const monitor: MonitorV1 = {
435
+ id: "test-monitor-9",
436
+ project: "test-project",
437
+ frequency: { every: 1, unit: "MINUTE" },
438
+ name: "Multi-Step Linear Test",
439
+ version: "1.0",
440
+ environment: "default",
441
+ nodes: [
442
+ {
443
+ id: "step1",
444
+ type: "HTTP_REQUEST",
445
+ method: "GET",
446
+ path: "/step1",
447
+ base: "https://api.example.com",
448
+ response_format: "JSON",
449
+ },
450
+ {
451
+ id: "step2",
452
+ type: "HTTP_REQUEST",
453
+ method: "GET",
454
+ path: "/step2",
455
+ base: "https://api.example.com",
456
+ response_format: "JSON",
457
+ },
458
+ {
459
+ id: "step3",
460
+ type: "HTTP_REQUEST",
461
+ method: "GET",
462
+ path: "/step3",
463
+ base: "https://api.example.com",
464
+ response_format: "JSON",
465
+ },
466
+ {
467
+ id: "step4",
468
+ type: "HTTP_REQUEST",
469
+ method: "GET",
470
+ path: "/step4",
471
+ base: "https://api.example.com",
472
+ response_format: "JSON",
473
+ },
474
+ ],
475
+ edges: [
476
+ {
477
+ from: START,
478
+ to: "step1",
479
+ },
480
+ {
481
+ from: "step1",
482
+ to: "step2",
483
+ },
484
+ {
485
+ from: "step2",
486
+ to: "step3",
487
+ },
488
+ {
489
+ from: "step3",
490
+ to: "step4",
491
+ },
492
+ {
493
+ from: "step4",
494
+ to: END,
495
+ },
496
+ ],
497
+ };
498
+
499
+ stubClient
500
+ .addStub({
501
+ match: /\/step1$/,
502
+ response: { status: 200, statusText: "OK", data: { step: 1 } },
503
+ })
504
+ .addStub({
505
+ match: /\/step2$/,
506
+ response: { status: 200, statusText: "OK", data: { step: 2 } },
507
+ })
508
+ .addStub({
509
+ match: /\/step3$/,
510
+ response: { status: 200, statusText: "OK", data: { step: 3 } },
511
+ })
512
+ .addStub({
513
+ match: /\/step4$/,
514
+ response: { status: 200, statusText: "OK", data: { step: 4 } },
515
+ });
516
+
517
+ const result = await executeMonitorV1(monitor, organizationId, options);
518
+
519
+ expect(result.success).toBe(true);
520
+ expect(result.results).toHaveLength(4);
521
+ expect(result.results[0].response).toEqual({ step: 1 });
522
+ expect(result.results[1].response).toEqual({ step: 2 });
523
+ expect(result.results[2].response).toEqual({ step: 3 });
524
+ expect(result.results[3].response).toEqual({ step: 4 });
525
+ });
526
+ });
527
+
528
+ describe("Wait Node", () => {
529
+ it.skip("should execute a wait node successfully", async () => {
530
+ const monitor: MonitorV1 = {
531
+ id: "test-monitor-10",
532
+ project: "test-project",
533
+ frequency: { every: 1, unit: "MINUTE" },
534
+ name: "Wait Test",
535
+ version: "1.0",
536
+ environment: "default",
537
+ nodes: [
538
+ {
539
+ id: "wait-node",
540
+ type: "WAIT",
541
+ duration_ms: 100,
542
+ },
543
+ ],
544
+ edges: [
545
+ {
546
+ from: START,
547
+ to: "wait-node",
548
+ },
549
+ {
550
+ from: "wait-node",
551
+ to: END,
552
+ },
553
+ ],
554
+ };
555
+
556
+ const startTime = Date.now();
557
+ const result = await executeMonitorV1(monitor, organizationId, options);
558
+ const endTime = Date.now();
559
+
560
+ expect(result.success).toBe(true);
561
+ expect(result.results).toHaveLength(1);
562
+ expect(result.results[0].nodeId).toBe("wait-node");
563
+ expect(result.results[0].success).toBe(true);
564
+ expect(result.results[0].duration_ms).toBeGreaterThanOrEqual(100);
565
+ expect(endTime - startTime).toBeGreaterThanOrEqual(100);
566
+ });
567
+
568
+ it("should execute endpoints with waits in between", async () => {
569
+ const monitor: MonitorV1 = {
570
+ id: "test-monitor-11",
571
+ project: "test-project",
572
+ frequency: { every: 1, unit: "MINUTE" },
573
+ name: "HttpRequest-Wait-HttpRequest Test",
574
+ version: "1.0",
575
+ environment: "default",
576
+ nodes: [
577
+ {
578
+ id: "first-request",
579
+ type: "HTTP_REQUEST",
580
+ method: "GET",
581
+ path: "/first",
582
+ base: "https://api.example.com",
583
+ response_format: "JSON",
584
+ },
585
+ {
586
+ id: "wait",
587
+ type: "WAIT",
588
+ duration_ms: 50,
589
+ },
590
+ {
591
+ id: "second-request",
592
+ type: "HTTP_REQUEST",
593
+ method: "GET",
594
+ path: "/second",
595
+ base: "https://api.example.com",
596
+ response_format: "JSON",
597
+ },
598
+ ],
599
+ edges: [
600
+ {
601
+ from: START,
602
+ to: "first-request",
603
+ },
604
+ {
605
+ from: "first-request",
606
+ to: "wait",
607
+ },
608
+ {
609
+ from: "wait",
610
+ to: "second-request",
611
+ },
612
+ {
613
+ from: "second-request",
614
+ to: END,
615
+ },
616
+ ],
617
+ };
618
+
619
+ stubClient
620
+ .addStub({
621
+ match: /\/first$/,
622
+ response: { status: 200, statusText: "OK", data: { step: 1 } },
623
+ })
624
+ .addStub({
625
+ match: /\/second$/,
626
+ response: { status: 200, statusText: "OK", data: { step: 2 } },
627
+ });
628
+
629
+ const result = await executeMonitorV1(monitor, organizationId, options);
630
+
631
+ expect(result.success).toBe(true);
632
+ expect(result.results).toHaveLength(3);
633
+ expect(result.results[1].nodeId).toBe("wait");
634
+ expect(result.results[1].duration_ms).toBeGreaterThanOrEqual(50);
635
+ });
636
+ });
637
+
638
+ describe("Assertion Node", () => {
639
+ it("should execute an assertion node (currently no-op)", async () => {
640
+ const monitor: MonitorV1 = {
641
+ id: "test-monitor-12",
642
+ project: "test-project",
643
+ frequency: { every: 1, unit: "MINUTE" },
644
+ name: "Assertion Test",
645
+ version: "1.0",
646
+ environment: "default",
647
+ nodes: [
648
+ {
649
+ id: "get-data",
650
+ type: "HTTP_REQUEST",
651
+ method: "GET",
652
+ path: "/data",
653
+ base: "https://api.example.com",
654
+ response_format: "JSON",
655
+ },
656
+ {
657
+ id: "assert",
658
+ type: "ASSERTION",
659
+ assertions: [],
660
+ },
661
+ ],
662
+ edges: [
663
+ {
664
+ from: START,
665
+ to: "get-data",
666
+ },
667
+ {
668
+ from: "get-data",
669
+ to: "assert",
670
+ },
671
+ {
672
+ from: "assert",
673
+ to: END,
674
+ },
675
+ ],
676
+ };
677
+
678
+ stubClient.addStub({
679
+ match: "https://api.example.com/data",
680
+ response: {
681
+ status: 200,
682
+ statusText: "OK",
683
+ data: { value: 42 },
684
+ },
685
+ });
686
+
687
+ const result = await executeMonitorV1(monitor, organizationId, options);
688
+
689
+ expect(result.success).toBe(true);
690
+ expect(result.results).toHaveLength(2);
691
+ expect(result.results[1].nodeId).toBe("assert");
692
+ expect(result.results[1].success).toBe(true);
693
+ });
694
+ });
695
+
696
+ describe("Error Handling", () => {
697
+ it("should handle failed HTTP requests gracefully", async () => {
698
+ const monitor: MonitorV1 = {
699
+ id: "test-monitor-13",
700
+ project: "test-project",
701
+ frequency: { every: 1, unit: "MINUTE" },
702
+ name: "Failed Request Test",
703
+ version: "1.0",
704
+ environment: "default",
705
+ nodes: [
706
+ {
707
+ id: "failing-request",
708
+ type: "HTTP_REQUEST",
709
+ method: "GET",
710
+ path: "/fail",
711
+ base: "https://api.example.com",
712
+ response_format: "JSON",
713
+ },
714
+ ],
715
+ edges: [
716
+ {
717
+ from: START,
718
+ to: "failing-request",
719
+ },
720
+ {
721
+ from: "failing-request",
722
+ to: END,
723
+ },
724
+ ],
725
+ };
726
+
727
+ // Don't add a stub - this will cause the request to fail
728
+
729
+ const result = await executeMonitorV1(monitor, organizationId, options);
730
+
731
+ expect(result.success).toBe(false);
732
+ expect(result.errors).toHaveLength(1);
733
+ expect(result.errors[0]).toContain("failing-request");
734
+ expect(result.errors[0]).toContain("No stub matched request");
735
+ expect(result.results[0].success).toBe(false);
736
+ expect(result.results[0].error).toBeDefined();
737
+ });
738
+
739
+ it("should handle disconnected nodes gracefully", async () => {
740
+ const monitor: MonitorV1 = {
741
+ id: "test-monitor-14",
742
+ project: "test-project",
743
+ frequency: { every: 1, unit: "MINUTE" },
744
+ name: "Disconnected Nodes Test",
745
+ version: "1.0",
746
+ environment: "default",
747
+ nodes: [
748
+ {
749
+ id: "node1",
750
+ type: "HTTP_REQUEST",
751
+ method: "GET",
752
+ path: "/path",
753
+ base: "https://api.example.com",
754
+ response_format: "JSON",
755
+ },
756
+ {
757
+ id: "node2",
758
+ type: "HTTP_REQUEST",
759
+ method: "GET",
760
+ path: "/path",
761
+ base: "https://api.example.com",
762
+ response_format: "JSON",
763
+ },
764
+ ],
765
+ edges: [
766
+ { from: START, to: END }, // Direct path that skips nodes
767
+ { from: "node1", to: "node2" }, // Circular - disconnected from main flow
768
+ { from: "node2", to: "node1" },
769
+ ],
770
+ };
771
+
772
+ stubClient.addStub({
773
+ match: "https://api.example.com/path",
774
+ response: {
775
+ status: 200,
776
+ statusText: "OK",
777
+ data: {},
778
+ },
779
+ });
780
+
781
+ const result = await executeMonitorV1(monitor, organizationId, options);
782
+
783
+ // Graph can execute but disconnected nodes are not executed
784
+ expect(result.success).toBe(true);
785
+ expect(result.results).toHaveLength(0); // No nodes executed
786
+ });
787
+
788
+ it("should continue execution after a failed node", async () => {
789
+ const monitor: MonitorV1 = {
790
+ id: "test-monitor-15",
791
+ project: "test-project",
792
+ frequency: { every: 1, unit: "MINUTE" },
793
+ name: "Continue After Failure Test",
794
+ version: "1.0",
795
+ environment: "default",
796
+ nodes: [
797
+ {
798
+ id: "success-node",
799
+ type: "HTTP_REQUEST",
800
+ method: "GET",
801
+ path: "/success",
802
+ base: "https://api.example.com",
803
+ response_format: "JSON",
804
+ },
805
+ {
806
+ id: "fail-node",
807
+ type: "HTTP_REQUEST",
808
+ method: "GET",
809
+ path: "/fail",
810
+ base: "https://api.example.com",
811
+ response_format: "JSON",
812
+ },
813
+ ],
814
+ edges: [
815
+ {
816
+ from: START,
817
+ to: "success-node",
818
+ },
819
+ {
820
+ from: "success-node",
821
+ to: "fail-node",
822
+ },
823
+ {
824
+ from: "fail-node",
825
+ to: END,
826
+ },
827
+ ],
828
+ };
829
+
830
+ stubClient.addStub({
831
+ match: /\/success$/,
832
+ response: { status: 200, statusText: "OK", data: { ok: true } },
833
+ });
834
+ // No stub for /fail - it will fail
835
+
836
+ const result = await executeMonitorV1(monitor, organizationId, options);
837
+
838
+ expect(result.success).toBe(false);
839
+ expect(result.results).toHaveLength(2);
840
+ expect(result.results[0].success).toBe(true);
841
+ expect(result.results[1].success).toBe(false);
842
+ });
843
+ });
844
+
845
+ describe("Response Storage", () => {
846
+ it("should store successful responses for downstream nodes", async () => {
847
+ const monitor: MonitorV1 = {
848
+ id: "test-monitor-16",
849
+ project: "test-project",
850
+ frequency: { every: 1, unit: "MINUTE" },
851
+ name: "Response Storage Test",
852
+ version: "1.0",
853
+ environment: "default",
854
+ nodes: [
855
+ {
856
+ id: "get-user-id",
857
+ type: "HTTP_REQUEST",
858
+ method: "GET",
859
+ path: "/user",
860
+ base: "https://api.example.com",
861
+ response_format: "JSON",
862
+ },
863
+ {
864
+ id: "get-profile",
865
+ type: "HTTP_REQUEST",
866
+ method: "GET",
867
+ path: "/profile",
868
+ base: "https://api.example.com",
869
+ response_format: "JSON",
870
+ },
871
+ ],
872
+ edges: [
873
+ {
874
+ from: START,
875
+ to: "get-user-id",
876
+ },
877
+ {
878
+ from: "get-user-id",
879
+ to: "get-profile",
880
+ },
881
+ {
882
+ from: "get-profile",
883
+ to: END,
884
+ },
885
+ ],
886
+ };
887
+
888
+ stubClient
889
+ .addStub({
890
+ match: /\/user$/,
891
+ response: {
892
+ status: 200,
893
+ statusText: "OK",
894
+ data: { userId: 123 },
895
+ },
896
+ })
897
+ .addStub({
898
+ match: /\/profile$/,
899
+ response: {
900
+ status: 200,
901
+ statusText: "OK",
902
+ data: { name: "John Doe", age: 30 },
903
+ },
904
+ });
905
+
906
+ const result = await executeMonitorV1(monitor, organizationId, options);
907
+
908
+ expect(result.success).toBe(true);
909
+ expect(result.results[0].response).toEqual({ userId: 123 });
910
+ expect(result.results[1].response).toEqual({ name: "John Doe", age: 30 });
911
+ });
912
+
913
+ it("should not store failed responses", async () => {
914
+ const monitor: MonitorV1 = {
915
+ id: "test-monitor-17",
916
+ project: "test-project",
917
+ frequency: { every: 1, unit: "MINUTE" },
918
+ name: "Failed Response Not Stored Test",
919
+ version: "1.0",
920
+ environment: "default",
921
+ nodes: [
922
+ {
923
+ id: "failing-endpoint",
924
+ type: "HTTP_REQUEST",
925
+ method: "GET",
926
+ path: "/fail",
927
+ base: "https://api.example.com",
928
+ response_format: "JSON",
929
+ },
930
+ ],
931
+ edges: [
932
+ {
933
+ from: START,
934
+ to: "failing-endpoint",
935
+ },
936
+ {
937
+ from: "failing-endpoint",
938
+ to: END,
939
+ },
940
+ ],
941
+ };
942
+
943
+ // No stub configured - will fail
944
+
945
+ const result = await executeMonitorV1(monitor, organizationId, options);
946
+
947
+ expect(result.success).toBe(false);
948
+ expect(result.results[0].response).toBeUndefined();
949
+ });
950
+ });
951
+
952
+ describe("Timing and Performance", () => {
953
+ it("should track total execution duration", async () => {
954
+ const monitor: MonitorV1 = {
955
+ id: "test-monitor-18",
956
+ name: "Timing Test",
957
+ version: "1.0",
958
+ environment: "default",
959
+ project: "test-project",
960
+ frequency: { every: 1, unit: "MINUTE" },
961
+ nodes: [
962
+ {
963
+ id: "endpoint",
964
+ type: "HTTP_REQUEST",
965
+ method: "GET",
966
+ path: "/data",
967
+ base: "https://api.example.com",
968
+ response_format: "JSON",
969
+ },
970
+ ],
971
+ edges: [
972
+ {
973
+ from: START,
974
+ to: "endpoint",
975
+ },
976
+ {
977
+ from: "endpoint",
978
+ to: END,
979
+ },
980
+ ],
981
+ };
982
+
983
+ stubClient.addStub({
984
+ match: "https://api.example.com/data",
985
+ response: {
986
+ status: 200,
987
+ statusText: "OK",
988
+ data: { test: true },
989
+ },
990
+ });
991
+
992
+ const result = await executeMonitorV1(monitor, organizationId, options);
993
+
994
+ expect(result.totalDuration_ms).toBeGreaterThanOrEqual(0);
995
+ expect(result.totalDuration_ms).toBeGreaterThanOrEqual(
996
+ result.results[0].duration_ms,
997
+ );
998
+ });
999
+
1000
+ it.skip("should track individual node durations", async () => {
1001
+ const monitor: MonitorV1 = {
1002
+ id: "test-monitor-19",
1003
+ project: "test-project",
1004
+ frequency: { every: 1, unit: "MINUTE" },
1005
+ name: "Node Duration Test",
1006
+ version: "1.0",
1007
+ environment: "default",
1008
+ nodes: [
1009
+ {
1010
+ id: "node1",
1011
+ type: "HTTP_REQUEST",
1012
+ method: "GET",
1013
+ path: "/1",
1014
+ base: "https://api.example.com",
1015
+ response_format: "JSON",
1016
+ },
1017
+ {
1018
+ id: "wait",
1019
+ type: "WAIT",
1020
+ duration_ms: 50,
1021
+ },
1022
+ {
1023
+ id: "node2",
1024
+ type: "HTTP_REQUEST",
1025
+ method: "GET",
1026
+ path: "/2",
1027
+ base: "https://api.example.com",
1028
+ response_format: "JSON",
1029
+ },
1030
+ ],
1031
+ edges: [
1032
+ {
1033
+ from: START,
1034
+ to: "node1",
1035
+ },
1036
+ {
1037
+ from: "node1",
1038
+ to: "wait",
1039
+ },
1040
+ {
1041
+ from: "wait",
1042
+ to: "node2",
1043
+ },
1044
+ {
1045
+ from: "node2",
1046
+ to: END,
1047
+ },
1048
+ ],
1049
+ };
1050
+
1051
+ stubClient
1052
+ .addStub({
1053
+ match: /\/1$/,
1054
+ response: { status: 200, statusText: "OK", data: {} },
1055
+ })
1056
+ .addStub({
1057
+ match: /\/2$/,
1058
+ response: { status: 200, statusText: "OK", data: {} },
1059
+ });
1060
+
1061
+ const result = await executeMonitorV1(monitor, organizationId, options);
1062
+
1063
+ expect(result.success).toBe(true);
1064
+ result.results.forEach((nodeResult) => {
1065
+ expect(nodeResult.duration_ms).toBeGreaterThanOrEqual(0);
1066
+ });
1067
+ expect(result.results[1].duration_ms).toBeGreaterThanOrEqual(50);
1068
+ });
1069
+ });
1070
+
1071
+ describe("Edge Cases", () => {
1072
+ it("should handle empty monitor (no nodes)", async () => {
1073
+ const monitor: MonitorV1 = {
1074
+ id: "test-monitor-20",
1075
+ project: "test-project",
1076
+ frequency: { every: 1, unit: "MINUTE" },
1077
+ name: "Empty Monitor Test",
1078
+ version: "1.0",
1079
+ environment: "default",
1080
+ nodes: [],
1081
+ edges: [
1082
+ {
1083
+ from: START,
1084
+ to: END,
1085
+ },
1086
+ ],
1087
+ };
1088
+
1089
+ const result = await executeMonitorV1(monitor, organizationId, options);
1090
+
1091
+ // Empty monitor with just START->END should succeed with no results
1092
+ expect(result.success).toBe(true);
1093
+ expect(result.results).toHaveLength(0);
1094
+ expect(result.errors).toHaveLength(0);
1095
+ });
1096
+
1097
+ it("should handle single node with no edges", async () => {
1098
+ const monitor: MonitorV1 = {
1099
+ id: "test-monitor-21",
1100
+ project: "test-project",
1101
+ frequency: { every: 1, unit: "MINUTE" },
1102
+ name: "Single Node Test",
1103
+ version: "1.0",
1104
+ environment: "default",
1105
+ nodes: [
1106
+ {
1107
+ id: "only-node",
1108
+ type: "HTTP_REQUEST",
1109
+ method: "GET",
1110
+ path: "/single",
1111
+ base: "https://api.example.com",
1112
+ response_format: "JSON",
1113
+ },
1114
+ ],
1115
+ edges: [
1116
+ {
1117
+ from: START,
1118
+ to: "only-node",
1119
+ },
1120
+ {
1121
+ from: "only-node",
1122
+ to: END,
1123
+ },
1124
+ ],
1125
+ };
1126
+
1127
+ stubClient.addStub({
1128
+ match: "https://api.example.com/single",
1129
+ response: {
1130
+ status: 200,
1131
+ statusText: "OK",
1132
+ data: { alone: true },
1133
+ },
1134
+ });
1135
+
1136
+ const result = await executeMonitorV1(monitor, organizationId, options);
1137
+
1138
+ expect(result.success).toBe(true);
1139
+ expect(result.results).toHaveLength(1);
1140
+ });
1141
+
1142
+ it("should handle complex response data types", async () => {
1143
+ const monitor: MonitorV1 = {
1144
+ id: "test-monitor-22",
1145
+ project: "test-project",
1146
+ frequency: { every: 1, unit: "MINUTE" },
1147
+ name: "Complex Data Test",
1148
+ version: "1.0",
1149
+ environment: "default",
1150
+ nodes: [
1151
+ {
1152
+ id: "complex",
1153
+ type: "HTTP_REQUEST",
1154
+ method: "GET",
1155
+ path: "/complex",
1156
+ base: "https://api.example.com",
1157
+ response_format: "JSON",
1158
+ },
1159
+ ],
1160
+ edges: [
1161
+ {
1162
+ from: START,
1163
+ to: "complex",
1164
+ },
1165
+ {
1166
+ from: "complex",
1167
+ to: END,
1168
+ },
1169
+ ],
1170
+ };
1171
+
1172
+ const complexData = {
1173
+ string: "test",
1174
+ number: 42,
1175
+ boolean: true,
1176
+ null: null,
1177
+ array: [1, 2, 3],
1178
+ nested: {
1179
+ deep: {
1180
+ value: "nested",
1181
+ },
1182
+ },
1183
+ };
1184
+
1185
+ stubClient.addStub({
1186
+ match: "https://api.example.com/complex",
1187
+ response: {
1188
+ status: 200,
1189
+ statusText: "OK",
1190
+ data: complexData,
1191
+ },
1192
+ });
1193
+
1194
+ const result = await executeMonitorV1(monitor, organizationId, options);
1195
+
1196
+ expect(result.success).toBe(true);
1197
+ expect(result.results[0].response).toEqual(complexData);
1198
+ });
1199
+ });
1200
+
1201
+ describe("Event Emission", () => {
1202
+ let emitter: LocalEventEmitter;
1203
+ let events: ExecutionEvent[];
1204
+
1205
+ beforeEach(() => {
1206
+ emitter = new LocalEventEmitter();
1207
+ events = [];
1208
+ emitter.subscribe((event) => events.push(event));
1209
+ });
1210
+
1211
+ it("should emit MONITOR_START and MONITOR_END events", async () => {
1212
+ const monitor: MonitorV1 = {
1213
+ id: "event-test-1",
1214
+ project: "test-project",
1215
+ frequency: { every: 1, unit: "MINUTE" },
1216
+ name: "Event Test Monitor",
1217
+ version: "1.0",
1218
+ environment: "default",
1219
+ nodes: [
1220
+ {
1221
+ id: "node-1",
1222
+ type: "HTTP_REQUEST",
1223
+ method: "GET",
1224
+ path: "/test",
1225
+ base: "https://api.example.com",
1226
+ response_format: "JSON",
1227
+ },
1228
+ ],
1229
+ edges: [
1230
+ {
1231
+ from: START,
1232
+ to: "node-1",
1233
+ },
1234
+ {
1235
+ from: "node-1",
1236
+ to: END,
1237
+ },
1238
+ ],
1239
+ };
1240
+
1241
+ stubClient.addStub({
1242
+ match: "https://api.example.com/test",
1243
+ response: { status: 200, statusText: "OK", data: { ok: true } },
1244
+ });
1245
+
1246
+ await executeMonitorV1(monitor, organizationId, {
1247
+ ...options,
1248
+ eventEmitter: emitter,
1249
+ });
1250
+
1251
+ const planStartEvents = events.filter((e) => e.type === "MONITOR_START");
1252
+ const planEndEvents = events.filter((e) => e.type === "MONITOR_END");
1253
+
1254
+ expect(planStartEvents).toHaveLength(1);
1255
+ expect(planEndEvents).toHaveLength(1);
1256
+
1257
+ const planStart = planStartEvents[0];
1258
+ expect(planStart).toMatchObject({
1259
+ type: "MONITOR_START",
1260
+ monitor_id: "event-test-1",
1261
+ monitor_name: "Event Test Monitor",
1262
+ monitor_version: "1.0",
1263
+ node_count: 1,
1264
+ edge_count: 2,
1265
+ });
1266
+
1267
+ const planEnd = planEndEvents[0];
1268
+ expect(planEnd).toMatchObject({
1269
+ type: "MONITOR_END",
1270
+ success: true,
1271
+ node_result_count: 1,
1272
+ error_count: 0,
1273
+ });
1274
+ });
1275
+
1276
+ it("should emit NODE_START and NODE_END events for each node", async () => {
1277
+ const monitor: MonitorV1 = {
1278
+ id: "event-test-2",
1279
+ project: "test-project",
1280
+ frequency: { every: 1, unit: "MINUTE" },
1281
+ name: "Multi-Node Event Test",
1282
+ version: "1.0",
1283
+ environment: "default",
1284
+ nodes: [
1285
+ {
1286
+ id: "endpoint-1",
1287
+ type: "HTTP_REQUEST",
1288
+ method: "GET",
1289
+ path: "/first",
1290
+ base: "https://api.example.com",
1291
+ response_format: "JSON",
1292
+ },
1293
+ {
1294
+ id: "wait-1",
1295
+ type: "WAIT",
1296
+ duration_ms: 10,
1297
+ },
1298
+ {
1299
+ id: "endpoint-2",
1300
+ type: "HTTP_REQUEST",
1301
+ method: "GET",
1302
+ path: "/second",
1303
+ base: "https://api.example.com",
1304
+ response_format: "JSON",
1305
+ },
1306
+ ],
1307
+ edges: [
1308
+ {
1309
+ from: START,
1310
+ to: "endpoint-1",
1311
+ },
1312
+ {
1313
+ from: "endpoint-1",
1314
+ to: "wait-1",
1315
+ },
1316
+ {
1317
+ from: "wait-1",
1318
+ to: "endpoint-2",
1319
+ },
1320
+ {
1321
+ from: "endpoint-2",
1322
+ to: END,
1323
+ },
1324
+ ],
1325
+ };
1326
+
1327
+ stubClient
1328
+ .addStub({
1329
+ match: /\/first$/,
1330
+ response: { status: 200, statusText: "OK", data: { step: 1 } },
1331
+ })
1332
+ .addStub({
1333
+ match: /\/second$/,
1334
+ response: { status: 200, statusText: "OK", data: { step: 2 } },
1335
+ });
1336
+
1337
+ await executeMonitorV1(monitor, organizationId, {
1338
+ ...options,
1339
+ eventEmitter: emitter,
1340
+ });
1341
+
1342
+ const nodeStartEvents = events.filter((e) => e.type === "NODE_START");
1343
+ const nodeEndEvents = events.filter((e) => e.type === "NODE_END");
1344
+
1345
+ expect(nodeStartEvents).toHaveLength(3);
1346
+ expect(nodeEndEvents).toHaveLength(3);
1347
+
1348
+ // Check node types
1349
+ expect(nodeStartEvents[0]).toMatchObject({
1350
+ node_id: "endpoint-1",
1351
+ node_type: "HTTP_REQUEST",
1352
+ });
1353
+ expect(nodeStartEvents[1]).toMatchObject({
1354
+ node_id: "wait-1",
1355
+ node_type: "WAIT",
1356
+ });
1357
+ expect(nodeStartEvents[2]).toMatchObject({
1358
+ node_id: "endpoint-2",
1359
+ node_type: "HTTP_REQUEST",
1360
+ });
1361
+ });
1362
+
1363
+ it("should emit HTTP_REQUEST and HTTP_RESPONSE events for endpoint nodes", async () => {
1364
+ const monitor: MonitorV1 = {
1365
+ id: "event-test-3",
1366
+ project: "test-project",
1367
+ frequency: { every: 1, unit: "MINUTE" },
1368
+ name: "HTTP Event Test",
1369
+ version: "1.0",
1370
+ environment: "default",
1371
+ nodes: [
1372
+ {
1373
+ id: "http-node",
1374
+ type: "HTTP_REQUEST",
1375
+ method: "POST",
1376
+ path: "/create",
1377
+ base: "https://api.example.com",
1378
+ headers: { "Content-Type": { $literal: "application/json" } },
1379
+ body: { name: "test" },
1380
+ response_format: "JSON",
1381
+ },
1382
+ ],
1383
+ edges: [
1384
+ {
1385
+ from: START,
1386
+ to: "http-node",
1387
+ },
1388
+ {
1389
+ from: "http-node",
1390
+ to: END,
1391
+ },
1392
+ ],
1393
+ };
1394
+
1395
+ stubClient.addStub({
1396
+ match: "https://api.example.com/create",
1397
+ response: { status: 201, statusText: "Created", data: { id: 123 } },
1398
+ });
1399
+
1400
+ await executeMonitorV1(monitor, organizationId, {
1401
+ ...options,
1402
+ eventEmitter: emitter,
1403
+ });
1404
+
1405
+ const httpRequestEvents = events.filter((e) => e.type === "HTTP_REQUEST");
1406
+ const httpResponseEvents = events.filter(
1407
+ (e) => e.type === "HTTP_RESPONSE",
1408
+ );
1409
+
1410
+ expect(httpRequestEvents).toHaveLength(1);
1411
+ expect(httpResponseEvents).toHaveLength(1);
1412
+
1413
+ const httpRequest = httpRequestEvents[0];
1414
+ expect(httpRequest).toMatchObject({
1415
+ type: "HTTP_REQUEST",
1416
+ node_id: "http-node",
1417
+ attempt: 1,
1418
+ method: "POST",
1419
+ url: "https://api.example.com/create",
1420
+ has_body: true,
1421
+ });
1422
+ expect(httpRequest.headers).toEqual({
1423
+ "Content-Type": "application/json",
1424
+ });
1425
+
1426
+ const httpResponse = httpResponseEvents[0];
1427
+ expect(httpResponse).toMatchObject({
1428
+ type: "HTTP_RESPONSE",
1429
+ node_id: "http-node",
1430
+ attempt: 1,
1431
+ status: 201,
1432
+ status_text: "Created",
1433
+ has_body: true,
1434
+ });
1435
+ expect(httpResponse.duration_ms).toBeGreaterThanOrEqual(0);
1436
+ });
1437
+
1438
+ it("should emit WAIT_START event for wait nodes", async () => {
1439
+ const monitor: MonitorV1 = {
1440
+ id: "event-test-4",
1441
+ project: "test-project",
1442
+ frequency: { every: 1, unit: "MINUTE" },
1443
+ name: "Wait Event Test",
1444
+ version: "1.0",
1445
+ environment: "default",
1446
+ nodes: [
1447
+ {
1448
+ id: "wait-node",
1449
+ type: "WAIT",
1450
+ duration_ms: 50,
1451
+ },
1452
+ ],
1453
+ edges: [
1454
+ {
1455
+ from: START,
1456
+ to: "wait-node",
1457
+ },
1458
+ {
1459
+ from: "wait-node",
1460
+ to: END,
1461
+ },
1462
+ ],
1463
+ };
1464
+
1465
+ await executeMonitorV1(monitor, organizationId, {
1466
+ ...options,
1467
+ eventEmitter: emitter,
1468
+ });
1469
+
1470
+ const waitStartEvents = events.filter((e) => e.type === "WAIT_START");
1471
+
1472
+ expect(waitStartEvents).toHaveLength(1);
1473
+ expect(waitStartEvents[0]).toMatchObject({
1474
+ type: "WAIT_START",
1475
+ node_id: "wait-node",
1476
+ duration_ms: 50,
1477
+ });
1478
+ });
1479
+
1480
+ it("should emit ERROR event on HTTP request failure", async () => {
1481
+ const monitor: MonitorV1 = {
1482
+ id: "event-test-5",
1483
+ project: "test-project",
1484
+ frequency: { every: 1, unit: "MINUTE" },
1485
+ name: "Error Event Test",
1486
+ version: "1.0",
1487
+ environment: "default",
1488
+ nodes: [
1489
+ {
1490
+ id: "failing-node",
1491
+ type: "HTTP_REQUEST",
1492
+ method: "GET",
1493
+ path: "/fail",
1494
+ base: "https://api.example.com",
1495
+ response_format: "JSON",
1496
+ },
1497
+ ],
1498
+ edges: [
1499
+ {
1500
+ from: START,
1501
+ to: "failing-node",
1502
+ },
1503
+ {
1504
+ from: "failing-node",
1505
+ to: END,
1506
+ },
1507
+ ],
1508
+ };
1509
+
1510
+ // No stub - request will fail
1511
+
1512
+ const result = await executeMonitorV1(monitor, organizationId, {
1513
+ ...options,
1514
+ eventEmitter: emitter,
1515
+ });
1516
+
1517
+ result.errors;
1518
+
1519
+ // Should emit error for the failed HTTP request
1520
+ expect(result.errors.length).toBeGreaterThan(0);
1521
+ });
1522
+
1523
+ it("should maintain monotonic sequence numbers", async () => {
1524
+ const monitor: MonitorV1 = {
1525
+ id: "event-test-6",
1526
+ project: "test-project",
1527
+ frequency: { every: 1, unit: "MINUTE" },
1528
+ name: "Sequence Test",
1529
+ version: "1.0",
1530
+ environment: "default",
1531
+ nodes: [
1532
+ {
1533
+ id: "node-1",
1534
+ type: "HTTP_REQUEST",
1535
+ method: "GET",
1536
+ path: "/test",
1537
+ base: "https://api.example.com",
1538
+ response_format: "JSON",
1539
+ },
1540
+ ],
1541
+ edges: [
1542
+ {
1543
+ from: START,
1544
+ to: "node-1",
1545
+ },
1546
+ {
1547
+ from: "node-1",
1548
+ to: END,
1549
+ },
1550
+ ],
1551
+ };
1552
+
1553
+ stubClient.addStub({
1554
+ match: "https://api.example.com/test",
1555
+ response: { status: 200, statusText: "OK", data: {} },
1556
+ });
1557
+
1558
+ await executeMonitorV1(monitor, organizationId, {
1559
+ ...options,
1560
+ eventEmitter: emitter,
1561
+ });
1562
+
1563
+ // Check that all events have the same executionId
1564
+ const executionIds = [...new Set(events.map((e) => e.execution_id))];
1565
+ expect(executionIds).toHaveLength(1);
1566
+
1567
+ // Check that sequence numbers are monotonically increasing
1568
+ const seqs = events.map((e) => e.seq);
1569
+ for (let i = 0; i < seqs.length - 1; i++) {
1570
+ expect(seqs[i + 1]).toBe(seqs[i] + 1);
1571
+ }
1572
+
1573
+ // First event should be seq 0
1574
+ expect(seqs[0]).toBe(0);
1575
+ });
1576
+
1577
+ it("should use provided executionId if given", async () => {
1578
+ const customExecutionId = "custom-exec-id-123";
1579
+
1580
+ const monitor: MonitorV1 = {
1581
+ id: "event-test-7",
1582
+ project: "test-project",
1583
+ frequency: { every: 1, unit: "MINUTE" },
1584
+ name: "Custom Execution ID Test",
1585
+ version: "1.0",
1586
+ environment: "default",
1587
+ nodes: [
1588
+ {
1589
+ id: "node-1",
1590
+ type: "HTTP_REQUEST",
1591
+ method: "GET",
1592
+ path: "/test",
1593
+ base: "https://api.example.com",
1594
+ response_format: "JSON",
1595
+ },
1596
+ ],
1597
+ edges: [
1598
+ {
1599
+ from: START,
1600
+ to: "node-1",
1601
+ },
1602
+ {
1603
+ from: "node-1",
1604
+ to: END,
1605
+ },
1606
+ ],
1607
+ };
1608
+
1609
+ stubClient.addStub({
1610
+ match: "https://api.example.com/test",
1611
+ response: { status: 200, statusText: "OK", data: {} },
1612
+ });
1613
+
1614
+ await executeMonitorV1(monitor, organizationId, {
1615
+ ...options,
1616
+ eventEmitter: emitter,
1617
+ executionId: customExecutionId,
1618
+ });
1619
+
1620
+ // All events should have the custom executionId
1621
+ events.forEach((event) => {
1622
+ expect(event.execution_id).toBe(customExecutionId);
1623
+ });
1624
+ });
1625
+
1626
+ it("should emit events in correct order", async () => {
1627
+ const monitor: MonitorV1 = {
1628
+ id: "event-test-8",
1629
+ project: "test-project",
1630
+ frequency: { every: 1, unit: "MINUTE" },
1631
+ name: "Event Order Test",
1632
+ version: "1.0",
1633
+ environment: "default",
1634
+ nodes: [
1635
+ {
1636
+ id: "node-1",
1637
+ type: "HTTP_REQUEST",
1638
+ method: "GET",
1639
+ path: "/test",
1640
+ base: "https://api.example.com",
1641
+ response_format: "JSON",
1642
+ },
1643
+ ],
1644
+ edges: [
1645
+ {
1646
+ from: START,
1647
+ to: "node-1",
1648
+ },
1649
+ {
1650
+ from: "node-1",
1651
+ to: END,
1652
+ },
1653
+ ],
1654
+ };
1655
+
1656
+ stubClient.addStub({
1657
+ match: "https://api.example.com/test",
1658
+ response: { status: 200, statusText: "OK", data: {} },
1659
+ });
1660
+
1661
+ await executeMonitorV1(monitor, organizationId, {
1662
+ ...options,
1663
+ eventEmitter: emitter,
1664
+ });
1665
+
1666
+ const eventTypes = events.map((e) => e.type);
1667
+
1668
+ // Expected order: MONITOR_START, NODE_START, HTTP_REQUEST, HTTP_RESPONSE, NODE_END, MONITOR_END
1669
+ expect(eventTypes[0]).toBe("MONITOR_START");
1670
+ expect(eventTypes[1]).toBe("NODE_START");
1671
+ expect(eventTypes[2]).toBe("HTTP_REQUEST");
1672
+ expect(eventTypes[3]).toBe("HTTP_RESPONSE");
1673
+ expect(eventTypes[4]).toBe("NODE_END");
1674
+ expect(eventTypes[5]).toBe("MONITOR_END");
1675
+ });
1676
+
1677
+ it("should handle failed HTTP requests correctly", async () => {
1678
+ const monitor: MonitorV1 = {
1679
+ id: "event-test-9",
1680
+ name: "Failed Request Event Test",
1681
+ version: "1.0",
1682
+ environment: "default",
1683
+ project: "test-project",
1684
+ frequency: { every: 1, unit: "MINUTE" },
1685
+ nodes: [
1686
+ {
1687
+ id: "failing-node",
1688
+ type: "HTTP_REQUEST",
1689
+ method: "GET",
1690
+ path: "/fail",
1691
+ base: "https://api.example.com",
1692
+ response_format: "JSON",
1693
+ },
1694
+ ],
1695
+ edges: [
1696
+ {
1697
+ from: START,
1698
+ to: "failing-node",
1699
+ },
1700
+ {
1701
+ from: "failing-node",
1702
+ to: END,
1703
+ },
1704
+ ],
1705
+ };
1706
+
1707
+ // No stub - request will fail
1708
+
1709
+ await executeMonitorV1(monitor, organizationId, {
1710
+ ...options,
1711
+ eventEmitter: emitter,
1712
+ });
1713
+
1714
+ const httpResponseEvents = events.filter(
1715
+ (e) => e.type === "HTTP_RESPONSE",
1716
+ );
1717
+ const nodeEndEvents = events.filter((e) => e.type === "NODE_END");
1718
+
1719
+ expect(httpResponseEvents).toHaveLength(1);
1720
+ expect(httpResponseEvents[0]).toMatchObject({
1721
+ status: 0,
1722
+ status_text: "Error",
1723
+ has_body: false,
1724
+ });
1725
+
1726
+ expect(nodeEndEvents[0]).toMatchObject({
1727
+ success: false,
1728
+ });
1729
+ expect(nodeEndEvents[0].error).toBeDefined();
1730
+ });
1731
+ });
1732
+ });