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