@auto-engineer/job-graph-processor 1.28.0 → 1.30.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +61 -0
- package/dist/src/commands/process-job-graph.d.ts.map +1 -1
- package/dist/src/commands/process-job-graph.js +15 -1
- package/dist/src/commands/process-job-graph.js.map +1 -1
- package/dist/src/evolve.d.ts +1 -0
- package/dist/src/evolve.d.ts.map +1 -1
- package/dist/src/evolve.js +10 -0
- package/dist/src/evolve.js.map +1 -1
- package/dist/src/evolve.specs.js +64 -1
- package/dist/src/evolve.specs.js.map +1 -1
- package/dist/src/graph-processor.d.ts +2 -0
- package/dist/src/graph-processor.d.ts.map +1 -1
- package/dist/src/graph-processor.js +4 -2
- package/dist/src/graph-processor.js.map +1 -1
- package/dist/src/graph-processor.specs.js +44 -5
- package/dist/src/graph-processor.specs.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/pipeline-e2e.specs.d.ts +2 -0
- package/dist/src/pipeline-e2e.specs.d.ts.map +1 -0
- package/dist/src/pipeline-e2e.specs.js +127 -0
- package/dist/src/pipeline-e2e.specs.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +11 -0
- package/package.json +3 -3
- package/src/commands/process-job-graph.ts +17 -2
- package/src/evolve.specs.ts +73 -1
- package/src/evolve.ts +10 -0
- package/src/graph-processor.specs.ts +48 -5
- package/src/graph-processor.ts +10 -3
- package/src/index.ts +1 -1
- package/src/pipeline-e2e.specs.ts +157 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Command, Event, MessageBus } from '@auto-engineer/message-bus';
|
|
2
2
|
import type { FailurePolicy } from '../evolve';
|
|
3
|
-
import type { DispatchFn } from '../graph-processor';
|
|
3
|
+
import type { DispatchFn, PublishEventFn } from '../graph-processor';
|
|
4
4
|
import { createGraphProcessor, type ProcessGraphCommand } from '../graph-processor';
|
|
5
5
|
import type { Job } from '../graph-validator';
|
|
6
6
|
|
|
7
7
|
interface ContextWithMessageBus {
|
|
8
8
|
messageBus: MessageBus;
|
|
9
9
|
sendCommand?: (type: string, data: unknown, correlationId?: string) => Promise<void>;
|
|
10
|
+
emit?: (type: string, data: unknown) => Promise<void>;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function isContextWithMessageBus(context: unknown): context is ContextWithMessageBus {
|
|
@@ -27,6 +28,15 @@ function buildDispatch(context: ContextWithMessageBus): DispatchFn | undefined {
|
|
|
27
28
|
return (cmd) => ctxSendCommand(cmd.type, cmd.data, cmd.correlationId);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function buildPublishEvent(context: ContextWithMessageBus): PublishEventFn | undefined {
|
|
32
|
+
if (typeof context.emit !== 'function') return undefined;
|
|
33
|
+
const ctxEmit = context.emit;
|
|
34
|
+
return (event) => {
|
|
35
|
+
context.messageBus.publishEvent(event);
|
|
36
|
+
ctxEmit(event.type, event.data);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function toProcessGraphCommand(command: Command): ProcessGraphCommand {
|
|
31
41
|
const data = command.data as { graphId: string; jobs: Job[]; failurePolicy: FailurePolicy };
|
|
32
42
|
return { type: command.type, data };
|
|
@@ -60,6 +70,7 @@ export const commandHandler = {
|
|
|
60
70
|
{ name: 'GraphDispatched', displayName: 'Graph Dispatched' },
|
|
61
71
|
{ name: 'GraphFailed', displayName: 'Graph Failed' },
|
|
62
72
|
{ name: 'GraphProcessed', displayName: 'Graph Processed' },
|
|
73
|
+
{ name: 'GraphProcessingFailed', displayName: 'Graph Processing Failed' },
|
|
63
74
|
],
|
|
64
75
|
handle: async (command: Command, context?: unknown): Promise<Event | Event[]> => {
|
|
65
76
|
const { graphId } = command.data;
|
|
@@ -72,7 +83,11 @@ export const commandHandler = {
|
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
const dispatch = buildDispatch(context);
|
|
75
|
-
const
|
|
86
|
+
const publishEvent = buildPublishEvent(context);
|
|
87
|
+
const processor = createGraphProcessor(context.messageBus, {
|
|
88
|
+
...(dispatch !== undefined && { dispatch }),
|
|
89
|
+
...(publishEvent !== undefined && { publishEvent }),
|
|
90
|
+
});
|
|
76
91
|
return processor.submit(toProcessGraphCommand(command));
|
|
77
92
|
},
|
|
78
93
|
};
|
package/src/evolve.specs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { evolve, getReadyJobs, getTransitiveDependents, initialState, isGraphComplete } from './evolve';
|
|
2
|
+
import { evolve, getReadyJobs, getTransitiveDependents, hasFailedJobs, initialState, isGraphComplete } from './evolve';
|
|
3
3
|
|
|
4
4
|
describe('evolve', () => {
|
|
5
5
|
it('ignores job events before graph submission', () => {
|
|
@@ -233,3 +233,75 @@ describe('isGraphComplete', () => {
|
|
|
233
233
|
expect(isGraphComplete(state)).toBe(true);
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
|
+
|
|
237
|
+
describe('hasFailedJobs', () => {
|
|
238
|
+
it('returns false before graph submission', () => {
|
|
239
|
+
expect(hasFailedJobs(initialState())).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns false when all jobs succeeded', () => {
|
|
243
|
+
let state = evolve(initialState(), {
|
|
244
|
+
type: 'GraphSubmitted',
|
|
245
|
+
data: {
|
|
246
|
+
graphId: 'g1',
|
|
247
|
+
jobs: [{ id: 'a', dependsOn: [], target: 'build', payload: {} }],
|
|
248
|
+
failurePolicy: 'halt',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
state = evolve(state, {
|
|
252
|
+
type: 'JobDispatched',
|
|
253
|
+
data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' },
|
|
254
|
+
});
|
|
255
|
+
state = evolve(state, { type: 'JobSucceeded', data: { jobId: 'a' } });
|
|
256
|
+
|
|
257
|
+
expect(hasFailedJobs(state)).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns true when a job failed', () => {
|
|
261
|
+
let state = evolve(initialState(), {
|
|
262
|
+
type: 'GraphSubmitted',
|
|
263
|
+
data: {
|
|
264
|
+
graphId: 'g1',
|
|
265
|
+
jobs: [{ id: 'a', dependsOn: [], target: 'build', payload: {} }],
|
|
266
|
+
failurePolicy: 'halt',
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
state = evolve(state, {
|
|
270
|
+
type: 'JobDispatched',
|
|
271
|
+
data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' },
|
|
272
|
+
});
|
|
273
|
+
state = evolve(state, { type: 'JobFailed', data: { jobId: 'a', error: 'build error' } });
|
|
274
|
+
|
|
275
|
+
expect(hasFailedJobs(state)).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns true when a job was skipped', () => {
|
|
279
|
+
let state = evolve(initialState(), {
|
|
280
|
+
type: 'GraphSubmitted',
|
|
281
|
+
data: {
|
|
282
|
+
graphId: 'g1',
|
|
283
|
+
jobs: [{ id: 'a', dependsOn: [], target: 'build', payload: {} }],
|
|
284
|
+
failurePolicy: 'halt',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
state = evolve(state, { type: 'JobSkipped', data: { jobId: 'a', reason: 'dependency failed' } });
|
|
288
|
+
|
|
289
|
+
expect(hasFailedJobs(state)).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('returns false when jobs are still pending', () => {
|
|
293
|
+
const state = evolve(initialState(), {
|
|
294
|
+
type: 'GraphSubmitted',
|
|
295
|
+
data: {
|
|
296
|
+
graphId: 'g1',
|
|
297
|
+
jobs: [
|
|
298
|
+
{ id: 'a', dependsOn: [], target: 'build', payload: {} },
|
|
299
|
+
{ id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
|
|
300
|
+
],
|
|
301
|
+
failurePolicy: 'halt',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(hasFailedJobs(state)).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
package/src/evolve.ts
CHANGED
|
@@ -85,6 +85,16 @@ export function isGraphComplete(state: GraphState): boolean {
|
|
|
85
85
|
return true;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
const FAILED_STATUSES: ReadonlySet<JobStatus> = new Set(['failed', 'skipped', 'timed-out']);
|
|
89
|
+
|
|
90
|
+
export function hasFailedJobs(state: GraphState): boolean {
|
|
91
|
+
if (state.status !== 'processing') return false;
|
|
92
|
+
for (const job of state.jobs.values()) {
|
|
93
|
+
if (FAILED_STATUSES.has(job.status)) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
export function getTransitiveDependents(state: GraphState, jobId: string): string[] {
|
|
89
99
|
if (state.status !== 'processing') return [];
|
|
90
100
|
const dependents: Set<string> = new Set();
|
|
@@ -297,14 +297,56 @@ describe('createGraphProcessor', () => {
|
|
|
297
297
|
});
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
-
it('
|
|
300
|
+
it('uses custom publishEvent instead of messageBus.publishEvent when provided', async () => {
|
|
301
|
+
const bus = createMessageBus();
|
|
302
|
+
const published: Array<{ type: string; data: Record<string, unknown> }> = [];
|
|
303
|
+
const busPublished: Array<{ type: string; data: Record<string, unknown> }> = [];
|
|
304
|
+
bus.subscribeToEvent('GraphProcessed', {
|
|
305
|
+
name: 'busTracker',
|
|
306
|
+
handle: (event) => {
|
|
307
|
+
busPublished.push(event);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
const processor = createGraphProcessor(bus, {
|
|
311
|
+
publishEvent: (event) => {
|
|
312
|
+
published.push(event as { type: string; data: Record<string, unknown> });
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
processor.submit({
|
|
317
|
+
type: 'ProcessGraph',
|
|
318
|
+
data: {
|
|
319
|
+
graphId: 'g1',
|
|
320
|
+
jobs: [{ id: 'a', dependsOn: [], target: 'build', payload: {} }],
|
|
321
|
+
failurePolicy: 'halt',
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await bus.publishEvent({
|
|
326
|
+
type: 'BuildCompleted',
|
|
327
|
+
data: { output: 'ok' },
|
|
328
|
+
correlationId: 'graph:g1:a',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(published).toEqual([{ type: 'GraphProcessed', data: { graphId: 'g1' } }]);
|
|
332
|
+
expect(busPublished).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('publishes GraphProcessingFailed when a job fails via correlation', async () => {
|
|
301
336
|
const bus = createMessageBus();
|
|
302
337
|
const processor = createGraphProcessor(bus);
|
|
303
|
-
const
|
|
338
|
+
const failed: Array<{ type: string; data: Record<string, unknown> }> = [];
|
|
339
|
+
const succeeded: Array<{ type: string; data: Record<string, unknown> }> = [];
|
|
340
|
+
bus.subscribeToEvent('GraphProcessingFailed', {
|
|
341
|
+
name: 'failureTracker',
|
|
342
|
+
handle: (event) => {
|
|
343
|
+
failed.push(event);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
304
346
|
bus.subscribeToEvent('GraphProcessed', {
|
|
305
|
-
name: '
|
|
347
|
+
name: 'successTracker',
|
|
306
348
|
handle: (event) => {
|
|
307
|
-
|
|
349
|
+
succeeded.push(event);
|
|
308
350
|
},
|
|
309
351
|
});
|
|
310
352
|
|
|
@@ -326,6 +368,7 @@ describe('createGraphProcessor', () => {
|
|
|
326
368
|
correlationId: 'graph:g1:a',
|
|
327
369
|
});
|
|
328
370
|
|
|
329
|
-
expect(
|
|
371
|
+
expect(failed).toEqual([{ type: 'GraphProcessingFailed', data: { graphId: 'g1' } }]);
|
|
372
|
+
expect(succeeded).toEqual([]);
|
|
330
373
|
});
|
|
331
374
|
});
|
package/src/graph-processor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Event, MessageBus } from '@auto-engineer/message-bus';
|
|
2
2
|
import { applyPolicy } from './apply-policy';
|
|
3
3
|
import type { FailurePolicy, GraphState } from './evolve';
|
|
4
|
-
import { evolve, getReadyJobs, initialState, isGraphComplete } from './evolve';
|
|
4
|
+
import { evolve, getReadyJobs, hasFailedJobs, initialState, isGraphComplete } from './evolve';
|
|
5
5
|
import type { Job } from './graph-validator';
|
|
6
6
|
import { validateGraph } from './graph-validator';
|
|
7
7
|
import { classifyJobEvent } from './handle-job-event';
|
|
@@ -24,7 +24,12 @@ interface DispatchedJob {
|
|
|
24
24
|
|
|
25
25
|
export type DispatchFn = (command: { type: string; data: unknown; correlationId: string }) => Promise<void>;
|
|
26
26
|
|
|
27
|
-
export
|
|
27
|
+
export type PublishEventFn = (event: Event) => void;
|
|
28
|
+
|
|
29
|
+
export function createGraphProcessor(
|
|
30
|
+
messageBus: MessageBus,
|
|
31
|
+
options?: { dispatch?: DispatchFn; publishEvent?: PublishEventFn },
|
|
32
|
+
) {
|
|
28
33
|
const dispatch: DispatchFn =
|
|
29
34
|
options?.dispatch ??
|
|
30
35
|
((cmd) =>
|
|
@@ -33,6 +38,7 @@ export function createGraphProcessor(messageBus: MessageBus, options?: { dispatc
|
|
|
33
38
|
data: cmd.data as Record<string, unknown>,
|
|
34
39
|
correlationId: cmd.correlationId,
|
|
35
40
|
}));
|
|
41
|
+
const publishEvent: PublishEventFn = options?.publishEvent ?? ((event) => messageBus.publishEvent(event));
|
|
36
42
|
const graphs = new Map<string, { state: GraphState; jobById: Record<string, Job> }>();
|
|
37
43
|
|
|
38
44
|
function submit(command: ProcessGraphCommand): Event {
|
|
@@ -113,7 +119,8 @@ export function createGraphProcessor(messageBus: MessageBus, options?: { dispatc
|
|
|
113
119
|
|
|
114
120
|
if (isGraphComplete(state)) {
|
|
115
121
|
graphs.delete(graphId);
|
|
116
|
-
|
|
122
|
+
const eventType = hasFailedJobs(state) ? 'GraphProcessingFailed' : 'GraphProcessed';
|
|
123
|
+
publishEvent({ type: eventType, data: { graphId } });
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const COMMANDS = [processJobGraphHandler];
|
|
|
5
5
|
export { applyPolicy } from './apply-policy';
|
|
6
6
|
export type { FailurePolicy, GraphState, JobGraphEvent, JobStatus } from './evolve';
|
|
7
7
|
|
|
8
|
-
export { evolve, getReadyJobs, getTransitiveDependents, initialState, isGraphComplete } from './evolve';
|
|
8
|
+
export { evolve, getReadyJobs, getTransitiveDependents, hasFailedJobs, initialState, isGraphComplete } from './evolve';
|
|
9
9
|
export { createGraphProcessor } from './graph-processor';
|
|
10
10
|
export type { Job } from './graph-validator';
|
|
11
11
|
export { validateGraph } from './graph-validator';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Command, Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import type { CommandHandlerWithMetadata } from '@auto-engineer/pipeline';
|
|
3
|
+
import { define, PipelineServer } from '@auto-engineer/pipeline';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { commandHandler as processJobGraphHandler } from './commands/process-job-graph';
|
|
6
|
+
|
|
7
|
+
interface StoredMessage {
|
|
8
|
+
message: { type: string; data?: Record<string, unknown> };
|
|
9
|
+
messageType: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
|
13
|
+
const res = await fetch(url, options);
|
|
14
|
+
return res.json() as Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function delay(ms: number): Promise<void> {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('Job Graph Pipeline E2E', () => {
|
|
22
|
+
it('routes GraphProcessed through pipeline to trigger downstream command', async () => {
|
|
23
|
+
const implementCalls: Command[] = [];
|
|
24
|
+
|
|
25
|
+
const mockJobHandler: CommandHandlerWithMetadata = {
|
|
26
|
+
name: 'MockJob',
|
|
27
|
+
events: ['MockJobCompleted'],
|
|
28
|
+
handle: async () => ({ type: 'MockJobCompleted', data: { result: 'ok' } }),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const implementHandler: CommandHandlerWithMetadata = {
|
|
32
|
+
name: 'ImplementReactApp',
|
|
33
|
+
events: ['ReactAppImplemented'],
|
|
34
|
+
handle: async (cmd: Command) => {
|
|
35
|
+
implementCalls.push(cmd);
|
|
36
|
+
return { type: 'ReactAppImplemented', data: { clientDir: cmd.data.clientDir } };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const pipeline = define('job-graph-e2e')
|
|
41
|
+
.on('JobGraphReady')
|
|
42
|
+
.emit('ProcessJobGraph', (e: Event) => ({
|
|
43
|
+
graphId: e.data.graphId,
|
|
44
|
+
jobs: e.data.jobs,
|
|
45
|
+
failurePolicy: e.data.failurePolicy,
|
|
46
|
+
}))
|
|
47
|
+
.on('GraphProcessed')
|
|
48
|
+
.emit('ImplementReactApp', { clientDir: './client' })
|
|
49
|
+
.build();
|
|
50
|
+
|
|
51
|
+
const server = new PipelineServer({ port: 0 });
|
|
52
|
+
server.registerCommandHandlers([
|
|
53
|
+
processJobGraphHandler as CommandHandlerWithMetadata,
|
|
54
|
+
mockJobHandler,
|
|
55
|
+
implementHandler,
|
|
56
|
+
]);
|
|
57
|
+
server.registerPipeline(pipeline);
|
|
58
|
+
await server.start();
|
|
59
|
+
|
|
60
|
+
const triggerEvent: Event = {
|
|
61
|
+
type: 'JobGraphReady',
|
|
62
|
+
data: {
|
|
63
|
+
graphId: 'e2e-happy',
|
|
64
|
+
jobs: [
|
|
65
|
+
{ id: 'a', dependsOn: [], target: 'MockJob', payload: {} },
|
|
66
|
+
{ id: 'b', dependsOn: ['a'], target: 'MockJob', payload: {} },
|
|
67
|
+
],
|
|
68
|
+
failurePolicy: 'halt',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await server.getMessageBus().publishEvent(triggerEvent);
|
|
73
|
+
await server.emitPipelineStartedEvent();
|
|
74
|
+
|
|
75
|
+
await fetchJson(`http://localhost:${server.port}/command`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({ type: 'ProcessJobGraph', data: triggerEvent.data }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await delay(500);
|
|
82
|
+
|
|
83
|
+
const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
84
|
+
const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
|
|
85
|
+
|
|
86
|
+
expect(eventTypes).toContain('GraphDispatched');
|
|
87
|
+
expect(eventTypes).toContain('GraphProcessed');
|
|
88
|
+
expect(eventTypes).toContain('ReactAppImplemented');
|
|
89
|
+
expect(implementCalls.length).toBeGreaterThanOrEqual(1);
|
|
90
|
+
|
|
91
|
+
await server.stop();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('routes GraphProcessingFailed and does NOT trigger downstream success handler', async () => {
|
|
95
|
+
const implementCalls: Command[] = [];
|
|
96
|
+
|
|
97
|
+
const failingJobHandler: CommandHandlerWithMetadata = {
|
|
98
|
+
name: 'FailingJob',
|
|
99
|
+
events: ['FailingJobFailed'],
|
|
100
|
+
handle: async () => ({
|
|
101
|
+
type: 'FailingJobFailed',
|
|
102
|
+
data: { error: 'build failed' },
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const implementHandler: CommandHandlerWithMetadata = {
|
|
107
|
+
name: 'ImplementReactApp',
|
|
108
|
+
events: ['ReactAppImplemented'],
|
|
109
|
+
handle: async (cmd: Command) => {
|
|
110
|
+
implementCalls.push(cmd);
|
|
111
|
+
return { type: 'ReactAppImplemented', data: { clientDir: cmd.data.clientDir } };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const pipeline = define('job-graph-fail-e2e')
|
|
116
|
+
.on('GraphProcessed')
|
|
117
|
+
.emit('ImplementReactApp', { clientDir: './client' })
|
|
118
|
+
.build();
|
|
119
|
+
|
|
120
|
+
const server = new PipelineServer({ port: 0 });
|
|
121
|
+
server.registerCommandHandlers([
|
|
122
|
+
processJobGraphHandler as CommandHandlerWithMetadata,
|
|
123
|
+
failingJobHandler,
|
|
124
|
+
implementHandler,
|
|
125
|
+
]);
|
|
126
|
+
server.registerPipeline(pipeline);
|
|
127
|
+
await server.start();
|
|
128
|
+
|
|
129
|
+
await fetchJson(`http://localhost:${server.port}/command`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
type: 'ProcessJobGraph',
|
|
134
|
+
data: {
|
|
135
|
+
graphId: 'e2e-fail',
|
|
136
|
+
jobs: [
|
|
137
|
+
{ id: 'a', dependsOn: [], target: 'FailingJob', payload: {} },
|
|
138
|
+
{ id: 'b', dependsOn: ['a'], target: 'FailingJob', payload: {} },
|
|
139
|
+
],
|
|
140
|
+
failurePolicy: 'halt',
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await delay(500);
|
|
146
|
+
|
|
147
|
+
const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
148
|
+
const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
|
|
149
|
+
|
|
150
|
+
expect(eventTypes).toContain('GraphDispatched');
|
|
151
|
+
expect(eventTypes).toContain('GraphProcessingFailed');
|
|
152
|
+
expect(eventTypes).not.toContain('GraphProcessed');
|
|
153
|
+
expect(implementCalls).toEqual([]);
|
|
154
|
+
|
|
155
|
+
await server.stop();
|
|
156
|
+
});
|
|
157
|
+
});
|