@auto-engineer/pipeline 1.108.0 → 1.109.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 +49 -0
- package/dist/src/builder/define.d.ts +5 -2
- package/dist/src/builder/define.d.ts.map +1 -1
- package/dist/src/builder/define.js +16 -2
- package/dist/src/builder/define.js.map +1 -1
- package/dist/src/core/descriptors.d.ts +3 -1
- package/dist/src/core/descriptors.d.ts.map +1 -1
- package/dist/src/server/pipeline-server-v2.d.ts +4 -41
- package/dist/src/server/pipeline-server-v2.d.ts.map +1 -1
- package/dist/src/server/pipeline-server-v2.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts +1 -0
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +20 -2
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/v2-runtime-bridge.d.ts +1 -0
- package/dist/src/server/v2-runtime-bridge.d.ts.map +1 -1
- package/dist/src/server/v2-runtime-bridge.js +31 -3
- package/dist/src/server/v2-runtime-bridge.js.map +1 -1
- package/dist/src/testing/fixtures/kanban-todo.config.js +1 -1
- package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/builder/define.specs.ts +47 -0
- package/src/builder/define.ts +31 -1
- package/src/core/descriptors.ts +4 -0
- package/src/plugins/plugin-loader.specs.ts +5 -5
- package/src/server/pipeline-server-v2.ts +6 -1
- package/src/server/pipeline-server.ts +21 -2
- package/src/server/v2-runtime-bridge.specs.ts +104 -1
- package/src/server/v2-runtime-bridge.ts +67 -3
- package/src/testing/fixtures/kanban-todo.config.ts +1 -1
package/package.json
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"get-port": "^7.1.0",
|
|
15
15
|
"jose": "^5.9.6",
|
|
16
16
|
"nanoid": "^5.0.0",
|
|
17
|
-
"@auto-engineer/file-store": "1.
|
|
18
|
-
"@auto-engineer/message-bus": "1.
|
|
17
|
+
"@auto-engineer/file-store": "1.109.0",
|
|
18
|
+
"@auto-engineer/message-bus": "1.109.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/cors": "^2.8.17",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
|
-
"version": "1.
|
|
27
|
+
"version": "1.109.0",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
|
|
30
30
|
"test": "vitest run --reporter=dot",
|
|
@@ -110,6 +110,14 @@ describe('settled()', () => {
|
|
|
110
110
|
expect(descriptor.handler).toBe(handler);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('should accept handler with emit parameter', () => {
|
|
114
|
+
const handler = vi.fn((_events, _send, _emit) => undefined);
|
|
115
|
+
const pipeline = define('test').settled(['CheckTests']).dispatch({ dispatches: [] }, handler).build();
|
|
116
|
+
|
|
117
|
+
const descriptor = pipeline.descriptor.handlers[0] as SettledHandlerDescriptor;
|
|
118
|
+
expect(descriptor.handler).toBe(handler);
|
|
119
|
+
});
|
|
120
|
+
|
|
113
121
|
it('should chain multiple settled handlers', () => {
|
|
114
122
|
const handler1 = vi.fn();
|
|
115
123
|
const handler2 = vi.fn();
|
|
@@ -165,6 +173,28 @@ describe('settled()', () => {
|
|
|
165
173
|
expect(descriptor.sourceEventTypes).toEqual(['SliceImplemented']);
|
|
166
174
|
});
|
|
167
175
|
|
|
176
|
+
it('supports maxRetries option on SettledBuilder', () => {
|
|
177
|
+
const pipeline = define('test')
|
|
178
|
+
.settled(['CheckTests'])
|
|
179
|
+
.maxRetries(0)
|
|
180
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
181
|
+
.build();
|
|
182
|
+
|
|
183
|
+
const descriptor = pipeline.descriptor.handlers[0] as SettledHandlerDescriptor;
|
|
184
|
+
expect(descriptor.maxRetries).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('supports maxRetries option on SettledChain', () => {
|
|
188
|
+
const pipeline = define('test')
|
|
189
|
+
.settled(['CheckTests'])
|
|
190
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
191
|
+
.maxRetries(5)
|
|
192
|
+
.build();
|
|
193
|
+
|
|
194
|
+
const descriptor = pipeline.descriptor.handlers[0] as SettledHandlerDescriptor;
|
|
195
|
+
expect(descriptor.maxRetries).toBe(5);
|
|
196
|
+
});
|
|
197
|
+
|
|
168
198
|
it('has no sourceEventTypes for top-level settled', () => {
|
|
169
199
|
const pipeline = define('test')
|
|
170
200
|
.settled(['CheckTests'])
|
|
@@ -664,4 +694,21 @@ describe('declare().accepts()', () => {
|
|
|
664
694
|
accepts: ['ImplementComponent'],
|
|
665
695
|
});
|
|
666
696
|
});
|
|
697
|
+
|
|
698
|
+
it('should chain settled() from HandleChain', () => {
|
|
699
|
+
const handler = vi.fn();
|
|
700
|
+
const pipeline = define('test')
|
|
701
|
+
.on('ComponentImplemented')
|
|
702
|
+
.handle(async () => {})
|
|
703
|
+
.settled(['CheckTests', 'CheckTypes'])
|
|
704
|
+
.dispatch({ dispatches: [] }, handler)
|
|
705
|
+
.build();
|
|
706
|
+
|
|
707
|
+
expect(pipeline.descriptor.handlers).toHaveLength(2);
|
|
708
|
+
expect(pipeline.descriptor.handlers[0].type).toBe('custom');
|
|
709
|
+
expect(pipeline.descriptor.handlers[1].type).toBe('settled');
|
|
710
|
+
const settled = pipeline.descriptor.handlers[1] as SettledHandlerDescriptor;
|
|
711
|
+
expect(settled.commandTypes).toEqual(['CheckTests', 'CheckTypes']);
|
|
712
|
+
expect(settled.sourceEventTypes).toEqual(['ComponentImplemented']);
|
|
713
|
+
});
|
|
667
714
|
});
|
package/src/builder/define.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
KeyExtractor,
|
|
12
12
|
PipelineDescriptor,
|
|
13
13
|
RunAwaitHandlerDescriptor,
|
|
14
|
+
SettledEmitFunction,
|
|
14
15
|
SettledHandler,
|
|
15
16
|
SettledHandlerDescriptor,
|
|
16
17
|
SuccessContext,
|
|
@@ -43,11 +44,13 @@ export interface DispatchOptions<D extends readonly string[] = readonly string[]
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface SettledBuilder {
|
|
47
|
+
maxRetries(n: number): SettledBuilder;
|
|
46
48
|
dispatch<const D extends readonly string[]>(
|
|
47
49
|
options: DispatchOptions<D>,
|
|
48
50
|
handler: (
|
|
49
51
|
events: Record<string, Event[]>,
|
|
50
52
|
send: (commandType: D[number], data: unknown) => void,
|
|
53
|
+
emit: SettledEmitFunction,
|
|
51
54
|
) => undefined | { persist: boolean },
|
|
52
55
|
): SettledChain;
|
|
53
56
|
}
|
|
@@ -56,6 +59,7 @@ export interface SettledChain {
|
|
|
56
59
|
declare(commandType: string): DeclareBuilder;
|
|
57
60
|
on(eventType: string): TriggerBuilder;
|
|
58
61
|
settled(commandTypes: readonly string[], label?: string): SettledBuilder;
|
|
62
|
+
maxRetries(n: number): SettledChain;
|
|
59
63
|
build(): Pipeline;
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -140,6 +144,7 @@ export interface EmitChain {
|
|
|
140
144
|
export interface HandleChain {
|
|
141
145
|
declare(commandType: string): DeclareBuilder;
|
|
142
146
|
on(eventType: string): TriggerBuilder;
|
|
147
|
+
settled(commandTypes: readonly string[], label?: string): SettledBuilder;
|
|
143
148
|
build(): Pipeline;
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -450,6 +455,11 @@ class HandleChainImpl implements HandleChain {
|
|
|
450
455
|
return new TriggerBuilderImpl(this.parent, eventType);
|
|
451
456
|
}
|
|
452
457
|
|
|
458
|
+
settled(commandTypes: readonly string[], label?: string): SettledBuilder {
|
|
459
|
+
this.finalizeHandler();
|
|
460
|
+
return new SettledBuilderImpl(this.parent, commandTypes, this.eventType, label);
|
|
461
|
+
}
|
|
462
|
+
|
|
453
463
|
build(): Pipeline {
|
|
454
464
|
this.finalizeHandler();
|
|
455
465
|
return this.parent.build();
|
|
@@ -710,6 +720,8 @@ class PhasedTerminalImpl implements PhasedTerminal {
|
|
|
710
720
|
}
|
|
711
721
|
|
|
712
722
|
class SettledBuilderImpl implements SettledBuilder {
|
|
723
|
+
private maxRetriesValue?: number;
|
|
724
|
+
|
|
713
725
|
constructor(
|
|
714
726
|
private readonly parent: PipelineBuilderImpl,
|
|
715
727
|
private readonly commandTypes: readonly string[],
|
|
@@ -717,11 +729,17 @@ class SettledBuilderImpl implements SettledBuilder {
|
|
|
717
729
|
private readonly customLabel?: string,
|
|
718
730
|
) {}
|
|
719
731
|
|
|
732
|
+
maxRetries(n: number): SettledBuilder {
|
|
733
|
+
this.maxRetriesValue = n;
|
|
734
|
+
return this;
|
|
735
|
+
}
|
|
736
|
+
|
|
720
737
|
dispatch<const D extends readonly string[]>(
|
|
721
738
|
options: DispatchOptions<D>,
|
|
722
739
|
handler: (
|
|
723
740
|
events: Record<string, Event[]>,
|
|
724
741
|
send: (commandType: D[number], data: unknown) => void,
|
|
742
|
+
emit: SettledEmitFunction,
|
|
725
743
|
) => undefined | { persist: boolean },
|
|
726
744
|
): SettledChain {
|
|
727
745
|
return new SettledChainImpl(
|
|
@@ -731,11 +749,14 @@ class SettledBuilderImpl implements SettledBuilder {
|
|
|
731
749
|
options.dispatches,
|
|
732
750
|
this.sourceEventType,
|
|
733
751
|
this.customLabel,
|
|
752
|
+
this.maxRetriesValue,
|
|
734
753
|
);
|
|
735
754
|
}
|
|
736
755
|
}
|
|
737
756
|
|
|
738
757
|
class SettledChainImpl implements SettledChain {
|
|
758
|
+
private maxRetriesValue?: number;
|
|
759
|
+
|
|
739
760
|
constructor(
|
|
740
761
|
private readonly parent: PipelineBuilderImpl,
|
|
741
762
|
private readonly commandTypes: readonly string[],
|
|
@@ -743,7 +764,10 @@ class SettledChainImpl implements SettledChain {
|
|
|
743
764
|
private readonly dispatches?: readonly string[],
|
|
744
765
|
private readonly sourceEventType?: string,
|
|
745
766
|
private readonly customLabel?: string,
|
|
746
|
-
|
|
767
|
+
maxRetriesFromBuilder?: number,
|
|
768
|
+
) {
|
|
769
|
+
this.maxRetriesValue = maxRetriesFromBuilder;
|
|
770
|
+
}
|
|
747
771
|
|
|
748
772
|
declare(commandType: string): DeclareBuilder {
|
|
749
773
|
this.finalizeHandler();
|
|
@@ -760,6 +784,11 @@ class SettledChainImpl implements SettledChain {
|
|
|
760
784
|
return new SettledBuilderImpl(this.parent, commandTypes, undefined, label);
|
|
761
785
|
}
|
|
762
786
|
|
|
787
|
+
maxRetries(n: number): SettledChain {
|
|
788
|
+
this.maxRetriesValue = n;
|
|
789
|
+
return this;
|
|
790
|
+
}
|
|
791
|
+
|
|
763
792
|
build(): Pipeline {
|
|
764
793
|
this.finalizeHandler();
|
|
765
794
|
return this.parent.build();
|
|
@@ -776,6 +805,7 @@ class SettledChainImpl implements SettledChain {
|
|
|
776
805
|
settledId: this.parent.nextSettledId(),
|
|
777
806
|
label: settledLabel,
|
|
778
807
|
sourceEventTypes: this.sourceEventType ? [this.sourceEventType] : undefined,
|
|
808
|
+
maxRetries: this.maxRetriesValue,
|
|
779
809
|
};
|
|
780
810
|
this.parent.addHandler(descriptor);
|
|
781
811
|
}
|
package/src/core/descriptors.ts
CHANGED
|
@@ -75,9 +75,12 @@ export interface CustomHandlerDescriptor {
|
|
|
75
75
|
|
|
76
76
|
type SettledSendFunction = (commandType: string, data: unknown) => void;
|
|
77
77
|
|
|
78
|
+
export type SettledEmitFunction = (eventType: string, data: unknown, correlationId?: string) => void;
|
|
79
|
+
|
|
78
80
|
export type SettledHandler = (
|
|
79
81
|
events: Record<string, Event[]>,
|
|
80
82
|
send: SettledSendFunction,
|
|
83
|
+
emit: SettledEmitFunction,
|
|
81
84
|
) => undefined | { persist: boolean };
|
|
82
85
|
|
|
83
86
|
export interface SettledHandlerDescriptor {
|
|
@@ -88,6 +91,7 @@ export interface SettledHandlerDescriptor {
|
|
|
88
91
|
settledId?: string;
|
|
89
92
|
label?: string;
|
|
90
93
|
sourceEventTypes?: readonly string[];
|
|
94
|
+
maxRetries?: number;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export interface AcceptsDescriptor {
|
|
@@ -25,12 +25,12 @@ describe('PluginLoader', () => {
|
|
|
25
25
|
it('should load COMMANDS from workspace package', async () => {
|
|
26
26
|
const mockHandler = createMockHandler('CheckTests', ['TestsCheckPassed', 'TestsCheckFailed']);
|
|
27
27
|
const deps = createMockDeps({
|
|
28
|
-
existsSync: vi.fn().mockImplementation((path: string) => path.includes('packages/
|
|
28
|
+
existsSync: vi.fn().mockImplementation((path: string) => path.includes('packages/checks')),
|
|
29
29
|
importModule: vi.fn().mockResolvedValue({ COMMANDS: [mockHandler] }),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const loader = new PluginLoader('/workspace', deps);
|
|
33
|
-
const handlers = await loader.loadPlugin('@auto-engineer/
|
|
33
|
+
const handlers = await loader.loadPlugin('@auto-engineer/checks');
|
|
34
34
|
|
|
35
35
|
expect(handlers).toHaveLength(1);
|
|
36
36
|
expect(handlers[0].name).toBe('CheckTests');
|
|
@@ -94,7 +94,7 @@ describe('PluginLoader', () => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
const loader = new PluginLoader('/workspace', deps);
|
|
97
|
-
const handlers = await loader.loadPlugin('@auto-engineer/
|
|
97
|
+
const handlers = await loader.loadPlugin('@auto-engineer/checks');
|
|
98
98
|
|
|
99
99
|
expect(handlers[0]).toEqual({
|
|
100
100
|
name: 'CheckTypes',
|
|
@@ -257,7 +257,7 @@ describe('PluginLoader', () => {
|
|
|
257
257
|
describe('integration', { timeout: 30000 }, () => {
|
|
258
258
|
it('should load real package using default deps', async () => {
|
|
259
259
|
const loader = new PluginLoader();
|
|
260
|
-
const handlers = await loader.loadPlugin('@auto-engineer/
|
|
260
|
+
const handlers = await loader.loadPlugin('@auto-engineer/checks');
|
|
261
261
|
const handlerNames = handlers.map((h) => h.name);
|
|
262
262
|
expect(handlerNames).toContain('CheckTests');
|
|
263
263
|
expect(handlerNames).toContain('CheckTypes');
|
|
@@ -267,7 +267,7 @@ describe('PluginLoader', () => {
|
|
|
267
267
|
it('should load handlers from multiple real packages', async () => {
|
|
268
268
|
const loader = new PluginLoader();
|
|
269
269
|
const handlers = await loader.loadPlugins([
|
|
270
|
-
'@auto-engineer/
|
|
270
|
+
'@auto-engineer/checks',
|
|
271
271
|
'@auto-engineer/server-generator-apollo-emmett',
|
|
272
272
|
]);
|
|
273
273
|
const handlerNames = handlers.map((h) => h.name);
|
|
@@ -3,7 +3,12 @@ import cors from 'cors';
|
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import { createPipelineEngine } from '../engine/pipeline-engine.js';
|
|
5
5
|
|
|
6
|
-
export async function createPipelineServerV2(config?: { port?: number }) {
|
|
6
|
+
export async function createPipelineServerV2(config?: { port?: number }): Promise<{
|
|
7
|
+
engine: Awaited<ReturnType<typeof createPipelineEngine>>;
|
|
8
|
+
app: ReturnType<typeof express>;
|
|
9
|
+
start(): Promise<number>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}> {
|
|
7
12
|
const engine = await createPipelineEngine();
|
|
8
13
|
const app = express();
|
|
9
14
|
app.use(cors());
|
|
@@ -92,6 +92,9 @@ export class PipelineServer {
|
|
|
92
92
|
onDispatch: (commandType, data, correlationId) => {
|
|
93
93
|
void this.dispatchFromSettled(commandType, data, correlationId);
|
|
94
94
|
},
|
|
95
|
+
onEmit: (eventType, data, correlationId) => {
|
|
96
|
+
void this.emitFromSettled(eventType, data, correlationId);
|
|
97
|
+
},
|
|
95
98
|
});
|
|
96
99
|
this.phasedBridge = createPhasedBridge({
|
|
97
100
|
onDispatch: (commandType, data, correlationId) => {
|
|
@@ -1142,7 +1145,8 @@ export class PipelineServer {
|
|
|
1142
1145
|
await this.updateNodeStatus(this.currentSessionId, command.type, 'running');
|
|
1143
1146
|
const sourceEventType = this.requestIdToSourceEvent.get(command.requestId);
|
|
1144
1147
|
this.requestIdToSourceEvent.delete(command.requestId);
|
|
1145
|
-
|
|
1148
|
+
const settledCorrelationId = this.currentSessionId;
|
|
1149
|
+
this.settledBridge.onCommandStarted(command, settledCorrelationId, sourceEventType);
|
|
1146
1150
|
|
|
1147
1151
|
const ctx = this.createContext(command.correlationId, signal);
|
|
1148
1152
|
let events: Event[];
|
|
@@ -1200,7 +1204,8 @@ export class PipelineServer {
|
|
|
1200
1204
|
const sourceCommand = this.eventCommandMapper.getSourceCommand(eventWithIds.type);
|
|
1201
1205
|
if (sourceCommand !== undefined) {
|
|
1202
1206
|
const result = eventWithIds.type.includes('Failed') ? 'failure' : 'success';
|
|
1203
|
-
|
|
1207
|
+
const settledCorrelationId = this.currentSessionId;
|
|
1208
|
+
this.settledBridge.onEventReceived(eventWithIds, sourceCommand, result, settledCorrelationId, sourceEventType);
|
|
1204
1209
|
}
|
|
1205
1210
|
|
|
1206
1211
|
this.routeEventToPhasedExecutor(eventWithIds);
|
|
@@ -1231,6 +1236,20 @@ export class PipelineServer {
|
|
|
1231
1236
|
await this.processCommand(command);
|
|
1232
1237
|
}
|
|
1233
1238
|
|
|
1239
|
+
/* v8 ignore next 10 - integration callback tested via v2-runtime-bridge.specs.ts */
|
|
1240
|
+
private async emitFromSettled(eventType: string, data: unknown, correlationId: string): Promise<void> {
|
|
1241
|
+
const requestId = `req-${nanoid()}`;
|
|
1242
|
+
const eventWithIds: EventWithCorrelation = {
|
|
1243
|
+
type: eventType,
|
|
1244
|
+
data: data as Record<string, unknown>,
|
|
1245
|
+
correlationId,
|
|
1246
|
+
};
|
|
1247
|
+
await this.emitDomainEventEmitted(correlationId, requestId, eventType, data as Record<string, unknown>);
|
|
1248
|
+
this.sseManager.broadcast(eventWithIds);
|
|
1249
|
+
await this.messageBus.publishEvent(eventWithIds);
|
|
1250
|
+
await this.routeEventToPipelines(eventWithIds);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1234
1253
|
/* v8 ignore next 10 - integration callback tested via phased-bridge.specs.ts */
|
|
1235
1254
|
private async handlePhasedComplete(event: Event, correlationId: string): Promise<void> {
|
|
1236
1255
|
const requestId = `req-${nanoid()}`;
|
|
@@ -189,6 +189,56 @@ describe('V2RuntimeBridge', () => {
|
|
|
189
189
|
|
|
190
190
|
expect(dispatched).toEqual([{ commandType: 'NextCommand', data: { payload: true }, correlationId: 'corr-1' }]);
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
it('passes emit function that calls onEmit with correlationId', () => {
|
|
194
|
+
const emitted: Array<{ eventType: string; data: unknown; correlationId: string }> = [];
|
|
195
|
+
const bridge = createV2RuntimeBridge({
|
|
196
|
+
onDispatch: () => {},
|
|
197
|
+
onEmit: (eventType, data, correlationId) => {
|
|
198
|
+
emitted.push({ eventType, data, correlationId });
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
bridge.registerSettled({
|
|
203
|
+
type: 'settled',
|
|
204
|
+
commandTypes: ['A'],
|
|
205
|
+
handler: (_events, _send, emit) => {
|
|
206
|
+
emit('ChecksPassed', { component: 'Foo' }, 'graph:g1:job-a');
|
|
207
|
+
return undefined;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
212
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
|
|
213
|
+
|
|
214
|
+
expect(emitted).toEqual([
|
|
215
|
+
{ eventType: 'ChecksPassed', data: { component: 'Foo' }, correlationId: 'graph:g1:job-a' },
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('emit uses settled correlationId when no explicit correlationId provided', () => {
|
|
220
|
+
const emitted: Array<{ eventType: string; data: unknown; correlationId: string }> = [];
|
|
221
|
+
const bridge = createV2RuntimeBridge({
|
|
222
|
+
onDispatch: () => {},
|
|
223
|
+
onEmit: (eventType, data, correlationId) => {
|
|
224
|
+
emitted.push({ eventType, data, correlationId });
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
bridge.registerSettled({
|
|
229
|
+
type: 'settled',
|
|
230
|
+
commandTypes: ['A'],
|
|
231
|
+
handler: (_events, _send, emit) => {
|
|
232
|
+
emit('ChecksPassed', { component: 'Bar' });
|
|
233
|
+
return undefined;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
238
|
+
bridge.onEventReceived(makeEvent('ADone', 'corr-1'), 'A');
|
|
239
|
+
|
|
240
|
+
expect(emitted).toEqual([{ eventType: 'ChecksPassed', data: { component: 'Bar' }, correlationId: 'corr-1' }]);
|
|
241
|
+
});
|
|
192
242
|
});
|
|
193
243
|
|
|
194
244
|
describe('RetryCommands', () => {
|
|
@@ -215,6 +265,59 @@ describe('V2RuntimeBridge', () => {
|
|
|
215
265
|
});
|
|
216
266
|
});
|
|
217
267
|
|
|
268
|
+
describe('SettledFailed', () => {
|
|
269
|
+
it('invokes handler on SettledFailed after max retries exhausted', () => {
|
|
270
|
+
const handlerCalls: Record<string, Event[]>[] = [];
|
|
271
|
+
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
272
|
+
|
|
273
|
+
bridge.registerSettled(
|
|
274
|
+
{
|
|
275
|
+
type: 'settled',
|
|
276
|
+
commandTypes: ['A'],
|
|
277
|
+
handler: (events) => {
|
|
278
|
+
handlerCalls.push(events);
|
|
279
|
+
return undefined;
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{ maxRetries: 0 },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
286
|
+
bridge.onEventReceived(makeEvent('AFailed', 'corr-1', { error: 'oops' }), 'A', 'failure');
|
|
287
|
+
|
|
288
|
+
expect(handlerCalls).toHaveLength(1);
|
|
289
|
+
expect(handlerCalls[0]).toEqual({
|
|
290
|
+
A: [{ type: 'AFailed', data: { error: 'oops' }, correlationId: 'corr-1' }],
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('passes send function to handler on SettledFailed', () => {
|
|
295
|
+
const dispatched: Array<{ commandType: string; data: unknown; correlationId: string }> = [];
|
|
296
|
+
const bridge = createV2RuntimeBridge({
|
|
297
|
+
onDispatch: (commandType, data, correlationId) => {
|
|
298
|
+
dispatched.push({ commandType, data, correlationId });
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
bridge.registerSettled(
|
|
303
|
+
{
|
|
304
|
+
type: 'settled',
|
|
305
|
+
commandTypes: ['A'],
|
|
306
|
+
handler: (_events, send) => {
|
|
307
|
+
send('RetryCommand', { retry: true });
|
|
308
|
+
return undefined;
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{ maxRetries: 0 },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
bridge.onCommandStarted(makeCommand('A', 'corr-1'));
|
|
315
|
+
bridge.onEventReceived(makeEvent('AFailed', 'corr-1'), 'A', 'failure');
|
|
316
|
+
|
|
317
|
+
expect(dispatched).toEqual([{ commandType: 'RetryCommand', data: { retry: true }, correlationId: 'corr-1' }]);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
218
321
|
describe('persist: true resets instance', () => {
|
|
219
322
|
it('allows receiving new commands after reset', () => {
|
|
220
323
|
const bridge = createV2RuntimeBridge({ onDispatch: () => {} });
|
|
@@ -334,7 +437,7 @@ describe('V2RuntimeBridge', () => {
|
|
|
334
437
|
bridge.onEventReceived(makeEvent('AFailed', 'corr-1'), 'A', 'failure');
|
|
335
438
|
|
|
336
439
|
const stats = bridge.getSettledStats('corr-1', 'template-A');
|
|
337
|
-
expect(stats).toEqual({ status: 'error', pendingCount: 0, endedCount:
|
|
440
|
+
expect(stats).toEqual({ status: 'error', pendingCount: 0, endedCount: 1 });
|
|
338
441
|
});
|
|
339
442
|
|
|
340
443
|
it('returns idle for unknown templateId', () => {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Command, Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import createDebug from 'debug';
|
|
2
3
|
import type { SettledHandlerDescriptor } from '../core/descriptors.js';
|
|
3
4
|
import type { SettledInput, SettledOutput, SettledState } from '../engine/workflows/settled-workflow.js';
|
|
4
5
|
import { decide, evolve, initialState } from '../engine/workflows/settled-workflow.js';
|
|
5
6
|
|
|
7
|
+
const debug = createDebug('auto:pipeline:settled-bridge');
|
|
8
|
+
|
|
6
9
|
type NodeStatus = 'idle' | 'running' | 'success' | 'error';
|
|
7
10
|
|
|
8
11
|
export interface SettledStats {
|
|
@@ -13,6 +16,7 @@ export interface SettledStats {
|
|
|
13
16
|
|
|
14
17
|
interface V2RuntimeBridgeOptions {
|
|
15
18
|
onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
|
|
19
|
+
onEmit?: (eventType: string, data: unknown, correlationId: string) => void;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
interface RegisteredSettled {
|
|
@@ -106,15 +110,33 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
function handleOutputs(outputs: SettledOutput[], registration: RegisteredSettled, correlationId: string): void {
|
|
113
|
+
debug(
|
|
114
|
+
'handleOutputs: templateId=%s, correlationId=%s, outputs=%o',
|
|
115
|
+
registration.templateId,
|
|
116
|
+
correlationId,
|
|
117
|
+
outputs.map((o) => o.type),
|
|
118
|
+
);
|
|
109
119
|
for (const output of outputs) {
|
|
110
|
-
if (output.type === 'AllSettled') {
|
|
120
|
+
if (output.type === 'AllSettled' || output.type === 'SettledFailed') {
|
|
121
|
+
debug(' %s triggered for %s', output.type, registration.templateId);
|
|
111
122
|
const events = collectBufferedEvents(registration.templateId, correlationId, registration.commandTypes);
|
|
123
|
+
debug(
|
|
124
|
+
' collected events: %o',
|
|
125
|
+
Object.keys(events).map((k) => `${k}:${events[k].length}`),
|
|
126
|
+
);
|
|
112
127
|
|
|
113
128
|
const send = (commandType: string, data: unknown) => {
|
|
129
|
+
debug(' dispatching %s with data keys: %o', commandType, Object.keys(data as object));
|
|
114
130
|
options.onDispatch(commandType, data, correlationId);
|
|
115
131
|
};
|
|
116
132
|
|
|
117
|
-
const
|
|
133
|
+
const emit = (eventType: string, data: unknown, emitCorrelationId?: string) => {
|
|
134
|
+
debug(' emitting %s', eventType);
|
|
135
|
+
options.onEmit?.(eventType, data, emitCorrelationId ?? correlationId);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handlerResult = registration.descriptor.handler(events, send, emit);
|
|
139
|
+
debug(' handler returned: %o', handlerResult);
|
|
118
140
|
const persist =
|
|
119
141
|
handlerResult !== null &&
|
|
120
142
|
handlerResult !== undefined &&
|
|
@@ -132,6 +154,7 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
if (output.type === 'RetryCommands') {
|
|
157
|
+
debug(' RetryCommands: %o', output.data.commandTypes);
|
|
135
158
|
for (const ct of output.data.commandTypes) {
|
|
136
159
|
options.onDispatch(ct, {}, correlationId);
|
|
137
160
|
eventBuffer.delete(bufferKey(registration.templateId, correlationId, ct));
|
|
@@ -151,10 +174,18 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
151
174
|
templateId,
|
|
152
175
|
descriptor,
|
|
153
176
|
commandTypes,
|
|
154
|
-
maxRetries: config?.maxRetries ?? 3,
|
|
177
|
+
maxRetries: descriptor.maxRetries ?? config?.maxRetries ?? 3,
|
|
155
178
|
sourceEventTypes: descriptor.sourceEventTypes,
|
|
156
179
|
};
|
|
157
180
|
|
|
181
|
+
debug(
|
|
182
|
+
'registerSettled: templateId=%s, commandTypes=%o, sourceEventTypes=%o, label=%s',
|
|
183
|
+
templateId,
|
|
184
|
+
commandTypes,
|
|
185
|
+
descriptor.sourceEventTypes,
|
|
186
|
+
descriptor.label,
|
|
187
|
+
);
|
|
188
|
+
|
|
158
189
|
registrations.set(templateId, registration);
|
|
159
190
|
|
|
160
191
|
for (const ct of commandTypes) {
|
|
@@ -167,7 +198,15 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
167
198
|
onCommandStarted(command: Command, sessionCorrelationId?: string, sourceEventType?: string): void {
|
|
168
199
|
const { type: commandType, correlationId, requestId } = command;
|
|
169
200
|
|
|
201
|
+
debug(
|
|
202
|
+
'onCommandStarted: command=%s, sourceEventType=%s, sessionCorrelationId=%s',
|
|
203
|
+
commandType,
|
|
204
|
+
sourceEventType,
|
|
205
|
+
sessionCorrelationId,
|
|
206
|
+
);
|
|
207
|
+
|
|
170
208
|
if (!isValidId(correlationId) || !isValidId(requestId)) {
|
|
209
|
+
debug(' skipping: invalid correlationId or requestId');
|
|
171
210
|
return;
|
|
172
211
|
}
|
|
173
212
|
|
|
@@ -175,20 +214,26 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
175
214
|
|
|
176
215
|
const templateIds = commandToTemplateIds.get(commandType);
|
|
177
216
|
if (!templateIds) {
|
|
217
|
+
debug(' skipping: no templateIds for command type');
|
|
178
218
|
return;
|
|
179
219
|
}
|
|
180
220
|
|
|
221
|
+
debug(' found %d templateIds: %o', templateIds.size, [...templateIds]);
|
|
222
|
+
|
|
181
223
|
for (const templateId of templateIds) {
|
|
182
224
|
const registration = registrations.get(templateId);
|
|
183
225
|
if (registration) {
|
|
226
|
+
debug(' checking registration %s, sourceEventTypes=%o', templateId, registration.sourceEventTypes);
|
|
184
227
|
if (
|
|
185
228
|
sourceEventType &&
|
|
186
229
|
registration.sourceEventTypes &&
|
|
187
230
|
registration.sourceEventTypes.length > 0 &&
|
|
188
231
|
!registration.sourceEventTypes.includes(sourceEventType)
|
|
189
232
|
) {
|
|
233
|
+
debug(' filtered out: sourceEventType %s not in %o', sourceEventType, registration.sourceEventTypes);
|
|
190
234
|
continue;
|
|
191
235
|
}
|
|
236
|
+
debug(' processing StartSettled for %s', templateId);
|
|
192
237
|
const history = getHistory(templateId, keyCorrelationId);
|
|
193
238
|
const input: SettledInput = {
|
|
194
239
|
type: 'StartSettled',
|
|
@@ -208,7 +253,16 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
208
253
|
): void {
|
|
209
254
|
const correlationId = event.correlationId;
|
|
210
255
|
|
|
256
|
+
debug(
|
|
257
|
+
'onEventReceived: event=%s, sourceCommand=%s, result=%s, sourceEventType=%s',
|
|
258
|
+
event.type,
|
|
259
|
+
sourceCommandType,
|
|
260
|
+
result,
|
|
261
|
+
sourceEventType,
|
|
262
|
+
);
|
|
263
|
+
|
|
211
264
|
if (!isValidId(correlationId)) {
|
|
265
|
+
debug(' skipping: invalid correlationId');
|
|
212
266
|
return;
|
|
213
267
|
}
|
|
214
268
|
|
|
@@ -216,23 +270,29 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
216
270
|
|
|
217
271
|
const templateIds = commandToTemplateIds.get(sourceCommandType);
|
|
218
272
|
if (!templateIds) {
|
|
273
|
+
debug(' skipping: no templateIds for sourceCommand');
|
|
219
274
|
return;
|
|
220
275
|
}
|
|
221
276
|
|
|
277
|
+
debug(' found %d templateIds: %o', templateIds.size, [...templateIds]);
|
|
278
|
+
|
|
222
279
|
for (const templateId of templateIds) {
|
|
223
280
|
const registration = registrations.get(templateId)!;
|
|
281
|
+
debug(' checking registration %s, sourceEventTypes=%o', templateId, registration.sourceEventTypes);
|
|
224
282
|
if (
|
|
225
283
|
sourceEventType &&
|
|
226
284
|
registration.sourceEventTypes &&
|
|
227
285
|
registration.sourceEventTypes.length > 0 &&
|
|
228
286
|
!registration.sourceEventTypes.includes(sourceEventType)
|
|
229
287
|
) {
|
|
288
|
+
debug(' filtered out: sourceEventType %s not in %o', sourceEventType, registration.sourceEventTypes);
|
|
230
289
|
continue;
|
|
231
290
|
}
|
|
232
291
|
const existing = ensureBuffer(templateId, keyCorrelationId, sourceCommandType);
|
|
233
292
|
existing.push(event);
|
|
234
293
|
|
|
235
294
|
const history = getHistory(templateId, keyCorrelationId);
|
|
295
|
+
debug(' processing CommandCompleted for %s, history length=%d', templateId, history.length);
|
|
236
296
|
const input: SettledInput = {
|
|
237
297
|
type: 'CommandCompleted',
|
|
238
298
|
data: {
|
|
@@ -243,6 +303,10 @@ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
|
|
|
243
303
|
};
|
|
244
304
|
const outputs = processInput(input, history, registration.maxRetries);
|
|
245
305
|
|
|
306
|
+
debug(
|
|
307
|
+
' outputs: %o',
|
|
308
|
+
outputs.map((o) => o.type),
|
|
309
|
+
);
|
|
246
310
|
handleOutputs(outputs, registration, keyCorrelationId);
|
|
247
311
|
}
|
|
248
312
|
},
|
|
@@ -3,7 +3,7 @@ import { createKanbanFullPipeline } from './kanban-full.pipeline';
|
|
|
3
3
|
|
|
4
4
|
export default pipelineConfig({
|
|
5
5
|
plugins: [
|
|
6
|
-
'@auto-engineer/
|
|
6
|
+
'@auto-engineer/checks',
|
|
7
7
|
'@auto-engineer/server-generator-apollo-emmett',
|
|
8
8
|
'@auto-engineer/narrative',
|
|
9
9
|
'@auto-engineer/generate-react-client',
|