@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.
- package/README.md +152 -0
- package/dist/adapters/axios.d.ts +5 -0
- package/dist/adapters/axios.d.ts.map +1 -0
- package/dist/adapters/axios.js +36 -0
- package/dist/adapters/axios.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/stub.d.ts +22 -0
- package/dist/adapters/stub.d.ts.map +1 -0
- package/dist/adapters/stub.js +36 -0
- package/dist/adapters/stub.js.map +1 -0
- package/dist/events/adapters/in-memory.d.ts +52 -0
- package/dist/events/adapters/in-memory.d.ts.map +1 -0
- package/dist/events/adapters/in-memory.js +70 -0
- package/dist/events/adapters/in-memory.js.map +1 -0
- package/dist/events/adapters/in-memory.test.d.ts +2 -0
- package/dist/events/adapters/in-memory.test.d.ts.map +1 -0
- package/dist/events/adapters/in-memory.test.js +109 -0
- package/dist/events/adapters/in-memory.test.js.map +1 -0
- package/dist/events/adapters/index.d.ts +9 -0
- package/dist/events/adapters/index.d.ts.map +1 -0
- package/dist/events/adapters/index.js +9 -0
- package/dist/events/adapters/index.js.map +1 -0
- package/dist/events/adapters/kinesis.d.ts +91 -0
- package/dist/events/adapters/kinesis.d.ts.map +1 -0
- package/dist/events/adapters/kinesis.js +136 -0
- package/dist/events/adapters/kinesis.js.map +1 -0
- package/dist/events/adapters/kinesis.test.d.ts +2 -0
- package/dist/events/adapters/kinesis.test.d.ts.map +1 -0
- package/dist/events/adapters/kinesis.test.js +249 -0
- package/dist/events/adapters/kinesis.test.js.map +1 -0
- package/dist/events/emitter.d.ts +68 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +83 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/events/emitter.test.d.ts +2 -0
- package/dist/events/emitter.test.d.ts.map +1 -0
- package/dist/events/emitter.test.js +262 -0
- package/dist/events/emitter.test.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/types.d.ts +112 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +9 -0
- package/dist/events/types.js.map +1 -0
- package/dist/executor.d.ts +4 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +799 -0
- package/dist/executor.js.map +1 -0
- package/dist/executor.test.d.ts +2 -0
- package/dist/executor.test.d.ts.map +1 -0
- package/dist/executor.test.js +1584 -0
- package/dist/executor.test.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/secrets/factory.d.ts +121 -0
- package/dist/secrets/factory.d.ts.map +1 -0
- package/dist/secrets/factory.js +137 -0
- package/dist/secrets/factory.js.map +1 -0
- package/dist/secrets/index.d.ts +14 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +18 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/providers/aws.d.ts +63 -0
- package/dist/secrets/providers/aws.d.ts.map +1 -0
- package/dist/secrets/providers/aws.js +110 -0
- package/dist/secrets/providers/aws.js.map +1 -0
- package/dist/secrets/providers/env.d.ts +36 -0
- package/dist/secrets/providers/env.d.ts.map +1 -0
- package/dist/secrets/providers/env.js +37 -0
- package/dist/secrets/providers/env.js.map +1 -0
- package/dist/secrets/providers/index.d.ts +7 -0
- package/dist/secrets/providers/index.d.ts.map +1 -0
- package/dist/secrets/providers/index.js +7 -0
- package/dist/secrets/providers/index.js.map +1 -0
- package/dist/secrets/providers/vault.d.ts +75 -0
- package/dist/secrets/providers/vault.d.ts.map +1 -0
- package/dist/secrets/providers/vault.js +143 -0
- package/dist/secrets/providers/vault.js.map +1 -0
- package/dist/secrets/registry.d.ts +39 -0
- package/dist/secrets/registry.d.ts.map +1 -0
- package/dist/secrets/registry.js +134 -0
- package/dist/secrets/registry.js.map +1 -0
- package/dist/secrets/resolver.d.ts +45 -0
- package/dist/secrets/resolver.d.ts.map +1 -0
- package/dist/secrets/resolver.js +188 -0
- package/dist/secrets/resolver.js.map +1 -0
- package/dist/secrets/secrets.test.d.ts +2 -0
- package/dist/secrets/secrets.test.d.ts.map +1 -0
- package/dist/secrets/secrets.test.js +317 -0
- package/dist/secrets/secrets.test.js.map +1 -0
- package/dist/secrets/types.d.ts +70 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +42 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/shared.d.ts +8 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +30 -0
- package/dist/shared.js.map +1 -0
- package/dist/test-monitor-types.d.ts +43 -0
- package/dist/test-monitor-types.d.ts.map +1 -0
- package/dist/test-monitor-types.js +2 -0
- package/dist/test-monitor-types.js.map +1 -0
- package/dist/test-plan-types.d.ts +43 -0
- package/dist/test-plan-types.d.ts.map +1 -0
- package/dist/test-plan-types.js +2 -0
- package/dist/test-plan-types.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/dates.d.ts +11 -0
- package/dist/utils/dates.d.ts.map +1 -0
- package/dist/utils/dates.js +13 -0
- package/dist/utils/dates.js.map +1 -0
- package/package.json +39 -0
- package/src/adapters/axios.ts +39 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/stub.ts +47 -0
- package/src/events/adapters/README.md +144 -0
- package/src/events/adapters/in-memory.test.ts +146 -0
- package/src/events/adapters/in-memory.ts +93 -0
- package/src/events/adapters/index.ts +9 -0
- package/src/events/adapters/kinesis.test.ts +323 -0
- package/src/events/adapters/kinesis.ts +211 -0
- package/src/events/emitter.test.ts +327 -0
- package/src/events/emitter.ts +133 -0
- package/src/events/index.ts +3 -0
- package/src/events/types.ts +136 -0
- package/src/executor.test.ts +1732 -0
- package/src/executor.ts +1075 -0
- package/src/index.ts +81 -0
- package/src/secrets/factory.ts +248 -0
- package/src/secrets/index.ts +48 -0
- package/src/secrets/providers/aws.ts +178 -0
- package/src/secrets/providers/env.ts +66 -0
- package/src/secrets/providers/index.ts +15 -0
- package/src/secrets/providers/vault.ts +257 -0
- package/src/secrets/resolver.ts +269 -0
- package/src/secrets/secrets.test.ts +402 -0
- package/src/secrets/types.ts +106 -0
- package/src/shared.ts +46 -0
- package/src/test-monitor-types.ts +49 -0
- package/src/types.ts +114 -0
- package/src/utils/dates.ts +13 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
package/src/executor.ts
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Assertion,
|
|
3
|
+
Assertions,
|
|
4
|
+
Node,
|
|
5
|
+
MonitorV1,
|
|
6
|
+
Wait,
|
|
7
|
+
HttpRequest,
|
|
8
|
+
} from "@griffin-app/griffin-hub-sdk";
|
|
9
|
+
|
|
10
|
+
import type { BinaryPredicate, UnaryPredicate } from "@griffin-app/griffin-core/types";
|
|
11
|
+
import type {
|
|
12
|
+
ExecutionOptions,
|
|
13
|
+
ExecutionResult,
|
|
14
|
+
NodeResult,
|
|
15
|
+
HttpRequestResult,
|
|
16
|
+
WaitResult,
|
|
17
|
+
JSONValue,
|
|
18
|
+
NodeResponseData,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
import { START, END } from "./types.js";
|
|
21
|
+
import { createStateGraph, graphStore, StateGraphRegistry } from "ts-edge";
|
|
22
|
+
import type { ExecutionEvent, BaseEvent } from "./events/index.js";
|
|
23
|
+
import { randomUUID } from "crypto";
|
|
24
|
+
import {
|
|
25
|
+
resolveSecretsInMonitor,
|
|
26
|
+
planHasSecrets,
|
|
27
|
+
SecretResolutionError,
|
|
28
|
+
} from "./secrets/index.js";
|
|
29
|
+
import { utcNow } from "./utils/dates.js";
|
|
30
|
+
|
|
31
|
+
// Define context type that matches ts-edge's GraphNodeExecuteContext (not exported from library)
|
|
32
|
+
interface NodeExecuteContext {
|
|
33
|
+
stream: (chunk: string) => void;
|
|
34
|
+
metadata: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execution context that tracks event emission state throughout a monitor execution.
|
|
39
|
+
* Maintains sequence counter and provides event creation helpers.
|
|
40
|
+
*/
|
|
41
|
+
class ExecutionContext {
|
|
42
|
+
private seq = 0;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
public readonly executionId: string,
|
|
46
|
+
public readonly monitor: MonitorV1,
|
|
47
|
+
public readonly organizationId: string,
|
|
48
|
+
private readonly emitter?: ExecutionOptions["eventEmitter"],
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create base event properties with auto-incrementing sequence number.
|
|
53
|
+
*/
|
|
54
|
+
private createBaseEvent(): BaseEvent {
|
|
55
|
+
return {
|
|
56
|
+
event_id: randomUUID(),
|
|
57
|
+
seq: this.seq++,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
monitor_id: this.monitor.id,
|
|
60
|
+
execution_id: this.executionId,
|
|
61
|
+
organization_id: this.organizationId,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Emit an event (best-effort, non-blocking).
|
|
67
|
+
* Accepts an event object without base properties (those are added automatically).
|
|
68
|
+
*/
|
|
69
|
+
emit(event: Partial<ExecutionEvent>): void {
|
|
70
|
+
if (!this.emitter) return;
|
|
71
|
+
|
|
72
|
+
const fullEvent = {
|
|
73
|
+
...this.createBaseEvent(),
|
|
74
|
+
...event,
|
|
75
|
+
} as ExecutionEvent;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
this.emitter.emit(fullEvent);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// Don't let event emission errors break execution
|
|
81
|
+
console.error("Error emitting event:", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Emit an error event with proper error formatting.
|
|
87
|
+
*/
|
|
88
|
+
emitError(error: unknown, context?: string): void {
|
|
89
|
+
const errorName = error instanceof Error ? error.constructor.name : "Error";
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
92
|
+
|
|
93
|
+
this.emit({
|
|
94
|
+
type: "ERROR",
|
|
95
|
+
error_name: errorName,
|
|
96
|
+
message,
|
|
97
|
+
context,
|
|
98
|
+
// Only include stack in local mode (best-effort detection)
|
|
99
|
+
...(stack && { stack }),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Dynamic state graph type for runtime-constructed graphs
|
|
104
|
+
// - ExecutionState: the shared state type
|
|
105
|
+
// - string: node names are arbitrary strings (not known at compile time)
|
|
106
|
+
// - never: no nodes are pre-marked as "connected" (having outgoing edges)
|
|
107
|
+
type DynamicStateGraph = StateGraphRegistry<ExecutionState, string, never>;
|
|
108
|
+
|
|
109
|
+
// State shared across all nodes during execution
|
|
110
|
+
interface ExecutionState {
|
|
111
|
+
responses: Record<string, NodeResponseData>;
|
|
112
|
+
results: NodeResult[];
|
|
113
|
+
errors: string[];
|
|
114
|
+
executionContext: ExecutionContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildNode(
|
|
118
|
+
monitor: MonitorV1,
|
|
119
|
+
node: Node,
|
|
120
|
+
options: ExecutionOptions,
|
|
121
|
+
): {
|
|
122
|
+
name: string;
|
|
123
|
+
execute: (
|
|
124
|
+
state: ExecutionState,
|
|
125
|
+
context: NodeExecuteContext,
|
|
126
|
+
) => Promise<ExecutionState>;
|
|
127
|
+
} {
|
|
128
|
+
switch (node.type) {
|
|
129
|
+
case "HTTP_REQUEST": {
|
|
130
|
+
return {
|
|
131
|
+
name: node.id,
|
|
132
|
+
execute: async (
|
|
133
|
+
state: ExecutionState,
|
|
134
|
+
tsEdgeContext: NodeExecuteContext,
|
|
135
|
+
): Promise<ExecutionState> => {
|
|
136
|
+
const { responses, results, errors, executionContext } = state;
|
|
137
|
+
const nodeStartTime = Date.now();
|
|
138
|
+
|
|
139
|
+
// Emit NODE_START event
|
|
140
|
+
executionContext.emit({
|
|
141
|
+
type: "NODE_START",
|
|
142
|
+
node_id: node.id,
|
|
143
|
+
node_type: node.type,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Handle NODE_STREAM events from ts-edge
|
|
147
|
+
const originalStream = tsEdgeContext.stream;
|
|
148
|
+
tsEdgeContext.stream = (chunk: string) => {
|
|
149
|
+
executionContext.emit({
|
|
150
|
+
type: "NODE_STREAM",
|
|
151
|
+
node_id: node.id,
|
|
152
|
+
chunk,
|
|
153
|
+
});
|
|
154
|
+
originalStream(chunk);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const result = await executeHttpRequest(
|
|
158
|
+
node.id,
|
|
159
|
+
node,
|
|
160
|
+
options,
|
|
161
|
+
executionContext,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Store complete response data for downstream assertions
|
|
165
|
+
if (result.success && result.response !== undefined) {
|
|
166
|
+
responses[node.id] = {
|
|
167
|
+
body: result.response,
|
|
168
|
+
headers: result.headers || {},
|
|
169
|
+
status: result.status || 0,
|
|
170
|
+
duration_ms: result.duration_ms || 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Record result in state
|
|
175
|
+
results.push({
|
|
176
|
+
nodeId: node.id,
|
|
177
|
+
success: result.success,
|
|
178
|
+
response: result.response,
|
|
179
|
+
error: result.error,
|
|
180
|
+
duration_ms: result.duration_ms,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Track errors
|
|
184
|
+
if (!result.success && result.error) {
|
|
185
|
+
errors.push(`${node.id}: ${result.error}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Emit NODE_END event
|
|
189
|
+
executionContext.emit({
|
|
190
|
+
type: "NODE_END",
|
|
191
|
+
node_id: node.id,
|
|
192
|
+
node_type: node.type,
|
|
193
|
+
success: result.success,
|
|
194
|
+
duration_ms: Date.now() - nodeStartTime,
|
|
195
|
+
error: result.error,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return { responses, results, errors, executionContext };
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
case "WAIT": {
|
|
203
|
+
return {
|
|
204
|
+
name: node.id,
|
|
205
|
+
execute: async (
|
|
206
|
+
state: ExecutionState,
|
|
207
|
+
tsEdgeContext: NodeExecuteContext,
|
|
208
|
+
): Promise<ExecutionState> => {
|
|
209
|
+
const { responses, results, errors, executionContext } = state;
|
|
210
|
+
const nodeStartTime = Date.now();
|
|
211
|
+
|
|
212
|
+
// Emit NODE_START event
|
|
213
|
+
executionContext.emit({
|
|
214
|
+
type: "NODE_START",
|
|
215
|
+
node_id: node.id,
|
|
216
|
+
node_type: node.type,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const result = await executeWait(node.id, node, executionContext);
|
|
220
|
+
|
|
221
|
+
// Record result in state
|
|
222
|
+
results.push({
|
|
223
|
+
nodeId: node.id,
|
|
224
|
+
success: result.success,
|
|
225
|
+
duration_ms: result.duration_ms,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Emit NODE_END event
|
|
229
|
+
executionContext.emit({
|
|
230
|
+
type: "NODE_END",
|
|
231
|
+
node_id: node.id,
|
|
232
|
+
node_type: node.type,
|
|
233
|
+
success: result.success,
|
|
234
|
+
duration_ms: Date.now() - nodeStartTime,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return { responses, results, errors, executionContext };
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
case "ASSERTION": {
|
|
242
|
+
return {
|
|
243
|
+
name: node.id,
|
|
244
|
+
execute: async (
|
|
245
|
+
state: ExecutionState,
|
|
246
|
+
tsEdgeContext: NodeExecuteContext,
|
|
247
|
+
): Promise<ExecutionState> => {
|
|
248
|
+
const { responses, results, errors, executionContext } = state;
|
|
249
|
+
const nodeStartTime = Date.now();
|
|
250
|
+
|
|
251
|
+
// Emit NODE_START event
|
|
252
|
+
executionContext.emit({
|
|
253
|
+
type: "NODE_START",
|
|
254
|
+
node_id: node.id,
|
|
255
|
+
node_type: node.type,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const result = await executeAssertions(
|
|
259
|
+
node.id,
|
|
260
|
+
node,
|
|
261
|
+
responses,
|
|
262
|
+
executionContext,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Record result in state
|
|
266
|
+
results.push(result);
|
|
267
|
+
|
|
268
|
+
// Track errors
|
|
269
|
+
if (!result.success && result.error) {
|
|
270
|
+
errors.push(`${node.id}: ${result.error}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Emit NODE_END event
|
|
274
|
+
executionContext.emit({
|
|
275
|
+
type: "NODE_END",
|
|
276
|
+
node_id: node.id,
|
|
277
|
+
node_type: node.type,
|
|
278
|
+
success: result.success,
|
|
279
|
+
duration_ms: Date.now() - nodeStartTime,
|
|
280
|
+
error: result.error,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return { responses, results, errors, executionContext };
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildGraph(
|
|
291
|
+
monitor: MonitorV1,
|
|
292
|
+
options: ExecutionOptions,
|
|
293
|
+
executionContext: ExecutionContext,
|
|
294
|
+
): DynamicStateGraph {
|
|
295
|
+
// Create a state store for execution
|
|
296
|
+
const store = graphStore<ExecutionState>(() => ({
|
|
297
|
+
responses: {},
|
|
298
|
+
results: [],
|
|
299
|
+
errors: [],
|
|
300
|
+
executionContext,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const graph: DynamicStateGraph = createStateGraph(store)
|
|
304
|
+
.addNode({
|
|
305
|
+
name: START,
|
|
306
|
+
execute: () => ({}),
|
|
307
|
+
})
|
|
308
|
+
.addNode({
|
|
309
|
+
name: END,
|
|
310
|
+
execute: () => ({}),
|
|
311
|
+
}) as DynamicStateGraph;
|
|
312
|
+
|
|
313
|
+
// Add all nodes - cast back to DynamicStateGraph to maintain our dynamic type
|
|
314
|
+
const graphWithNodes = monitor.nodes.reduce<DynamicStateGraph>(
|
|
315
|
+
(g, node) =>
|
|
316
|
+
g.addNode(buildNode(monitor, node, options)) as DynamicStateGraph,
|
|
317
|
+
graph,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Add all edges
|
|
321
|
+
// Cast the edge method to accept string arguments since ts-edge expects literal types
|
|
322
|
+
// but we have runtime strings from the monitor
|
|
323
|
+
const graphWithEdges = monitor.edges.reduce<DynamicStateGraph>((g, edge) => {
|
|
324
|
+
const addEdge = g.edge as (from: string, to: string) => DynamicStateGraph;
|
|
325
|
+
return addEdge(edge.from, edge.to);
|
|
326
|
+
}, graphWithNodes);
|
|
327
|
+
|
|
328
|
+
return graphWithEdges;
|
|
329
|
+
}
|
|
330
|
+
export async function executeMonitorV1(
|
|
331
|
+
monitor: MonitorV1,
|
|
332
|
+
organizationId: string,
|
|
333
|
+
options: ExecutionOptions,
|
|
334
|
+
): Promise<ExecutionResult> {
|
|
335
|
+
const startTime = Date.now();
|
|
336
|
+
|
|
337
|
+
// Generate or use provided executionId
|
|
338
|
+
const executionId = options.executionId || randomUUID();
|
|
339
|
+
|
|
340
|
+
// Create execution context for event emission
|
|
341
|
+
const executionContext = new ExecutionContext(
|
|
342
|
+
executionId,
|
|
343
|
+
monitor,
|
|
344
|
+
organizationId,
|
|
345
|
+
options.eventEmitter,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// Resolve secrets if the monitor contains any
|
|
350
|
+
let resolvedMonitor = monitor;
|
|
351
|
+
if (planHasSecrets(monitor)) {
|
|
352
|
+
if (!options.secretProvider) {
|
|
353
|
+
throw new SecretResolutionError(
|
|
354
|
+
"Monitor contains secret references but no secret provider was provided",
|
|
355
|
+
{ ref: "unknown" },
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
executionContext.emit({
|
|
360
|
+
type: "NODE_START",
|
|
361
|
+
node_id: "__SECRETS__",
|
|
362
|
+
node_type: "HTTP_REQUEST",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
resolvedMonitor = await resolveSecretsInMonitor(
|
|
367
|
+
monitor,
|
|
368
|
+
options.secretProvider,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
executionContext.emit({
|
|
372
|
+
type: "NODE_END",
|
|
373
|
+
node_id: "__SECRETS__",
|
|
374
|
+
node_type: "HTTP_REQUEST",
|
|
375
|
+
success: true,
|
|
376
|
+
duration_ms: Date.now() - startTime,
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
executionContext.emit({
|
|
380
|
+
type: "NODE_END",
|
|
381
|
+
node_id: "__SECRETS__",
|
|
382
|
+
node_type: "HTTP_REQUEST",
|
|
383
|
+
success: false,
|
|
384
|
+
duration_ms: Date.now() - startTime,
|
|
385
|
+
error: error instanceof Error ? error.message : String(error),
|
|
386
|
+
});
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Emit MONITOR_START event
|
|
392
|
+
executionContext.emit({
|
|
393
|
+
type: "MONITOR_START",
|
|
394
|
+
monitor_name: resolvedMonitor.name,
|
|
395
|
+
monitor_version: resolvedMonitor.version,
|
|
396
|
+
node_count: resolvedMonitor.nodes.length,
|
|
397
|
+
edge_count: resolvedMonitor.edges.length,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Call onStart callback if provided
|
|
401
|
+
if (options.statusCallbacks?.onStart) {
|
|
402
|
+
try {
|
|
403
|
+
await options.statusCallbacks.onStart();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error("Error in onStart callback:", error);
|
|
406
|
+
// Don't fail execution due to callback errors
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Build execution graph (state-based)
|
|
411
|
+
const graph = buildGraph(resolvedMonitor, options, executionContext);
|
|
412
|
+
|
|
413
|
+
// Compile and run the state graph
|
|
414
|
+
const app = graph.compile(START, END);
|
|
415
|
+
const graphResult = await app.run();
|
|
416
|
+
|
|
417
|
+
// Extract final state - the output is the ExecutionState
|
|
418
|
+
if (!graphResult.isOk) {
|
|
419
|
+
const errorMessage = graphResult.error.message;
|
|
420
|
+
executionContext.emitError(graphResult.error, "graph_execution");
|
|
421
|
+
|
|
422
|
+
const finalResults = graphResult.output?.results || [];
|
|
423
|
+
const finalErrors = graphResult.output?.errors || [errorMessage];
|
|
424
|
+
|
|
425
|
+
// Emit MONITOR_END event
|
|
426
|
+
executionContext.emit({
|
|
427
|
+
type: "MONITOR_END",
|
|
428
|
+
success: false,
|
|
429
|
+
total_duration_ms: Date.now() - startTime,
|
|
430
|
+
node_result_count: finalResults.length,
|
|
431
|
+
error_count: finalErrors.length,
|
|
432
|
+
errors: finalErrors,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Call onComplete callback if provided
|
|
436
|
+
if (options.statusCallbacks?.onComplete) {
|
|
437
|
+
try {
|
|
438
|
+
await options.statusCallbacks.onComplete({
|
|
439
|
+
status: "failed",
|
|
440
|
+
completedAt: utcNow(),
|
|
441
|
+
duration_ms: Date.now() - startTime,
|
|
442
|
+
success: false,
|
|
443
|
+
errors: finalErrors,
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error("Error in onComplete callback:", error);
|
|
447
|
+
// Don't fail execution due to callback errors
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Flush events before returning
|
|
452
|
+
await options.eventEmitter?.flush?.();
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
success: false,
|
|
456
|
+
results: finalResults,
|
|
457
|
+
errors: finalErrors,
|
|
458
|
+
totalDuration_ms: Date.now() - startTime,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const finalState = graphResult.output;
|
|
463
|
+
const success = finalState.errors.length === 0;
|
|
464
|
+
const duration = Date.now() - startTime;
|
|
465
|
+
|
|
466
|
+
// Emit MONITOR_END event
|
|
467
|
+
executionContext.emit({
|
|
468
|
+
type: "MONITOR_END",
|
|
469
|
+
success,
|
|
470
|
+
total_duration_ms: duration,
|
|
471
|
+
node_result_count: finalState.results.length,
|
|
472
|
+
error_count: finalState.errors.length,
|
|
473
|
+
errors: finalState.errors,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Call onComplete callback if provided
|
|
477
|
+
if (options.statusCallbacks?.onComplete) {
|
|
478
|
+
try {
|
|
479
|
+
await options.statusCallbacks.onComplete({
|
|
480
|
+
status: success ? "completed" : "failed",
|
|
481
|
+
completedAt: utcNow(),
|
|
482
|
+
duration_ms: duration,
|
|
483
|
+
success,
|
|
484
|
+
...(finalState.errors.length > 0 && { errors: finalState.errors }),
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error("Error in onComplete callback:", error);
|
|
488
|
+
// Don't fail execution due to callback errors
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Flush events before returning
|
|
493
|
+
await options.eventEmitter?.flush?.();
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
success,
|
|
497
|
+
results: finalState.results,
|
|
498
|
+
errors: finalState.errors,
|
|
499
|
+
totalDuration_ms: duration,
|
|
500
|
+
};
|
|
501
|
+
} catch (error: unknown) {
|
|
502
|
+
// Catch any unexpected errors
|
|
503
|
+
executionContext.emitError(error, "unexpected_error");
|
|
504
|
+
|
|
505
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
506
|
+
const duration = Date.now() - startTime;
|
|
507
|
+
|
|
508
|
+
// Emit MONITOR_END event
|
|
509
|
+
executionContext.emit({
|
|
510
|
+
type: "MONITOR_END",
|
|
511
|
+
success: false,
|
|
512
|
+
total_duration_ms: duration,
|
|
513
|
+
node_result_count: 0,
|
|
514
|
+
error_count: 1,
|
|
515
|
+
errors: [errorMessage],
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Call onComplete callback if provided
|
|
519
|
+
if (options.statusCallbacks?.onComplete) {
|
|
520
|
+
try {
|
|
521
|
+
await options.statusCallbacks.onComplete({
|
|
522
|
+
status: "failed",
|
|
523
|
+
completedAt: utcNow(),
|
|
524
|
+
duration_ms: duration,
|
|
525
|
+
success: false,
|
|
526
|
+
errors: [errorMessage],
|
|
527
|
+
});
|
|
528
|
+
} catch (callbackError) {
|
|
529
|
+
console.error("Error in onComplete callback:", callbackError);
|
|
530
|
+
// Don't fail execution due to callback errors
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Flush events before throwing
|
|
535
|
+
await options.eventEmitter?.flush?.();
|
|
536
|
+
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function executeHttpRequest(
|
|
542
|
+
nodeId: string,
|
|
543
|
+
endpoint: HttpRequest,
|
|
544
|
+
options: ExecutionOptions,
|
|
545
|
+
context: ExecutionContext,
|
|
546
|
+
): Promise<HttpRequestResult> {
|
|
547
|
+
const startTime = Date.now();
|
|
548
|
+
|
|
549
|
+
// Only JSON response format is currently supported
|
|
550
|
+
if (endpoint.response_format !== "JSON") {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`Unsupported response format: ${endpoint.response_format}. Only JSON is currently supported.`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// endpoint.base and endpoint.path are already resolved strings
|
|
557
|
+
const baseUrl = endpoint.base;
|
|
558
|
+
const path = endpoint.path;
|
|
559
|
+
const url = `${baseUrl}${path}`;
|
|
560
|
+
|
|
561
|
+
// TODO: Add retry configuration from monitor (node-level or monitor-level)
|
|
562
|
+
// For now, we always attempt once (attempt: 1)
|
|
563
|
+
const attempt = 1;
|
|
564
|
+
|
|
565
|
+
// After secret resolution, headers are guaranteed to be plain strings
|
|
566
|
+
// Cast is safe because resolveSecretsInMonitor substitutes all SecretRefs
|
|
567
|
+
const resolvedHeaders = endpoint.headers as
|
|
568
|
+
| Record<string, string>
|
|
569
|
+
| undefined;
|
|
570
|
+
|
|
571
|
+
// Emit HTTP_REQUEST event before making the call
|
|
572
|
+
context.emit({
|
|
573
|
+
type: "HTTP_REQUEST",
|
|
574
|
+
node_id: nodeId,
|
|
575
|
+
attempt,
|
|
576
|
+
method: endpoint.method,
|
|
577
|
+
url: url,
|
|
578
|
+
headers: resolvedHeaders,
|
|
579
|
+
has_body: endpoint.body !== undefined,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
const response = await options.httpClient.request({
|
|
584
|
+
method: endpoint.method,
|
|
585
|
+
url,
|
|
586
|
+
headers: resolvedHeaders,
|
|
587
|
+
body: endpoint.body,
|
|
588
|
+
timeout: options.timeout || 30000,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const duration_ms = Date.now() - startTime;
|
|
592
|
+
|
|
593
|
+
// Emit HTTP_RESPONSE event after receiving response
|
|
594
|
+
context.emit({
|
|
595
|
+
type: "HTTP_RESPONSE",
|
|
596
|
+
node_id: nodeId,
|
|
597
|
+
attempt,
|
|
598
|
+
status: response.status,
|
|
599
|
+
status_text: response.statusText,
|
|
600
|
+
duration_ms,
|
|
601
|
+
has_body: response.data !== undefined,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Parse JSON response if it's a string, otherwise use as-is
|
|
605
|
+
const parsedResponse: JSONValue =
|
|
606
|
+
typeof response.data === "string"
|
|
607
|
+
? JSON.parse(response.data)
|
|
608
|
+
: response.data;
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
success: true,
|
|
612
|
+
response: parsedResponse,
|
|
613
|
+
headers: response.headers || {},
|
|
614
|
+
status: response.status,
|
|
615
|
+
duration_ms,
|
|
616
|
+
};
|
|
617
|
+
} catch (error: unknown) {
|
|
618
|
+
const duration_ms = Date.now() - startTime;
|
|
619
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
620
|
+
// Emit failed HTTP_RESPONSE event
|
|
621
|
+
context.emit({
|
|
622
|
+
type: "HTTP_RESPONSE",
|
|
623
|
+
node_id: nodeId,
|
|
624
|
+
attempt,
|
|
625
|
+
status: 0,
|
|
626
|
+
status_text: "Error",
|
|
627
|
+
duration_ms,
|
|
628
|
+
has_body: false,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
error: errorMessage,
|
|
634
|
+
duration_ms,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function executeWait(
|
|
640
|
+
nodeId: string,
|
|
641
|
+
waitNode: Wait,
|
|
642
|
+
context: ExecutionContext,
|
|
643
|
+
): Promise<WaitResult> {
|
|
644
|
+
const startTime = Date.now();
|
|
645
|
+
|
|
646
|
+
context.emit({
|
|
647
|
+
type: "WAIT_START",
|
|
648
|
+
node_id: nodeId,
|
|
649
|
+
duration_ms: waitNode.duration_ms,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
await new Promise((resolve) => setTimeout(resolve, waitNode.duration_ms));
|
|
653
|
+
return {
|
|
654
|
+
success: true,
|
|
655
|
+
duration_ms: Date.now() - startTime,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Extract value from response data using JSONPath
|
|
661
|
+
*/
|
|
662
|
+
function extractJsonValue(json: JSONValue, path: string[]): unknown {
|
|
663
|
+
// TODO: Implement JSONPath extraction
|
|
664
|
+
// If no path, return the top-level object
|
|
665
|
+
if (!path || path.length === 0) {
|
|
666
|
+
return json;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let current: unknown = json;
|
|
670
|
+
|
|
671
|
+
for (const segment of path) {
|
|
672
|
+
if (typeof current === "object" && current !== null) {
|
|
673
|
+
if (Array.isArray(current)) {
|
|
674
|
+
// Try to interpret the segment as an array index
|
|
675
|
+
const idx = Number(segment);
|
|
676
|
+
if (!isNaN(idx) && idx >= 0 && idx < current.length) {
|
|
677
|
+
current = current[idx];
|
|
678
|
+
} else {
|
|
679
|
+
// If not a valid index, return undefined
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
// Plain object: access property by key
|
|
684
|
+
if (Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
685
|
+
current = (current as Record<string, unknown>)[segment];
|
|
686
|
+
} else {
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// Cannot traverse further - path goes too deep
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return current;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function evaluateStatusAssertion(
|
|
699
|
+
assertion: { subject: "status"; predicate: BinaryPredicate; nodeId: string },
|
|
700
|
+
responses: Record<string, NodeResponseData>,
|
|
701
|
+
) {
|
|
702
|
+
const { nodeId, predicate } = assertion;
|
|
703
|
+
const value = responses[nodeId].status;
|
|
704
|
+
return evaluateBinaryPredicate(value, predicate, `${nodeId}.status`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function evaluateHeadersAssertion(
|
|
708
|
+
assertion: {
|
|
709
|
+
subject: "headers";
|
|
710
|
+
predicate: UnaryPredicate | BinaryPredicate;
|
|
711
|
+
nodeId: string;
|
|
712
|
+
headerName: string;
|
|
713
|
+
},
|
|
714
|
+
responses: Record<string, NodeResponseData>,
|
|
715
|
+
) {
|
|
716
|
+
const { nodeId, headerName, predicate } = assertion;
|
|
717
|
+
const value = responses[nodeId].headers[headerName];
|
|
718
|
+
switch (predicate.type) {
|
|
719
|
+
case "unary":
|
|
720
|
+
return evaluateUnaryPredicate(
|
|
721
|
+
value,
|
|
722
|
+
predicate.operator,
|
|
723
|
+
`${nodeId}.headers.${headerName}`,
|
|
724
|
+
);
|
|
725
|
+
case "binary":
|
|
726
|
+
return evaluateBinaryPredicate(
|
|
727
|
+
value,
|
|
728
|
+
predicate,
|
|
729
|
+
`${nodeId}.headers.${headerName}`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function evaluateLatencyAssertion(
|
|
734
|
+
assertion: { subject: "latency"; predicate: BinaryPredicate; nodeId: string },
|
|
735
|
+
responses: Record<string, NodeResponseData>,
|
|
736
|
+
) {
|
|
737
|
+
const { nodeId, predicate } = assertion;
|
|
738
|
+
const value = responses[nodeId].duration_ms;
|
|
739
|
+
return evaluateBinaryPredicate(value, predicate, `${nodeId}.duration_ms`);
|
|
740
|
+
}
|
|
741
|
+
function evaluateBodyAssertion(
|
|
742
|
+
assertion: {
|
|
743
|
+
subject: "body";
|
|
744
|
+
predicate: UnaryPredicate | BinaryPredicate;
|
|
745
|
+
nodeId: string;
|
|
746
|
+
responseType: "JSON" | "XML";
|
|
747
|
+
path: string[];
|
|
748
|
+
},
|
|
749
|
+
responses: Record<string, NodeResponseData>,
|
|
750
|
+
) {
|
|
751
|
+
const { nodeId, responseType, path, predicate } = assertion;
|
|
752
|
+
let value: unknown;
|
|
753
|
+
switch (responseType) {
|
|
754
|
+
case "JSON":
|
|
755
|
+
value = extractJsonValue(responses[nodeId].body, path);
|
|
756
|
+
break;
|
|
757
|
+
case "XML":
|
|
758
|
+
throw new Error(`XML assertions are not supported yet`);
|
|
759
|
+
}
|
|
760
|
+
switch (predicate.type) {
|
|
761
|
+
case "unary":
|
|
762
|
+
return evaluateUnaryPredicate(
|
|
763
|
+
value,
|
|
764
|
+
predicate.operator,
|
|
765
|
+
`${nodeId}.body.${path.join(".")}`,
|
|
766
|
+
);
|
|
767
|
+
case "binary":
|
|
768
|
+
return evaluateBinaryPredicate(
|
|
769
|
+
value,
|
|
770
|
+
predicate,
|
|
771
|
+
`${nodeId}.body.${path.join(".")}`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function evaluateAssertion(
|
|
777
|
+
assertion: Assertion,
|
|
778
|
+
responses: Record<string, NodeResponseData>,
|
|
779
|
+
): { passed: boolean; message: string } {
|
|
780
|
+
switch (assertion.subject) {
|
|
781
|
+
case "status":
|
|
782
|
+
return evaluateStatusAssertion(assertion, responses);
|
|
783
|
+
case "headers":
|
|
784
|
+
return evaluateHeadersAssertion(assertion, responses);
|
|
785
|
+
case "latency":
|
|
786
|
+
return evaluateLatencyAssertion(assertion, responses);
|
|
787
|
+
case "body":
|
|
788
|
+
return evaluateBodyAssertion(assertion, responses);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Evaluate unary predicates
|
|
795
|
+
*/
|
|
796
|
+
function evaluateUnaryPredicate(
|
|
797
|
+
value: unknown,
|
|
798
|
+
predicate:
|
|
799
|
+
| "IS_NULL"
|
|
800
|
+
| "IS_NOT_NULL"
|
|
801
|
+
| "IS_TRUE"
|
|
802
|
+
| "IS_FALSE"
|
|
803
|
+
| "IS_EMPTY"
|
|
804
|
+
| "IS_NOT_EMPTY",
|
|
805
|
+
prefix: string,
|
|
806
|
+
//nodeId: string,
|
|
807
|
+
//accessor: string,
|
|
808
|
+
//pathStr: string,
|
|
809
|
+
): { passed: boolean; message: string } {
|
|
810
|
+
switch (predicate) {
|
|
811
|
+
case "IS_NULL":
|
|
812
|
+
return {
|
|
813
|
+
passed: value === null,
|
|
814
|
+
message:
|
|
815
|
+
value === null
|
|
816
|
+
? `${prefix} is null`
|
|
817
|
+
: `Expected ${prefix} to be null, got ${JSON.stringify(value)}`,
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
case "IS_NOT_NULL":
|
|
821
|
+
return {
|
|
822
|
+
passed: value !== null && value !== undefined,
|
|
823
|
+
message:
|
|
824
|
+
value !== null && value !== undefined
|
|
825
|
+
? `${prefix} is not null`
|
|
826
|
+
: `Expected ${prefix} to not be null`,
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
case "IS_TRUE":
|
|
830
|
+
return {
|
|
831
|
+
passed: value === true,
|
|
832
|
+
message:
|
|
833
|
+
value === true
|
|
834
|
+
? `${prefix} is true`
|
|
835
|
+
: `Expected ${prefix} to be true, got ${JSON.stringify(value)}`,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
case "IS_FALSE":
|
|
839
|
+
return {
|
|
840
|
+
passed: value === false,
|
|
841
|
+
message:
|
|
842
|
+
value === false
|
|
843
|
+
? `${prefix} is false`
|
|
844
|
+
: `Expected ${prefix} to be false, got ${JSON.stringify(value)}`,
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
case "IS_EMPTY": {
|
|
848
|
+
const isEmpty =
|
|
849
|
+
value === "" ||
|
|
850
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
851
|
+
(typeof value === "object" &&
|
|
852
|
+
value !== null &&
|
|
853
|
+
Object.keys(value).length === 0);
|
|
854
|
+
return {
|
|
855
|
+
passed: isEmpty,
|
|
856
|
+
message: isEmpty
|
|
857
|
+
? `${prefix} is empty`
|
|
858
|
+
: `Expected ${prefix} to be empty, got ${JSON.stringify(value)}`,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
case "IS_NOT_EMPTY": {
|
|
863
|
+
const isNotEmpty =
|
|
864
|
+
value !== "" &&
|
|
865
|
+
!(Array.isArray(value) && value.length === 0) &&
|
|
866
|
+
!(
|
|
867
|
+
typeof value === "object" &&
|
|
868
|
+
value !== null &&
|
|
869
|
+
Object.keys(value).length === 0
|
|
870
|
+
);
|
|
871
|
+
return {
|
|
872
|
+
passed: isNotEmpty,
|
|
873
|
+
message: isNotEmpty
|
|
874
|
+
? `${prefix} is not empty`
|
|
875
|
+
: `Expected ${prefix} to not be empty`,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
default:
|
|
880
|
+
throw new Error(`Unknown unary predicate: ${predicate}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Evaluate binary predicates
|
|
886
|
+
*/
|
|
887
|
+
function evaluateBinaryPredicate(
|
|
888
|
+
value: unknown,
|
|
889
|
+
predicate: BinaryPredicate,
|
|
890
|
+
//nodeId: string,
|
|
891
|
+
//accessor: string,
|
|
892
|
+
//pathStr: string,
|
|
893
|
+
prefix: string,
|
|
894
|
+
): { passed: boolean; message: string } {
|
|
895
|
+
const { operator, expected } = predicate;
|
|
896
|
+
|
|
897
|
+
switch (operator) {
|
|
898
|
+
case "EQUAL": {
|
|
899
|
+
const isEqual = JSON.stringify(value) === JSON.stringify(expected);
|
|
900
|
+
return {
|
|
901
|
+
passed: isEqual,
|
|
902
|
+
message: isEqual
|
|
903
|
+
? `${prefix} equals ${JSON.stringify(expected)}`
|
|
904
|
+
: `Expected ${prefix} to equal ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
case "NOT_EQUAL": {
|
|
909
|
+
const isNotEqual = JSON.stringify(value) !== JSON.stringify(expected);
|
|
910
|
+
return {
|
|
911
|
+
passed: isNotEqual,
|
|
912
|
+
message: isNotEqual
|
|
913
|
+
? `${prefix} does not equal ${JSON.stringify(expected)}`
|
|
914
|
+
: `Expected ${prefix} to not equal ${JSON.stringify(expected)}`,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
case "GREATER_THAN": {
|
|
919
|
+
const isGT = typeof value === "number" && value > (expected as number);
|
|
920
|
+
return {
|
|
921
|
+
passed: isGT,
|
|
922
|
+
message: isGT
|
|
923
|
+
? `${prefix} (${value}) > ${expected}`
|
|
924
|
+
: `Expected ${prefix} to be greater than ${expected}, got ${JSON.stringify(value)}`,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
case "LESS_THAN": {
|
|
929
|
+
const isLT = typeof value === "number" && value < (expected as number);
|
|
930
|
+
return {
|
|
931
|
+
passed: isLT,
|
|
932
|
+
message: isLT
|
|
933
|
+
? `${prefix} (${value}) < ${expected}`
|
|
934
|
+
: `Expected ${prefix} to be less than ${expected}, got ${JSON.stringify(value)}`,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
case "GREATER_THAN_OR_EQUAL": {
|
|
939
|
+
const isGTE = typeof value === "number" && value >= (expected as number);
|
|
940
|
+
return {
|
|
941
|
+
passed: isGTE,
|
|
942
|
+
message: isGTE
|
|
943
|
+
? `${prefix} (${value}) >= ${expected}`
|
|
944
|
+
: `Expected ${prefix} to be >= ${expected}, got ${JSON.stringify(value)}`,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
case "LESS_THAN_OR_EQUAL": {
|
|
949
|
+
const isLTE = typeof value === "number" && value <= (expected as number);
|
|
950
|
+
return {
|
|
951
|
+
passed: isLTE,
|
|
952
|
+
message: isLTE
|
|
953
|
+
? `${prefix} (${value}) <= ${expected}`
|
|
954
|
+
: `Expected ${prefix} to be <= ${expected}, got ${JSON.stringify(value)}`,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
case "CONTAINS": {
|
|
959
|
+
const contains =
|
|
960
|
+
typeof value === "string" && value.includes(expected as string);
|
|
961
|
+
return {
|
|
962
|
+
passed: contains,
|
|
963
|
+
message: contains
|
|
964
|
+
? `${prefix} contains "${expected}"`
|
|
965
|
+
: `Expected ${prefix} to contain "${expected}", got "${value}"`,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
case "NOT_CONTAINS": {
|
|
970
|
+
const notContains =
|
|
971
|
+
typeof value === "string" && !value.includes(expected as string);
|
|
972
|
+
return {
|
|
973
|
+
passed: notContains,
|
|
974
|
+
message: notContains
|
|
975
|
+
? `${prefix} does not contain "${expected}"`
|
|
976
|
+
: `Expected ${prefix} to not contain "${expected}", got "${value}"`,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
case "STARTS_WITH": {
|
|
981
|
+
const startsWith =
|
|
982
|
+
typeof value === "string" && value.startsWith(expected as string);
|
|
983
|
+
return {
|
|
984
|
+
passed: startsWith,
|
|
985
|
+
message: startsWith
|
|
986
|
+
? `${prefix} starts with "${expected}"`
|
|
987
|
+
: `Expected ${prefix} to start with "${expected}", got "${value}"`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
case "NOT_STARTS_WITH": {
|
|
992
|
+
const notStartsWith =
|
|
993
|
+
typeof value === "string" && !value.startsWith(expected as string);
|
|
994
|
+
return {
|
|
995
|
+
passed: notStartsWith,
|
|
996
|
+
message: notStartsWith
|
|
997
|
+
? `${prefix} does not start with "${expected}"`
|
|
998
|
+
: `Expected ${prefix} to not start with "${expected}", got "${value}"`,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
case "ENDS_WITH": {
|
|
1003
|
+
const endsWith =
|
|
1004
|
+
typeof value === "string" && value.endsWith(expected as string);
|
|
1005
|
+
return {
|
|
1006
|
+
passed: endsWith,
|
|
1007
|
+
message: endsWith
|
|
1008
|
+
? `${prefix} ends with "${expected}"`
|
|
1009
|
+
: `Expected ${prefix} to end with "${expected}", got "${value}"`,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
case "NOT_ENDS_WITH": {
|
|
1014
|
+
const notEndsWith =
|
|
1015
|
+
typeof value === "string" && !value.endsWith(expected as string);
|
|
1016
|
+
return {
|
|
1017
|
+
passed: notEndsWith,
|
|
1018
|
+
message: notEndsWith
|
|
1019
|
+
? `${prefix} does not end with "${expected}"`
|
|
1020
|
+
: `Expected ${prefix} to not end with "${expected}", got "${value}"`,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
default:
|
|
1025
|
+
throw new Error(`Unknown binary predicate operator: ${operator}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function executeAssertions(
|
|
1030
|
+
nodeId: string,
|
|
1031
|
+
assertionNode: Assertions,
|
|
1032
|
+
responses: Record<string, NodeResponseData>,
|
|
1033
|
+
context: ExecutionContext,
|
|
1034
|
+
): Promise<NodeResult> {
|
|
1035
|
+
const startTime = Date.now();
|
|
1036
|
+
const errors: string[] = [];
|
|
1037
|
+
|
|
1038
|
+
for (let i = 0; i < assertionNode.assertions.length; i++) {
|
|
1039
|
+
const assertion = assertionNode.assertions[i];
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
const result = evaluateAssertion(assertion, responses);
|
|
1043
|
+
|
|
1044
|
+
context.emit({
|
|
1045
|
+
type: "ASSERTION_RESULT",
|
|
1046
|
+
node_id: nodeId,
|
|
1047
|
+
assertion_index: i,
|
|
1048
|
+
passed: result.passed,
|
|
1049
|
+
message: result.message,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
if (!result.passed) {
|
|
1053
|
+
errors.push(result.message);
|
|
1054
|
+
}
|
|
1055
|
+
} catch (error: unknown) {
|
|
1056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1057
|
+
errors.push(`Assertion ${i} failed: ${message}`);
|
|
1058
|
+
|
|
1059
|
+
context.emit({
|
|
1060
|
+
type: "ASSERTION_RESULT",
|
|
1061
|
+
node_id: nodeId,
|
|
1062
|
+
assertion_index: i,
|
|
1063
|
+
passed: false,
|
|
1064
|
+
message,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
nodeId,
|
|
1071
|
+
success: errors.length === 0,
|
|
1072
|
+
error: errors.length > 0 ? errors.join("; ") : undefined,
|
|
1073
|
+
duration_ms: Date.now() - startTime,
|
|
1074
|
+
};
|
|
1075
|
+
}
|