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