@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.
Files changed (114) 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/emitter.d.ts +68 -0
  15. package/dist/events/emitter.d.ts.map +1 -0
  16. package/dist/events/emitter.js +83 -0
  17. package/dist/events/emitter.js.map +1 -0
  18. package/dist/events/emitter.test.d.ts +2 -0
  19. package/dist/events/emitter.test.d.ts.map +1 -0
  20. package/dist/events/emitter.test.js +251 -0
  21. package/dist/events/emitter.test.js.map +1 -0
  22. package/dist/events/index.d.ts +3 -0
  23. package/dist/events/index.d.ts.map +1 -0
  24. package/dist/events/index.js +3 -0
  25. package/dist/events/index.js.map +1 -0
  26. package/dist/events/types.d.ts +109 -0
  27. package/dist/events/types.d.ts.map +1 -0
  28. package/dist/events/types.js +9 -0
  29. package/dist/events/types.js.map +1 -0
  30. package/dist/executor.d.ts +4 -0
  31. package/dist/executor.d.ts.map +1 -0
  32. package/dist/executor.js +732 -0
  33. package/dist/executor.js.map +1 -0
  34. package/dist/executor.test.d.ts +2 -0
  35. package/dist/executor.test.d.ts.map +1 -0
  36. package/dist/executor.test.js +1524 -0
  37. package/dist/executor.test.js.map +1 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +12 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/secrets/index.d.ts +14 -0
  43. package/dist/secrets/index.d.ts.map +1 -0
  44. package/dist/secrets/index.js +18 -0
  45. package/dist/secrets/index.js.map +1 -0
  46. package/dist/secrets/providers/aws.d.ts +63 -0
  47. package/dist/secrets/providers/aws.d.ts.map +1 -0
  48. package/dist/secrets/providers/aws.js +111 -0
  49. package/dist/secrets/providers/aws.js.map +1 -0
  50. package/dist/secrets/providers/env.d.ts +36 -0
  51. package/dist/secrets/providers/env.d.ts.map +1 -0
  52. package/dist/secrets/providers/env.js +37 -0
  53. package/dist/secrets/providers/env.js.map +1 -0
  54. package/dist/secrets/providers/index.d.ts +7 -0
  55. package/dist/secrets/providers/index.d.ts.map +1 -0
  56. package/dist/secrets/providers/index.js +7 -0
  57. package/dist/secrets/providers/index.js.map +1 -0
  58. package/dist/secrets/providers/vault.d.ts +75 -0
  59. package/dist/secrets/providers/vault.d.ts.map +1 -0
  60. package/dist/secrets/providers/vault.js +143 -0
  61. package/dist/secrets/providers/vault.js.map +1 -0
  62. package/dist/secrets/registry.d.ts +61 -0
  63. package/dist/secrets/registry.d.ts.map +1 -0
  64. package/dist/secrets/registry.js +182 -0
  65. package/dist/secrets/registry.js.map +1 -0
  66. package/dist/secrets/resolver.d.ts +40 -0
  67. package/dist/secrets/resolver.d.ts.map +1 -0
  68. package/dist/secrets/resolver.js +178 -0
  69. package/dist/secrets/resolver.js.map +1 -0
  70. package/dist/secrets/secrets.test.d.ts +2 -0
  71. package/dist/secrets/secrets.test.d.ts.map +1 -0
  72. package/dist/secrets/secrets.test.js +243 -0
  73. package/dist/secrets/secrets.test.js.map +1 -0
  74. package/dist/secrets/types.d.ts +71 -0
  75. package/dist/secrets/types.d.ts.map +1 -0
  76. package/dist/secrets/types.js +38 -0
  77. package/dist/secrets/types.js.map +1 -0
  78. package/dist/shared.d.ts +8 -0
  79. package/dist/shared.d.ts.map +1 -0
  80. package/dist/shared.js +30 -0
  81. package/dist/shared.js.map +1 -0
  82. package/dist/test-plan-types.d.ts +43 -0
  83. package/dist/test-plan-types.d.ts.map +1 -0
  84. package/dist/test-plan-types.js +2 -0
  85. package/dist/test-plan-types.js.map +1 -0
  86. package/dist/types.d.ts +77 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +3 -0
  89. package/dist/types.js.map +1 -0
  90. package/package.json +35 -0
  91. package/src/adapters/axios.ts +41 -0
  92. package/src/adapters/index.ts +2 -0
  93. package/src/adapters/stub.ts +47 -0
  94. package/src/events/emitter.test.ts +316 -0
  95. package/src/events/emitter.ts +133 -0
  96. package/src/events/index.ts +2 -0
  97. package/src/events/types.ts +132 -0
  98. package/src/executor.test.ts +1674 -0
  99. package/src/executor.ts +986 -0
  100. package/src/index.ts +69 -0
  101. package/src/secrets/index.ts +41 -0
  102. package/src/secrets/providers/aws.ts +179 -0
  103. package/src/secrets/providers/env.ts +66 -0
  104. package/src/secrets/providers/index.ts +15 -0
  105. package/src/secrets/providers/vault.ts +257 -0
  106. package/src/secrets/registry.ts +234 -0
  107. package/src/secrets/resolver.ts +249 -0
  108. package/src/secrets/secrets.test.ts +318 -0
  109. package/src/secrets/types.ts +105 -0
  110. package/src/shared.ts +46 -0
  111. package/src/test-plan-types.ts +49 -0
  112. package/src/types.ts +95 -0
  113. package/tsconfig.json +20 -0
  114. package/vitest.config.ts +14 -0
@@ -0,0 +1,47 @@
1
+ import type { HttpClientAdapter, HttpRequest, HttpResponse } from "../types.js";
2
+
3
+ export interface StubResponse {
4
+ /** URL pattern to match (string for exact, RegExp for pattern, or function for custom logic) */
5
+ match: string | RegExp | ((req: HttpRequest) => boolean);
6
+ response: HttpResponse;
7
+ }
8
+
9
+ export class StubAdapter implements HttpClientAdapter {
10
+ private stubs: StubResponse[] = [];
11
+
12
+ /**
13
+ * Add a stub response for matching requests
14
+ * @param stub - The stub configuration
15
+ * @returns this (for chaining)
16
+ */
17
+ addStub(stub: StubResponse): this {
18
+ this.stubs.push(stub);
19
+ return this;
20
+ }
21
+
22
+ /**
23
+ * Clear all configured stubs
24
+ */
25
+ clearStubs(): void {
26
+ this.stubs = [];
27
+ }
28
+
29
+ async request(req: HttpRequest): Promise<HttpResponse> {
30
+ for (const stub of this.stubs) {
31
+ if (this.matches(stub.match, req)) {
32
+ return stub.response;
33
+ }
34
+ }
35
+ throw new Error(`No stub matched request: ${req.method} ${req.url}`);
36
+ }
37
+
38
+ private matches(match: StubResponse["match"], req: HttpRequest): boolean {
39
+ if (typeof match === "string") {
40
+ return req.url === match;
41
+ }
42
+ if (match instanceof RegExp) {
43
+ return match.test(req.url);
44
+ }
45
+ return match(req);
46
+ }
47
+ }
@@ -0,0 +1,316 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import {
3
+ LocalEventEmitter,
4
+ DurableEventEmitter,
5
+ type DurableEventBusAdapter,
6
+ } from "./emitter.js";
7
+ import type { ExecutionEvent } from "./types.js";
8
+
9
+ describe("LocalEventEmitter", () => {
10
+ let emitter: LocalEventEmitter;
11
+
12
+ beforeEach(() => {
13
+ emitter = new LocalEventEmitter();
14
+ });
15
+
16
+ it("should deliver events to subscribers synchronously", () => {
17
+ const events: ExecutionEvent[] = [];
18
+ emitter.subscribe((event) => events.push(event));
19
+
20
+ const testEvent: ExecutionEvent = {
21
+ type: "PLAN_START",
22
+ eventId: "123",
23
+ seq: 0,
24
+ timestamp: Date.now(),
25
+ planId: "plan-1",
26
+ executionId: "exec-1",
27
+ planName: "Test Plan",
28
+ planVersion: "1.0",
29
+ nodeCount: 2,
30
+ edgeCount: 1,
31
+ };
32
+
33
+ emitter.emit(testEvent);
34
+
35
+ expect(events).toHaveLength(1);
36
+ expect(events[0]).toEqual(testEvent);
37
+ });
38
+
39
+ it("should deliver events to multiple subscribers", () => {
40
+ const events1: ExecutionEvent[] = [];
41
+ const events2: ExecutionEvent[] = [];
42
+
43
+ emitter.subscribe((event) => events1.push(event));
44
+ emitter.subscribe((event) => events2.push(event));
45
+
46
+ const testEvent: ExecutionEvent = {
47
+ type: "NODE_START",
48
+ eventId: "456",
49
+ seq: 1,
50
+ timestamp: Date.now(),
51
+ planId: "plan-1",
52
+ executionId: "exec-1",
53
+ nodeId: "node-1",
54
+ nodeType: "endpoint",
55
+ };
56
+
57
+ emitter.emit(testEvent);
58
+
59
+ expect(events1).toHaveLength(1);
60
+ expect(events2).toHaveLength(1);
61
+ expect(events1[0]).toEqual(testEvent);
62
+ expect(events2[0]).toEqual(testEvent);
63
+ });
64
+
65
+ it("should allow unsubscribing", () => {
66
+ const events: ExecutionEvent[] = [];
67
+ const unsubscribe = emitter.subscribe((event) => events.push(event));
68
+
69
+ const testEvent: ExecutionEvent = {
70
+ type: "NODE_END",
71
+ eventId: "789",
72
+ seq: 2,
73
+ timestamp: Date.now(),
74
+ planId: "plan-1",
75
+ executionId: "exec-1",
76
+ nodeId: "node-1",
77
+ nodeType: "endpoint",
78
+ success: true,
79
+ duration_ms: 100,
80
+ };
81
+
82
+ emitter.emit(testEvent);
83
+ expect(events).toHaveLength(1);
84
+
85
+ unsubscribe();
86
+
87
+ emitter.emit(testEvent);
88
+ expect(events).toHaveLength(1); // Still 1, not 2
89
+ });
90
+
91
+ it("should not propagate listener errors to other listeners", () => {
92
+ const events: ExecutionEvent[] = [];
93
+ const consoleErrorSpy = vi
94
+ .spyOn(console, "error")
95
+ .mockImplementation(() => {});
96
+
97
+ emitter.subscribe(() => {
98
+ throw new Error("Listener error");
99
+ });
100
+ emitter.subscribe((event) => events.push(event));
101
+
102
+ const testEvent: ExecutionEvent = {
103
+ type: "PLAN_END",
104
+ eventId: "abc",
105
+ seq: 3,
106
+ timestamp: Date.now(),
107
+ planId: "plan-1",
108
+ executionId: "exec-1",
109
+ success: true,
110
+ totalDuration_ms: 1000,
111
+ nodeResultCount: 2,
112
+ errorCount: 0,
113
+ errors: [],
114
+ };
115
+
116
+ emitter.emit(testEvent);
117
+
118
+ expect(events).toHaveLength(1);
119
+ expect(consoleErrorSpy).toHaveBeenCalled();
120
+
121
+ consoleErrorSpy.mockRestore();
122
+ });
123
+ });
124
+
125
+ describe("DurableEventEmitter", () => {
126
+ let adapter: DurableEventBusAdapter;
127
+ let publishSpy: ReturnType<
128
+ typeof vi.fn<(events: ExecutionEvent[]) => Promise<void>>
129
+ >;
130
+ let emitter: DurableEventEmitter;
131
+
132
+ beforeEach(() => {
133
+ publishSpy = vi
134
+ .fn<(events: ExecutionEvent[]) => Promise<void>>()
135
+ .mockResolvedValue(undefined);
136
+ adapter = { publish: publishSpy };
137
+ });
138
+
139
+ afterEach(() => {
140
+ emitter?.destroy();
141
+ });
142
+
143
+ it("should batch events and flush on batch size", async () => {
144
+ emitter = new DurableEventEmitter(adapter, {
145
+ batchSize: 3,
146
+ flushIntervalMs: 10000,
147
+ });
148
+
149
+ const event1: ExecutionEvent = {
150
+ type: "NODE_START",
151
+ eventId: "1",
152
+ seq: 0,
153
+ timestamp: Date.now(),
154
+ planId: "plan-1",
155
+ executionId: "exec-1",
156
+ nodeId: "node-1",
157
+ nodeType: "endpoint",
158
+ };
159
+
160
+ const event2: ExecutionEvent = {
161
+ type: "NODE_START",
162
+ eventId: "2",
163
+ seq: 1,
164
+ timestamp: Date.now(),
165
+ planId: "plan-1",
166
+ executionId: "exec-1",
167
+ nodeId: "node-2",
168
+ nodeType: "wait",
169
+ };
170
+
171
+ const event3: ExecutionEvent = {
172
+ type: "NODE_START",
173
+ eventId: "3",
174
+ seq: 2,
175
+ timestamp: Date.now(),
176
+ planId: "plan-1",
177
+ executionId: "exec-1",
178
+ nodeId: "node-3",
179
+ nodeType: "assertion",
180
+ };
181
+
182
+ emitter.emit(event1);
183
+ emitter.emit(event2);
184
+ expect(publishSpy).not.toHaveBeenCalled();
185
+
186
+ emitter.emit(event3); // Should trigger auto-flush
187
+
188
+ // Wait for async flush
189
+ await new Promise((resolve) => setTimeout(resolve, 10));
190
+
191
+ expect(publishSpy).toHaveBeenCalledTimes(1);
192
+ expect(publishSpy).toHaveBeenCalledWith([event1, event2, event3]);
193
+ });
194
+
195
+ it("should flush on interval", async () => {
196
+ emitter = new DurableEventEmitter(adapter, {
197
+ batchSize: 100,
198
+ flushIntervalMs: 50,
199
+ });
200
+
201
+ const event: ExecutionEvent = {
202
+ type: "HTTP_REQUEST",
203
+ eventId: "1",
204
+ seq: 0,
205
+ timestamp: Date.now(),
206
+ planId: "plan-1",
207
+ executionId: "exec-1",
208
+ nodeId: "node-1",
209
+ attempt: 1,
210
+ method: "GET",
211
+ url: "https://api.example.com/test",
212
+ hasBody: false,
213
+ };
214
+
215
+ emitter.emit(event);
216
+
217
+ // Wait for interval flush
218
+ await new Promise((resolve) => setTimeout(resolve, 100));
219
+
220
+ expect(publishSpy).toHaveBeenCalledWith([event]);
221
+ });
222
+
223
+ it("should flush manually when requested", async () => {
224
+ emitter = new DurableEventEmitter(adapter, {
225
+ batchSize: 100,
226
+ flushIntervalMs: 10000,
227
+ });
228
+
229
+ const event: ExecutionEvent = {
230
+ type: "HTTP_RESPONSE",
231
+ eventId: "1",
232
+ seq: 0,
233
+ timestamp: Date.now(),
234
+ planId: "plan-1",
235
+ executionId: "exec-1",
236
+ nodeId: "node-1",
237
+ attempt: 1,
238
+ status: 200,
239
+ statusText: "OK",
240
+ duration_ms: 100,
241
+ hasBody: true,
242
+ };
243
+
244
+ emitter.emit(event);
245
+ await emitter.flush();
246
+
247
+ expect(publishSpy).toHaveBeenCalledWith([event]);
248
+ });
249
+
250
+ it("should not emit after being destroyed", async () => {
251
+ emitter = new DurableEventEmitter(adapter, {
252
+ batchSize: 100,
253
+ flushIntervalMs: 50,
254
+ });
255
+
256
+ const consoleWarnSpy = vi
257
+ .spyOn(console, "warn")
258
+ .mockImplementation(() => {});
259
+
260
+ emitter.destroy();
261
+
262
+ const event: ExecutionEvent = {
263
+ type: "ERROR",
264
+ eventId: "1",
265
+ seq: 0,
266
+ timestamp: Date.now(),
267
+ planId: "plan-1",
268
+ executionId: "exec-1",
269
+ errorName: "TestError",
270
+ message: "Test error message",
271
+ };
272
+
273
+ emitter.emit(event);
274
+
275
+ await new Promise((resolve) => setTimeout(resolve, 100));
276
+
277
+ expect(publishSpy).not.toHaveBeenCalled();
278
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
279
+ "Cannot emit event: emitter is destroyed",
280
+ );
281
+
282
+ consoleWarnSpy.mockRestore();
283
+ });
284
+
285
+ it("should handle publish failures gracefully", async () => {
286
+ const consoleErrorSpy = vi
287
+ .spyOn(console, "error")
288
+ .mockImplementation(() => {});
289
+ publishSpy.mockRejectedValueOnce(new Error("Publish failed"));
290
+
291
+ emitter = new DurableEventEmitter(adapter, {
292
+ batchSize: 1,
293
+ flushIntervalMs: 10000,
294
+ });
295
+
296
+ const event: ExecutionEvent = {
297
+ type: "WAIT_START",
298
+ eventId: "1",
299
+ seq: 0,
300
+ timestamp: Date.now(),
301
+ planId: "plan-1",
302
+ executionId: "exec-1",
303
+ nodeId: "wait-1",
304
+ duration_ms: 1000,
305
+ };
306
+
307
+ emitter.emit(event);
308
+
309
+ await new Promise((resolve) => setTimeout(resolve, 10));
310
+
311
+ expect(publishSpy).toHaveBeenCalled();
312
+ expect(consoleErrorSpy).toHaveBeenCalled();
313
+
314
+ consoleErrorSpy.mockRestore();
315
+ });
316
+ });
@@ -0,0 +1,133 @@
1
+ import type { ExecutionEvent } from "./types.js";
2
+
3
+ /**
4
+ * Pluggable event emitter interface for execution observability.
5
+ *
6
+ * Implementations can be synchronous (local) or asynchronous (durable bus).
7
+ * The executor treats emission as best-effort and non-blocking.
8
+ */
9
+ export interface ExecutionEventEmitter {
10
+ /**
11
+ * Emit a single event. May be synchronous or asynchronous depending on implementation.
12
+ * Errors during emission should not propagate to the executor.
13
+ */
14
+ emit(event: ExecutionEvent): void | Promise<void>;
15
+
16
+ /**
17
+ * Optional: flush any buffered events (for batching implementations).
18
+ * Called at the end of execution to ensure all events are delivered.
19
+ */
20
+ flush?(): Promise<void>;
21
+
22
+ /**
23
+ * Optional: cleanup resources (timers, connections, etc.).
24
+ */
25
+ destroy?(): void;
26
+ }
27
+
28
+ /**
29
+ * In-memory event emitter for local/development use.
30
+ * Events are delivered synchronously to all subscribers.
31
+ */
32
+ export class LocalEventEmitter implements ExecutionEventEmitter {
33
+ private listeners: Array<(event: ExecutionEvent) => void> = [];
34
+
35
+ /**
36
+ * Subscribe to all execution events.
37
+ * @returns Unsubscribe function
38
+ */
39
+ subscribe(listener: (event: ExecutionEvent) => void): () => void {
40
+ this.listeners.push(listener);
41
+ return () => {
42
+ this.listeners = this.listeners.filter((l) => l !== listener);
43
+ };
44
+ }
45
+
46
+ emit(event: ExecutionEvent): void {
47
+ for (const listener of this.listeners) {
48
+ try {
49
+ listener(event);
50
+ } catch (error) {
51
+ // Don't let listener errors break execution
52
+ console.error("Error in event listener:", error);
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Adapter interface for durable event buses (SQS, Kafka, Redis Streams, etc.).
60
+ * Implementations provided by callers based on their infrastructure.
61
+ */
62
+ export interface DurableEventBusAdapter {
63
+ /**
64
+ * Publish a batch of events to the durable bus.
65
+ * Should handle serialization, retries, and error handling internally.
66
+ */
67
+ publish(events: ExecutionEvent[]): Promise<void>;
68
+ }
69
+
70
+ /**
71
+ * Event emitter that batches events for efficient delivery to durable event buses.
72
+ * Flushes on interval and when batch size is reached.
73
+ */
74
+ export class DurableEventEmitter implements ExecutionEventEmitter {
75
+ private buffer: ExecutionEvent[] = [];
76
+ private flushInterval: NodeJS.Timeout | null = null;
77
+ private isDestroyed = false;
78
+
79
+ constructor(
80
+ private adapter: DurableEventBusAdapter,
81
+ private options: {
82
+ /** Number of events to batch before auto-flushing (default: 50) */
83
+ batchSize?: number;
84
+ /** Milliseconds between auto-flushes (default: 100) */
85
+ flushIntervalMs?: number;
86
+ } = {},
87
+ ) {
88
+ const intervalMs = options.flushIntervalMs ?? 100;
89
+ this.flushInterval = setInterval(() => {
90
+ this.flush().catch((error) => {
91
+ console.error("Error flushing events:", error);
92
+ });
93
+ }, intervalMs);
94
+ }
95
+
96
+ emit(event: ExecutionEvent): void {
97
+ if (this.isDestroyed) {
98
+ console.warn("Cannot emit event: emitter is destroyed");
99
+ return;
100
+ }
101
+
102
+ this.buffer.push(event);
103
+
104
+ // Auto-flush when batch size is reached
105
+ if (this.buffer.length >= (this.options.batchSize ?? 50)) {
106
+ this.flush().catch((error) => {
107
+ console.error("Error auto-flushing events:", error);
108
+ });
109
+ }
110
+ }
111
+
112
+ async flush(): Promise<void> {
113
+ if (this.buffer.length === 0) return;
114
+
115
+ const events = this.buffer;
116
+ this.buffer = [];
117
+
118
+ try {
119
+ await this.adapter.publish(events);
120
+ } catch (error) {
121
+ console.error("Failed to publish events:", error);
122
+ // Events are lost on failure - caller adapter should implement retries if needed
123
+ }
124
+ }
125
+
126
+ destroy(): void {
127
+ this.isDestroyed = true;
128
+ if (this.flushInterval) {
129
+ clearInterval(this.flushInterval);
130
+ this.flushInterval = null;
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./emitter.js";
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Event types for test plan execution.
3
+ *
4
+ * All events are JSON-serializable for compatibility with various event buses.
5
+ * Events include stable identifiers (executionId, eventId) and monotonic sequence
6
+ * numbers for deduplication and ordering.
7
+ */
8
+
9
+ /** Base event envelope with correlation and ordering metadata */
10
+ export interface BaseEvent {
11
+ /** Unique identifier for this specific event */
12
+ eventId: string;
13
+ /** Monotonic sequence number within this execution (0-indexed) */
14
+ seq: number;
15
+ /** Unix timestamp in milliseconds */
16
+ timestamp: number;
17
+ /** ID of the test plan being executed */
18
+ planId: string;
19
+ /** Unique identifier for this execution run */
20
+ executionId: string;
21
+ }
22
+
23
+ /** Plan-level events */
24
+ export interface PlanStartEvent extends BaseEvent {
25
+ type: "PLAN_START";
26
+ planName: string;
27
+ planVersion: string;
28
+ nodeCount: number;
29
+ edgeCount: number;
30
+ }
31
+
32
+ export interface PlanEndEvent extends BaseEvent {
33
+ type: "PLAN_END";
34
+ success: boolean;
35
+ totalDuration_ms: number;
36
+ nodeResultCount: number;
37
+ errorCount: number;
38
+ errors: string[];
39
+ }
40
+
41
+ /** Node-level events (domain-enriched from ts-edge events) */
42
+ export interface NodeStartEvent extends BaseEvent {
43
+ type: "NODE_START";
44
+ nodeId: string;
45
+ nodeType: "endpoint" | "wait" | "assertion";
46
+ }
47
+
48
+ export interface NodeEndEvent extends BaseEvent {
49
+ type: "NODE_END";
50
+ nodeId: string;
51
+ nodeType: "endpoint" | "wait" | "assertion";
52
+ success: boolean;
53
+ duration_ms: number;
54
+ error?: string;
55
+ }
56
+
57
+ /** HTTP-specific events (for endpoint nodes) */
58
+ export interface HttpRequestEvent extends BaseEvent {
59
+ type: "HTTP_REQUEST";
60
+ nodeId: string;
61
+ attempt: number;
62
+ method: string;
63
+ url: string;
64
+ headers?: Record<string, string>;
65
+ hasBody: boolean;
66
+ }
67
+
68
+ export interface HttpResponseEvent extends BaseEvent {
69
+ type: "HTTP_RESPONSE";
70
+ nodeId: string;
71
+ attempt: number;
72
+ status: number;
73
+ statusText: string;
74
+ duration_ms: number;
75
+ hasBody: boolean;
76
+ }
77
+
78
+ export interface HttpRetryEvent extends BaseEvent {
79
+ type: "HTTP_RETRY";
80
+ nodeId: string;
81
+ attempt: number;
82
+ maxAttempts: number;
83
+ reason: string;
84
+ nextRetryDelayMs?: number;
85
+ }
86
+
87
+ /** Assertion events */
88
+ export interface AssertionResultEvent extends BaseEvent {
89
+ type: "ASSERTION_RESULT";
90
+ nodeId: string;
91
+ assertionIndex: number;
92
+ passed: boolean;
93
+ message: string;
94
+ }
95
+
96
+ /** Wait events */
97
+ export interface WaitStartEvent extends BaseEvent {
98
+ type: "WAIT_START";
99
+ nodeId: string;
100
+ duration_ms: number;
101
+ }
102
+
103
+ /** Streaming/progress events from nodes */
104
+ export interface NodeStreamEvent extends BaseEvent {
105
+ type: "NODE_STREAM";
106
+ nodeId: string;
107
+ chunk: string;
108
+ }
109
+
110
+ /** System/executor-level errors (not node failures) */
111
+ export interface ErrorEvent extends BaseEvent {
112
+ type: "ERROR";
113
+ errorName: string;
114
+ message: string;
115
+ context?: string;
116
+ /** Only included in local/development mode */
117
+ stack?: string;
118
+ }
119
+
120
+ /** Union type of all execution events */
121
+ export type ExecutionEvent =
122
+ | PlanStartEvent
123
+ | PlanEndEvent
124
+ | NodeStartEvent
125
+ | NodeEndEvent
126
+ | HttpRequestEvent
127
+ | HttpResponseEvent
128
+ | HttpRetryEvent
129
+ | AssertionResultEvent
130
+ | WaitStartEvent
131
+ | NodeStreamEvent
132
+ | ErrorEvent;