@auto-engineer/pipeline 1.28.0 → 1.29.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 +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +45 -0
- package/dist/src/builder/define.d.ts +8 -1
- package/dist/src/builder/define.d.ts.map +1 -1
- package/dist/src/builder/define.js +35 -0
- package/dist/src/builder/define.js.map +1 -1
- package/dist/src/core/descriptors.d.ts +7 -2
- package/dist/src/core/descriptors.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.js +1 -1
- package/dist/src/runtime/pipeline-runtime.js.map +1 -1
- package/dist/src/runtime/settled-tracker.d.ts +1 -1
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
- package/dist/src/runtime/settled-tracker.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +8 -6
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts.map +1 -1
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +0 -8
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
- package/dist/src/testing/fixtures/kanban-todo.config.d.ts.map +1 -1
- package/dist/src/testing/fixtures/kanban-todo.config.js +0 -1
- package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -1
- package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -1
- package/dist/src/testing/fixtures/kanban.pipeline.js +0 -2
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +4 -0
- package/package.json +3 -3
- package/scripts/run-kanban-e2e.ts +3 -3
- package/scripts/start-server.ts +1 -1
- package/src/builder/define.specs.ts +82 -5
- package/src/builder/define.ts +52 -1
- package/src/core/descriptors.ts +8 -2
- package/src/index.ts +2 -0
- package/src/plugins/plugin-loader.specs.ts +0 -7
- package/src/runtime/pipeline-runtime.ts +1 -1
- package/src/runtime/settled-tracker.ts +2 -2
- package/src/server/full-orchestration.e2e.specs.ts +4 -16
- package/src/server/pipeline-server.e2e.specs.ts +27 -33
- package/src/server/pipeline-server.specs.ts +41 -6
- package/src/server/pipeline-server.ts +10 -6
- package/src/testing/fixtures/kanban-full.pipeline.specs.ts +0 -24
- package/src/testing/fixtures/kanban-full.pipeline.ts +0 -14
- package/src/testing/fixtures/kanban-todo.config.ts +0 -1
- package/src/testing/fixtures/kanban.pipeline.specs.ts +0 -1
- package/src/testing/fixtures/kanban.pipeline.ts +0 -7
- package/src/testing/kanban-todo.e2e.specs.ts +1 -1
- package/src/testing/real-execution.e2e.specs.ts +6 -6
package/src/builder/define.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Event } from '@auto-engineer/message-bus';
|
|
2
2
|
import type {
|
|
3
|
+
AcceptsDescriptor,
|
|
3
4
|
CustomHandlerDescriptor,
|
|
4
5
|
EmitHandlerDescriptor,
|
|
5
6
|
EventPredicate,
|
|
@@ -23,10 +24,15 @@ export interface Pipeline {
|
|
|
23
24
|
toGraph(): GraphIR;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export interface DeclareBuilder {
|
|
28
|
+
accepts(targets: string[]): PipelineBuilder;
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
export interface PipelineBuilder {
|
|
27
32
|
version(v: string): PipelineBuilder;
|
|
28
33
|
description(d: string): PipelineBuilder;
|
|
29
34
|
key<E>(name: string, extractor: (event: E) => string): PipelineBuilder;
|
|
35
|
+
declare(commandType: string): DeclareBuilder;
|
|
30
36
|
on(eventType: string): TriggerBuilder;
|
|
31
37
|
settled(commandTypes: readonly string[]): SettledBuilder;
|
|
32
38
|
build(): Pipeline;
|
|
@@ -42,11 +48,12 @@ export interface SettledBuilder {
|
|
|
42
48
|
handler: (
|
|
43
49
|
events: Record<string, Event[]>,
|
|
44
50
|
send: (commandType: D[number], data: unknown) => void,
|
|
45
|
-
) =>
|
|
51
|
+
) => undefined | { persist: boolean },
|
|
46
52
|
): SettledChain;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export interface SettledChain {
|
|
56
|
+
declare(commandType: string): DeclareBuilder;
|
|
50
57
|
on(eventType: string): TriggerBuilder;
|
|
51
58
|
settled(commandTypes: readonly string[]): SettledBuilder;
|
|
52
59
|
build(): Pipeline;
|
|
@@ -124,12 +131,14 @@ export interface GatherChain {
|
|
|
124
131
|
|
|
125
132
|
export interface EmitChain {
|
|
126
133
|
emit(commandType: string, data: unknown): EmitChain;
|
|
134
|
+
declare(commandType: string): DeclareBuilder;
|
|
127
135
|
on(eventType: string): TriggerBuilder;
|
|
128
136
|
settled(commandTypes: readonly string[]): SettledBuilder;
|
|
129
137
|
build(): Pipeline;
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
export interface HandleChain {
|
|
141
|
+
declare(commandType: string): DeclareBuilder;
|
|
133
142
|
on(eventType: string): TriggerBuilder;
|
|
134
143
|
build(): Pipeline;
|
|
135
144
|
}
|
|
@@ -167,6 +176,10 @@ class PipelineBuilderImpl implements PipelineBuilder {
|
|
|
167
176
|
return this;
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
declare(commandType: string): DeclareBuilder {
|
|
180
|
+
return new DeclareBuilderImpl(this, commandType);
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
on(eventType: string): TriggerBuilder {
|
|
171
184
|
return new TriggerBuilderImpl(this, eventType);
|
|
172
185
|
}
|
|
@@ -195,6 +208,18 @@ class PipelineBuilderImpl implements PipelineBuilder {
|
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
class DeclareBuilderImpl implements DeclareBuilder {
|
|
212
|
+
constructor(
|
|
213
|
+
private readonly parent: PipelineBuilderImpl,
|
|
214
|
+
private readonly commandType: string,
|
|
215
|
+
) {}
|
|
216
|
+
|
|
217
|
+
accepts(targets: string[]): PipelineBuilder {
|
|
218
|
+
this.parent.addHandler({ type: 'accepts', commandType: this.commandType, accepts: targets });
|
|
219
|
+
return this.parent;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
198
223
|
type GraphBuilderContext = {
|
|
199
224
|
nodeMap: Map<string, GraphNode>;
|
|
200
225
|
edges: GraphEdge[];
|
|
@@ -254,6 +279,14 @@ function processCustomHandler(ctx: GraphBuilderContext, handler: CustomHandlerDe
|
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
|
|
282
|
+
function processAcceptsHandler(ctx: GraphBuilderContext, handler: AcceptsDescriptor): void {
|
|
283
|
+
addNode(ctx, `cmd:${handler.commandType}`, 'command', handler.commandType);
|
|
284
|
+
for (const target of handler.accepts) {
|
|
285
|
+
addNode(ctx, `cmd:${target}`, 'command', target);
|
|
286
|
+
ctx.edges.push({ from: `cmd:${handler.commandType}`, to: `cmd:${target}` });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
257
290
|
function processSettledHandler(ctx: GraphBuilderContext, handler: SettledHandlerDescriptor): void {
|
|
258
291
|
const settledNodeId = `settled:${handler.commandTypes.join(',')}`;
|
|
259
292
|
addNode(ctx, settledNodeId, 'settled', 'Settled');
|
|
@@ -294,6 +327,9 @@ function extractGraph(descriptor: PipelineDescriptor): GraphIR {
|
|
|
294
327
|
case 'settled':
|
|
295
328
|
processSettledHandler(ctx, handler);
|
|
296
329
|
break;
|
|
330
|
+
case 'accepts':
|
|
331
|
+
processAcceptsHandler(ctx, handler);
|
|
332
|
+
break;
|
|
297
333
|
}
|
|
298
334
|
}
|
|
299
335
|
|
|
@@ -356,6 +392,11 @@ class EmitChainImpl implements EmitChain {
|
|
|
356
392
|
return new EmitChainImpl(this.parent, this.eventType, [...this.commands, { commandType, data }], this.predicate);
|
|
357
393
|
}
|
|
358
394
|
|
|
395
|
+
declare(commandType: string): DeclareBuilder {
|
|
396
|
+
this.finalizeHandler();
|
|
397
|
+
return new DeclareBuilderImpl(this.parent, commandType);
|
|
398
|
+
}
|
|
399
|
+
|
|
359
400
|
on(eventType: string): TriggerBuilder {
|
|
360
401
|
this.finalizeHandler();
|
|
361
402
|
return new TriggerBuilderImpl(this.parent, eventType);
|
|
@@ -393,6 +434,11 @@ class HandleChainImpl implements HandleChain {
|
|
|
393
434
|
private readonly declaredEmits?: string[],
|
|
394
435
|
) {}
|
|
395
436
|
|
|
437
|
+
declare(commandType: string): DeclareBuilder {
|
|
438
|
+
this.finalizeHandler();
|
|
439
|
+
return new DeclareBuilderImpl(this.parent, commandType);
|
|
440
|
+
}
|
|
441
|
+
|
|
396
442
|
on(eventType: string): TriggerBuilder {
|
|
397
443
|
this.finalizeHandler();
|
|
398
444
|
return new TriggerBuilderImpl(this.parent, eventType);
|
|
@@ -682,6 +728,11 @@ class SettledChainImpl implements SettledChain {
|
|
|
682
728
|
private readonly dispatches?: readonly string[],
|
|
683
729
|
) {}
|
|
684
730
|
|
|
731
|
+
declare(commandType: string): DeclareBuilder {
|
|
732
|
+
this.finalizeHandler();
|
|
733
|
+
return new DeclareBuilderImpl(this.parent, commandType);
|
|
734
|
+
}
|
|
735
|
+
|
|
685
736
|
on(eventType: string): TriggerBuilder {
|
|
686
737
|
this.finalizeHandler();
|
|
687
738
|
return new TriggerBuilderImpl(this.parent, eventType);
|
package/src/core/descriptors.ts
CHANGED
|
@@ -78,7 +78,7 @@ type SettledSendFunction = (commandType: string, data: unknown) => void;
|
|
|
78
78
|
export type SettledHandler = (
|
|
79
79
|
events: Record<string, Event[]>,
|
|
80
80
|
send: SettledSendFunction,
|
|
81
|
-
) =>
|
|
81
|
+
) => undefined | { persist: boolean };
|
|
82
82
|
|
|
83
83
|
export interface SettledHandlerDescriptor {
|
|
84
84
|
type: 'settled';
|
|
@@ -87,13 +87,19 @@ export interface SettledHandlerDescriptor {
|
|
|
87
87
|
dispatches?: readonly string[];
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export interface AcceptsDescriptor {
|
|
91
|
+
type: 'accepts';
|
|
92
|
+
commandType: string;
|
|
93
|
+
accepts: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
export type EventHandlerDescriptor =
|
|
91
97
|
| EmitHandlerDescriptor
|
|
92
98
|
| RunAwaitHandlerDescriptor
|
|
93
99
|
| ForEachPhasedDescriptor
|
|
94
100
|
| CustomHandlerDescriptor;
|
|
95
101
|
|
|
96
|
-
export type HandlerDescriptor = EventHandlerDescriptor | SettledHandlerDescriptor;
|
|
102
|
+
export type HandlerDescriptor = EventHandlerDescriptor | SettledHandlerDescriptor | AcceptsDescriptor;
|
|
97
103
|
|
|
98
104
|
export interface PipelineDescriptor {
|
|
99
105
|
name: string;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { EventHandler, EventSubscription, MessageBus } from '@auto-engineer/message-bus';
|
|
2
2
|
export type {
|
|
3
3
|
CompletionConfig,
|
|
4
|
+
DeclareBuilder,
|
|
4
5
|
EmitChain,
|
|
5
6
|
ForEachBuilder,
|
|
6
7
|
GatherBuilder,
|
|
@@ -17,6 +18,7 @@ export type {
|
|
|
17
18
|
} from './builder/define';
|
|
18
19
|
export { define } from './builder/define';
|
|
19
20
|
export type {
|
|
21
|
+
AcceptsDescriptor,
|
|
20
22
|
CustomHandlerDescriptor,
|
|
21
23
|
EmitHandlerDescriptor,
|
|
22
24
|
EventPredicate,
|
|
@@ -277,12 +277,5 @@ describe('PluginLoader', () => {
|
|
|
277
277
|
expect(handlerNames).toContain('GenerateServer');
|
|
278
278
|
expect(handlers.length).toBeGreaterThan(3);
|
|
279
279
|
});
|
|
280
|
-
|
|
281
|
-
it('should load ExportSchema from narrative package node entry', async () => {
|
|
282
|
-
const loader = new PluginLoader();
|
|
283
|
-
const handlers = await loader.loadPlugin('@auto-engineer/narrative');
|
|
284
|
-
const handlerNames = handlers.map((h) => h.name);
|
|
285
|
-
expect(handlerNames).toContain('ExportSchema');
|
|
286
|
-
});
|
|
287
280
|
});
|
|
288
281
|
});
|
|
@@ -109,7 +109,7 @@ export class PipelineRuntime {
|
|
|
109
109
|
private buildHandlerIndex(): Map<string, EventHandlerDescriptor[]> {
|
|
110
110
|
const index = new Map<string, EventHandlerDescriptor[]>();
|
|
111
111
|
for (const handler of this.descriptor.handlers) {
|
|
112
|
-
if (handler.type === 'settled') {
|
|
112
|
+
if (handler.type === 'settled' || handler.type === 'accepts') {
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
115
|
const existing = index.get(handler.eventType) ?? [];
|
|
@@ -4,7 +4,7 @@ import type { PipelineReadModel } from '../store/pipeline-read-model';
|
|
|
4
4
|
|
|
5
5
|
type SendFunction = (commandType: string, data: unknown) => void;
|
|
6
6
|
|
|
7
|
-
type SettledHandler = (events: Record<string, Event[]>, send: SendFunction) =>
|
|
7
|
+
type SettledHandler = (events: Record<string, Event[]>, send: SendFunction) => undefined | { persist: boolean };
|
|
8
8
|
|
|
9
9
|
interface SettledHandlerRegistration {
|
|
10
10
|
commandTypes: readonly string[];
|
|
@@ -223,7 +223,7 @@ export class SettledTracker {
|
|
|
223
223
|
return events;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
private shouldPersist(result:
|
|
226
|
+
private shouldPersist(result: undefined | { persist: boolean }): boolean {
|
|
227
227
|
return (
|
|
228
228
|
result !== null &&
|
|
229
229
|
result !== undefined &&
|
|
@@ -51,8 +51,8 @@ describe('Full Orchestration E2E', () => {
|
|
|
51
51
|
expect(graph.edges.length).toBeGreaterThan(0);
|
|
52
52
|
|
|
53
53
|
const nodeIds = graph.nodes.map((n) => n.id);
|
|
54
|
-
expect(nodeIds).toContain('evt:
|
|
55
|
-
expect(nodeIds).toContain('cmd:
|
|
54
|
+
expect(nodeIds).toContain('evt:ServerGenerated');
|
|
55
|
+
expect(nodeIds).toContain('cmd:GenerateIA');
|
|
56
56
|
expect(nodeIds).toContain('evt:SliceImplemented');
|
|
57
57
|
expect(nodeIds).toContain('evt:AllComponentsImplemented');
|
|
58
58
|
});
|
|
@@ -260,11 +260,6 @@ describe('Full Orchestration E2E', () => {
|
|
|
260
260
|
describe('complete kanban workflow', () => {
|
|
261
261
|
it('should execute full kanban workflow with mock handlers', async () => {
|
|
262
262
|
const handlers = createMockHandlers([
|
|
263
|
-
{
|
|
264
|
-
name: 'ExportSchema',
|
|
265
|
-
events: ['SchemaExported'],
|
|
266
|
-
fn: () => ({ type: 'SchemaExported', data: { outputPath: './schema.json' } }),
|
|
267
|
-
},
|
|
268
263
|
{
|
|
269
264
|
name: 'GenerateServer',
|
|
270
265
|
events: ['ServerGenerated', 'SliceGenerated'],
|
|
@@ -340,7 +335,7 @@ describe('Full Orchestration E2E', () => {
|
|
|
340
335
|
await fetchJson(`http://localhost:${server.port}/command`, {
|
|
341
336
|
method: 'POST',
|
|
342
337
|
headers: { 'Content-Type': 'application/json' },
|
|
343
|
-
body: JSON.stringify({ type: '
|
|
338
|
+
body: JSON.stringify({ type: 'GenerateServer', data: {} }),
|
|
344
339
|
});
|
|
345
340
|
|
|
346
341
|
await delay(800);
|
|
@@ -349,7 +344,6 @@ describe('Full Orchestration E2E', () => {
|
|
|
349
344
|
const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
|
|
350
345
|
|
|
351
346
|
const expectedSubsequence = [
|
|
352
|
-
'SchemaExported',
|
|
353
347
|
'SliceGenerated',
|
|
354
348
|
'SliceImplemented',
|
|
355
349
|
'TestsCheckPassed',
|
|
@@ -363,7 +357,6 @@ describe('Full Orchestration E2E', () => {
|
|
|
363
357
|
expect(eventTypes).toContain('AllComponentsImplemented');
|
|
364
358
|
|
|
365
359
|
const missingEvents = findMissingEvents(eventTypes, [
|
|
366
|
-
'SchemaExported',
|
|
367
360
|
'SliceGenerated',
|
|
368
361
|
'SliceImplemented',
|
|
369
362
|
'ServerGenerated',
|
|
@@ -381,11 +374,6 @@ describe('Full Orchestration E2E', () => {
|
|
|
381
374
|
let typeCheckCallCount = 0;
|
|
382
375
|
|
|
383
376
|
const handlers = createMockHandlers([
|
|
384
|
-
{
|
|
385
|
-
name: 'ExportSchema',
|
|
386
|
-
events: ['SchemaExported'],
|
|
387
|
-
fn: () => ({ type: 'SchemaExported', data: { outputPath: './schema.json' } }),
|
|
388
|
-
},
|
|
389
377
|
{
|
|
390
378
|
name: 'GenerateServer',
|
|
391
379
|
events: ['ServerGenerated', 'SliceGenerated'],
|
|
@@ -461,7 +449,7 @@ describe('Full Orchestration E2E', () => {
|
|
|
461
449
|
await fetchJson(`http://localhost:${server.port}/command`, {
|
|
462
450
|
method: 'POST',
|
|
463
451
|
headers: { 'Content-Type': 'application/json' },
|
|
464
|
-
body: JSON.stringify({ type: '
|
|
452
|
+
body: JSON.stringify({ type: 'GenerateServer', data: {} }),
|
|
465
453
|
});
|
|
466
454
|
|
|
467
455
|
await delay(1000);
|
|
@@ -55,17 +55,14 @@ describe('PipelineServer E2E', () => {
|
|
|
55
55
|
describe('baseline endpoints (CLI E2E parity)', () => {
|
|
56
56
|
it('should return registry with expected shape', async () => {
|
|
57
57
|
const handler: CommandHandlerWithMetadata = {
|
|
58
|
-
name: '
|
|
59
|
-
alias: '
|
|
60
|
-
description: '
|
|
61
|
-
events: ['
|
|
62
|
-
handle: async () => ({ type: '
|
|
58
|
+
name: 'TestCommand',
|
|
59
|
+
alias: 'test:run',
|
|
60
|
+
description: 'Run test command',
|
|
61
|
+
events: ['TestDone'],
|
|
62
|
+
handle: async () => ({ type: 'TestDone', data: {} }),
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
const pipeline = define('kanban')
|
|
66
|
-
.on('SchemaExported')
|
|
67
|
-
.emit('GenerateServer', { modelPath: './.context/schema.json' })
|
|
68
|
-
.build();
|
|
65
|
+
const pipeline = define('kanban').on('TestDone').emit('GenerateServer', {}).build();
|
|
69
66
|
|
|
70
67
|
const server = new PipelineServer({ port: 0 });
|
|
71
68
|
server.registerCommandHandlers([handler]);
|
|
@@ -74,26 +71,26 @@ describe('PipelineServer E2E', () => {
|
|
|
74
71
|
|
|
75
72
|
const registry = await fetchJson<RegistryResponse>(`http://localhost:${server.port}/registry`);
|
|
76
73
|
|
|
77
|
-
expect(registry.eventHandlers).toContain('
|
|
78
|
-
expect(registry.commandHandlers).toContain('
|
|
74
|
+
expect(registry.eventHandlers).toContain('TestDone');
|
|
75
|
+
expect(registry.commandHandlers).toContain('TestCommand');
|
|
79
76
|
expect(registry.folds).toEqual([]);
|
|
80
77
|
expect(registry.commandsWithMetadata).toHaveLength(1);
|
|
81
|
-
expect(registry.commandsWithMetadata[0].alias).toBe('
|
|
82
|
-
expect(registry.commandsWithMetadata[0].description).toBe('
|
|
78
|
+
expect(registry.commandsWithMetadata[0].alias).toBe('test:run');
|
|
79
|
+
expect(registry.commandsWithMetadata[0].description).toBe('Run test command');
|
|
83
80
|
|
|
84
81
|
await server.stop();
|
|
85
82
|
});
|
|
86
83
|
|
|
87
84
|
it('should return pipeline with expected shape', async () => {
|
|
88
85
|
const handler: CommandHandlerWithMetadata = {
|
|
89
|
-
name: '
|
|
90
|
-
alias: '
|
|
91
|
-
description: '
|
|
92
|
-
events: ['
|
|
93
|
-
handle: async () => ({ type: '
|
|
86
|
+
name: 'TestCommand',
|
|
87
|
+
alias: 'test:run',
|
|
88
|
+
description: 'Run test command',
|
|
89
|
+
events: ['TestDone'],
|
|
90
|
+
handle: async () => ({ type: 'TestDone', data: {} }),
|
|
94
91
|
};
|
|
95
92
|
|
|
96
|
-
const pipeline = define('kanban').on('
|
|
93
|
+
const pipeline = define('kanban').on('TestDone').emit('GenerateServer', {}).build();
|
|
97
94
|
|
|
98
95
|
const server = new PipelineServer({ port: 0 });
|
|
99
96
|
server.registerCommandHandlers([handler]);
|
|
@@ -102,9 +99,9 @@ describe('PipelineServer E2E', () => {
|
|
|
102
99
|
|
|
103
100
|
const pipelineRes = await fetchJson<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
104
101
|
|
|
105
|
-
expect(pipelineRes.nodes.some((n) => n.id === 'evt:
|
|
102
|
+
expect(pipelineRes.nodes.some((n) => n.id === 'evt:TestDone')).toBe(true);
|
|
106
103
|
expect(pipelineRes.nodes.some((n) => n.id === 'cmd:GenerateServer')).toBe(true);
|
|
107
|
-
expect(pipelineRes.pipelineNodes.some((n) => n.id === '
|
|
104
|
+
expect(pipelineRes.pipelineNodes.some((n) => n.id === 'TestCommand')).toBe(true);
|
|
108
105
|
|
|
109
106
|
await server.stop();
|
|
110
107
|
});
|
|
@@ -131,8 +128,8 @@ describe('PipelineServer E2E', () => {
|
|
|
131
128
|
|
|
132
129
|
it('should accept command and return ack', async () => {
|
|
133
130
|
const handler: CommandHandlerWithMetadata = {
|
|
134
|
-
name: '
|
|
135
|
-
handle: async () => ({ type: '
|
|
131
|
+
name: 'TestCommand',
|
|
132
|
+
handle: async () => ({ type: 'TestDone', data: {} }),
|
|
136
133
|
};
|
|
137
134
|
|
|
138
135
|
const server = new PipelineServer({ port: 0 });
|
|
@@ -142,7 +139,7 @@ describe('PipelineServer E2E', () => {
|
|
|
142
139
|
const ack = await fetchJson<CommandAck>(`http://localhost:${server.port}/command`, {
|
|
143
140
|
method: 'POST',
|
|
144
141
|
headers: { 'Content-Type': 'application/json' },
|
|
145
|
-
body: JSON.stringify({ type: '
|
|
142
|
+
body: JSON.stringify({ type: 'TestCommand', data: {} }),
|
|
146
143
|
});
|
|
147
144
|
|
|
148
145
|
expect(ack.status).toBe('ack');
|
|
@@ -155,9 +152,9 @@ describe('PipelineServer E2E', () => {
|
|
|
155
152
|
describe('command execution and event routing', () => {
|
|
156
153
|
it('should execute command and route resulting event through pipeline', async () => {
|
|
157
154
|
const exportHandler: CommandHandlerWithMetadata = {
|
|
158
|
-
name: '
|
|
159
|
-
events: ['
|
|
160
|
-
handle: async () => ({ type: '
|
|
155
|
+
name: 'TestCommand',
|
|
156
|
+
events: ['TestDone'],
|
|
157
|
+
handle: async () => ({ type: 'TestDone', data: { path: './schema.json' } }),
|
|
161
158
|
};
|
|
162
159
|
|
|
163
160
|
const generateHandler: CommandHandlerWithMetadata = {
|
|
@@ -166,10 +163,7 @@ describe('PipelineServer E2E', () => {
|
|
|
166
163
|
handle: async () => ({ type: 'ServerGenerated', data: { slices: 3 } }),
|
|
167
164
|
};
|
|
168
165
|
|
|
169
|
-
const pipeline = define('kanban')
|
|
170
|
-
.on('SchemaExported')
|
|
171
|
-
.emit('GenerateServer', (e: { data: { path: string } }) => ({ modelPath: e.data.path }))
|
|
172
|
-
.build();
|
|
166
|
+
const pipeline = define('kanban').on('TestDone').emit('GenerateServer', {}).build();
|
|
173
167
|
|
|
174
168
|
const server = new PipelineServer({ port: 0 });
|
|
175
169
|
server.registerCommandHandlers([exportHandler, generateHandler]);
|
|
@@ -179,7 +173,7 @@ describe('PipelineServer E2E', () => {
|
|
|
179
173
|
await fetchJson(`http://localhost:${server.port}/command`, {
|
|
180
174
|
method: 'POST',
|
|
181
175
|
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body: JSON.stringify({ type: '
|
|
176
|
+
body: JSON.stringify({ type: 'TestCommand', data: {} }),
|
|
183
177
|
});
|
|
184
178
|
|
|
185
179
|
await new Promise((r) => setTimeout(r, 200));
|
|
@@ -187,7 +181,7 @@ describe('PipelineServer E2E', () => {
|
|
|
187
181
|
const messages = await fetchJson<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
188
182
|
const eventTypes = messages.filter((m) => m.messageType === 'event').map((m) => m.message.type);
|
|
189
183
|
|
|
190
|
-
expect(eventTypes).toContain('
|
|
184
|
+
expect(eventTypes).toContain('TestDone');
|
|
191
185
|
expect(eventTypes).toContain('ServerGenerated');
|
|
192
186
|
|
|
193
187
|
await server.stop();
|
|
@@ -1117,11 +1117,9 @@ describe('PipelineServer', () => {
|
|
|
1117
1117
|
handle: async () => ({ type: 'SliceImplemented', data: {} }),
|
|
1118
1118
|
};
|
|
1119
1119
|
const pipeline = define('test')
|
|
1120
|
-
.on('SchemaExported')
|
|
1121
|
-
.emit('GenerateServer', {})
|
|
1122
1120
|
.on('ServerGenerated')
|
|
1123
1121
|
.emit('GenerateIA', {})
|
|
1124
|
-
.on('
|
|
1122
|
+
.on('IAGenerated')
|
|
1125
1123
|
.emit('ImplementSlice', {})
|
|
1126
1124
|
.build();
|
|
1127
1125
|
const server = new PipelineServer({ port: 0 });
|
|
@@ -1130,9 +1128,8 @@ describe('PipelineServer', () => {
|
|
|
1130
1128
|
await server.start();
|
|
1131
1129
|
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
1132
1130
|
const mermaid = await res.text();
|
|
1133
|
-
expect(mermaid).toContain('
|
|
1134
|
-
expect(mermaid).toContain('
|
|
1135
|
-
expect(mermaid).toContain('GenerateServer --> evt_SliceGenerated');
|
|
1131
|
+
expect(mermaid).toContain('evt_ServerGenerated --> GenerateIA');
|
|
1132
|
+
expect(mermaid).toContain('evt_IAGenerated --> ImplementSlice');
|
|
1136
1133
|
await server.stop();
|
|
1137
1134
|
});
|
|
1138
1135
|
|
|
@@ -1202,6 +1199,44 @@ describe('PipelineServer', () => {
|
|
|
1202
1199
|
await server.stop();
|
|
1203
1200
|
});
|
|
1204
1201
|
|
|
1202
|
+
it('should show source commands whose events are listened to by the pipeline', async () => {
|
|
1203
|
+
const sourceHandler = {
|
|
1204
|
+
name: 'SourceCmd',
|
|
1205
|
+
events: ['SourceEvent'],
|
|
1206
|
+
handle: async () => ({ type: 'SourceEvent', data: {} }),
|
|
1207
|
+
};
|
|
1208
|
+
const nextHandler = {
|
|
1209
|
+
name: 'NextCmd',
|
|
1210
|
+
events: ['NextDone'],
|
|
1211
|
+
handle: async () => ({ type: 'NextDone', data: {} }),
|
|
1212
|
+
};
|
|
1213
|
+
const pipeline = define('test').on('SourceEvent').emit('NextCmd', {}).build();
|
|
1214
|
+
const server = new PipelineServer({ port: 0 });
|
|
1215
|
+
server.registerCommandHandlers([sourceHandler, nextHandler]);
|
|
1216
|
+
server.registerPipeline(pipeline);
|
|
1217
|
+
await server.start();
|
|
1218
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
1219
|
+
const mermaid = await res.text();
|
|
1220
|
+
expect(mermaid).toEqual(
|
|
1221
|
+
[
|
|
1222
|
+
'flowchart LR',
|
|
1223
|
+
' evt_SourceEvent([SourceEvent])',
|
|
1224
|
+
' NextCmd[NextCmd]',
|
|
1225
|
+
' SourceCmd[SourceCmd]',
|
|
1226
|
+
' evt_SourceEvent --> NextCmd',
|
|
1227
|
+
' SourceCmd --> evt_SourceEvent',
|
|
1228
|
+
'',
|
|
1229
|
+
' classDef event fill:#fff3e0,stroke:#e65100',
|
|
1230
|
+
' classDef eventFailed fill:#fff3e0,stroke:#e65100,color:#d32f2f',
|
|
1231
|
+
' classDef command fill:#e3f2fd,stroke:#1565c0',
|
|
1232
|
+
' classDef settled fill:#f3e5f5,stroke:#7b1fa2',
|
|
1233
|
+
' class evt_SourceEvent event',
|
|
1234
|
+
' class NextCmd,SourceCmd command',
|
|
1235
|
+
].join('\n'),
|
|
1236
|
+
);
|
|
1237
|
+
await server.stop();
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1205
1240
|
it('should show edges from commands to settled node', async () => {
|
|
1206
1241
|
const checkAHandler = {
|
|
1207
1242
|
name: 'CheckA',
|
|
@@ -205,7 +205,7 @@ export class PipelineServer {
|
|
|
205
205
|
const eventHandlers: string[] = [];
|
|
206
206
|
for (const pipeline of this.pipelines.values()) {
|
|
207
207
|
for (const handler of pipeline.descriptor.handlers) {
|
|
208
|
-
if (handler.type === 'settled') {
|
|
208
|
+
if (handler.type === 'settled' || handler.type === 'accepts') {
|
|
209
209
|
continue;
|
|
210
210
|
}
|
|
211
211
|
if (!eventHandlers.includes(handler.eventType)) {
|
|
@@ -715,13 +715,17 @@ export class PipelineServer {
|
|
|
715
715
|
const newEdges = [...graph.edges];
|
|
716
716
|
|
|
717
717
|
for (const [commandName, events] of Object.entries(commandToEvents)) {
|
|
718
|
-
|
|
718
|
+
const relevantEvents = events.filter((e) => pipelineEvents.has(e));
|
|
719
|
+
if (relevantEvents.length === 0) {
|
|
719
720
|
continue;
|
|
720
721
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
722
|
+
|
|
723
|
+
if (!commandNodes.has(commandName)) {
|
|
724
|
+
newNodes.push({ id: `cmd:${commandName}`, type: 'command', label: commandName });
|
|
725
|
+
commandNodes.add(commandName);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for (const eventName of relevantEvents) {
|
|
725
729
|
const eventId = `evt:${eventName}`;
|
|
726
730
|
if (!existingEventIds.has(eventId)) {
|
|
727
731
|
newNodes.push({ id: eventId, type: 'event', label: eventName });
|
|
@@ -52,13 +52,6 @@ describe('kanban-full.pipeline', () => {
|
|
|
52
52
|
expect(pipeline.descriptor.name).toBe('kanban-full');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it('should have emit handlers for schema export', () => {
|
|
56
|
-
const pipeline = createKanbanFullPipeline();
|
|
57
|
-
const emitHandlers = pipeline.descriptor.handlers.filter((h): h is EmitHandlerDescriptor => h.type === 'emit');
|
|
58
|
-
const eventTypes = emitHandlers.map((h) => h.eventType);
|
|
59
|
-
expect(eventTypes).toContain('SchemaExported');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
55
|
it('should have settled handler for slice checks', () => {
|
|
63
56
|
const pipeline = createKanbanFullPipeline();
|
|
64
57
|
const settledHandlers = pipeline.descriptor.handlers.filter((h) => h.type === 'settled');
|
|
@@ -92,23 +85,6 @@ describe('kanban-full.pipeline', () => {
|
|
|
92
85
|
return undefined;
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
it('should emit GenerateServer with modelPath and destination', () => {
|
|
96
|
-
const pipeline = createKanbanFullPipeline();
|
|
97
|
-
const cmd = findEmitCommand(pipeline, 'SchemaExported', 'GenerateServer');
|
|
98
|
-
expect(cmd).toBeDefined();
|
|
99
|
-
const data =
|
|
100
|
-
typeof cmd?.data === 'function'
|
|
101
|
-
? cmd.data({
|
|
102
|
-
type: 'SchemaExported',
|
|
103
|
-
data: { outputPath: './.context/schema.json', directory: '.' },
|
|
104
|
-
})
|
|
105
|
-
: cmd?.data;
|
|
106
|
-
expect(data).toEqual({
|
|
107
|
-
modelPath: './.context/schema.json',
|
|
108
|
-
destination: '.',
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
88
|
it('should emit ImplementSlice with slicePath, context, and aiOptions', () => {
|
|
113
89
|
const pipeline = createKanbanFullPipeline();
|
|
114
90
|
const cmd = findEmitCommand(pipeline, 'SliceGenerated', 'ImplementSlice');
|
|
@@ -6,11 +6,6 @@ interface Component {
|
|
|
6
6
|
filePath: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
interface SchemaExportedData {
|
|
10
|
-
directory: string;
|
|
11
|
-
outputPath: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
interface SliceGeneratedData {
|
|
15
10
|
slicePath: string;
|
|
16
11
|
}
|
|
@@ -89,15 +84,6 @@ function resolvePath(relativePath: string): string {
|
|
|
89
84
|
|
|
90
85
|
export function createKanbanFullPipeline() {
|
|
91
86
|
return define('kanban-full')
|
|
92
|
-
.on('SchemaExported')
|
|
93
|
-
.emit('GenerateServer', (e: { data: SchemaExportedData }) => {
|
|
94
|
-
projectRoot = e.data.directory;
|
|
95
|
-
return {
|
|
96
|
-
modelPath: e.data.outputPath,
|
|
97
|
-
destination: e.data.directory,
|
|
98
|
-
};
|
|
99
|
-
})
|
|
100
|
-
|
|
101
87
|
.on('SliceGenerated')
|
|
102
88
|
.emit('ImplementSlice', (e: { data: SliceGeneratedData }) => ({
|
|
103
89
|
slicePath: resolvePath(e.data.slicePath),
|
|
@@ -6,7 +6,6 @@ export default pipelineConfig({
|
|
|
6
6
|
'@auto-engineer/server-checks',
|
|
7
7
|
'@auto-engineer/server-generator-apollo-emmett',
|
|
8
8
|
'@auto-engineer/narrative',
|
|
9
|
-
'@auto-engineer/information-architect',
|
|
10
9
|
'@auto-engineer/generate-react-client',
|
|
11
10
|
'@auto-engineer/react-component-implementer',
|
|
12
11
|
'@auto-engineer/server-implementer',
|
|
@@ -26,7 +26,6 @@ describe('kanban.pipeline', () => {
|
|
|
26
26
|
it('should have handlers for slice workflow', () => {
|
|
27
27
|
const pipeline = createKanbanPipeline();
|
|
28
28
|
const eventTypes = pipeline.descriptor.handlers.filter((h) => h.type === 'emit').map((h) => h.eventType);
|
|
29
|
-
expect(eventTypes).toContain('SchemaExported');
|
|
30
29
|
expect(eventTypes).toContain('SliceGenerated');
|
|
31
30
|
});
|
|
32
31
|
});
|
|
@@ -7,10 +7,6 @@ interface Component {
|
|
|
7
7
|
filePath: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
interface SchemaExportedData {
|
|
11
|
-
outputPath: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
interface SliceGeneratedData {
|
|
15
11
|
slicePath: string;
|
|
16
12
|
}
|
|
@@ -68,9 +64,6 @@ function incrementRetryCount(slicePath: string): number {
|
|
|
68
64
|
|
|
69
65
|
export function createKanbanPipeline() {
|
|
70
66
|
return define('kanban')
|
|
71
|
-
.on('SchemaExported')
|
|
72
|
-
.emit('GenerateServer', (e: { data: SchemaExportedData }) => ({ modelPath: e.data.outputPath }))
|
|
73
|
-
|
|
74
67
|
.on('SliceGenerated')
|
|
75
68
|
.emit('ImplementSlice', (e: { data: SliceGeneratedData }) => ({ slicePath: e.data.slicePath }))
|
|
76
69
|
|
|
@@ -169,7 +169,7 @@ describe('Kanban-Todo Pipeline E2E Comparison', () => {
|
|
|
169
169
|
describe('Workflow Sequence Validation', () => {
|
|
170
170
|
it('should have correct causal dependencies in pipeline definition', () => {
|
|
171
171
|
const expectedDependencies: [string, string][] = [
|
|
172
|
-
['
|
|
172
|
+
['ServerGenerated', 'GenerateIA'],
|
|
173
173
|
['SliceGenerated', 'ImplementSlice'],
|
|
174
174
|
['SliceImplemented', 'CheckTests'],
|
|
175
175
|
['SliceImplemented', 'CheckTypes'],
|