@auto-engineer/pipeline 0.0.1
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 +5 -0
- package/LICENSE +10 -0
- package/claude.md +160 -0
- package/dist/src/builder/define.d.ts +90 -0
- package/dist/src/builder/define.d.ts.map +1 -0
- package/dist/src/builder/define.js +425 -0
- package/dist/src/builder/define.js.map +1 -0
- package/dist/src/builder/define.specs.d.ts +2 -0
- package/dist/src/builder/define.specs.d.ts.map +1 -0
- package/dist/src/builder/define.specs.js +435 -0
- package/dist/src/builder/define.specs.js.map +1 -0
- package/dist/src/config/pipeline-config.d.ts +13 -0
- package/dist/src/config/pipeline-config.d.ts.map +1 -0
- package/dist/src/config/pipeline-config.js +15 -0
- package/dist/src/config/pipeline-config.js.map +1 -0
- package/dist/src/core/descriptors.d.ts +84 -0
- package/dist/src/core/descriptors.d.ts.map +1 -0
- package/dist/src/core/descriptors.js +2 -0
- package/dist/src/core/descriptors.js.map +1 -0
- package/dist/src/core/descriptors.specs.d.ts +2 -0
- package/dist/src/core/descriptors.specs.d.ts.map +1 -0
- package/dist/src/core/descriptors.specs.js +24 -0
- package/dist/src/core/descriptors.specs.js.map +1 -0
- package/dist/src/core/types.d.ts +10 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +4 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/types.specs.d.ts +2 -0
- package/dist/src/core/types.specs.d.ts.map +1 -0
- package/dist/src/core/types.specs.js +40 -0
- package/dist/src/core/types.specs.js.map +1 -0
- package/dist/src/graph/types.d.ts +17 -0
- package/dist/src/graph/types.d.ts.map +1 -0
- package/dist/src/graph/types.js +2 -0
- package/dist/src/graph/types.js.map +1 -0
- package/dist/src/graph/types.specs.d.ts +2 -0
- package/dist/src/graph/types.specs.d.ts.map +1 -0
- package/dist/src/graph/types.specs.js +148 -0
- package/dist/src/graph/types.specs.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logging/event-logger.d.ts +21 -0
- package/dist/src/logging/event-logger.d.ts.map +1 -0
- package/dist/src/logging/event-logger.js +31 -0
- package/dist/src/logging/event-logger.js.map +1 -0
- package/dist/src/logging/event-logger.specs.d.ts +2 -0
- package/dist/src/logging/event-logger.specs.d.ts.map +1 -0
- package/dist/src/logging/event-logger.specs.js +81 -0
- package/dist/src/logging/event-logger.specs.js.map +1 -0
- package/dist/src/plugins/handler-adapter.d.ts +5 -0
- package/dist/src/plugins/handler-adapter.d.ts.map +1 -0
- package/dist/src/plugins/handler-adapter.js +17 -0
- package/dist/src/plugins/handler-adapter.js.map +1 -0
- package/dist/src/plugins/handler-adapter.specs.d.ts +2 -0
- package/dist/src/plugins/handler-adapter.specs.d.ts.map +1 -0
- package/dist/src/plugins/handler-adapter.specs.js +129 -0
- package/dist/src/plugins/handler-adapter.specs.js.map +1 -0
- package/dist/src/plugins/plugin-loader.d.ts +25 -0
- package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
- package/dist/src/plugins/plugin-loader.js +150 -0
- package/dist/src/plugins/plugin-loader.js.map +1 -0
- package/dist/src/plugins/plugin-loader.specs.d.ts +2 -0
- package/dist/src/plugins/plugin-loader.specs.d.ts.map +1 -0
- package/dist/src/plugins/plugin-loader.specs.js +246 -0
- package/dist/src/plugins/plugin-loader.specs.js.map +1 -0
- package/dist/src/runtime/await-tracker.d.ts +10 -0
- package/dist/src/runtime/await-tracker.d.ts.map +1 -0
- package/dist/src/runtime/await-tracker.js +42 -0
- package/dist/src/runtime/await-tracker.js.map +1 -0
- package/dist/src/runtime/await-tracker.specs.d.ts +2 -0
- package/dist/src/runtime/await-tracker.specs.d.ts.map +1 -0
- package/dist/src/runtime/await-tracker.specs.js +46 -0
- package/dist/src/runtime/await-tracker.specs.js.map +1 -0
- package/dist/src/runtime/context.d.ts +12 -0
- package/dist/src/runtime/context.d.ts.map +1 -0
- package/dist/src/runtime/context.js +2 -0
- package/dist/src/runtime/context.js.map +1 -0
- package/dist/src/runtime/context.specs.d.ts +2 -0
- package/dist/src/runtime/context.specs.d.ts.map +1 -0
- package/dist/src/runtime/context.specs.js +26 -0
- package/dist/src/runtime/context.specs.js.map +1 -0
- package/dist/src/runtime/event-command-map.d.ts +15 -0
- package/dist/src/runtime/event-command-map.d.ts.map +1 -0
- package/dist/src/runtime/event-command-map.js +26 -0
- package/dist/src/runtime/event-command-map.js.map +1 -0
- package/dist/src/runtime/event-command-map.specs.d.ts +2 -0
- package/dist/src/runtime/event-command-map.specs.d.ts.map +1 -0
- package/dist/src/runtime/event-command-map.specs.js +108 -0
- package/dist/src/runtime/event-command-map.specs.js.map +1 -0
- package/dist/src/runtime/phased-executor.d.ts +29 -0
- package/dist/src/runtime/phased-executor.d.ts.map +1 -0
- package/dist/src/runtime/phased-executor.js +164 -0
- package/dist/src/runtime/phased-executor.js.map +1 -0
- package/dist/src/runtime/phased-executor.specs.d.ts +2 -0
- package/dist/src/runtime/phased-executor.specs.d.ts.map +1 -0
- package/dist/src/runtime/phased-executor.specs.js +256 -0
- package/dist/src/runtime/phased-executor.specs.js.map +1 -0
- package/dist/src/runtime/pipeline-runtime.d.ts +17 -0
- package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -0
- package/dist/src/runtime/pipeline-runtime.js +87 -0
- package/dist/src/runtime/pipeline-runtime.js.map +1 -0
- package/dist/src/runtime/pipeline-runtime.specs.d.ts +2 -0
- package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +1 -0
- package/dist/src/runtime/pipeline-runtime.specs.js +192 -0
- package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -0
- package/dist/src/runtime/settled-tracker.d.ts +42 -0
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -0
- package/dist/src/runtime/settled-tracker.js +161 -0
- package/dist/src/runtime/settled-tracker.js.map +1 -0
- package/dist/src/runtime/settled-tracker.specs.d.ts +2 -0
- package/dist/src/runtime/settled-tracker.specs.d.ts.map +1 -0
- package/dist/src/runtime/settled-tracker.specs.js +361 -0
- package/dist/src/runtime/settled-tracker.specs.js.map +1 -0
- package/dist/src/server/full-orchestration.e2e.specs.d.ts +2 -0
- package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +1 -0
- package/dist/src/server/full-orchestration.e2e.specs.js +561 -0
- package/dist/src/server/full-orchestration.e2e.specs.js.map +1 -0
- package/dist/src/server/pipeline-server.d.ts +59 -0
- package/dist/src/server/pipeline-server.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.e2e.specs.d.ts +2 -0
- package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.e2e.specs.js +381 -0
- package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -0
- package/dist/src/server/pipeline-server.js +527 -0
- package/dist/src/server/pipeline-server.js.map +1 -0
- package/dist/src/server/pipeline-server.specs.d.ts +2 -0
- package/dist/src/server/pipeline-server.specs.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.specs.js +662 -0
- package/dist/src/server/pipeline-server.specs.js.map +1 -0
- package/dist/src/server/sse-manager.d.ts +12 -0
- package/dist/src/server/sse-manager.d.ts.map +1 -0
- package/dist/src/server/sse-manager.js +63 -0
- package/dist/src/server/sse-manager.js.map +1 -0
- package/dist/src/server/sse-manager.specs.d.ts +2 -0
- package/dist/src/server/sse-manager.specs.d.ts.map +1 -0
- package/dist/src/server/sse-manager.specs.js +158 -0
- package/dist/src/server/sse-manager.specs.js.map +1 -0
- package/dist/src/testing/event-capture.d.ts +14 -0
- package/dist/src/testing/event-capture.d.ts.map +1 -0
- package/dist/src/testing/event-capture.js +55 -0
- package/dist/src/testing/event-capture.js.map +1 -0
- package/dist/src/testing/event-capture.specs.d.ts +2 -0
- package/dist/src/testing/event-capture.specs.d.ts.map +1 -0
- package/dist/src/testing/event-capture.specs.js +114 -0
- package/dist/src/testing/event-capture.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts +7 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +168 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +2 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +263 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-todo.config.d.ts +3 -0
- package/dist/src/testing/fixtures/kanban-todo.config.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-todo.config.js +19 -0
- package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.d.ts +5 -0
- package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.js +76 -0
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +2 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js +29 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +1 -0
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts +2 -0
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/kanban-todo.e2e.specs.js +160 -0
- package/dist/src/testing/kanban-todo.e2e.specs.js.map +1 -0
- package/dist/src/testing/mock-handlers.d.ts +21 -0
- package/dist/src/testing/mock-handlers.d.ts.map +1 -0
- package/dist/src/testing/mock-handlers.js +34 -0
- package/dist/src/testing/mock-handlers.js.map +1 -0
- package/dist/src/testing/mock-handlers.specs.d.ts +2 -0
- package/dist/src/testing/mock-handlers.specs.d.ts.map +1 -0
- package/dist/src/testing/mock-handlers.specs.js +193 -0
- package/dist/src/testing/mock-handlers.specs.js.map +1 -0
- package/dist/src/testing/real-execution.e2e.specs.d.ts +2 -0
- package/dist/src/testing/real-execution.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/real-execution.e2e.specs.js +140 -0
- package/dist/src/testing/real-execution.e2e.specs.js.map +1 -0
- package/dist/src/testing/real-plugin.e2e.specs.d.ts +2 -0
- package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/real-plugin.e2e.specs.js +65 -0
- package/dist/src/testing/real-plugin.e2e.specs.js.map +1 -0
- package/dist/src/testing/server-startup.e2e.specs.d.ts +2 -0
- package/dist/src/testing/server-startup.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/server-startup.e2e.specs.js +104 -0
- package/dist/src/testing/server-startup.e2e.specs.js.map +1 -0
- package/dist/src/testing/snapshot-compare.d.ts +18 -0
- package/dist/src/testing/snapshot-compare.d.ts.map +1 -0
- package/dist/src/testing/snapshot-compare.js +86 -0
- package/dist/src/testing/snapshot-compare.js.map +1 -0
- package/dist/src/testing/snapshot-compare.specs.d.ts +2 -0
- package/dist/src/testing/snapshot-compare.specs.d.ts.map +1 -0
- package/dist/src/testing/snapshot-compare.specs.js +112 -0
- package/dist/src/testing/snapshot-compare.specs.js.map +1 -0
- package/dist/src/testing/snapshot-sanitize.d.ts +8 -0
- package/dist/src/testing/snapshot-sanitize.d.ts.map +1 -0
- package/dist/src/testing/snapshot-sanitize.js +10 -0
- package/dist/src/testing/snapshot-sanitize.js.map +1 -0
- package/dist/src/testing/snapshot-sanitize.specs.d.ts +2 -0
- package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +1 -0
- package/dist/src/testing/snapshot-sanitize.specs.js +104 -0
- package/dist/src/testing/snapshot-sanitize.specs.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/docs/testing-analysis.md +395 -0
- package/package.json +31 -0
- package/pipeline-api-new.md +1078 -0
- package/pomodoro-plan.md +651 -0
- package/scripts/run-kanban-e2e.ts +219 -0
- package/scripts/start-server.ts +64 -0
- package/snapshots/e2e-run-2025-12-22T15-52-03.json +613 -0
- package/snapshots/e2e-run-2025-12-22T16-51-30.json +699 -0
- package/src/builder/define.specs.ts +531 -0
- package/src/builder/define.ts +700 -0
- package/src/config/pipeline-config.ts +32 -0
- package/src/core/descriptors.specs.ts +28 -0
- package/src/core/descriptors.ts +99 -0
- package/src/core/types.specs.ts +44 -0
- package/src/core/types.ts +16 -0
- package/src/graph/types.specs.ts +176 -0
- package/src/graph/types.ts +19 -0
- package/src/index.ts +54 -0
- package/src/logging/event-logger.specs.ts +100 -0
- package/src/logging/event-logger.ts +50 -0
- package/src/plugins/handler-adapter.specs.ts +164 -0
- package/src/plugins/handler-adapter.ts +21 -0
- package/src/plugins/plugin-loader.specs.ts +295 -0
- package/src/plugins/plugin-loader.ts +202 -0
- package/src/runtime/await-tracker.specs.ts +52 -0
- package/src/runtime/await-tracker.ts +50 -0
- package/src/runtime/context.specs.ts +28 -0
- package/src/runtime/context.ts +13 -0
- package/src/runtime/event-command-map.specs.ts +136 -0
- package/src/runtime/event-command-map.ts +38 -0
- package/src/runtime/phased-executor.specs.ts +358 -0
- package/src/runtime/phased-executor.ts +224 -0
- package/src/runtime/pipeline-runtime.specs.ts +214 -0
- package/src/runtime/pipeline-runtime.ts +119 -0
- package/src/runtime/settled-tracker.specs.ts +448 -0
- package/src/runtime/settled-tracker.ts +237 -0
- package/src/server/full-orchestration.e2e.specs.ts +672 -0
- package/src/server/pipeline-server.e2e.specs.ts +505 -0
- package/src/server/pipeline-server.specs.ts +761 -0
- package/src/server/pipeline-server.ts +656 -0
- package/src/server/sse-manager.specs.ts +208 -0
- package/src/server/sse-manager.ts +79 -0
- package/src/testing/event-capture.specs.ts +143 -0
- package/src/testing/event-capture.ts +65 -0
- package/src/testing/fixtures/kanban-full.pipeline.specs.ts +337 -0
- package/src/testing/fixtures/kanban-full.pipeline.ts +225 -0
- package/src/testing/fixtures/kanban-todo.config.ts +19 -0
- package/src/testing/fixtures/kanban.pipeline.specs.ts +33 -0
- package/src/testing/fixtures/kanban.pipeline.ts +124 -0
- package/src/testing/kanban-todo.e2e.specs.ts +209 -0
- package/src/testing/mock-handlers.specs.ts +229 -0
- package/src/testing/mock-handlers.ts +58 -0
- package/src/testing/real-execution.e2e.specs.ts +193 -0
- package/src/testing/real-plugin.e2e.specs.ts +94 -0
- package/src/testing/server-startup.e2e.specs.ts +162 -0
- package/src/testing/snapshot-compare.specs.ts +136 -0
- package/src/testing/snapshot-compare.ts +106 -0
- package/src/testing/snapshot-sanitize.specs.ts +131 -0
- package/src/testing/snapshot-sanitize.ts +17 -0
- package/tsconfig.json +11 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import { define } from '../../builder/define';
|
|
3
|
+
|
|
4
|
+
interface Component {
|
|
5
|
+
id: string;
|
|
6
|
+
type: 'molecule' | 'organism' | 'page';
|
|
7
|
+
filePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SchemaExportedData {
|
|
11
|
+
outputPath: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SliceGeneratedData {
|
|
15
|
+
slicePath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SliceImplementedData {
|
|
19
|
+
slicePath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ClientGeneratedData {
|
|
23
|
+
components: Component[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CheckEventData {
|
|
27
|
+
target: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MAX_RETRIES = 3;
|
|
31
|
+
const retryState = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
function hasAnyFailures(events: Event[]): boolean {
|
|
34
|
+
return events.some((e) => e.type.includes('Failed'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectErrors(events: Event[]): string[] {
|
|
38
|
+
return events.filter((e) => e.type.includes('Failed')).map((e) => JSON.stringify(e.data));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractSlicePath(events: Record<string, Event[]>): string {
|
|
42
|
+
const firstEvent = events.CheckTests?.[0] ?? events.CheckTypes?.[0] ?? events.CheckLint?.[0];
|
|
43
|
+
const data = firstEvent?.data as CheckEventData | undefined;
|
|
44
|
+
return data?.target ?? 'unknown';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function gatherAllCheckEvents(events: Record<string, Event[]>): Event[] {
|
|
48
|
+
return [...(events.CheckTests ?? []), ...(events.CheckTypes ?? []), ...(events.CheckLint ?? [])];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldRetry(slicePath: string): boolean {
|
|
52
|
+
const attempts = retryState.get(slicePath) ?? 0;
|
|
53
|
+
return attempts < MAX_RETRIES;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function incrementRetryCount(slicePath: string): number {
|
|
57
|
+
const attempts = retryState.get(slicePath) ?? 0;
|
|
58
|
+
retryState.set(slicePath, attempts + 1);
|
|
59
|
+
return attempts + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createKanbanPipeline() {
|
|
63
|
+
return define('kanban')
|
|
64
|
+
.on('SchemaExported')
|
|
65
|
+
.emit('GenerateServer', (e: { data: SchemaExportedData }) => ({ modelPath: e.data.outputPath }))
|
|
66
|
+
|
|
67
|
+
.on('SliceGenerated')
|
|
68
|
+
.emit('ImplementSlice', (e: { data: SliceGeneratedData }) => ({ slicePath: e.data.slicePath }))
|
|
69
|
+
|
|
70
|
+
.on('SliceImplemented')
|
|
71
|
+
.emit('CheckTests', (e: { data: SliceImplementedData }) => ({ target: e.data.slicePath }))
|
|
72
|
+
.emit('CheckTypes', (e: { data: SliceImplementedData }) => ({ target: e.data.slicePath }))
|
|
73
|
+
.emit('CheckLint', (e: { data: SliceImplementedData }) => ({ target: e.data.slicePath }))
|
|
74
|
+
|
|
75
|
+
.settled(['CheckTests', 'CheckTypes', 'CheckLint'])
|
|
76
|
+
.dispatch({ dispatches: ['ImplementSlice'] }, (events, send) => {
|
|
77
|
+
const allEvents = gatherAllCheckEvents(events);
|
|
78
|
+
|
|
79
|
+
if (!hasAnyFailures(allEvents)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const slicePath = extractSlicePath(events);
|
|
84
|
+
|
|
85
|
+
if (!shouldRetry(slicePath)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const retryAttempt = incrementRetryCount(slicePath);
|
|
90
|
+
send('ImplementSlice', { slicePath, context: { errors: collectErrors(allEvents), retryAttempt } });
|
|
91
|
+
return { persist: true };
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
.on('ServerGenerated')
|
|
95
|
+
.emit('GenerateIA', (e: { data: { modelPath: string } }) => ({ modelPath: e.data.modelPath }))
|
|
96
|
+
.emit('StartServer', {})
|
|
97
|
+
|
|
98
|
+
.on('IAGenerated')
|
|
99
|
+
.emit('GenerateClient', {})
|
|
100
|
+
|
|
101
|
+
.on('ClientGenerated')
|
|
102
|
+
.forEach((e: { data: ClientGeneratedData }) => e.data.components)
|
|
103
|
+
.groupInto(['molecule', 'organism', 'page'], (c) => c.type)
|
|
104
|
+
.process('ImplementComponent', (c) => ({ filePath: c.filePath }))
|
|
105
|
+
.onComplete({
|
|
106
|
+
success: 'AllComponentsImplemented',
|
|
107
|
+
failure: 'ComponentsFailed',
|
|
108
|
+
itemKey: (e) => (e.data as { filePath?: string }).filePath ?? '',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
.build();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resetRetryState(): void {
|
|
115
|
+
retryState.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function setRetryCount(slicePath: string, count: number): void {
|
|
119
|
+
retryState.set(slicePath, count);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function testShouldRetry(slicePath: string): boolean {
|
|
123
|
+
return shouldRetry(slicePath);
|
|
124
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { loadPipelineConfig } from '../config/pipeline-config';
|
|
6
|
+
import { PipelineServer } from '../server/pipeline-server';
|
|
7
|
+
import { EventCapture } from './event-capture';
|
|
8
|
+
import { resetKanbanState } from './fixtures/kanban-full.pipeline';
|
|
9
|
+
import kanbanTodoConfig from './fixtures/kanban-todo.config';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PACKAGES_DIR = path.resolve(__dirname, '../../..');
|
|
13
|
+
const CLI_SNAPSHOTS_DIR = path.join(PACKAGES_DIR, 'cli/src/__tests__/e2e/__snapshots__/kanban-todo');
|
|
14
|
+
|
|
15
|
+
interface PipelineGraph {
|
|
16
|
+
nodes: Array<{ id: string; name: string; title?: string; alias?: string; status: string }>;
|
|
17
|
+
edges: Array<{ from: string; to: string }>;
|
|
18
|
+
commandToEvents: Record<string, string[]>;
|
|
19
|
+
eventToCommand: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Registry {
|
|
23
|
+
eventHandlers: string[];
|
|
24
|
+
commandHandlers: string[];
|
|
25
|
+
commandsWithMetadata: Array<{ name: string; alias?: string; description?: string }>;
|
|
26
|
+
folds: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadCliSnapshot<T>(filename: string): T | null {
|
|
30
|
+
const snapshotPath = path.join(CLI_SNAPSHOTS_DIR, filename);
|
|
31
|
+
if (!existsSync(snapshotPath)) {
|
|
32
|
+
console.log(`Snapshot not found: ${snapshotPath}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const content = readFileSync(snapshotPath, 'utf-8');
|
|
36
|
+
return JSON.parse(content) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('Kanban-Todo Pipeline E2E Comparison', () => {
|
|
40
|
+
let server: PipelineServer;
|
|
41
|
+
let eventCapture: EventCapture;
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
const loaded = await loadPipelineConfig(kanbanTodoConfig);
|
|
45
|
+
|
|
46
|
+
server = new PipelineServer({ port: 0 });
|
|
47
|
+
server.registerCommandHandlers(loaded.handlers);
|
|
48
|
+
server.registerPipeline(loaded.pipeline);
|
|
49
|
+
await server.start();
|
|
50
|
+
|
|
51
|
+
eventCapture = new EventCapture();
|
|
52
|
+
}, 30000);
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await server.stop();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
resetKanbanState();
|
|
60
|
+
eventCapture.clear();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Registry Comparison', () => {
|
|
64
|
+
it('should have matching command handlers', async () => {
|
|
65
|
+
const response = await fetch(`http://localhost:${server.port}/registry`);
|
|
66
|
+
const registry = (await response.json()) as Registry;
|
|
67
|
+
const cliRegistry = loadCliSnapshot<Registry>('registry.snapshot.json');
|
|
68
|
+
|
|
69
|
+
console.log('=== Pipeline Registry ===');
|
|
70
|
+
console.log('Command handlers:', registry.commandHandlers.sort().join(', '));
|
|
71
|
+
|
|
72
|
+
if (cliRegistry !== null) {
|
|
73
|
+
console.log('\n=== CLI Registry ===');
|
|
74
|
+
console.log('Command handlers:', cliRegistry.commandHandlers.sort().join(', '));
|
|
75
|
+
|
|
76
|
+
const pipelineCommands = new Set(registry.commandHandlers);
|
|
77
|
+
const cliCommands = new Set(cliRegistry.commandHandlers);
|
|
78
|
+
|
|
79
|
+
const missingInPipeline = cliRegistry.commandHandlers.filter((c) => !pipelineCommands.has(c));
|
|
80
|
+
const extraInPipeline = registry.commandHandlers.filter((c) => !cliCommands.has(c));
|
|
81
|
+
|
|
82
|
+
console.log('\n=== Differences ===');
|
|
83
|
+
console.log('Missing in pipeline:', missingInPipeline.join(', ') || 'none');
|
|
84
|
+
console.log('Extra in pipeline:', extraInPipeline.join(', ') || 'none');
|
|
85
|
+
|
|
86
|
+
expect(missingInPipeline.length).toBeLessThanOrEqual(3);
|
|
87
|
+
} else {
|
|
88
|
+
expect(registry.commandHandlers.length).toBeGreaterThan(10);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should have matching event handlers', async () => {
|
|
93
|
+
const response = await fetch(`http://localhost:${server.port}/registry`);
|
|
94
|
+
const registry = (await response.json()) as Registry;
|
|
95
|
+
const cliRegistry = loadCliSnapshot<Registry>('registry.snapshot.json');
|
|
96
|
+
|
|
97
|
+
console.log('=== Pipeline Event Handlers ===');
|
|
98
|
+
console.log(registry.eventHandlers.sort().join(', '));
|
|
99
|
+
|
|
100
|
+
if (cliRegistry !== null) {
|
|
101
|
+
console.log('\n=== CLI Event Handlers ===');
|
|
102
|
+
console.log(cliRegistry.eventHandlers.sort().join(', '));
|
|
103
|
+
|
|
104
|
+
const pipelineEvents = new Set(registry.eventHandlers);
|
|
105
|
+
const cliEvents = new Set(cliRegistry.eventHandlers);
|
|
106
|
+
|
|
107
|
+
const missingInPipeline = cliRegistry.eventHandlers.filter((e) => !pipelineEvents.has(e));
|
|
108
|
+
const extraInPipeline = registry.eventHandlers.filter((e) => !cliEvents.has(e));
|
|
109
|
+
|
|
110
|
+
console.log('\n=== Differences ===');
|
|
111
|
+
console.log('Missing in pipeline:', missingInPipeline.join(', ') || 'none');
|
|
112
|
+
console.log('Extra in pipeline:', extraInPipeline.join(', ') || 'none');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
expect(registry.eventHandlers.length).toBeGreaterThan(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Pipeline Graph Comparison', () => {
|
|
120
|
+
it('should have similar edge structure to CLI', async () => {
|
|
121
|
+
const response = await fetch(`http://localhost:${server.port}/pipeline`);
|
|
122
|
+
const graph = (await response.json()) as PipelineGraph;
|
|
123
|
+
const cliGraph = loadCliSnapshot<PipelineGraph>('pipeline-graph.snapshot.json');
|
|
124
|
+
|
|
125
|
+
console.log('=== Pipeline Graph ===');
|
|
126
|
+
console.log(`Nodes: ${graph.nodes.length}`);
|
|
127
|
+
console.log(`Edges: ${graph.edges.length}`);
|
|
128
|
+
|
|
129
|
+
if (cliGraph !== null) {
|
|
130
|
+
console.log('\n=== CLI Graph ===');
|
|
131
|
+
console.log(`Nodes: ${cliGraph.nodes.length}`);
|
|
132
|
+
console.log(`Edges: ${cliGraph.edges.length}`);
|
|
133
|
+
|
|
134
|
+
console.log('\n=== CLI Edges (by command name) ===');
|
|
135
|
+
cliGraph.edges.forEach((e) => {
|
|
136
|
+
const fromNode = cliGraph.nodes.find((n) => n.id === e.from);
|
|
137
|
+
const toNode = cliGraph.nodes.find((n) => n.id === e.to);
|
|
138
|
+
console.log(` ${fromNode?.name ?? e.from} → ${toNode?.name ?? e.to}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('\n=== Pipeline Edges ===');
|
|
143
|
+
graph.edges.forEach((e) => console.log(` ${e.from} → ${e.to}`));
|
|
144
|
+
|
|
145
|
+
expect(graph.edges.length).toBeGreaterThan(5);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should have commandToEvents mapping', async () => {
|
|
149
|
+
const response = await fetch(`http://localhost:${server.port}/pipeline`);
|
|
150
|
+
const graph = (await response.json()) as PipelineGraph;
|
|
151
|
+
const cliGraph = loadCliSnapshot<PipelineGraph>('pipeline-graph.snapshot.json');
|
|
152
|
+
|
|
153
|
+
console.log('=== Pipeline commandToEvents ===');
|
|
154
|
+
Object.entries(graph.commandToEvents).forEach(([cmd, events]) => {
|
|
155
|
+
console.log(` ${cmd}: [${events.join(', ')}]`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (cliGraph !== null) {
|
|
159
|
+
console.log('\n=== CLI commandToEvents ===');
|
|
160
|
+
Object.entries(cliGraph.commandToEvents).forEach(([cmd, events]) => {
|
|
161
|
+
console.log(` ${cmd}: [${events.join(', ')}]`);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
expect(Object.keys(graph.commandToEvents).length).toBeGreaterThan(0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Workflow Sequence Validation', () => {
|
|
170
|
+
it('should have correct causal dependencies in pipeline definition', () => {
|
|
171
|
+
const expectedDependencies: [string, string][] = [
|
|
172
|
+
['SchemaExported', 'GenerateServer'],
|
|
173
|
+
['SliceGenerated', 'ImplementSlice'],
|
|
174
|
+
['SliceImplemented', 'CheckTests'],
|
|
175
|
+
['SliceImplemented', 'CheckTypes'],
|
|
176
|
+
['SliceImplemented', 'CheckLint'],
|
|
177
|
+
['ServerGenerated', 'GenerateIA'],
|
|
178
|
+
['ServerGenerated', 'StartServer'],
|
|
179
|
+
['IAGenerated', 'GenerateClient'],
|
|
180
|
+
['ClientGenerated', 'ImplementComponent'],
|
|
181
|
+
['ClientGenerated', 'StartClient'],
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const pipelineHandlers = kanbanTodoConfig.pipeline.descriptor.handlers;
|
|
185
|
+
|
|
186
|
+
console.log('=== Pipeline Handler Event Types ===');
|
|
187
|
+
pipelineHandlers.forEach((h) => {
|
|
188
|
+
if (h.type === 'emit') {
|
|
189
|
+
const commands = h.commands.map((c) => c.commandType).join(', ');
|
|
190
|
+
console.log(` ${h.eventType} → [${commands}]`);
|
|
191
|
+
} else if (h.type === 'settled') {
|
|
192
|
+
console.log(` settled(${h.commandTypes.join(', ')}) → dispatch handler`);
|
|
193
|
+
} else if (h.type === 'foreach-phased') {
|
|
194
|
+
console.log(
|
|
195
|
+
` ${h.eventType} → forEach/phased → ${h.emitFactory({}, '', { type: '', data: {} }).commandType}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log('\n=== Expected Dependencies ===');
|
|
201
|
+
expectedDependencies.forEach(([from, to]) => {
|
|
202
|
+
console.log(` ${from} → ${to}`);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const emitHandlers = pipelineHandlers.filter((h) => h.type === 'emit');
|
|
206
|
+
expect(emitHandlers.length).toBeGreaterThan(5);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createMockHandlers, createStatefulHandler, getHandlerCallCount, resetCallCounts } from './mock-handlers';
|
|
3
|
+
|
|
4
|
+
describe('mock-handlers', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
resetCallCounts();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('createMockHandlers', () => {
|
|
10
|
+
it('should create handlers with correct names', () => {
|
|
11
|
+
const handlers = createMockHandlers([
|
|
12
|
+
{
|
|
13
|
+
name: 'CheckTests',
|
|
14
|
+
events: ['TestsCheckPassed'],
|
|
15
|
+
fn: () => ({ type: 'TestsCheckPassed', data: {} }),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'CheckTypes',
|
|
19
|
+
events: ['TypeCheckPassed'],
|
|
20
|
+
fn: () => ({ type: 'TypeCheckPassed', data: {} }),
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
expect(handlers).toHaveLength(2);
|
|
25
|
+
expect(handlers[0].name).toBe('CheckTests');
|
|
26
|
+
expect(handlers[1].name).toBe('CheckTypes');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should create handlers with correct events', () => {
|
|
30
|
+
const handlers = createMockHandlers([
|
|
31
|
+
{
|
|
32
|
+
name: 'CheckTests',
|
|
33
|
+
events: ['TestsCheckPassed', 'TestsCheckFailed'],
|
|
34
|
+
fn: () => ({ type: 'TestsCheckPassed', data: {} }),
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
expect(handlers[0].events).toEqual(['TestsCheckPassed', 'TestsCheckFailed']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should call handler function with command', async () => {
|
|
42
|
+
let receivedCommand: unknown = null;
|
|
43
|
+
const handlers = createMockHandlers([
|
|
44
|
+
{
|
|
45
|
+
name: 'TestHandler',
|
|
46
|
+
events: ['Done'],
|
|
47
|
+
fn: (cmd) => {
|
|
48
|
+
receivedCommand = cmd;
|
|
49
|
+
return { type: 'Done', data: {} };
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const command = { type: 'TestHandler', data: { foo: 'bar' } };
|
|
55
|
+
await handlers[0].handle(command);
|
|
56
|
+
|
|
57
|
+
expect(receivedCommand).toEqual(command);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return event from handler function', async () => {
|
|
61
|
+
const handlers = createMockHandlers([
|
|
62
|
+
{
|
|
63
|
+
name: 'TestHandler',
|
|
64
|
+
events: ['Result'],
|
|
65
|
+
fn: () => ({ type: 'Result', data: { value: 42 } }),
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const result = await handlers[0].handle({ type: 'TestHandler', data: {} });
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({ type: 'Result', data: { value: 42 } });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return array of events from handler function', async () => {
|
|
75
|
+
const handlers = createMockHandlers([
|
|
76
|
+
{
|
|
77
|
+
name: 'MultiEventHandler',
|
|
78
|
+
events: ['First', 'Second'],
|
|
79
|
+
fn: () => [
|
|
80
|
+
{ type: 'First', data: {} },
|
|
81
|
+
{ type: 'Second', data: {} },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const result = await handlers[0].handle({ type: 'MultiEventHandler', data: {} });
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual([
|
|
89
|
+
{ type: 'First', data: {} },
|
|
90
|
+
{ type: 'Second', data: {} },
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should pass attempt number to handler function', async () => {
|
|
95
|
+
const receivedAttempts: number[] = [];
|
|
96
|
+
const handlers = createMockHandlers([
|
|
97
|
+
{
|
|
98
|
+
name: 'TrackingHandler',
|
|
99
|
+
events: ['Done'],
|
|
100
|
+
fn: (_, attempt) => {
|
|
101
|
+
receivedAttempts.push(attempt);
|
|
102
|
+
return { type: 'Done', data: {} };
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
await handlers[0].handle({ type: 'TrackingHandler', data: {} });
|
|
108
|
+
await handlers[0].handle({ type: 'TrackingHandler', data: {} });
|
|
109
|
+
await handlers[0].handle({ type: 'TrackingHandler', data: {} });
|
|
110
|
+
|
|
111
|
+
expect(receivedAttempts).toEqual([1, 2, 3]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should reset call counts when creating new handlers', () => {
|
|
115
|
+
createMockHandlers([
|
|
116
|
+
{
|
|
117
|
+
name: 'First',
|
|
118
|
+
events: ['Done'],
|
|
119
|
+
fn: () => ({ type: 'Done', data: {} }),
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
expect(getHandlerCallCount('First')).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('getHandlerCallCount', () => {
|
|
128
|
+
it('should return 0 for uncalled handler', () => {
|
|
129
|
+
createMockHandlers([
|
|
130
|
+
{
|
|
131
|
+
name: 'Uncalled',
|
|
132
|
+
events: ['Done'],
|
|
133
|
+
fn: () => ({ type: 'Done', data: {} }),
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(getHandlerCallCount('Uncalled')).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should track call count for handler', async () => {
|
|
141
|
+
const handlers = createMockHandlers([
|
|
142
|
+
{
|
|
143
|
+
name: 'Called',
|
|
144
|
+
events: ['Done'],
|
|
145
|
+
fn: () => ({ type: 'Done', data: {} }),
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await handlers[0].handle({ type: 'Called', data: {} });
|
|
150
|
+
await handlers[0].handle({ type: 'Called', data: {} });
|
|
151
|
+
|
|
152
|
+
expect(getHandlerCallCount('Called')).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('resetCallCounts', () => {
|
|
157
|
+
it('should clear all call counts', async () => {
|
|
158
|
+
const handlers = createMockHandlers([
|
|
159
|
+
{
|
|
160
|
+
name: 'Handler1',
|
|
161
|
+
events: ['Done'],
|
|
162
|
+
fn: () => ({ type: 'Done', data: {} }),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Handler2',
|
|
166
|
+
events: ['Done'],
|
|
167
|
+
fn: () => ({ type: 'Done', data: {} }),
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await handlers[0].handle({ type: 'Handler1', data: {} });
|
|
172
|
+
await handlers[1].handle({ type: 'Handler2', data: {} });
|
|
173
|
+
|
|
174
|
+
resetCallCounts();
|
|
175
|
+
|
|
176
|
+
expect(getHandlerCallCount('Handler1')).toBe(0);
|
|
177
|
+
expect(getHandlerCallCount('Handler2')).toBe(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('createStatefulHandler', () => {
|
|
182
|
+
it('should fail for initial attempts', async () => {
|
|
183
|
+
const handler = createStatefulHandler({
|
|
184
|
+
name: 'CheckTests',
|
|
185
|
+
events: ['TestsCheckPassed', 'TestsCheckFailed'],
|
|
186
|
+
initialFails: 2,
|
|
187
|
+
failEvent: (cmd) => ({ type: 'TestsCheckFailed', data: { target: cmd.data } }),
|
|
188
|
+
successEvent: (cmd) => ({ type: 'TestsCheckPassed', data: { target: cmd.data } }),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const command = { type: 'CheckTests', data: { dir: './src' } };
|
|
192
|
+
|
|
193
|
+
const result1 = await handler.handle(command);
|
|
194
|
+
expect(Array.isArray(result1) ? result1[0].type : result1.type).toBe('TestsCheckFailed');
|
|
195
|
+
|
|
196
|
+
const result2 = await handler.handle(command);
|
|
197
|
+
expect(Array.isArray(result2) ? result2[0].type : result2.type).toBe('TestsCheckFailed');
|
|
198
|
+
|
|
199
|
+
const result3 = await handler.handle(command);
|
|
200
|
+
expect(Array.isArray(result3) ? result3[0].type : result3.type).toBe('TestsCheckPassed');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should preserve handler metadata', () => {
|
|
204
|
+
const handler = createStatefulHandler({
|
|
205
|
+
name: 'CheckTypes',
|
|
206
|
+
events: ['TypeCheckPassed', 'TypeCheckFailed'],
|
|
207
|
+
initialFails: 1,
|
|
208
|
+
failEvent: () => ({ type: 'TypeCheckFailed', data: {} }),
|
|
209
|
+
successEvent: () => ({ type: 'TypeCheckPassed', data: {} }),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(handler.name).toBe('CheckTypes');
|
|
213
|
+
expect(handler.events).toEqual(['TypeCheckPassed', 'TypeCheckFailed']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should succeed immediately when initialFails is 0', async () => {
|
|
217
|
+
const handler = createStatefulHandler({
|
|
218
|
+
name: 'AlwaysPass',
|
|
219
|
+
events: ['Passed'],
|
|
220
|
+
initialFails: 0,
|
|
221
|
+
failEvent: () => ({ type: 'Failed', data: {} }),
|
|
222
|
+
successEvent: () => ({ type: 'Passed', data: {} }),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await handler.handle({ type: 'AlwaysPass', data: {} });
|
|
226
|
+
expect(Array.isArray(result) ? result[0].type : result.type).toBe('Passed');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Command, Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import type { CommandHandlerWithMetadata } from '../server/pipeline-server';
|
|
3
|
+
|
|
4
|
+
type MockHandlerFn = (cmd: Command, attempt: number) => Event | Event[];
|
|
5
|
+
|
|
6
|
+
interface MockHandlerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
events: string[];
|
|
9
|
+
fn: MockHandlerFn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const callCounts = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
export function createMockHandlers(configs: MockHandlerConfig[]): CommandHandlerWithMetadata[] {
|
|
15
|
+
callCounts.clear();
|
|
16
|
+
|
|
17
|
+
return configs.map((config) => ({
|
|
18
|
+
name: config.name,
|
|
19
|
+
events: config.events,
|
|
20
|
+
handle: async (cmd: Command): Promise<Event | Event[]> => {
|
|
21
|
+
const currentCount = (callCounts.get(config.name) ?? 0) + 1;
|
|
22
|
+
callCounts.set(config.name, currentCount);
|
|
23
|
+
return config.fn(cmd, currentCount);
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getHandlerCallCount(handlerName: string): number {
|
|
29
|
+
return callCounts.get(handlerName) ?? 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resetCallCounts(): void {
|
|
33
|
+
callCounts.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface StatefulHandlerConfig {
|
|
37
|
+
name: string;
|
|
38
|
+
events: string[];
|
|
39
|
+
initialFails: number;
|
|
40
|
+
failEvent: (cmd: Command) => Event;
|
|
41
|
+
successEvent: (cmd: Command) => Event;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createStatefulHandler(config: StatefulHandlerConfig): CommandHandlerWithMetadata {
|
|
45
|
+
let callCount = 0;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name: config.name,
|
|
49
|
+
events: config.events,
|
|
50
|
+
handle: async (cmd: Command): Promise<Event> => {
|
|
51
|
+
callCount++;
|
|
52
|
+
if (callCount <= config.initialFails) {
|
|
53
|
+
return config.failEvent(cmd);
|
|
54
|
+
}
|
|
55
|
+
return config.successEvent(cmd);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|