@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,799 @@
1
+ import { START, END } from "./types.js";
2
+ import { createStateGraph, graphStore } from "ts-edge";
3
+ import { randomUUID } from "crypto";
4
+ import { resolveSecretsInMonitor, planHasSecrets, SecretResolutionError, } from "./secrets/index.js";
5
+ import { utcNow } from "./utils/dates.js";
6
+ /**
7
+ * Execution context that tracks event emission state throughout a monitor execution.
8
+ * Maintains sequence counter and provides event creation helpers.
9
+ */
10
+ class ExecutionContext {
11
+ executionId;
12
+ monitor;
13
+ organizationId;
14
+ emitter;
15
+ seq = 0;
16
+ constructor(executionId, monitor, organizationId, emitter) {
17
+ this.executionId = executionId;
18
+ this.monitor = monitor;
19
+ this.organizationId = organizationId;
20
+ this.emitter = emitter;
21
+ }
22
+ /**
23
+ * Create base event properties with auto-incrementing sequence number.
24
+ */
25
+ createBaseEvent() {
26
+ return {
27
+ event_id: randomUUID(),
28
+ seq: this.seq++,
29
+ timestamp: Date.now(),
30
+ monitor_id: this.monitor.id,
31
+ execution_id: this.executionId,
32
+ organization_id: this.organizationId,
33
+ };
34
+ }
35
+ /**
36
+ * Emit an event (best-effort, non-blocking).
37
+ * Accepts an event object without base properties (those are added automatically).
38
+ */
39
+ emit(event) {
40
+ if (!this.emitter)
41
+ return;
42
+ const fullEvent = {
43
+ ...this.createBaseEvent(),
44
+ ...event,
45
+ };
46
+ try {
47
+ this.emitter.emit(fullEvent);
48
+ }
49
+ catch (error) {
50
+ // Don't let event emission errors break execution
51
+ console.error("Error emitting event:", error);
52
+ }
53
+ }
54
+ /**
55
+ * Emit an error event with proper error formatting.
56
+ */
57
+ emitError(error, context) {
58
+ const errorName = error instanceof Error ? error.constructor.name : "Error";
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ const stack = error instanceof Error ? error.stack : undefined;
61
+ this.emit({
62
+ type: "ERROR",
63
+ error_name: errorName,
64
+ message,
65
+ context,
66
+ // Only include stack in local mode (best-effort detection)
67
+ ...(stack && { stack }),
68
+ });
69
+ }
70
+ }
71
+ function buildNode(monitor, node, options) {
72
+ switch (node.type) {
73
+ case "HTTP_REQUEST": {
74
+ return {
75
+ name: node.id,
76
+ execute: async (state, tsEdgeContext) => {
77
+ const { responses, results, errors, executionContext } = state;
78
+ const nodeStartTime = Date.now();
79
+ // Emit NODE_START event
80
+ executionContext.emit({
81
+ type: "NODE_START",
82
+ node_id: node.id,
83
+ node_type: node.type,
84
+ });
85
+ // Handle NODE_STREAM events from ts-edge
86
+ const originalStream = tsEdgeContext.stream;
87
+ tsEdgeContext.stream = (chunk) => {
88
+ executionContext.emit({
89
+ type: "NODE_STREAM",
90
+ node_id: node.id,
91
+ chunk,
92
+ });
93
+ originalStream(chunk);
94
+ };
95
+ const result = await executeHttpRequest(node.id, node, options, executionContext);
96
+ // Store complete response data for downstream assertions
97
+ if (result.success && result.response !== undefined) {
98
+ responses[node.id] = {
99
+ body: result.response,
100
+ headers: result.headers || {},
101
+ status: result.status || 0,
102
+ duration_ms: result.duration_ms || 0,
103
+ };
104
+ }
105
+ // Record result in state
106
+ results.push({
107
+ nodeId: node.id,
108
+ success: result.success,
109
+ response: result.response,
110
+ error: result.error,
111
+ duration_ms: result.duration_ms,
112
+ });
113
+ // Track errors
114
+ if (!result.success && result.error) {
115
+ errors.push(`${node.id}: ${result.error}`);
116
+ }
117
+ // Emit NODE_END event
118
+ executionContext.emit({
119
+ type: "NODE_END",
120
+ node_id: node.id,
121
+ node_type: node.type,
122
+ success: result.success,
123
+ duration_ms: Date.now() - nodeStartTime,
124
+ error: result.error,
125
+ });
126
+ return { responses, results, errors, executionContext };
127
+ },
128
+ };
129
+ }
130
+ case "WAIT": {
131
+ return {
132
+ name: node.id,
133
+ execute: async (state, tsEdgeContext) => {
134
+ const { responses, results, errors, executionContext } = state;
135
+ const nodeStartTime = Date.now();
136
+ // Emit NODE_START event
137
+ executionContext.emit({
138
+ type: "NODE_START",
139
+ node_id: node.id,
140
+ node_type: node.type,
141
+ });
142
+ const result = await executeWait(node.id, node, executionContext);
143
+ // Record result in state
144
+ results.push({
145
+ nodeId: node.id,
146
+ success: result.success,
147
+ duration_ms: result.duration_ms,
148
+ });
149
+ // Emit NODE_END event
150
+ executionContext.emit({
151
+ type: "NODE_END",
152
+ node_id: node.id,
153
+ node_type: node.type,
154
+ success: result.success,
155
+ duration_ms: Date.now() - nodeStartTime,
156
+ });
157
+ return { responses, results, errors, executionContext };
158
+ },
159
+ };
160
+ }
161
+ case "ASSERTION": {
162
+ return {
163
+ name: node.id,
164
+ execute: async (state, tsEdgeContext) => {
165
+ const { responses, results, errors, executionContext } = state;
166
+ const nodeStartTime = Date.now();
167
+ // Emit NODE_START event
168
+ executionContext.emit({
169
+ type: "NODE_START",
170
+ node_id: node.id,
171
+ node_type: node.type,
172
+ });
173
+ const result = await executeAssertions(node.id, node, responses, executionContext);
174
+ // Record result in state
175
+ results.push(result);
176
+ // Track errors
177
+ if (!result.success && result.error) {
178
+ errors.push(`${node.id}: ${result.error}`);
179
+ }
180
+ // Emit NODE_END event
181
+ executionContext.emit({
182
+ type: "NODE_END",
183
+ node_id: node.id,
184
+ node_type: node.type,
185
+ success: result.success,
186
+ duration_ms: Date.now() - nodeStartTime,
187
+ error: result.error,
188
+ });
189
+ return { responses, results, errors, executionContext };
190
+ },
191
+ };
192
+ }
193
+ }
194
+ }
195
+ function buildGraph(monitor, options, executionContext) {
196
+ // Create a state store for execution
197
+ const store = graphStore(() => ({
198
+ responses: {},
199
+ results: [],
200
+ errors: [],
201
+ executionContext,
202
+ }));
203
+ const graph = createStateGraph(store)
204
+ .addNode({
205
+ name: START,
206
+ execute: () => ({}),
207
+ })
208
+ .addNode({
209
+ name: END,
210
+ execute: () => ({}),
211
+ });
212
+ // Add all nodes - cast back to DynamicStateGraph to maintain our dynamic type
213
+ const graphWithNodes = monitor.nodes.reduce((g, node) => g.addNode(buildNode(monitor, node, options)), graph);
214
+ // Add all edges
215
+ // Cast the edge method to accept string arguments since ts-edge expects literal types
216
+ // but we have runtime strings from the monitor
217
+ const graphWithEdges = monitor.edges.reduce((g, edge) => {
218
+ const addEdge = g.edge;
219
+ return addEdge(edge.from, edge.to);
220
+ }, graphWithNodes);
221
+ return graphWithEdges;
222
+ }
223
+ export async function executeMonitorV1(monitor, organizationId, options) {
224
+ const startTime = Date.now();
225
+ // Generate or use provided executionId
226
+ const executionId = options.executionId || randomUUID();
227
+ // Create execution context for event emission
228
+ const executionContext = new ExecutionContext(executionId, monitor, organizationId, options.eventEmitter);
229
+ try {
230
+ // Resolve secrets if the monitor contains any
231
+ let resolvedMonitor = monitor;
232
+ if (planHasSecrets(monitor)) {
233
+ if (!options.secretProvider) {
234
+ throw new SecretResolutionError("Monitor contains secret references but no secret provider was provided", { ref: "unknown" });
235
+ }
236
+ executionContext.emit({
237
+ type: "NODE_START",
238
+ node_id: "__SECRETS__",
239
+ node_type: "HTTP_REQUEST",
240
+ });
241
+ try {
242
+ resolvedMonitor = await resolveSecretsInMonitor(monitor, options.secretProvider);
243
+ executionContext.emit({
244
+ type: "NODE_END",
245
+ node_id: "__SECRETS__",
246
+ node_type: "HTTP_REQUEST",
247
+ success: true,
248
+ duration_ms: Date.now() - startTime,
249
+ });
250
+ }
251
+ catch (error) {
252
+ executionContext.emit({
253
+ type: "NODE_END",
254
+ node_id: "__SECRETS__",
255
+ node_type: "HTTP_REQUEST",
256
+ success: false,
257
+ duration_ms: Date.now() - startTime,
258
+ error: error instanceof Error ? error.message : String(error),
259
+ });
260
+ throw error;
261
+ }
262
+ }
263
+ // Emit MONITOR_START event
264
+ executionContext.emit({
265
+ type: "MONITOR_START",
266
+ monitor_name: resolvedMonitor.name,
267
+ monitor_version: resolvedMonitor.version,
268
+ node_count: resolvedMonitor.nodes.length,
269
+ edge_count: resolvedMonitor.edges.length,
270
+ });
271
+ // Call onStart callback if provided
272
+ if (options.statusCallbacks?.onStart) {
273
+ try {
274
+ await options.statusCallbacks.onStart();
275
+ }
276
+ catch (error) {
277
+ console.error("Error in onStart callback:", error);
278
+ // Don't fail execution due to callback errors
279
+ }
280
+ }
281
+ // Build execution graph (state-based)
282
+ const graph = buildGraph(resolvedMonitor, options, executionContext);
283
+ // Compile and run the state graph
284
+ const app = graph.compile(START, END);
285
+ const graphResult = await app.run();
286
+ // Extract final state - the output is the ExecutionState
287
+ if (!graphResult.isOk) {
288
+ const errorMessage = graphResult.error.message;
289
+ executionContext.emitError(graphResult.error, "graph_execution");
290
+ const finalResults = graphResult.output?.results || [];
291
+ const finalErrors = graphResult.output?.errors || [errorMessage];
292
+ // Emit MONITOR_END event
293
+ executionContext.emit({
294
+ type: "MONITOR_END",
295
+ success: false,
296
+ total_duration_ms: Date.now() - startTime,
297
+ node_result_count: finalResults.length,
298
+ error_count: finalErrors.length,
299
+ errors: finalErrors,
300
+ });
301
+ // Call onComplete callback if provided
302
+ if (options.statusCallbacks?.onComplete) {
303
+ try {
304
+ await options.statusCallbacks.onComplete({
305
+ status: "failed",
306
+ completedAt: utcNow(),
307
+ duration_ms: Date.now() - startTime,
308
+ success: false,
309
+ errors: finalErrors,
310
+ });
311
+ }
312
+ catch (error) {
313
+ console.error("Error in onComplete callback:", error);
314
+ // Don't fail execution due to callback errors
315
+ }
316
+ }
317
+ // Flush events before returning
318
+ await options.eventEmitter?.flush?.();
319
+ return {
320
+ success: false,
321
+ results: finalResults,
322
+ errors: finalErrors,
323
+ totalDuration_ms: Date.now() - startTime,
324
+ };
325
+ }
326
+ const finalState = graphResult.output;
327
+ const success = finalState.errors.length === 0;
328
+ const duration = Date.now() - startTime;
329
+ // Emit MONITOR_END event
330
+ executionContext.emit({
331
+ type: "MONITOR_END",
332
+ success,
333
+ total_duration_ms: duration,
334
+ node_result_count: finalState.results.length,
335
+ error_count: finalState.errors.length,
336
+ errors: finalState.errors,
337
+ });
338
+ // Call onComplete callback if provided
339
+ if (options.statusCallbacks?.onComplete) {
340
+ try {
341
+ await options.statusCallbacks.onComplete({
342
+ status: success ? "completed" : "failed",
343
+ completedAt: utcNow(),
344
+ duration_ms: duration,
345
+ success,
346
+ ...(finalState.errors.length > 0 && { errors: finalState.errors }),
347
+ });
348
+ }
349
+ catch (error) {
350
+ console.error("Error in onComplete callback:", error);
351
+ // Don't fail execution due to callback errors
352
+ }
353
+ }
354
+ // Flush events before returning
355
+ await options.eventEmitter?.flush?.();
356
+ return {
357
+ success,
358
+ results: finalState.results,
359
+ errors: finalState.errors,
360
+ totalDuration_ms: duration,
361
+ };
362
+ }
363
+ catch (error) {
364
+ // Catch any unexpected errors
365
+ executionContext.emitError(error, "unexpected_error");
366
+ const errorMessage = error instanceof Error ? error.message : String(error);
367
+ const duration = Date.now() - startTime;
368
+ // Emit MONITOR_END event
369
+ executionContext.emit({
370
+ type: "MONITOR_END",
371
+ success: false,
372
+ total_duration_ms: duration,
373
+ node_result_count: 0,
374
+ error_count: 1,
375
+ errors: [errorMessage],
376
+ });
377
+ // Call onComplete callback if provided
378
+ if (options.statusCallbacks?.onComplete) {
379
+ try {
380
+ await options.statusCallbacks.onComplete({
381
+ status: "failed",
382
+ completedAt: utcNow(),
383
+ duration_ms: duration,
384
+ success: false,
385
+ errors: [errorMessage],
386
+ });
387
+ }
388
+ catch (callbackError) {
389
+ console.error("Error in onComplete callback:", callbackError);
390
+ // Don't fail execution due to callback errors
391
+ }
392
+ }
393
+ // Flush events before throwing
394
+ await options.eventEmitter?.flush?.();
395
+ throw error;
396
+ }
397
+ }
398
+ async function executeHttpRequest(nodeId, endpoint, options, context) {
399
+ const startTime = Date.now();
400
+ // Only JSON response format is currently supported
401
+ if (endpoint.response_format !== "JSON") {
402
+ throw new Error(`Unsupported response format: ${endpoint.response_format}. Only JSON is currently supported.`);
403
+ }
404
+ // endpoint.base and endpoint.path are already resolved strings
405
+ const baseUrl = endpoint.base;
406
+ const path = endpoint.path;
407
+ const url = `${baseUrl}${path}`;
408
+ // TODO: Add retry configuration from monitor (node-level or monitor-level)
409
+ // For now, we always attempt once (attempt: 1)
410
+ const attempt = 1;
411
+ // After secret resolution, headers are guaranteed to be plain strings
412
+ // Cast is safe because resolveSecretsInMonitor substitutes all SecretRefs
413
+ const resolvedHeaders = endpoint.headers;
414
+ // Emit HTTP_REQUEST event before making the call
415
+ context.emit({
416
+ type: "HTTP_REQUEST",
417
+ node_id: nodeId,
418
+ attempt,
419
+ method: endpoint.method,
420
+ url: url,
421
+ headers: resolvedHeaders,
422
+ has_body: endpoint.body !== undefined,
423
+ });
424
+ try {
425
+ const response = await options.httpClient.request({
426
+ method: endpoint.method,
427
+ url,
428
+ headers: resolvedHeaders,
429
+ body: endpoint.body,
430
+ timeout: options.timeout || 30000,
431
+ });
432
+ const duration_ms = Date.now() - startTime;
433
+ // Emit HTTP_RESPONSE event after receiving response
434
+ context.emit({
435
+ type: "HTTP_RESPONSE",
436
+ node_id: nodeId,
437
+ attempt,
438
+ status: response.status,
439
+ status_text: response.statusText,
440
+ duration_ms,
441
+ has_body: response.data !== undefined,
442
+ });
443
+ // Parse JSON response if it's a string, otherwise use as-is
444
+ const parsedResponse = typeof response.data === "string"
445
+ ? JSON.parse(response.data)
446
+ : response.data;
447
+ return {
448
+ success: true,
449
+ response: parsedResponse,
450
+ headers: response.headers || {},
451
+ status: response.status,
452
+ duration_ms,
453
+ };
454
+ }
455
+ catch (error) {
456
+ const duration_ms = Date.now() - startTime;
457
+ const errorMessage = error instanceof Error ? error.message : String(error);
458
+ // Emit failed HTTP_RESPONSE event
459
+ context.emit({
460
+ type: "HTTP_RESPONSE",
461
+ node_id: nodeId,
462
+ attempt,
463
+ status: 0,
464
+ status_text: "Error",
465
+ duration_ms,
466
+ has_body: false,
467
+ });
468
+ return {
469
+ success: false,
470
+ error: errorMessage,
471
+ duration_ms,
472
+ };
473
+ }
474
+ }
475
+ async function executeWait(nodeId, waitNode, context) {
476
+ const startTime = Date.now();
477
+ context.emit({
478
+ type: "WAIT_START",
479
+ node_id: nodeId,
480
+ duration_ms: waitNode.duration_ms,
481
+ });
482
+ await new Promise((resolve) => setTimeout(resolve, waitNode.duration_ms));
483
+ return {
484
+ success: true,
485
+ duration_ms: Date.now() - startTime,
486
+ };
487
+ }
488
+ /**
489
+ * Extract value from response data using JSONPath
490
+ */
491
+ function extractJsonValue(json, path) {
492
+ // TODO: Implement JSONPath extraction
493
+ // If no path, return the top-level object
494
+ if (!path || path.length === 0) {
495
+ return json;
496
+ }
497
+ let current = json;
498
+ for (const segment of path) {
499
+ if (typeof current === "object" && current !== null) {
500
+ if (Array.isArray(current)) {
501
+ // Try to interpret the segment as an array index
502
+ const idx = Number(segment);
503
+ if (!isNaN(idx) && idx >= 0 && idx < current.length) {
504
+ current = current[idx];
505
+ }
506
+ else {
507
+ // If not a valid index, return undefined
508
+ return undefined;
509
+ }
510
+ }
511
+ else {
512
+ // Plain object: access property by key
513
+ if (Object.prototype.hasOwnProperty.call(current, segment)) {
514
+ current = current[segment];
515
+ }
516
+ else {
517
+ return undefined;
518
+ }
519
+ }
520
+ }
521
+ else {
522
+ // Cannot traverse further - path goes too deep
523
+ return undefined;
524
+ }
525
+ }
526
+ return current;
527
+ }
528
+ function evaluateStatusAssertion(assertion, responses) {
529
+ const { nodeId, predicate } = assertion;
530
+ const value = responses[nodeId].status;
531
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.status`);
532
+ }
533
+ function evaluateHeadersAssertion(assertion, responses) {
534
+ const { nodeId, headerName, predicate } = assertion;
535
+ const value = responses[nodeId].headers[headerName];
536
+ switch (predicate.type) {
537
+ case "unary":
538
+ return evaluateUnaryPredicate(value, predicate.operator, `${nodeId}.headers.${headerName}`);
539
+ case "binary":
540
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.headers.${headerName}`);
541
+ }
542
+ }
543
+ function evaluateLatencyAssertion(assertion, responses) {
544
+ const { nodeId, predicate } = assertion;
545
+ const value = responses[nodeId].duration_ms;
546
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.duration_ms`);
547
+ }
548
+ function evaluateBodyAssertion(assertion, responses) {
549
+ const { nodeId, responseType, path, predicate } = assertion;
550
+ let value;
551
+ switch (responseType) {
552
+ case "JSON":
553
+ value = extractJsonValue(responses[nodeId].body, path);
554
+ break;
555
+ case "XML":
556
+ throw new Error(`XML assertions are not supported yet`);
557
+ }
558
+ switch (predicate.type) {
559
+ case "unary":
560
+ return evaluateUnaryPredicate(value, predicate.operator, `${nodeId}.body.${path.join(".")}`);
561
+ case "binary":
562
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.body.${path.join(".")}`);
563
+ }
564
+ }
565
+ function evaluateAssertion(assertion, responses) {
566
+ switch (assertion.subject) {
567
+ case "status":
568
+ return evaluateStatusAssertion(assertion, responses);
569
+ case "headers":
570
+ return evaluateHeadersAssertion(assertion, responses);
571
+ case "latency":
572
+ return evaluateLatencyAssertion(assertion, responses);
573
+ case "body":
574
+ return evaluateBodyAssertion(assertion, responses);
575
+ }
576
+ }
577
+ /**
578
+ * Evaluate unary predicates
579
+ */
580
+ function evaluateUnaryPredicate(value, predicate, prefix) {
581
+ switch (predicate) {
582
+ case "IS_NULL":
583
+ return {
584
+ passed: value === null,
585
+ message: value === null
586
+ ? `${prefix} is null`
587
+ : `Expected ${prefix} to be null, got ${JSON.stringify(value)}`,
588
+ };
589
+ case "IS_NOT_NULL":
590
+ return {
591
+ passed: value !== null && value !== undefined,
592
+ message: value !== null && value !== undefined
593
+ ? `${prefix} is not null`
594
+ : `Expected ${prefix} to not be null`,
595
+ };
596
+ case "IS_TRUE":
597
+ return {
598
+ passed: value === true,
599
+ message: value === true
600
+ ? `${prefix} is true`
601
+ : `Expected ${prefix} to be true, got ${JSON.stringify(value)}`,
602
+ };
603
+ case "IS_FALSE":
604
+ return {
605
+ passed: value === false,
606
+ message: value === false
607
+ ? `${prefix} is false`
608
+ : `Expected ${prefix} to be false, got ${JSON.stringify(value)}`,
609
+ };
610
+ case "IS_EMPTY": {
611
+ const isEmpty = value === "" ||
612
+ (Array.isArray(value) && value.length === 0) ||
613
+ (typeof value === "object" &&
614
+ value !== null &&
615
+ Object.keys(value).length === 0);
616
+ return {
617
+ passed: isEmpty,
618
+ message: isEmpty
619
+ ? `${prefix} is empty`
620
+ : `Expected ${prefix} to be empty, got ${JSON.stringify(value)}`,
621
+ };
622
+ }
623
+ case "IS_NOT_EMPTY": {
624
+ const isNotEmpty = value !== "" &&
625
+ !(Array.isArray(value) && value.length === 0) &&
626
+ !(typeof value === "object" &&
627
+ value !== null &&
628
+ Object.keys(value).length === 0);
629
+ return {
630
+ passed: isNotEmpty,
631
+ message: isNotEmpty
632
+ ? `${prefix} is not empty`
633
+ : `Expected ${prefix} to not be empty`,
634
+ };
635
+ }
636
+ default:
637
+ throw new Error(`Unknown unary predicate: ${predicate}`);
638
+ }
639
+ }
640
+ /**
641
+ * Evaluate binary predicates
642
+ */
643
+ function evaluateBinaryPredicate(value, predicate,
644
+ //nodeId: string,
645
+ //accessor: string,
646
+ //pathStr: string,
647
+ prefix) {
648
+ const { operator, expected } = predicate;
649
+ switch (operator) {
650
+ case "EQUAL": {
651
+ const isEqual = JSON.stringify(value) === JSON.stringify(expected);
652
+ return {
653
+ passed: isEqual,
654
+ message: isEqual
655
+ ? `${prefix} equals ${JSON.stringify(expected)}`
656
+ : `Expected ${prefix} to equal ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`,
657
+ };
658
+ }
659
+ case "NOT_EQUAL": {
660
+ const isNotEqual = JSON.stringify(value) !== JSON.stringify(expected);
661
+ return {
662
+ passed: isNotEqual,
663
+ message: isNotEqual
664
+ ? `${prefix} does not equal ${JSON.stringify(expected)}`
665
+ : `Expected ${prefix} to not equal ${JSON.stringify(expected)}`,
666
+ };
667
+ }
668
+ case "GREATER_THAN": {
669
+ const isGT = typeof value === "number" && value > expected;
670
+ return {
671
+ passed: isGT,
672
+ message: isGT
673
+ ? `${prefix} (${value}) > ${expected}`
674
+ : `Expected ${prefix} to be greater than ${expected}, got ${JSON.stringify(value)}`,
675
+ };
676
+ }
677
+ case "LESS_THAN": {
678
+ const isLT = typeof value === "number" && value < expected;
679
+ return {
680
+ passed: isLT,
681
+ message: isLT
682
+ ? `${prefix} (${value}) < ${expected}`
683
+ : `Expected ${prefix} to be less than ${expected}, got ${JSON.stringify(value)}`,
684
+ };
685
+ }
686
+ case "GREATER_THAN_OR_EQUAL": {
687
+ const isGTE = typeof value === "number" && value >= expected;
688
+ return {
689
+ passed: isGTE,
690
+ message: isGTE
691
+ ? `${prefix} (${value}) >= ${expected}`
692
+ : `Expected ${prefix} to be >= ${expected}, got ${JSON.stringify(value)}`,
693
+ };
694
+ }
695
+ case "LESS_THAN_OR_EQUAL": {
696
+ const isLTE = typeof value === "number" && value <= expected;
697
+ return {
698
+ passed: isLTE,
699
+ message: isLTE
700
+ ? `${prefix} (${value}) <= ${expected}`
701
+ : `Expected ${prefix} to be <= ${expected}, got ${JSON.stringify(value)}`,
702
+ };
703
+ }
704
+ case "CONTAINS": {
705
+ const contains = typeof value === "string" && value.includes(expected);
706
+ return {
707
+ passed: contains,
708
+ message: contains
709
+ ? `${prefix} contains "${expected}"`
710
+ : `Expected ${prefix} to contain "${expected}", got "${value}"`,
711
+ };
712
+ }
713
+ case "NOT_CONTAINS": {
714
+ const notContains = typeof value === "string" && !value.includes(expected);
715
+ return {
716
+ passed: notContains,
717
+ message: notContains
718
+ ? `${prefix} does not contain "${expected}"`
719
+ : `Expected ${prefix} to not contain "${expected}", got "${value}"`,
720
+ };
721
+ }
722
+ case "STARTS_WITH": {
723
+ const startsWith = typeof value === "string" && value.startsWith(expected);
724
+ return {
725
+ passed: startsWith,
726
+ message: startsWith
727
+ ? `${prefix} starts with "${expected}"`
728
+ : `Expected ${prefix} to start with "${expected}", got "${value}"`,
729
+ };
730
+ }
731
+ case "NOT_STARTS_WITH": {
732
+ const notStartsWith = typeof value === "string" && !value.startsWith(expected);
733
+ return {
734
+ passed: notStartsWith,
735
+ message: notStartsWith
736
+ ? `${prefix} does not start with "${expected}"`
737
+ : `Expected ${prefix} to not start with "${expected}", got "${value}"`,
738
+ };
739
+ }
740
+ case "ENDS_WITH": {
741
+ const endsWith = typeof value === "string" && value.endsWith(expected);
742
+ return {
743
+ passed: endsWith,
744
+ message: endsWith
745
+ ? `${prefix} ends with "${expected}"`
746
+ : `Expected ${prefix} to end with "${expected}", got "${value}"`,
747
+ };
748
+ }
749
+ case "NOT_ENDS_WITH": {
750
+ const notEndsWith = typeof value === "string" && !value.endsWith(expected);
751
+ return {
752
+ passed: notEndsWith,
753
+ message: notEndsWith
754
+ ? `${prefix} does not end with "${expected}"`
755
+ : `Expected ${prefix} to not end with "${expected}", got "${value}"`,
756
+ };
757
+ }
758
+ default:
759
+ throw new Error(`Unknown binary predicate operator: ${operator}`);
760
+ }
761
+ }
762
+ async function executeAssertions(nodeId, assertionNode, responses, context) {
763
+ const startTime = Date.now();
764
+ const errors = [];
765
+ for (let i = 0; i < assertionNode.assertions.length; i++) {
766
+ const assertion = assertionNode.assertions[i];
767
+ try {
768
+ const result = evaluateAssertion(assertion, responses);
769
+ context.emit({
770
+ type: "ASSERTION_RESULT",
771
+ node_id: nodeId,
772
+ assertion_index: i,
773
+ passed: result.passed,
774
+ message: result.message,
775
+ });
776
+ if (!result.passed) {
777
+ errors.push(result.message);
778
+ }
779
+ }
780
+ catch (error) {
781
+ const message = error instanceof Error ? error.message : String(error);
782
+ errors.push(`Assertion ${i} failed: ${message}`);
783
+ context.emit({
784
+ type: "ASSERTION_RESULT",
785
+ node_id: nodeId,
786
+ assertion_index: i,
787
+ passed: false,
788
+ message,
789
+ });
790
+ }
791
+ }
792
+ return {
793
+ nodeId,
794
+ success: errors.length === 0,
795
+ error: errors.length > 0 ? errors.join("; ") : undefined,
796
+ duration_ms: Date.now() - startTime,
797
+ };
798
+ }
799
+ //# sourceMappingURL=executor.js.map