@auto-engineer/pipeline 0.14.0 → 0.16.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 +5 -6
- package/CHANGELOG.md +24 -0
- package/README.md +279 -0
- package/dist/src/builder/define.d.ts +6 -2
- package/dist/src/builder/define.d.ts.map +1 -1
- package/dist/src/builder/define.js +17 -7
- package/dist/src/builder/define.js.map +1 -1
- package/dist/src/core/descriptors.d.ts +6 -2
- package/dist/src/core/descriptors.d.ts.map +1 -1
- package/dist/src/graph/filter-graph.d.ts +3 -0
- package/dist/src/graph/filter-graph.d.ts.map +1 -0
- package/dist/src/graph/filter-graph.js +80 -0
- package/dist/src/graph/filter-graph.js.map +1 -0
- package/dist/src/graph/types.d.ts +8 -0
- package/dist/src/graph/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/projections/await-tracker-projection.d.ts +31 -0
- package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
- package/dist/src/projections/await-tracker-projection.js +35 -0
- package/dist/src/projections/await-tracker-projection.js.map +1 -0
- package/dist/src/projections/index.d.ts +4 -0
- package/dist/src/projections/index.d.ts.map +1 -0
- package/dist/src/projections/index.js +4 -0
- package/dist/src/projections/index.js.map +1 -0
- package/dist/src/projections/item-status-projection.d.ts +22 -0
- package/dist/src/projections/item-status-projection.d.ts.map +1 -0
- package/dist/src/projections/item-status-projection.js +11 -0
- package/dist/src/projections/item-status-projection.js.map +1 -0
- package/dist/src/projections/latest-run-projection.d.ts +15 -0
- package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
- package/dist/src/projections/latest-run-projection.js +7 -0
- package/dist/src/projections/latest-run-projection.js.map +1 -0
- package/dist/src/projections/message-log-projection.d.ts +51 -0
- package/dist/src/projections/message-log-projection.d.ts.map +1 -0
- package/dist/src/projections/message-log-projection.js +51 -0
- package/dist/src/projections/message-log-projection.js.map +1 -0
- package/dist/src/projections/node-status-projection.d.ts +23 -0
- package/dist/src/projections/node-status-projection.d.ts.map +1 -0
- package/dist/src/projections/node-status-projection.js +10 -0
- package/dist/src/projections/node-status-projection.js.map +1 -0
- package/dist/src/projections/phased-execution-projection.d.ts +77 -0
- package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
- package/dist/src/projections/phased-execution-projection.js +54 -0
- package/dist/src/projections/phased-execution-projection.js.map +1 -0
- package/dist/src/projections/settled-instance-projection.d.ts +67 -0
- package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
- package/dist/src/projections/settled-instance-projection.js +66 -0
- package/dist/src/projections/settled-instance-projection.js.map +1 -0
- package/dist/src/projections/stats-projection.d.ts +9 -0
- package/dist/src/projections/stats-projection.d.ts.map +1 -0
- package/dist/src/projections/stats-projection.js +16 -0
- package/dist/src/projections/stats-projection.js.map +1 -0
- package/dist/src/runtime/await-tracker.d.ts +17 -7
- package/dist/src/runtime/await-tracker.d.ts.map +1 -1
- package/dist/src/runtime/await-tracker.js +32 -29
- package/dist/src/runtime/await-tracker.js.map +1 -1
- package/dist/src/runtime/context.d.ts +1 -1
- package/dist/src/runtime/context.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.d.ts +3 -3
- package/dist/src/runtime/event-command-map.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.js +6 -2
- package/dist/src/runtime/event-command-map.js.map +1 -1
- package/dist/src/runtime/phased-executor.d.ts +14 -9
- package/dist/src/runtime/phased-executor.d.ts.map +1 -1
- package/dist/src/runtime/phased-executor.js +113 -105
- package/dist/src/runtime/phased-executor.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
- package/dist/src/runtime/pipeline-runtime.js +2 -2
- package/dist/src/runtime/pipeline-runtime.js.map +1 -1
- package/dist/src/runtime/settled-tracker.d.ts +12 -10
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
- package/dist/src/runtime/settled-tracker.js +89 -80
- package/dist/src/runtime/settled-tracker.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts +31 -9
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +424 -123
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/sse-manager.d.ts +0 -1
- package/dist/src/server/sse-manager.d.ts.map +1 -1
- package/dist/src/server/sse-manager.js +0 -3
- package/dist/src/server/sse-manager.js.map +1 -1
- package/dist/src/store/index.d.ts +3 -0
- package/dist/src/store/index.d.ts.map +1 -0
- package/dist/src/store/index.js +3 -0
- package/dist/src/store/index.js.map +1 -0
- package/dist/src/store/pipeline-event-store.d.ts +10 -0
- package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
- package/dist/src/store/pipeline-event-store.js +112 -0
- package/dist/src/store/pipeline-event-store.js.map +1 -0
- package/dist/src/store/pipeline-read-model.d.ts +49 -0
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
- package/dist/src/store/pipeline-read-model.js +156 -0
- package/dist/src/store/pipeline-read-model.js.map +1 -0
- package/dist/src/store/sqlite-pipeline-event-store.d.ts +14 -0
- package/dist/src/store/sqlite-pipeline-event-store.d.ts.map +1 -0
- package/dist/src/store/sqlite-pipeline-event-store.js +20 -0
- package/dist/src/store/sqlite-pipeline-event-store.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
- package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +1216 -0
- package/package.json +6 -4
- package/src/builder/define.specs.ts +5 -4
- package/src/builder/define.ts +24 -11
- package/src/config/pipeline-config.specs.ts +32 -0
- package/src/core/descriptors.ts +7 -2
- package/src/graph/filter-graph.specs.ts +267 -0
- package/src/graph/filter-graph.ts +111 -0
- package/src/graph/types.specs.ts +0 -14
- package/src/graph/types.ts +10 -0
- package/src/index.ts +1 -0
- package/src/projections/await-tracker-projection.specs.ts +24 -0
- package/src/projections/await-tracker-projection.ts +68 -0
- package/src/projections/index.ts +11 -0
- package/src/projections/item-status-projection.specs.ts +130 -0
- package/src/projections/item-status-projection.ts +32 -0
- package/src/projections/latest-run-projection.ts +20 -0
- package/src/projections/message-log-projection.ts +113 -0
- package/src/projections/node-status-projection.ts +33 -0
- package/src/projections/phased-execution-projection.specs.ts +202 -0
- package/src/projections/phased-execution-projection.ts +146 -0
- package/src/projections/settled-instance-projection.specs.ts +296 -0
- package/src/projections/settled-instance-projection.ts +160 -0
- package/src/projections/stats-projection.ts +26 -0
- package/src/runtime/await-tracker.specs.ts +57 -34
- package/src/runtime/await-tracker.ts +43 -31
- package/src/runtime/context.ts +1 -1
- package/src/runtime/event-command-map.ts +11 -4
- package/src/runtime/phased-executor.specs.ts +357 -81
- package/src/runtime/phased-executor.ts +134 -128
- package/src/runtime/pipeline-runtime.specs.ts +65 -0
- package/src/runtime/pipeline-runtime.ts +6 -4
- package/src/runtime/settled-tracker.specs.ts +716 -120
- package/src/runtime/settled-tracker.ts +100 -102
- package/src/server/pipeline-server.e2e.specs.ts +10 -16
- package/src/server/pipeline-server.specs.ts +1441 -211
- package/src/server/pipeline-server.ts +535 -144
- package/src/server/sse-manager.specs.ts +67 -36
- package/src/server/sse-manager.ts +0 -4
- package/src/store/index.ts +2 -0
- package/src/store/pipeline-event-store.specs.ts +357 -0
- package/src/store/pipeline-event-store.ts +156 -0
- package/src/store/pipeline-read-model.specs.ts +1170 -0
- package/src/store/pipeline-read-model.ts +223 -0
- package/src/store/sqlite-pipeline-event-store.specs.ts +13 -0
- package/src/store/sqlite-pipeline-event-store.ts +36 -0
- package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
- package/src/testing/fixtures/kanban.pipeline.ts +2 -2
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -8
- package/claude.md +0 -160
- package/dist/src/__tests__/e2e/helpers.d.ts +0 -48
- package/dist/src/__tests__/e2e/helpers.d.ts.map +0 -1
- package/dist/src/__tests__/e2e/helpers.js +0 -253
- package/dist/src/__tests__/e2e/helpers.js.map +0 -1
- package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts +0 -2
- package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.d.ts.map +0 -1
- package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js +0 -195
- package/dist/src/__tests__/e2e/kanban-migration.e2e.specs.js.map +0 -1
- package/dist/src/__tests__/e2e/types.d.ts +0 -107
- package/dist/src/__tests__/e2e/types.d.ts.map +0 -1
- package/dist/src/__tests__/e2e/types.js +0 -2
- package/dist/src/__tests__/e2e/types.js.map +0 -1
- package/dist/src/builder/define.specs.d.ts +0 -2
- package/dist/src/builder/define.specs.d.ts.map +0 -1
- package/dist/src/builder/define.specs.js +0 -435
- package/dist/src/builder/define.specs.js.map +0 -1
- package/dist/src/core/descriptors.specs.d.ts +0 -2
- package/dist/src/core/descriptors.specs.d.ts.map +0 -1
- package/dist/src/core/descriptors.specs.js +0 -24
- package/dist/src/core/descriptors.specs.js.map +0 -1
- package/dist/src/core/types.specs.d.ts +0 -2
- package/dist/src/core/types.specs.d.ts.map +0 -1
- package/dist/src/core/types.specs.js +0 -40
- package/dist/src/core/types.specs.js.map +0 -1
- package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts +0 -15
- package/dist/src/file-syncer/crypto/jwe-encryptor.d.ts.map +0 -1
- package/dist/src/file-syncer/crypto/jwe-encryptor.js +0 -64
- package/dist/src/file-syncer/crypto/jwe-encryptor.js.map +0 -1
- package/dist/src/file-syncer/crypto/provider-resolver.d.ts +0 -24
- package/dist/src/file-syncer/crypto/provider-resolver.d.ts.map +0 -1
- package/dist/src/file-syncer/crypto/provider-resolver.js +0 -71
- package/dist/src/file-syncer/crypto/provider-resolver.js.map +0 -1
- package/dist/src/file-syncer/discovery/bareImports.d.ts +0 -3
- package/dist/src/file-syncer/discovery/bareImports.d.ts.map +0 -1
- package/dist/src/file-syncer/discovery/bareImports.js +0 -36
- package/dist/src/file-syncer/discovery/bareImports.js.map +0 -1
- package/dist/src/file-syncer/discovery/dts.d.ts +0 -8
- package/dist/src/file-syncer/discovery/dts.d.ts.map +0 -1
- package/dist/src/file-syncer/discovery/dts.js +0 -99
- package/dist/src/file-syncer/discovery/dts.js.map +0 -1
- package/dist/src/file-syncer/index.d.ts +0 -46
- package/dist/src/file-syncer/index.d.ts.map +0 -1
- package/dist/src/file-syncer/index.js +0 -392
- package/dist/src/file-syncer/index.js.map +0 -1
- package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts +0 -7
- package/dist/src/file-syncer/sync/resolveSyncFileSet.d.ts.map +0 -1
- package/dist/src/file-syncer/sync/resolveSyncFileSet.js +0 -86
- package/dist/src/file-syncer/sync/resolveSyncFileSet.js.map +0 -1
- package/dist/src/file-syncer/types/wire.d.ts +0 -14
- package/dist/src/file-syncer/types/wire.d.ts.map +0 -1
- package/dist/src/file-syncer/types/wire.js +0 -2
- package/dist/src/file-syncer/types/wire.js.map +0 -1
- package/dist/src/file-syncer/utils/hash.d.ts +0 -5
- package/dist/src/file-syncer/utils/hash.d.ts.map +0 -1
- package/dist/src/file-syncer/utils/hash.js +0 -19
- package/dist/src/file-syncer/utils/hash.js.map +0 -1
- package/dist/src/file-syncer/utils/path.d.ts +0 -13
- package/dist/src/file-syncer/utils/path.d.ts.map +0 -1
- package/dist/src/file-syncer/utils/path.js +0 -74
- package/dist/src/file-syncer/utils/path.js.map +0 -1
- package/dist/src/graph/types.specs.d.ts +0 -2
- package/dist/src/graph/types.specs.d.ts.map +0 -1
- package/dist/src/graph/types.specs.js +0 -148
- package/dist/src/graph/types.specs.js.map +0 -1
- package/dist/src/logging/event-logger.specs.d.ts +0 -2
- package/dist/src/logging/event-logger.specs.d.ts.map +0 -1
- package/dist/src/logging/event-logger.specs.js +0 -81
- package/dist/src/logging/event-logger.specs.js.map +0 -1
- package/dist/src/plugins/handler-adapter.specs.d.ts +0 -2
- package/dist/src/plugins/handler-adapter.specs.d.ts.map +0 -1
- package/dist/src/plugins/handler-adapter.specs.js +0 -129
- package/dist/src/plugins/handler-adapter.specs.js.map +0 -1
- package/dist/src/plugins/plugin-loader.specs.d.ts +0 -2
- package/dist/src/plugins/plugin-loader.specs.d.ts.map +0 -1
- package/dist/src/plugins/plugin-loader.specs.js +0 -246
- package/dist/src/plugins/plugin-loader.specs.js.map +0 -1
- package/dist/src/runtime/await-tracker.specs.d.ts +0 -2
- package/dist/src/runtime/await-tracker.specs.d.ts.map +0 -1
- package/dist/src/runtime/await-tracker.specs.js +0 -46
- package/dist/src/runtime/await-tracker.specs.js.map +0 -1
- package/dist/src/runtime/context.specs.d.ts +0 -2
- package/dist/src/runtime/context.specs.d.ts.map +0 -1
- package/dist/src/runtime/context.specs.js +0 -26
- package/dist/src/runtime/context.specs.js.map +0 -1
- package/dist/src/runtime/event-command-map.specs.d.ts +0 -2
- package/dist/src/runtime/event-command-map.specs.d.ts.map +0 -1
- package/dist/src/runtime/event-command-map.specs.js +0 -108
- package/dist/src/runtime/event-command-map.specs.js.map +0 -1
- package/dist/src/runtime/phased-executor.specs.d.ts +0 -2
- package/dist/src/runtime/phased-executor.specs.d.ts.map +0 -1
- package/dist/src/runtime/phased-executor.specs.js +0 -256
- package/dist/src/runtime/phased-executor.specs.js.map +0 -1
- package/dist/src/runtime/pipeline-runtime.specs.d.ts +0 -2
- package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +0 -1
- package/dist/src/runtime/pipeline-runtime.specs.js +0 -192
- package/dist/src/runtime/pipeline-runtime.specs.js.map +0 -1
- package/dist/src/runtime/settled-tracker.specs.d.ts +0 -2
- package/dist/src/runtime/settled-tracker.specs.d.ts.map +0 -1
- package/dist/src/runtime/settled-tracker.specs.js +0 -361
- package/dist/src/runtime/settled-tracker.specs.js.map +0 -1
- package/dist/src/server/full-orchestration.e2e.specs.d.ts +0 -2
- package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +0 -1
- package/dist/src/server/full-orchestration.e2e.specs.js +0 -561
- package/dist/src/server/full-orchestration.e2e.specs.js.map +0 -1
- package/dist/src/server/pipeline-server.e2e.specs.d.ts +0 -2
- package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +0 -1
- package/dist/src/server/pipeline-server.e2e.specs.js +0 -381
- package/dist/src/server/pipeline-server.e2e.specs.js.map +0 -1
- package/dist/src/server/pipeline-server.specs.d.ts +0 -2
- package/dist/src/server/pipeline-server.specs.d.ts.map +0 -1
- package/dist/src/server/pipeline-server.specs.js +0 -662
- package/dist/src/server/pipeline-server.specs.js.map +0 -1
- package/dist/src/server/sse-manager.specs.d.ts +0 -2
- package/dist/src/server/sse-manager.specs.d.ts.map +0 -1
- package/dist/src/server/sse-manager.specs.js +0 -158
- package/dist/src/server/sse-manager.specs.js.map +0 -1
- package/dist/src/testing/event-capture.specs.d.ts +0 -2
- package/dist/src/testing/event-capture.specs.d.ts.map +0 -1
- package/dist/src/testing/event-capture.specs.js +0 -114
- package/dist/src/testing/event-capture.specs.js.map +0 -1
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +0 -2
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +0 -1
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +0 -263
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +0 -1
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +0 -2
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +0 -1
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js +0 -29
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +0 -1
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts +0 -2
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +0 -1
- package/dist/src/testing/kanban-todo.e2e.specs.js +0 -160
- package/dist/src/testing/kanban-todo.e2e.specs.js.map +0 -1
- package/dist/src/testing/mock-handlers.specs.d.ts +0 -2
- package/dist/src/testing/mock-handlers.specs.d.ts.map +0 -1
- package/dist/src/testing/mock-handlers.specs.js +0 -193
- package/dist/src/testing/mock-handlers.specs.js.map +0 -1
- package/dist/src/testing/real-execution.e2e.specs.d.ts +0 -2
- package/dist/src/testing/real-execution.e2e.specs.d.ts.map +0 -1
- package/dist/src/testing/real-execution.e2e.specs.js +0 -140
- package/dist/src/testing/real-execution.e2e.specs.js.map +0 -1
- package/dist/src/testing/real-plugin.e2e.specs.d.ts +0 -2
- package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +0 -1
- package/dist/src/testing/real-plugin.e2e.specs.js +0 -65
- package/dist/src/testing/real-plugin.e2e.specs.js.map +0 -1
- package/dist/src/testing/server-startup.e2e.specs.d.ts +0 -2
- package/dist/src/testing/server-startup.e2e.specs.d.ts.map +0 -1
- package/dist/src/testing/server-startup.e2e.specs.js +0 -104
- package/dist/src/testing/server-startup.e2e.specs.js.map +0 -1
- package/dist/src/testing/snapshot-compare.specs.d.ts +0 -2
- package/dist/src/testing/snapshot-compare.specs.d.ts.map +0 -1
- package/dist/src/testing/snapshot-compare.specs.js +0 -112
- package/dist/src/testing/snapshot-compare.specs.js.map +0 -1
- package/dist/src/testing/snapshot-sanitize.specs.d.ts +0 -2
- package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +0 -1
- package/dist/src/testing/snapshot-sanitize.specs.js +0 -104
- package/dist/src/testing/snapshot-sanitize.specs.js.map +0 -1
- package/docs/testing-analysis.md +0 -395
- package/pomodoro-plan.md +0 -651
- package/src/core/descriptors.specs.ts +0 -28
- package/src/core/types.specs.ts +0 -44
- package/src/runtime/context.specs.ts +0 -28
|
@@ -25,20 +25,15 @@ interface GraphNode {
|
|
|
25
25
|
id: string;
|
|
26
26
|
type: string;
|
|
27
27
|
label: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
id: string;
|
|
32
|
-
name: string;
|
|
33
|
-
title: string;
|
|
34
|
-
status: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
pendingCount?: number;
|
|
30
|
+
endedCount?: number;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
interface PipelineResponse {
|
|
38
|
-
nodes:
|
|
39
|
-
edges: Array<{ from: string; to: string }>;
|
|
40
|
-
|
|
41
|
-
eventToCommand: Record<string, string>;
|
|
34
|
+
nodes: GraphNode[];
|
|
35
|
+
edges: Array<{ from: string; to: string; backLink?: boolean }>;
|
|
36
|
+
latestRun?: string;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
interface GraphResponse {
|
|
@@ -162,6 +157,28 @@ describe('PipelineServer', () => {
|
|
|
162
157
|
expect(data.folds).toEqual([]);
|
|
163
158
|
await server.stop();
|
|
164
159
|
});
|
|
160
|
+
|
|
161
|
+
it('should exclude settled handlers from eventHandlers list', async () => {
|
|
162
|
+
const handler = {
|
|
163
|
+
name: 'CheckTests',
|
|
164
|
+
events: ['TestsPassed'],
|
|
165
|
+
handle: async () => ({ type: 'TestsPassed', data: {} }),
|
|
166
|
+
};
|
|
167
|
+
const pipeline = define('test')
|
|
168
|
+
.on('Start')
|
|
169
|
+
.emit('CheckTests', {})
|
|
170
|
+
.settled(['CheckTests'])
|
|
171
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
172
|
+
.build();
|
|
173
|
+
const server = new PipelineServer({ port: 0 });
|
|
174
|
+
server.registerCommandHandlers([handler]);
|
|
175
|
+
server.registerPipeline(pipeline);
|
|
176
|
+
await server.start();
|
|
177
|
+
const data = await fetchAs<RegistryResponse>(`http://localhost:${server.port}/registry`);
|
|
178
|
+
expect(data.eventHandlers).toContain('Start');
|
|
179
|
+
expect(data.eventHandlers).not.toContain('settled:CheckTests');
|
|
180
|
+
await server.stop();
|
|
181
|
+
});
|
|
165
182
|
});
|
|
166
183
|
|
|
167
184
|
describe('GET /pipeline', () => {
|
|
@@ -175,234 +192,773 @@ describe('PipelineServer', () => {
|
|
|
175
192
|
await server.stop();
|
|
176
193
|
});
|
|
177
194
|
|
|
178
|
-
it('should
|
|
195
|
+
it('should use displayName as label for command graph nodes', async () => {
|
|
179
196
|
const handler = {
|
|
180
|
-
name: '
|
|
181
|
-
|
|
182
|
-
handle: async () => ({ type: '
|
|
197
|
+
name: 'Cmd',
|
|
198
|
+
displayName: 'My Command',
|
|
199
|
+
handle: async () => ({ type: 'Done', data: {} }),
|
|
183
200
|
};
|
|
201
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).build();
|
|
184
202
|
const server = new PipelineServer({ port: 0 });
|
|
185
203
|
server.registerCommandHandlers([handler]);
|
|
204
|
+
server.registerPipeline(pipeline);
|
|
186
205
|
await server.start();
|
|
187
|
-
const data = await fetchAs<
|
|
188
|
-
|
|
206
|
+
const data = await fetchAs<GraphResponse>(`http://localhost:${server.port}/pipeline`);
|
|
207
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:Cmd');
|
|
208
|
+
expect(cmdNode?.label).toBe('My Command');
|
|
209
|
+
await server.stop();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should use command name as graph node label when displayName not provided', async () => {
|
|
213
|
+
const handler = {
|
|
214
|
+
name: 'SimpleCmd',
|
|
215
|
+
handle: async () => ({ type: 'Done', data: {} }),
|
|
216
|
+
};
|
|
217
|
+
const pipeline = define('test').on('Start').emit('SimpleCmd', {}).build();
|
|
218
|
+
const server = new PipelineServer({ port: 0 });
|
|
219
|
+
server.registerCommandHandlers([handler]);
|
|
220
|
+
server.registerPipeline(pipeline);
|
|
221
|
+
await server.start();
|
|
222
|
+
const data = await fetchAs<GraphResponse>(`http://localhost:${server.port}/pipeline`);
|
|
223
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:SimpleCmd');
|
|
224
|
+
expect(cmdNode?.label).toBe('SimpleCmd');
|
|
225
|
+
await server.stop();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should filter out event nodes when excludeTypes=event', async () => {
|
|
229
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).build();
|
|
230
|
+
const server = new PipelineServer({ port: 0 });
|
|
231
|
+
server.registerPipeline(pipeline);
|
|
232
|
+
await server.start();
|
|
233
|
+
const data = await fetchAs<GraphResponse>(`http://localhost:${server.port}/pipeline?excludeTypes=event`);
|
|
234
|
+
expect(data.nodes.every((n) => n.type !== 'event')).toBe(true);
|
|
189
235
|
await server.stop();
|
|
190
236
|
});
|
|
191
237
|
|
|
192
|
-
it('should
|
|
238
|
+
it('should reconnect edges when maintainEdges=true and filter commands', async () => {
|
|
193
239
|
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
194
240
|
const server = new PipelineServer({ port: 0 });
|
|
195
241
|
server.registerPipeline(pipeline);
|
|
196
242
|
await server.start();
|
|
197
|
-
const data = await fetchAs<
|
|
198
|
-
|
|
243
|
+
const data = await fetchAs<GraphResponse>(
|
|
244
|
+
`http://localhost:${server.port}/pipeline?excludeTypes=command&maintainEdges=true`,
|
|
245
|
+
);
|
|
246
|
+
expect(data.nodes.every((n) => n.type !== 'command')).toBe(true);
|
|
247
|
+
expect(data.edges).toHaveLength(0);
|
|
248
|
+
await server.stop();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should filter multiple node types', async () => {
|
|
252
|
+
const pipeline = define('test')
|
|
253
|
+
.on('Start')
|
|
254
|
+
.emit('CheckA', {})
|
|
255
|
+
.emit('CheckB', {})
|
|
256
|
+
.settled(['CheckA', 'CheckB'])
|
|
257
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
258
|
+
.build();
|
|
259
|
+
const server = new PipelineServer({ port: 0 });
|
|
260
|
+
server.registerPipeline(pipeline);
|
|
261
|
+
await server.start();
|
|
262
|
+
const data = await fetchAs<GraphResponse>(`http://localhost:${server.port}/pipeline?excludeTypes=event,settled`);
|
|
263
|
+
expect(data.nodes.every((n) => n.type !== 'event' && n.type !== 'settled')).toBe(true);
|
|
264
|
+
await server.stop();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should reconnect commands through events when filtering events with maintainEdges=true', async () => {
|
|
268
|
+
const generateHandler = {
|
|
269
|
+
name: 'Generate',
|
|
270
|
+
events: ['Generated'],
|
|
271
|
+
handle: async () => ({ type: 'Generated', data: {} }),
|
|
272
|
+
};
|
|
273
|
+
const pipeline = define('test').on('Start').emit('Generate', {}).on('Generated').emit('Process', {}).build();
|
|
274
|
+
const server = new PipelineServer({ port: 0 });
|
|
275
|
+
server.registerCommandHandlers([generateHandler]);
|
|
276
|
+
server.registerPipeline(pipeline);
|
|
277
|
+
await server.start();
|
|
278
|
+
const data = await fetchAs<GraphResponse>(
|
|
279
|
+
`http://localhost:${server.port}/pipeline?excludeTypes=event&maintainEdges=true`,
|
|
280
|
+
);
|
|
281
|
+
expect(data.nodes.every((n) => n.type !== 'event')).toBe(true);
|
|
282
|
+
expect(data.edges.some((e) => e.from === 'cmd:Generate' && e.to === 'cmd:Process')).toBe(true);
|
|
199
283
|
await server.stop();
|
|
200
284
|
});
|
|
201
285
|
|
|
202
|
-
it('should
|
|
286
|
+
it('should have status idle on command nodes by default', async () => {
|
|
203
287
|
const handler = {
|
|
204
288
|
name: 'Cmd',
|
|
205
|
-
|
|
206
|
-
description: 'Test command',
|
|
289
|
+
events: ['Done'],
|
|
207
290
|
handle: async () => ({ type: 'Done', data: {} }),
|
|
208
291
|
};
|
|
292
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).build();
|
|
209
293
|
const server = new PipelineServer({ port: 0 });
|
|
210
294
|
server.registerCommandHandlers([handler]);
|
|
295
|
+
server.registerPipeline(pipeline);
|
|
211
296
|
await server.start();
|
|
212
297
|
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
213
|
-
const cmdNode = data.nodes.find((n) => n.id === 'Cmd');
|
|
214
|
-
expect(cmdNode).
|
|
215
|
-
expect(cmdNode?.name).toBe('Cmd');
|
|
216
|
-
expect(cmdNode?.title).toBe('Test command');
|
|
217
|
-
expect(cmdNode?.status).toBe('None');
|
|
298
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:Cmd');
|
|
299
|
+
expect(cmdNode?.status).toBe('idle');
|
|
218
300
|
await server.stop();
|
|
219
301
|
});
|
|
220
|
-
});
|
|
221
302
|
|
|
222
|
-
|
|
223
|
-
it('should accept command', async () => {
|
|
303
|
+
it('should not have status on event nodes', async () => {
|
|
224
304
|
const handler = {
|
|
225
305
|
name: 'Cmd',
|
|
306
|
+
events: ['Done'],
|
|
226
307
|
handle: async () => ({ type: 'Done', data: {} }),
|
|
227
308
|
};
|
|
309
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).build();
|
|
228
310
|
const server = new PipelineServer({ port: 0 });
|
|
229
311
|
server.registerCommandHandlers([handler]);
|
|
312
|
+
server.registerPipeline(pipeline);
|
|
230
313
|
await server.start();
|
|
231
|
-
const data = await fetchAs<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
body: JSON.stringify({ type: 'Cmd', data: {} }),
|
|
235
|
-
});
|
|
236
|
-
expect(data.status).toBe('ack');
|
|
314
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
315
|
+
const eventNode = data.nodes.find((n) => n.id === 'evt:Start');
|
|
316
|
+
expect(eventNode?.status).toBeUndefined();
|
|
237
317
|
await server.stop();
|
|
238
318
|
});
|
|
239
319
|
|
|
240
|
-
it('should
|
|
320
|
+
it('should have idle status on settled nodes when no correlationId provided', async () => {
|
|
321
|
+
const handler = {
|
|
322
|
+
name: 'CheckTests',
|
|
323
|
+
events: ['TestsPassed'],
|
|
324
|
+
handle: async () => ({ type: 'TestsPassed', data: {} }),
|
|
325
|
+
};
|
|
326
|
+
const pipeline = define('test')
|
|
327
|
+
.on('Start')
|
|
328
|
+
.emit('CheckTests', {})
|
|
329
|
+
.settled(['CheckTests'])
|
|
330
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
331
|
+
.build();
|
|
241
332
|
const server = new PipelineServer({ port: 0 });
|
|
333
|
+
server.registerCommandHandlers([handler]);
|
|
334
|
+
server.registerPipeline(pipeline);
|
|
242
335
|
await server.start();
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
expect(res.status).toBe(404);
|
|
249
|
-
const data = await res.json<CommandResponse>();
|
|
250
|
-
expect(data.status).toBe('nack');
|
|
336
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
337
|
+
const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
|
|
338
|
+
expect(settledNode?.status).toBe('idle');
|
|
339
|
+
expect(settledNode?.pendingCount).toBe(0);
|
|
340
|
+
expect(settledNode?.endedCount).toBe(0);
|
|
251
341
|
await server.stop();
|
|
252
342
|
});
|
|
253
343
|
|
|
254
|
-
it('should
|
|
344
|
+
it('should have status from computeSettledStats on settled nodes when correlationId provided', async () => {
|
|
255
345
|
const handler = {
|
|
256
|
-
name: '
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{ type: 'EventB', data: { b: 2 } },
|
|
260
|
-
],
|
|
346
|
+
name: 'CheckTests',
|
|
347
|
+
events: ['TestsPassed'],
|
|
348
|
+
handle: async () => ({ type: 'TestsPassed', data: {} }),
|
|
261
349
|
};
|
|
350
|
+
const pipeline = define('test')
|
|
351
|
+
.on('Start')
|
|
352
|
+
.emit('CheckTests', {})
|
|
353
|
+
.settled(['CheckTests'])
|
|
354
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
355
|
+
.build();
|
|
262
356
|
const server = new PipelineServer({ port: 0 });
|
|
263
357
|
server.registerCommandHandlers([handler]);
|
|
358
|
+
server.registerPipeline(pipeline);
|
|
264
359
|
await server.start();
|
|
265
|
-
|
|
360
|
+
|
|
361
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
266
362
|
method: 'POST',
|
|
267
363
|
headers: { 'Content-Type': 'application/json' },
|
|
268
|
-
body: JSON.stringify({ type: '
|
|
364
|
+
body: JSON.stringify({ type: 'CheckTests', data: {} }),
|
|
269
365
|
});
|
|
366
|
+
|
|
270
367
|
await new Promise((r) => setTimeout(r, 100));
|
|
271
|
-
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
272
|
-
expect(msgs.some((m) => m.message.type === 'EventA')).toBe(true);
|
|
273
|
-
expect(msgs.some((m) => m.message.type === 'EventB')).toBe(true);
|
|
274
|
-
await server.stop();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
368
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
expect(
|
|
369
|
+
const data = await fetchAs<PipelineResponse>(
|
|
370
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
371
|
+
);
|
|
372
|
+
const settledNode = data.nodes.find((n) => n.id === 'settled:CheckTests');
|
|
373
|
+
expect(settledNode?.status).toBeDefined();
|
|
374
|
+
expect(settledNode?.pendingCount).toBeDefined();
|
|
375
|
+
expect(settledNode?.endedCount).toBeDefined();
|
|
284
376
|
await server.stop();
|
|
285
377
|
});
|
|
286
|
-
});
|
|
287
378
|
|
|
288
|
-
|
|
289
|
-
|
|
379
|
+
it('should show running status for command being executed', async () => {
|
|
380
|
+
let resolveHandler: () => void = () => {};
|
|
381
|
+
const handlerPromise = new Promise<void>((resolve) => {
|
|
382
|
+
resolveHandler = resolve;
|
|
383
|
+
});
|
|
384
|
+
const handler = {
|
|
385
|
+
name: 'SlowCmd',
|
|
386
|
+
events: ['Done'],
|
|
387
|
+
handle: async () => {
|
|
388
|
+
await handlerPromise;
|
|
389
|
+
return { type: 'Done', data: {} };
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
const pipeline = define('test').on('Start').emit('SlowCmd', {}).build();
|
|
290
393
|
const server = new PipelineServer({ port: 0 });
|
|
394
|
+
server.registerCommandHandlers([handler]);
|
|
395
|
+
server.registerPipeline(pipeline);
|
|
291
396
|
await server.start();
|
|
292
|
-
|
|
293
|
-
|
|
397
|
+
|
|
398
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
401
|
+
body: JSON.stringify({ type: 'SlowCmd', data: {} }),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
405
|
+
|
|
406
|
+
const data = await fetchAs<PipelineResponse>(
|
|
407
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
408
|
+
);
|
|
409
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:SlowCmd');
|
|
410
|
+
expect(cmdNode?.status).toBe('running');
|
|
411
|
+
|
|
412
|
+
resolveHandler();
|
|
294
413
|
await server.stop();
|
|
295
414
|
});
|
|
296
|
-
});
|
|
297
415
|
|
|
298
|
-
|
|
299
|
-
|
|
416
|
+
it('should show success status after command completes with success event', async () => {
|
|
417
|
+
const handler = {
|
|
418
|
+
name: 'SuccessCmd',
|
|
419
|
+
events: ['CmdCompleted'],
|
|
420
|
+
handle: async () => ({ type: 'CmdCompleted', data: {} }),
|
|
421
|
+
};
|
|
422
|
+
const pipeline = define('test').on('Start').emit('SuccessCmd', {}).build();
|
|
300
423
|
const server = new PipelineServer({ port: 0 });
|
|
424
|
+
server.registerCommandHandlers([handler]);
|
|
425
|
+
server.registerPipeline(pipeline);
|
|
301
426
|
await server.start();
|
|
302
|
-
|
|
303
|
-
|
|
427
|
+
|
|
428
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
431
|
+
body: JSON.stringify({ type: 'SuccessCmd', data: {} }),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
435
|
+
|
|
436
|
+
const data = await fetchAs<PipelineResponse>(
|
|
437
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
438
|
+
);
|
|
439
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:SuccessCmd');
|
|
440
|
+
expect(cmdNode?.status).toBe('success');
|
|
304
441
|
await server.stop();
|
|
305
442
|
});
|
|
306
|
-
});
|
|
307
443
|
|
|
308
|
-
|
|
309
|
-
it('should route events through pipeline', async () => {
|
|
444
|
+
it('should show error status after command completes with failed event', async () => {
|
|
310
445
|
const handler = {
|
|
311
|
-
name: '
|
|
312
|
-
|
|
446
|
+
name: 'FailCmd',
|
|
447
|
+
events: ['CmdFailed'],
|
|
448
|
+
handle: async () => ({ type: 'CmdFailed', data: { error: 'Something went wrong' } }),
|
|
313
449
|
};
|
|
314
|
-
const pipeline = define('test').on('
|
|
450
|
+
const pipeline = define('test').on('Start').emit('FailCmd', {}).build();
|
|
315
451
|
const server = new PipelineServer({ port: 0 });
|
|
316
452
|
server.registerCommandHandlers([handler]);
|
|
317
453
|
server.registerPipeline(pipeline);
|
|
318
454
|
await server.start();
|
|
319
|
-
|
|
455
|
+
|
|
456
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
320
457
|
method: 'POST',
|
|
321
458
|
headers: { 'Content-Type': 'application/json' },
|
|
322
|
-
body: JSON.stringify({ type: '
|
|
459
|
+
body: JSON.stringify({ type: 'FailCmd', data: {} }),
|
|
323
460
|
});
|
|
461
|
+
|
|
324
462
|
await new Promise((r) => setTimeout(r, 100));
|
|
325
|
-
|
|
326
|
-
|
|
463
|
+
|
|
464
|
+
const data = await fetchAs<PipelineResponse>(
|
|
465
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
466
|
+
);
|
|
467
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:FailCmd');
|
|
468
|
+
expect(cmdNode?.status).toBe('error');
|
|
327
469
|
await server.stop();
|
|
328
470
|
});
|
|
329
471
|
|
|
330
|
-
it('should
|
|
472
|
+
it('should broadcast PipelineRunStarted event when new correlationId is first seen', async () => {
|
|
331
473
|
const handler = {
|
|
332
|
-
name: '
|
|
474
|
+
name: 'StartCmd',
|
|
475
|
+
events: ['Started'],
|
|
333
476
|
handle: async () => ({ type: 'Started', data: {} }),
|
|
334
477
|
};
|
|
335
|
-
const pipeline = define('test')
|
|
336
|
-
.on('Started')
|
|
337
|
-
.handle(async (_e, ctx) => {
|
|
338
|
-
await ctx.emit('CustomEvent', { emitted: true });
|
|
339
|
-
})
|
|
340
|
-
.build();
|
|
478
|
+
const pipeline = define('test').on('Trigger').emit('StartCmd', {}).build();
|
|
341
479
|
const server = new PipelineServer({ port: 0 });
|
|
342
480
|
server.registerCommandHandlers([handler]);
|
|
343
481
|
server.registerPipeline(pipeline);
|
|
344
482
|
await server.start();
|
|
345
|
-
|
|
483
|
+
|
|
484
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
346
485
|
method: 'POST',
|
|
347
486
|
headers: { 'Content-Type': 'application/json' },
|
|
348
|
-
body: JSON.stringify({ type: '
|
|
487
|
+
body: JSON.stringify({ type: 'StartCmd', data: {} }),
|
|
349
488
|
});
|
|
489
|
+
|
|
350
490
|
await new Promise((r) => setTimeout(r, 100));
|
|
491
|
+
|
|
351
492
|
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
352
|
-
|
|
493
|
+
const pipelineRunStarted = msgs.find((m) => m.message.type === 'PipelineRunStarted');
|
|
494
|
+
expect(pipelineRunStarted).toBeDefined();
|
|
495
|
+
expect((pipelineRunStarted?.message as { correlationId?: string }).correlationId).toBe(
|
|
496
|
+
commandResponse.correlationId,
|
|
497
|
+
);
|
|
498
|
+
expect((pipelineRunStarted?.message as { data?: { triggerCommand?: string } }).data?.triggerCommand).toBe(
|
|
499
|
+
'StartCmd',
|
|
500
|
+
);
|
|
353
501
|
await server.stop();
|
|
354
502
|
});
|
|
355
|
-
});
|
|
356
503
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
504
|
+
it('should broadcast NodeStatusChanged event when command starts running', async () => {
|
|
505
|
+
const handler = {
|
|
506
|
+
name: 'RunCmd',
|
|
507
|
+
events: ['RunDone'],
|
|
508
|
+
handle: async () => ({ type: 'RunDone', data: {} }),
|
|
509
|
+
};
|
|
510
|
+
const pipeline = define('test').on('Trigger').emit('RunCmd', {}).build();
|
|
360
511
|
const server = new PipelineServer({ port: 0 });
|
|
512
|
+
server.registerCommandHandlers([handler]);
|
|
361
513
|
server.registerPipeline(pipeline);
|
|
362
514
|
await server.start();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
515
|
+
|
|
516
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
517
|
+
method: 'POST',
|
|
518
|
+
headers: { 'Content-Type': 'application/json' },
|
|
519
|
+
body: JSON.stringify({ type: 'RunCmd', data: {} }),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
523
|
+
|
|
524
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
525
|
+
type NodeStatusChangedMessage = {
|
|
526
|
+
type: string;
|
|
527
|
+
correlationId?: string;
|
|
528
|
+
data?: { nodeId?: string; status?: string; previousStatus?: string };
|
|
529
|
+
};
|
|
530
|
+
const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
|
|
531
|
+
const runningEvent = nodeStatusChanged.find(
|
|
532
|
+
(m) => (m.message as NodeStatusChangedMessage).data?.status === 'running',
|
|
533
|
+
);
|
|
534
|
+
expect(runningEvent).toBeDefined();
|
|
535
|
+
expect((runningEvent?.message as NodeStatusChangedMessage).data?.nodeId).toBe('cmd:RunCmd');
|
|
536
|
+
expect((runningEvent?.message as NodeStatusChangedMessage).data?.previousStatus).toBe('idle');
|
|
537
|
+
expect((runningEvent?.message as NodeStatusChangedMessage).correlationId).toBe(commandResponse.correlationId);
|
|
367
538
|
await server.stop();
|
|
368
539
|
});
|
|
369
540
|
|
|
370
|
-
it('should
|
|
371
|
-
const
|
|
541
|
+
it('should broadcast NodeStatusChanged event when command completes', async () => {
|
|
542
|
+
const handler = {
|
|
543
|
+
name: 'CompleteCmd',
|
|
544
|
+
events: ['CompleteDone'],
|
|
545
|
+
handle: async () => ({ type: 'CompleteDone', data: {} }),
|
|
546
|
+
};
|
|
547
|
+
const pipeline = define('test').on('Trigger').emit('CompleteCmd', {}).build();
|
|
372
548
|
const server = new PipelineServer({ port: 0 });
|
|
549
|
+
server.registerCommandHandlers([handler]);
|
|
373
550
|
server.registerPipeline(pipeline);
|
|
374
551
|
await server.start();
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
552
|
+
|
|
553
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
556
|
+
body: JSON.stringify({ type: 'CompleteCmd', data: {} }),
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
560
|
+
|
|
561
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
562
|
+
type NodeStatusChangedMessage = {
|
|
563
|
+
type: string;
|
|
564
|
+
correlationId?: string;
|
|
565
|
+
data?: { nodeId?: string; status?: string; previousStatus?: string };
|
|
566
|
+
};
|
|
567
|
+
const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
|
|
568
|
+
const successEvent = nodeStatusChanged.find(
|
|
569
|
+
(m) => (m.message as NodeStatusChangedMessage).data?.status === 'success',
|
|
570
|
+
);
|
|
571
|
+
expect(successEvent).toBeDefined();
|
|
572
|
+
expect((successEvent?.message as NodeStatusChangedMessage).data?.nodeId).toBe('cmd:CompleteCmd');
|
|
573
|
+
expect((successEvent?.message as NodeStatusChangedMessage).data?.previousStatus).toBe('running');
|
|
574
|
+
expect((successEvent?.message as NodeStatusChangedMessage).correlationId).toBe(commandResponse.correlationId);
|
|
378
575
|
await server.stop();
|
|
379
576
|
});
|
|
380
577
|
|
|
381
|
-
it('should
|
|
382
|
-
const
|
|
578
|
+
it('should persist status across multiple /pipeline calls', async () => {
|
|
579
|
+
const handler = {
|
|
580
|
+
name: 'PersistCmd',
|
|
581
|
+
events: ['PersistDone'],
|
|
582
|
+
handle: async () => ({ type: 'PersistDone', data: {} }),
|
|
583
|
+
};
|
|
584
|
+
const pipeline = define('test').on('Trigger').emit('PersistCmd', {}).build();
|
|
383
585
|
const server = new PipelineServer({ port: 0 });
|
|
586
|
+
server.registerCommandHandlers([handler]);
|
|
384
587
|
server.registerPipeline(pipeline);
|
|
385
588
|
await server.start();
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
589
|
+
|
|
590
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
593
|
+
body: JSON.stringify({ type: 'PersistCmd', data: {} }),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
597
|
+
|
|
598
|
+
const firstCall = await fetchAs<PipelineResponse>(
|
|
599
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
600
|
+
);
|
|
601
|
+
expect(firstCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
|
|
602
|
+
|
|
603
|
+
const secondCall = await fetchAs<PipelineResponse>(
|
|
604
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
605
|
+
);
|
|
606
|
+
expect(secondCall.nodes.find((n) => n.id === 'cmd:PersistCmd')?.status).toBe('success');
|
|
607
|
+
|
|
389
608
|
await server.stop();
|
|
390
609
|
});
|
|
391
610
|
|
|
392
|
-
it('should
|
|
393
|
-
const
|
|
611
|
+
it('should track status independently for different correlationIds', async () => {
|
|
612
|
+
const handler = {
|
|
613
|
+
name: 'IndependentCmd',
|
|
614
|
+
events: ['IndependentDone'],
|
|
615
|
+
handle: async () => ({ type: 'IndependentDone', data: {} }),
|
|
616
|
+
};
|
|
617
|
+
const pipeline = define('test').on('Trigger').emit('IndependentCmd', {}).build();
|
|
394
618
|
const server = new PipelineServer({ port: 0 });
|
|
619
|
+
server.registerCommandHandlers([handler]);
|
|
395
620
|
server.registerPipeline(pipeline);
|
|
396
621
|
await server.start();
|
|
397
|
-
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
398
|
-
const mermaid = await res.text();
|
|
399
|
-
expect(mermaid).toContain('-->');
|
|
400
|
-
await server.stop();
|
|
401
|
-
});
|
|
402
622
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
623
|
+
const run1 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
624
|
+
method: 'POST',
|
|
625
|
+
headers: { 'Content-Type': 'application/json' },
|
|
626
|
+
body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const run2 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: { 'Content-Type': 'application/json' },
|
|
632
|
+
body: JSON.stringify({ type: 'IndependentCmd', data: {} }),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
636
|
+
|
|
637
|
+
expect(run1.correlationId).not.toBe(run2.correlationId);
|
|
638
|
+
|
|
639
|
+
const pipeline1 = await fetchAs<PipelineResponse>(
|
|
640
|
+
`http://localhost:${server.port}/pipeline?correlationId=${run1.correlationId}`,
|
|
641
|
+
);
|
|
642
|
+
const pipeline2 = await fetchAs<PipelineResponse>(
|
|
643
|
+
`http://localhost:${server.port}/pipeline?correlationId=${run2.correlationId}`,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
expect(pipeline1.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
|
|
647
|
+
expect(pipeline2.nodes.find((n) => n.id === 'cmd:IndependentCmd')?.status).toBe('success');
|
|
648
|
+
|
|
649
|
+
await server.stop();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should show idle status for all command nodes when no correlationId provided', async () => {
|
|
653
|
+
const handler = {
|
|
654
|
+
name: 'IdleCmd',
|
|
655
|
+
events: ['IdleDone'],
|
|
656
|
+
handle: async () => ({ type: 'IdleDone', data: {} }),
|
|
657
|
+
};
|
|
658
|
+
const pipeline = define('test').on('Trigger').emit('IdleCmd', {}).build();
|
|
659
|
+
const server = new PipelineServer({ port: 0 });
|
|
660
|
+
server.registerCommandHandlers([handler]);
|
|
661
|
+
server.registerPipeline(pipeline);
|
|
662
|
+
await server.start();
|
|
663
|
+
|
|
664
|
+
await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
665
|
+
method: 'POST',
|
|
666
|
+
headers: { 'Content-Type': 'application/json' },
|
|
667
|
+
body: JSON.stringify({ type: 'IdleCmd', data: {} }),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
671
|
+
|
|
672
|
+
const pipelineWithoutCorrelation = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
673
|
+
const cmdNode = pipelineWithoutCorrelation.nodes.find((n) => n.id === 'cmd:IdleCmd');
|
|
674
|
+
expect(cmdNode?.status).toBe('idle');
|
|
675
|
+
|
|
676
|
+
await server.stop();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('should return latestRun with the most recent correlationId', async () => {
|
|
680
|
+
const handler = {
|
|
681
|
+
name: 'LatestCmd',
|
|
682
|
+
events: ['LatestDone'],
|
|
683
|
+
handle: async () => ({ type: 'LatestDone', data: {} }),
|
|
684
|
+
};
|
|
685
|
+
const pipeline = define('test').on('Trigger').emit('LatestCmd', {}).build();
|
|
686
|
+
const server = new PipelineServer({ port: 0 });
|
|
687
|
+
server.registerCommandHandlers([handler]);
|
|
688
|
+
server.registerPipeline(pipeline);
|
|
689
|
+
await server.start();
|
|
690
|
+
|
|
691
|
+
const run1 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
692
|
+
method: 'POST',
|
|
693
|
+
headers: { 'Content-Type': 'application/json' },
|
|
694
|
+
body: JSON.stringify({ type: 'LatestCmd', data: {} }),
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
698
|
+
|
|
699
|
+
const run2 = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
700
|
+
method: 'POST',
|
|
701
|
+
headers: { 'Content-Type': 'application/json' },
|
|
702
|
+
body: JSON.stringify({ type: 'LatestCmd', data: {} }),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
706
|
+
|
|
707
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
708
|
+
expect(data.latestRun).toBe(run2.correlationId);
|
|
709
|
+
expect(data.latestRun).not.toBe(run1.correlationId);
|
|
710
|
+
|
|
711
|
+
await server.stop();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe('POST /command', () => {
|
|
716
|
+
it('should accept command', async () => {
|
|
717
|
+
const handler = {
|
|
718
|
+
name: 'Cmd',
|
|
719
|
+
handle: async () => ({ type: 'Done', data: {} }),
|
|
720
|
+
};
|
|
721
|
+
const server = new PipelineServer({ port: 0 });
|
|
722
|
+
server.registerCommandHandlers([handler]);
|
|
723
|
+
await server.start();
|
|
724
|
+
const data = await fetchAs<CommandResponse>(`http://localhost:${server.port}/command`, {
|
|
725
|
+
method: 'POST',
|
|
726
|
+
headers: { 'Content-Type': 'application/json' },
|
|
727
|
+
body: JSON.stringify({ type: 'Cmd', data: {} }),
|
|
728
|
+
});
|
|
729
|
+
expect(data.status).toBe('ack');
|
|
730
|
+
await server.stop();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('should return 404 for unknown command', async () => {
|
|
734
|
+
const server = new PipelineServer({ port: 0 });
|
|
735
|
+
await server.start();
|
|
736
|
+
const res = await fetchWithStatus(`http://localhost:${server.port}/command`, {
|
|
737
|
+
method: 'POST',
|
|
738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
739
|
+
body: JSON.stringify({ type: 'UnknownCmd', data: {} }),
|
|
740
|
+
});
|
|
741
|
+
expect(res.status).toBe(404);
|
|
742
|
+
const data = await res.json<CommandResponse>();
|
|
743
|
+
expect(data.status).toBe('nack');
|
|
744
|
+
await server.stop();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should handle command that returns multiple events', async () => {
|
|
748
|
+
const handler = {
|
|
749
|
+
name: 'Multi',
|
|
750
|
+
handle: async () => [
|
|
751
|
+
{ type: 'EventA', data: { a: 1 } },
|
|
752
|
+
{ type: 'EventB', data: { b: 2 } },
|
|
753
|
+
],
|
|
754
|
+
};
|
|
755
|
+
const server = new PipelineServer({ port: 0 });
|
|
756
|
+
server.registerCommandHandlers([handler]);
|
|
757
|
+
await server.start();
|
|
758
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
759
|
+
method: 'POST',
|
|
760
|
+
headers: { 'Content-Type': 'application/json' },
|
|
761
|
+
body: JSON.stringify({ type: 'Multi', data: {} }),
|
|
762
|
+
});
|
|
763
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
764
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
765
|
+
expect(msgs.some((m) => m.message.type === 'EventA')).toBe(true);
|
|
766
|
+
expect(msgs.some((m) => m.message.type === 'EventB')).toBe(true);
|
|
767
|
+
await server.stop();
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
describe('GET /messages', () => {
|
|
772
|
+
it('should return messages', async () => {
|
|
773
|
+
const server = new PipelineServer({ port: 0 });
|
|
774
|
+
await server.start();
|
|
775
|
+
const data = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
776
|
+
expect(Array.isArray(data)).toBe(true);
|
|
777
|
+
await server.stop();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
describe('GET /stats', () => {
|
|
782
|
+
it('should return stats', async () => {
|
|
783
|
+
const server = new PipelineServer({ port: 0 });
|
|
784
|
+
await server.start();
|
|
785
|
+
const data = await fetchAs<StatsResponse>(`http://localhost:${server.port}/stats`);
|
|
786
|
+
expect(data.totalMessages).toBeDefined();
|
|
787
|
+
await server.stop();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
describe('event routing', () => {
|
|
792
|
+
it('should route events through pipeline', async () => {
|
|
793
|
+
const handler = {
|
|
794
|
+
name: 'Init',
|
|
795
|
+
handle: async () => ({ type: 'Ready', data: {} }),
|
|
796
|
+
};
|
|
797
|
+
const pipeline = define('test').on('Ready').emit('Next', {}).build();
|
|
798
|
+
const server = new PipelineServer({ port: 0 });
|
|
799
|
+
server.registerCommandHandlers([handler]);
|
|
800
|
+
server.registerPipeline(pipeline);
|
|
801
|
+
await server.start();
|
|
802
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
headers: { 'Content-Type': 'application/json' },
|
|
805
|
+
body: JSON.stringify({ type: 'Init', data: {} }),
|
|
806
|
+
});
|
|
807
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
808
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
809
|
+
expect(msgs.some((m) => m.message.type === 'Next')).toBe(true);
|
|
810
|
+
await server.stop();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('should handle custom handler that emits events', async () => {
|
|
814
|
+
const handler = {
|
|
815
|
+
name: 'Start',
|
|
816
|
+
handle: async () => ({ type: 'Started', data: {} }),
|
|
817
|
+
};
|
|
818
|
+
const pipeline = define('test')
|
|
819
|
+
.on('Started')
|
|
820
|
+
.handle(async (_e, ctx) => {
|
|
821
|
+
await ctx.emit('CustomEvent', { emitted: true });
|
|
822
|
+
})
|
|
823
|
+
.build();
|
|
824
|
+
const server = new PipelineServer({ port: 0 });
|
|
825
|
+
server.registerCommandHandlers([handler]);
|
|
826
|
+
server.registerPipeline(pipeline);
|
|
827
|
+
await server.start();
|
|
828
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
829
|
+
method: 'POST',
|
|
830
|
+
headers: { 'Content-Type': 'application/json' },
|
|
831
|
+
body: JSON.stringify({ type: 'Start', data: {} }),
|
|
832
|
+
});
|
|
833
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
834
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
835
|
+
expect(msgs.some((m) => m.message.type === 'CustomEvent')).toBe(true);
|
|
836
|
+
await server.stop();
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
describe('GET /pipeline/mermaid', () => {
|
|
841
|
+
it('should return mermaid diagram as text', async () => {
|
|
842
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
843
|
+
const server = new PipelineServer({ port: 0 });
|
|
844
|
+
server.registerPipeline(pipeline);
|
|
845
|
+
await server.start();
|
|
846
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
847
|
+
expect(res.headers.get('content-type')).toContain('text/plain');
|
|
848
|
+
const mermaid = await res.text();
|
|
849
|
+
expect(mermaid).toContain('flowchart LR');
|
|
850
|
+
await server.stop();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should filter out event nodes when excludeTypes=event', async () => {
|
|
854
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
855
|
+
const server = new PipelineServer({ port: 0 });
|
|
856
|
+
server.registerPipeline(pipeline);
|
|
857
|
+
await server.start();
|
|
858
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid?excludeTypes=event`);
|
|
859
|
+
const mermaid = await res.text();
|
|
860
|
+
expect(mermaid).not.toContain('evt_Start');
|
|
861
|
+
expect(mermaid).toContain('Process[Process]');
|
|
862
|
+
await server.stop();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('should filter out settled nodes when excludeTypes=settled', async () => {
|
|
866
|
+
const checkAHandler = {
|
|
867
|
+
name: 'CheckA',
|
|
868
|
+
events: ['CheckAPassed', 'CheckAFailed'],
|
|
869
|
+
handle: async () => ({ type: 'CheckAPassed', data: {} }),
|
|
870
|
+
};
|
|
871
|
+
const pipeline = define('test')
|
|
872
|
+
.on('Start')
|
|
873
|
+
.emit('CheckA', {})
|
|
874
|
+
.settled(['CheckA'])
|
|
875
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
876
|
+
.build();
|
|
877
|
+
const server = new PipelineServer({ port: 0 });
|
|
878
|
+
server.registerCommandHandlers([checkAHandler]);
|
|
879
|
+
server.registerPipeline(pipeline);
|
|
880
|
+
await server.start();
|
|
881
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid?excludeTypes=settled`);
|
|
882
|
+
const mermaid = await res.text();
|
|
883
|
+
expect(mermaid).not.toContain('settled_');
|
|
884
|
+
expect(mermaid).toContain('CheckA');
|
|
885
|
+
await server.stop();
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('should use displayName as label for command nodes in mermaid diagram', async () => {
|
|
889
|
+
const handler = {
|
|
890
|
+
name: 'Cmd',
|
|
891
|
+
displayName: 'My Command',
|
|
892
|
+
handle: async () => ({ type: 'Done', data: {} }),
|
|
893
|
+
};
|
|
894
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).build();
|
|
895
|
+
const server = new PipelineServer({ port: 0 });
|
|
896
|
+
server.registerCommandHandlers([handler]);
|
|
897
|
+
server.registerPipeline(pipeline);
|
|
898
|
+
await server.start();
|
|
899
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
900
|
+
const mermaid = await res.text();
|
|
901
|
+
expect(mermaid).toContain('Cmd[My Command]');
|
|
902
|
+
await server.stop();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('should include event nodes in mermaid diagram', async () => {
|
|
906
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
907
|
+
const server = new PipelineServer({ port: 0 });
|
|
908
|
+
server.registerPipeline(pipeline);
|
|
909
|
+
await server.start();
|
|
910
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
911
|
+
const mermaid = await res.text();
|
|
912
|
+
expect(mermaid).toContain('evt_Start');
|
|
913
|
+
await server.stop();
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should use displayName as label for event nodes in mermaid diagram', async () => {
|
|
917
|
+
const handler = {
|
|
918
|
+
name: 'Cmd',
|
|
919
|
+
events: [{ name: 'CmdCompleted', displayName: 'Command Completed' }],
|
|
920
|
+
handle: async () => ({ type: 'CmdCompleted', data: {} }),
|
|
921
|
+
};
|
|
922
|
+
const nextHandler = {
|
|
923
|
+
name: 'NextCmd',
|
|
924
|
+
handle: async () => ({ type: 'Done', data: {} }),
|
|
925
|
+
};
|
|
926
|
+
const pipeline = define('test').on('Start').emit('Cmd', {}).on('CmdCompleted').emit('NextCmd', {}).build();
|
|
927
|
+
const server = new PipelineServer({ port: 0 });
|
|
928
|
+
server.registerCommandHandlers([handler, nextHandler]);
|
|
929
|
+
server.registerPipeline(pipeline);
|
|
930
|
+
await server.start();
|
|
931
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
932
|
+
const mermaid = await res.text();
|
|
933
|
+
expect(mermaid).toContain('evt_CmdCompleted([Command Completed])');
|
|
934
|
+
await server.stop();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it('should include command nodes in mermaid diagram', async () => {
|
|
938
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
939
|
+
const server = new PipelineServer({ port: 0 });
|
|
940
|
+
server.registerPipeline(pipeline);
|
|
941
|
+
await server.start();
|
|
942
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
943
|
+
const mermaid = await res.text();
|
|
944
|
+
expect(mermaid).toContain('Process[Process]');
|
|
945
|
+
await server.stop();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should include edges in mermaid diagram', async () => {
|
|
949
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
950
|
+
const server = new PipelineServer({ port: 0 });
|
|
951
|
+
server.registerPipeline(pipeline);
|
|
952
|
+
await server.start();
|
|
953
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
954
|
+
const mermaid = await res.text();
|
|
955
|
+
expect(mermaid).toContain('-->');
|
|
956
|
+
await server.stop();
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('should style commands as blue and events as orange', async () => {
|
|
960
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
961
|
+
const server = new PipelineServer({ port: 0 });
|
|
406
962
|
server.registerPipeline(pipeline);
|
|
407
963
|
await server.start();
|
|
408
964
|
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
@@ -560,7 +1116,7 @@ describe('PipelineServer', () => {
|
|
|
560
1116
|
await server.stop();
|
|
561
1117
|
});
|
|
562
1118
|
|
|
563
|
-
it('should show edges from
|
|
1119
|
+
it('should show edges from commands to settled node', async () => {
|
|
564
1120
|
const checkAHandler = {
|
|
565
1121
|
name: 'CheckA',
|
|
566
1122
|
events: ['CheckAPassed', 'CheckAFailed'],
|
|
@@ -584,12 +1140,8 @@ describe('PipelineServer', () => {
|
|
|
584
1140
|
await server.start();
|
|
585
1141
|
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
586
1142
|
const mermaid = await res.text();
|
|
587
|
-
expect(mermaid).toContain('
|
|
588
|
-
expect(mermaid).toContain('
|
|
589
|
-
expect(mermaid).toContain('evt_CheckBPassed --> settled_CheckA_CheckB');
|
|
590
|
-
expect(mermaid).toContain('evt_CheckBFailed --> settled_CheckA_CheckB');
|
|
591
|
-
expect(mermaid).not.toMatch(/CheckA --> settled_/);
|
|
592
|
-
expect(mermaid).not.toMatch(/CheckB --> settled_/);
|
|
1143
|
+
expect(mermaid).toContain('CheckA --> settled_CheckA_CheckB');
|
|
1144
|
+
expect(mermaid).toContain('CheckB --> settled_CheckA_CheckB');
|
|
593
1145
|
await server.stop();
|
|
594
1146
|
});
|
|
595
1147
|
|
|
@@ -604,126 +1156,642 @@ describe('PipelineServer', () => {
|
|
|
604
1156
|
events: ['RetryDone'],
|
|
605
1157
|
handle: async () => ({ type: 'RetryDone', data: {} }),
|
|
606
1158
|
};
|
|
607
|
-
const pipeline = define('test')
|
|
608
|
-
.on('Start')
|
|
609
|
-
.emit('CheckA', {})
|
|
610
|
-
.settled(['CheckA'])
|
|
611
|
-
.dispatch({ dispatches: ['RetryCommand'] }, () => {})
|
|
612
|
-
.build();
|
|
1159
|
+
const pipeline = define('test')
|
|
1160
|
+
.on('Start')
|
|
1161
|
+
.emit('CheckA', {})
|
|
1162
|
+
.settled(['CheckA'])
|
|
1163
|
+
.dispatch({ dispatches: ['RetryCommand'] }, () => {})
|
|
1164
|
+
.build();
|
|
1165
|
+
const server = new PipelineServer({ port: 0 });
|
|
1166
|
+
server.registerCommandHandlers([checkHandler, retryHandler]);
|
|
1167
|
+
server.registerPipeline(pipeline);
|
|
1168
|
+
await server.start();
|
|
1169
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
1170
|
+
const mermaid = await res.text();
|
|
1171
|
+
expect(mermaid).toContain('settled_CheckA -.->|retry| RetryCommand');
|
|
1172
|
+
await server.stop();
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('should style backLink edges in red', async () => {
|
|
1176
|
+
const checkHandler = {
|
|
1177
|
+
name: 'CheckA',
|
|
1178
|
+
events: ['CheckAPassed', 'CheckAFailed'],
|
|
1179
|
+
handle: async () => ({ type: 'CheckAPassed', data: {} }),
|
|
1180
|
+
};
|
|
1181
|
+
const retryHandler = {
|
|
1182
|
+
name: 'RetryCommand',
|
|
1183
|
+
events: ['RetryDone'],
|
|
1184
|
+
handle: async () => ({ type: 'RetryDone', data: {} }),
|
|
1185
|
+
};
|
|
1186
|
+
const pipeline = define('test')
|
|
1187
|
+
.on('Start')
|
|
1188
|
+
.emit('CheckA', {})
|
|
1189
|
+
.settled(['CheckA'])
|
|
1190
|
+
.dispatch({ dispatches: ['RetryCommand'] }, () => {})
|
|
1191
|
+
.build();
|
|
1192
|
+
const server = new PipelineServer({ port: 0 });
|
|
1193
|
+
server.registerCommandHandlers([checkHandler, retryHandler]);
|
|
1194
|
+
server.registerPipeline(pipeline);
|
|
1195
|
+
await server.start();
|
|
1196
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
1197
|
+
const mermaid = await res.text();
|
|
1198
|
+
expect(mermaid).toContain('linkStyle');
|
|
1199
|
+
expect(mermaid).toMatch(/stroke:#[a-fA-F0-9]{6}|stroke:red/);
|
|
1200
|
+
await server.stop();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('should mark event-to-command edges as backLink when they create cycles', async () => {
|
|
1204
|
+
const generateHandler = {
|
|
1205
|
+
name: 'GenerateIA',
|
|
1206
|
+
events: ['IAGenerated', 'IAValidationFailed'],
|
|
1207
|
+
handle: async () => ({ type: 'IAGenerated', data: {} }),
|
|
1208
|
+
};
|
|
1209
|
+
const pipeline = define('test')
|
|
1210
|
+
.on('Start')
|
|
1211
|
+
.emit('GenerateIA', {})
|
|
1212
|
+
.on('IAValidationFailed')
|
|
1213
|
+
.emit('GenerateIA', {})
|
|
1214
|
+
.build();
|
|
1215
|
+
const server = new PipelineServer({ port: 0 });
|
|
1216
|
+
server.registerCommandHandlers([generateHandler]);
|
|
1217
|
+
server.registerPipeline(pipeline);
|
|
1218
|
+
await server.start();
|
|
1219
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1220
|
+
const backLinkEdge = data.edges.find((e) => e.from === 'evt:IAValidationFailed' && e.to === 'cmd:GenerateIA');
|
|
1221
|
+
expect(backLinkEdge).toBeDefined();
|
|
1222
|
+
expect(backLinkEdge?.backLink).toBe(true);
|
|
1223
|
+
await server.stop();
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
it('should NOT mark forward edges as backLink when cycle is broken by settled dispatch', async () => {
|
|
1227
|
+
const implHandler = {
|
|
1228
|
+
name: 'ImplementSlice',
|
|
1229
|
+
events: ['SliceImplemented'],
|
|
1230
|
+
handle: async () => ({ type: 'SliceImplemented', data: {} }),
|
|
1231
|
+
};
|
|
1232
|
+
const checkHandler = {
|
|
1233
|
+
name: 'CheckTests',
|
|
1234
|
+
events: ['TestsCheckPassed', 'TestsCheckFailed'],
|
|
1235
|
+
handle: async () => ({ type: 'TestsCheckPassed', data: {} }),
|
|
1236
|
+
};
|
|
1237
|
+
const pipeline = define('test')
|
|
1238
|
+
.on('Start')
|
|
1239
|
+
.emit('ImplementSlice', {})
|
|
1240
|
+
.on('SliceImplemented')
|
|
1241
|
+
.emit('CheckTests', {})
|
|
1242
|
+
.settled(['CheckTests'])
|
|
1243
|
+
.dispatch({ dispatches: ['ImplementSlice'] }, () => {})
|
|
1244
|
+
.build();
|
|
1245
|
+
const server = new PipelineServer({ port: 0 });
|
|
1246
|
+
server.registerCommandHandlers([implHandler, checkHandler]);
|
|
1247
|
+
server.registerPipeline(pipeline);
|
|
1248
|
+
await server.start();
|
|
1249
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1250
|
+
const forwardEdge = data.edges.find((e) => e.from === 'evt:SliceImplemented' && e.to === 'cmd:CheckTests');
|
|
1251
|
+
expect(forwardEdge).toBeDefined();
|
|
1252
|
+
expect(forwardEdge?.backLink).toBeUndefined();
|
|
1253
|
+
await server.stop();
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it('should handle diamond graph patterns when detecting backlinks', async () => {
|
|
1257
|
+
const cmdAHandler = {
|
|
1258
|
+
name: 'CmdA',
|
|
1259
|
+
events: ['EventA'],
|
|
1260
|
+
handle: async () => ({ type: 'EventA', data: {} }),
|
|
1261
|
+
};
|
|
1262
|
+
const cmdBHandler = {
|
|
1263
|
+
name: 'CmdB',
|
|
1264
|
+
events: ['EventB'],
|
|
1265
|
+
handle: async () => ({ type: 'EventB', data: {} }),
|
|
1266
|
+
};
|
|
1267
|
+
const cmdCHandler = {
|
|
1268
|
+
name: 'CmdC',
|
|
1269
|
+
events: ['EventC'],
|
|
1270
|
+
handle: async () => ({ type: 'EventC', data: {} }),
|
|
1271
|
+
};
|
|
1272
|
+
const cmdDHandler = {
|
|
1273
|
+
name: 'CmdD',
|
|
1274
|
+
events: ['EventD'],
|
|
1275
|
+
handle: async () => ({ type: 'EventD', data: {} }),
|
|
1276
|
+
};
|
|
1277
|
+
const pipeline = define('test')
|
|
1278
|
+
.on('Start')
|
|
1279
|
+
.emit('CmdA', {})
|
|
1280
|
+
.on('EventA')
|
|
1281
|
+
.emit('CmdB', {})
|
|
1282
|
+
.on('EventA')
|
|
1283
|
+
.emit('CmdC', {})
|
|
1284
|
+
.on('EventB')
|
|
1285
|
+
.emit('CmdD', {})
|
|
1286
|
+
.on('EventC')
|
|
1287
|
+
.emit('CmdD', {})
|
|
1288
|
+
.on('EventD')
|
|
1289
|
+
.emit('CmdA', {})
|
|
1290
|
+
.build();
|
|
1291
|
+
const server = new PipelineServer({ port: 0 });
|
|
1292
|
+
server.registerCommandHandlers([cmdAHandler, cmdBHandler, cmdCHandler, cmdDHandler]);
|
|
1293
|
+
server.registerPipeline(pipeline);
|
|
1294
|
+
await server.start();
|
|
1295
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1296
|
+
const backLinkEdge = data.edges.find((e) => e.from === 'evt:EventD' && e.to === 'cmd:CmdA');
|
|
1297
|
+
expect(backLinkEdge).toBeDefined();
|
|
1298
|
+
expect(backLinkEdge?.backLink).toBe(true);
|
|
1299
|
+
await server.stop();
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('should add event nodes from settled handler commandToEvents when not already added', async () => {
|
|
1303
|
+
const checkAHandler = {
|
|
1304
|
+
name: 'CheckA',
|
|
1305
|
+
events: ['CheckAPassed', 'CheckAFailed'],
|
|
1306
|
+
handle: async () => ({ type: 'CheckAPassed', data: {} }),
|
|
1307
|
+
};
|
|
1308
|
+
const checkBHandler = {
|
|
1309
|
+
name: 'CheckB',
|
|
1310
|
+
events: ['CheckBPassed', 'CheckBFailed'],
|
|
1311
|
+
handle: async () => ({ type: 'CheckBPassed', data: {} }),
|
|
1312
|
+
};
|
|
1313
|
+
const pipeline = define('test')
|
|
1314
|
+
.on('Start')
|
|
1315
|
+
.emit('CheckA', {})
|
|
1316
|
+
.emit('CheckB', {})
|
|
1317
|
+
.settled(['CheckA', 'CheckB'])
|
|
1318
|
+
.dispatch({ dispatches: [] }, () => {})
|
|
1319
|
+
.build();
|
|
1320
|
+
const server = new PipelineServer({ port: 0 });
|
|
1321
|
+
server.registerCommandHandlers([checkAHandler, checkBHandler]);
|
|
1322
|
+
server.registerPipeline(pipeline);
|
|
1323
|
+
await server.start();
|
|
1324
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/mermaid`);
|
|
1325
|
+
const mermaid = await res.text();
|
|
1326
|
+
expect(mermaid).toContain('evt_CheckAPassed');
|
|
1327
|
+
expect(mermaid).toContain('evt_CheckAFailed');
|
|
1328
|
+
expect(mermaid).toContain('evt_CheckBPassed');
|
|
1329
|
+
expect(mermaid).toContain('evt_CheckBFailed');
|
|
1330
|
+
await server.stop();
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
describe('GET /pipeline/diagram', () => {
|
|
1335
|
+
it('should return HTML content type', async () => {
|
|
1336
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
1337
|
+
const server = new PipelineServer({ port: 0 });
|
|
1338
|
+
server.registerPipeline(pipeline);
|
|
1339
|
+
await server.start();
|
|
1340
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
|
|
1341
|
+
expect(res.headers.get('content-type')).toContain('text/html');
|
|
1342
|
+
await server.stop();
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('should filter nodes when excludeTypes is provided', async () => {
|
|
1346
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
1347
|
+
const server = new PipelineServer({ port: 0 });
|
|
1348
|
+
server.registerPipeline(pipeline);
|
|
1349
|
+
await server.start();
|
|
1350
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/diagram?excludeTypes=event`);
|
|
1351
|
+
const html = await res.text();
|
|
1352
|
+
expect(html).not.toContain('evt_Start');
|
|
1353
|
+
expect(html).toContain('Process');
|
|
1354
|
+
await server.stop();
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it('should include mermaid.js script', async () => {
|
|
1358
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
1359
|
+
const server = new PipelineServer({ port: 0 });
|
|
1360
|
+
server.registerPipeline(pipeline);
|
|
1361
|
+
await server.start();
|
|
1362
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
|
|
1363
|
+
const html = await res.text();
|
|
1364
|
+
expect(html).toContain('mermaid');
|
|
1365
|
+
await server.stop();
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('should include the pipeline mermaid definition', async () => {
|
|
1369
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
1370
|
+
const server = new PipelineServer({ port: 0 });
|
|
1371
|
+
server.registerPipeline(pipeline);
|
|
1372
|
+
await server.start();
|
|
1373
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
|
|
1374
|
+
const html = await res.text();
|
|
1375
|
+
expect(html).toContain('flowchart LR');
|
|
1376
|
+
expect(html).toContain('evt_Start');
|
|
1377
|
+
await server.stop();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it('should have a valid HTML structure', async () => {
|
|
1381
|
+
const pipeline = define('test').on('Start').emit('Process', {}).build();
|
|
1382
|
+
const server = new PipelineServer({ port: 0 });
|
|
1383
|
+
server.registerPipeline(pipeline);
|
|
1384
|
+
await server.start();
|
|
1385
|
+
const res = await fetch(`http://localhost:${server.port}/pipeline/diagram`);
|
|
1386
|
+
const html = await res.text();
|
|
1387
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
1388
|
+
expect(html).toContain('<html');
|
|
1389
|
+
expect(html).toContain('</html>');
|
|
1390
|
+
await server.stop();
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
describe('item-level tracking', () => {
|
|
1395
|
+
it('should extract itemKey from command data using registered extractor', async () => {
|
|
1396
|
+
const handler = {
|
|
1397
|
+
name: 'ImplementSlice',
|
|
1398
|
+
events: ['SliceImplemented'],
|
|
1399
|
+
handle: async () => ({ type: 'SliceImplemented', data: {} }),
|
|
1400
|
+
};
|
|
1401
|
+
const pipeline = define('test').on('Trigger').emit('ImplementSlice', {}).build();
|
|
1402
|
+
const server = new PipelineServer({ port: 0 });
|
|
1403
|
+
server.registerCommandHandlers([handler]);
|
|
1404
|
+
server.registerPipeline(pipeline);
|
|
1405
|
+
server.registerItemKeyExtractor('ImplementSlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
1406
|
+
await server.start();
|
|
1407
|
+
|
|
1408
|
+
const commandResponse = await fetchAs<{ correlationId: string }>(`http://localhost:${server.port}/command`, {
|
|
1409
|
+
method: 'POST',
|
|
1410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1411
|
+
body: JSON.stringify({ type: 'ImplementSlice', data: { slicePath: '/server/slice-1' } }),
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1415
|
+
|
|
1416
|
+
const data = await fetchAs<PipelineResponse>(
|
|
1417
|
+
`http://localhost:${server.port}/pipeline?correlationId=${commandResponse.correlationId}`,
|
|
1418
|
+
);
|
|
1419
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
|
|
1420
|
+
expect(cmdNode?.pendingCount).toBe(0);
|
|
1421
|
+
expect(cmdNode?.endedCount).toBe(1);
|
|
1422
|
+
|
|
1423
|
+
await server.stop();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('should count multiple parallel items correctly', async () => {
|
|
1427
|
+
const handler = {
|
|
1428
|
+
name: 'ImplementSlice',
|
|
1429
|
+
events: ['SliceImplemented'],
|
|
1430
|
+
handle: async () => ({ type: 'SliceImplemented', data: {} }),
|
|
1431
|
+
};
|
|
1432
|
+
const pipeline = define('test').on('Trigger').emit('ImplementSlice', {}).build();
|
|
1433
|
+
const server = new PipelineServer({ port: 0 });
|
|
1434
|
+
server.registerCommandHandlers([handler]);
|
|
1435
|
+
server.registerPipeline(pipeline);
|
|
1436
|
+
server.registerItemKeyExtractor('ImplementSlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
1437
|
+
await server.start();
|
|
1438
|
+
|
|
1439
|
+
const correlationId = `corr-parallel-test`;
|
|
1440
|
+
|
|
1441
|
+
await Promise.all([
|
|
1442
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1443
|
+
method: 'POST',
|
|
1444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
+
body: JSON.stringify({
|
|
1446
|
+
type: 'ImplementSlice',
|
|
1447
|
+
data: { slicePath: '/server/slice-1' },
|
|
1448
|
+
correlationId,
|
|
1449
|
+
}),
|
|
1450
|
+
}),
|
|
1451
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1452
|
+
method: 'POST',
|
|
1453
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1454
|
+
body: JSON.stringify({
|
|
1455
|
+
type: 'ImplementSlice',
|
|
1456
|
+
data: { slicePath: '/server/slice-2' },
|
|
1457
|
+
correlationId,
|
|
1458
|
+
}),
|
|
1459
|
+
}),
|
|
1460
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1461
|
+
method: 'POST',
|
|
1462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1463
|
+
body: JSON.stringify({
|
|
1464
|
+
type: 'ImplementSlice',
|
|
1465
|
+
data: { slicePath: '/server/slice-3' },
|
|
1466
|
+
correlationId,
|
|
1467
|
+
}),
|
|
1468
|
+
}),
|
|
1469
|
+
]);
|
|
1470
|
+
|
|
1471
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1472
|
+
|
|
1473
|
+
const data = await fetchAs<PipelineResponse>(
|
|
1474
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1475
|
+
);
|
|
1476
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:ImplementSlice');
|
|
1477
|
+
expect(cmdNode?.pendingCount).toBe(0);
|
|
1478
|
+
expect(cmdNode?.endedCount).toBe(3);
|
|
1479
|
+
|
|
1480
|
+
await server.stop();
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it('should show pending count while commands are running', async () => {
|
|
1484
|
+
const resolveHandlers: Array<() => void> = [];
|
|
1485
|
+
const handler = {
|
|
1486
|
+
name: 'SlowSlice',
|
|
1487
|
+
events: ['SlowSliceDone'],
|
|
1488
|
+
handle: async () => {
|
|
1489
|
+
await new Promise<void>((resolve) => {
|
|
1490
|
+
resolveHandlers.push(resolve);
|
|
1491
|
+
});
|
|
1492
|
+
return { type: 'SlowSliceDone', data: {} };
|
|
1493
|
+
},
|
|
1494
|
+
};
|
|
1495
|
+
const pipeline = define('test').on('Trigger').emit('SlowSlice', {}).build();
|
|
613
1496
|
const server = new PipelineServer({ port: 0 });
|
|
614
|
-
server.registerCommandHandlers([
|
|
1497
|
+
server.registerCommandHandlers([handler]);
|
|
615
1498
|
server.registerPipeline(pipeline);
|
|
1499
|
+
server.registerItemKeyExtractor('SlowSlice', (d) => (d as { id?: string }).id);
|
|
616
1500
|
await server.start();
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
1501
|
+
|
|
1502
|
+
const correlationId = `corr-slow-test`;
|
|
1503
|
+
|
|
1504
|
+
void fetch(`http://localhost:${server.port}/command`, {
|
|
1505
|
+
method: 'POST',
|
|
1506
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1507
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-1' }, correlationId }),
|
|
1508
|
+
});
|
|
1509
|
+
void fetch(`http://localhost:${server.port}/command`, {
|
|
1510
|
+
method: 'POST',
|
|
1511
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1512
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-2' }, correlationId }),
|
|
1513
|
+
});
|
|
1514
|
+
void fetch(`http://localhost:${server.port}/command`, {
|
|
1515
|
+
method: 'POST',
|
|
1516
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1517
|
+
body: JSON.stringify({ type: 'SlowSlice', data: { id: 'item-3' }, correlationId }),
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1521
|
+
|
|
1522
|
+
const midwayData = await fetchAs<PipelineResponse>(
|
|
1523
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1524
|
+
);
|
|
1525
|
+
const midwayNode = midwayData.nodes.find((n) => n.id === 'cmd:SlowSlice');
|
|
1526
|
+
expect(midwayNode?.pendingCount).toBe(3);
|
|
1527
|
+
expect(midwayNode?.endedCount).toBe(0);
|
|
1528
|
+
expect(midwayNode?.status).toBe('running');
|
|
1529
|
+
|
|
1530
|
+
resolveHandlers.forEach((r) => r());
|
|
1531
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1532
|
+
|
|
1533
|
+
const finalData = await fetchAs<PipelineResponse>(
|
|
1534
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1535
|
+
);
|
|
1536
|
+
const finalNode = finalData.nodes.find((n) => n.id === 'cmd:SlowSlice');
|
|
1537
|
+
expect(finalNode?.pendingCount).toBe(0);
|
|
1538
|
+
expect(finalNode?.endedCount).toBe(3);
|
|
1539
|
+
expect(finalNode?.status).toBe('success');
|
|
1540
|
+
|
|
620
1541
|
await server.stop();
|
|
621
1542
|
});
|
|
622
1543
|
|
|
623
|
-
it('should
|
|
624
|
-
const
|
|
625
|
-
name: '
|
|
626
|
-
events: ['
|
|
627
|
-
handle: async (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1544
|
+
it('should show error status when any item fails', async () => {
|
|
1545
|
+
const handler = {
|
|
1546
|
+
name: 'MixedSlice',
|
|
1547
|
+
events: ['MixedSliceDone', 'MixedSliceFailed'],
|
|
1548
|
+
handle: async (cmd: { data: { shouldFail?: boolean } }) => {
|
|
1549
|
+
if (cmd.data.shouldFail === true) {
|
|
1550
|
+
return { type: 'MixedSliceFailed', data: {} };
|
|
1551
|
+
}
|
|
1552
|
+
return { type: 'MixedSliceDone', data: {} };
|
|
1553
|
+
},
|
|
633
1554
|
};
|
|
634
|
-
const pipeline = define('test')
|
|
635
|
-
.on('Start')
|
|
636
|
-
.emit('CheckA', {})
|
|
637
|
-
.settled(['CheckA'])
|
|
638
|
-
.dispatch({ dispatches: ['RetryCommand'] }, () => {})
|
|
639
|
-
.build();
|
|
1555
|
+
const pipeline = define('test').on('Trigger').emit('MixedSlice', {}).build();
|
|
640
1556
|
const server = new PipelineServer({ port: 0 });
|
|
641
|
-
server.registerCommandHandlers([
|
|
1557
|
+
server.registerCommandHandlers([handler]);
|
|
642
1558
|
server.registerPipeline(pipeline);
|
|
1559
|
+
server.registerItemKeyExtractor('MixedSlice', (d) => (d as { id?: string }).id);
|
|
643
1560
|
await server.start();
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
1561
|
+
|
|
1562
|
+
const correlationId = `corr-mixed-test`;
|
|
1563
|
+
|
|
1564
|
+
await Promise.all([
|
|
1565
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1566
|
+
method: 'POST',
|
|
1567
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1568
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-1' }, correlationId }),
|
|
1569
|
+
}),
|
|
1570
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1571
|
+
method: 'POST',
|
|
1572
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1573
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'fail-1', shouldFail: true }, correlationId }),
|
|
1574
|
+
}),
|
|
1575
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
1576
|
+
method: 'POST',
|
|
1577
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1578
|
+
body: JSON.stringify({ type: 'MixedSlice', data: { id: 'pass-2' }, correlationId }),
|
|
1579
|
+
}),
|
|
1580
|
+
]);
|
|
1581
|
+
|
|
1582
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1583
|
+
|
|
1584
|
+
const data = await fetchAs<PipelineResponse>(
|
|
1585
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1586
|
+
);
|
|
1587
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:MixedSlice');
|
|
1588
|
+
expect(cmdNode?.pendingCount).toBe(0);
|
|
1589
|
+
expect(cmdNode?.endedCount).toBe(3);
|
|
1590
|
+
expect(cmdNode?.status).toBe('error');
|
|
1591
|
+
|
|
648
1592
|
await server.stop();
|
|
649
1593
|
});
|
|
650
1594
|
|
|
651
|
-
it('should
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1595
|
+
it('should reset item to running when retry command arrives for same itemKey', async () => {
|
|
1596
|
+
let attemptCount = 0;
|
|
1597
|
+
const handler = {
|
|
1598
|
+
name: 'RetrySlice',
|
|
1599
|
+
events: ['RetrySliceDone', 'RetrySliceFailed'],
|
|
1600
|
+
handle: async () => {
|
|
1601
|
+
attemptCount++;
|
|
1602
|
+
if (attemptCount === 1) {
|
|
1603
|
+
return { type: 'RetrySliceFailed', data: {} };
|
|
1604
|
+
}
|
|
1605
|
+
return { type: 'RetrySliceDone', data: {} };
|
|
1606
|
+
},
|
|
661
1607
|
};
|
|
662
|
-
const pipeline = define('test')
|
|
663
|
-
.on('Start')
|
|
664
|
-
.emit('CheckA', {})
|
|
665
|
-
.emit('CheckB', {})
|
|
666
|
-
.settled(['CheckA', 'CheckB'])
|
|
667
|
-
.dispatch({ dispatches: [] }, () => {})
|
|
668
|
-
.build();
|
|
1608
|
+
const pipeline = define('test').on('Trigger').emit('RetrySlice', {}).build();
|
|
669
1609
|
const server = new PipelineServer({ port: 0 });
|
|
670
|
-
server.registerCommandHandlers([
|
|
1610
|
+
server.registerCommandHandlers([handler]);
|
|
671
1611
|
server.registerPipeline(pipeline);
|
|
1612
|
+
server.registerItemKeyExtractor('RetrySlice', (d) => (d as { slicePath?: string }).slicePath);
|
|
672
1613
|
await server.start();
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1614
|
+
|
|
1615
|
+
const correlationId = `corr-retry-test`;
|
|
1616
|
+
const slicePath = '/server/retry-slice';
|
|
1617
|
+
|
|
1618
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1619
|
+
method: 'POST',
|
|
1620
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1621
|
+
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }, correlationId }),
|
|
1622
|
+
});
|
|
1623
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1624
|
+
|
|
1625
|
+
const afterFailure = await fetchAs<PipelineResponse>(
|
|
1626
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1627
|
+
);
|
|
1628
|
+
expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetrySlice')?.status).toBe('error');
|
|
1629
|
+
|
|
1630
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1631
|
+
method: 'POST',
|
|
1632
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1633
|
+
body: JSON.stringify({ type: 'RetrySlice', data: { slicePath }, correlationId }),
|
|
1634
|
+
});
|
|
1635
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1636
|
+
|
|
1637
|
+
const afterRetry = await fetchAs<PipelineResponse>(
|
|
1638
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1639
|
+
);
|
|
1640
|
+
const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetrySlice');
|
|
1641
|
+
expect(node?.status).toBe('success');
|
|
1642
|
+
expect(node?.pendingCount).toBe(0);
|
|
1643
|
+
expect(node?.endedCount).toBe(1);
|
|
1644
|
+
|
|
679
1645
|
await server.stop();
|
|
680
1646
|
});
|
|
681
|
-
});
|
|
682
1647
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1648
|
+
it('should include pendingCount and endedCount in NodeStatusChanged events', async () => {
|
|
1649
|
+
const handler = {
|
|
1650
|
+
name: 'CountSlice',
|
|
1651
|
+
events: ['CountSliceDone'],
|
|
1652
|
+
handle: async () => ({ type: 'CountSliceDone', data: {} }),
|
|
1653
|
+
};
|
|
1654
|
+
const pipeline = define('test').on('Trigger').emit('CountSlice', {}).build();
|
|
686
1655
|
const server = new PipelineServer({ port: 0 });
|
|
1656
|
+
server.registerCommandHandlers([handler]);
|
|
687
1657
|
server.registerPipeline(pipeline);
|
|
1658
|
+
server.registerItemKeyExtractor('CountSlice', (d) => (d as { id?: string }).id);
|
|
688
1659
|
await server.start();
|
|
689
|
-
|
|
690
|
-
|
|
1660
|
+
|
|
1661
|
+
const correlationId = `corr-counts-event-test`;
|
|
1662
|
+
|
|
1663
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1664
|
+
method: 'POST',
|
|
1665
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1666
|
+
body: JSON.stringify({ type: 'CountSlice', data: { id: 'item-1' }, correlationId }),
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1670
|
+
|
|
1671
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
1672
|
+
type NodeStatusChangedMessage = {
|
|
1673
|
+
type: string;
|
|
1674
|
+
correlationId?: string;
|
|
1675
|
+
data?: {
|
|
1676
|
+
nodeId?: string;
|
|
1677
|
+
status?: string;
|
|
1678
|
+
previousStatus?: string;
|
|
1679
|
+
pendingCount?: number;
|
|
1680
|
+
endedCount?: number;
|
|
1681
|
+
};
|
|
1682
|
+
};
|
|
1683
|
+
const nodeStatusChanged = msgs.filter((m) => m.message.type === 'NodeStatusChanged');
|
|
1684
|
+
const successEvent = nodeStatusChanged.find(
|
|
1685
|
+
(m) => (m.message as NodeStatusChangedMessage).data?.status === 'success',
|
|
1686
|
+
);
|
|
1687
|
+
expect(successEvent).toBeDefined();
|
|
1688
|
+
expect((successEvent?.message as NodeStatusChangedMessage).data?.pendingCount).toBe(0);
|
|
1689
|
+
expect((successEvent?.message as NodeStatusChangedMessage).data?.endedCount).toBe(1);
|
|
1690
|
+
|
|
691
1691
|
await server.stop();
|
|
692
1692
|
});
|
|
693
1693
|
|
|
694
|
-
it('should
|
|
695
|
-
const
|
|
1694
|
+
it('should use requestId as fallback when no itemKey extractor is registered', async () => {
|
|
1695
|
+
const handler = {
|
|
1696
|
+
name: 'NoExtractorCmd',
|
|
1697
|
+
events: ['NoExtractorDone'],
|
|
1698
|
+
handle: async () => ({ type: 'NoExtractorDone', data: {} }),
|
|
1699
|
+
};
|
|
1700
|
+
const pipeline = define('test').on('Trigger').emit('NoExtractorCmd', {}).build();
|
|
696
1701
|
const server = new PipelineServer({ port: 0 });
|
|
1702
|
+
server.registerCommandHandlers([handler]);
|
|
697
1703
|
server.registerPipeline(pipeline);
|
|
698
1704
|
await server.start();
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
1705
|
+
|
|
1706
|
+
const correlationId = `corr-no-extractor-test`;
|
|
1707
|
+
|
|
1708
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1709
|
+
method: 'POST',
|
|
1710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1711
|
+
body: JSON.stringify({ type: 'NoExtractorCmd', data: {}, correlationId }),
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1715
|
+
|
|
1716
|
+
const data = await fetchAs<PipelineResponse>(
|
|
1717
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1718
|
+
);
|
|
1719
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:NoExtractorCmd');
|
|
1720
|
+
expect(cmdNode?.pendingCount).toBe(0);
|
|
1721
|
+
expect(cmdNode?.endedCount).toBe(1);
|
|
1722
|
+
expect(cmdNode?.status).toBe('success');
|
|
1723
|
+
|
|
702
1724
|
await server.stop();
|
|
703
1725
|
});
|
|
704
1726
|
|
|
705
|
-
it('should
|
|
706
|
-
const
|
|
1727
|
+
it('should show idle status with zero counts when no correlationId provided', async () => {
|
|
1728
|
+
const handler = {
|
|
1729
|
+
name: 'IdleCountCmd',
|
|
1730
|
+
events: ['IdleCountDone'],
|
|
1731
|
+
handle: async () => ({ type: 'IdleCountDone', data: {} }),
|
|
1732
|
+
};
|
|
1733
|
+
const pipeline = define('test').on('Trigger').emit('IdleCountCmd', {}).build();
|
|
707
1734
|
const server = new PipelineServer({ port: 0 });
|
|
1735
|
+
server.registerCommandHandlers([handler]);
|
|
708
1736
|
server.registerPipeline(pipeline);
|
|
709
1737
|
await server.start();
|
|
710
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
expect(
|
|
1738
|
+
|
|
1739
|
+
const data = await fetchAs<PipelineResponse>(`http://localhost:${server.port}/pipeline`);
|
|
1740
|
+
const cmdNode = data.nodes.find((n) => n.id === 'cmd:IdleCountCmd');
|
|
1741
|
+
expect(cmdNode?.status).toBe('idle');
|
|
1742
|
+
expect(cmdNode?.pendingCount).toBe(0);
|
|
1743
|
+
expect(cmdNode?.endedCount).toBe(0);
|
|
1744
|
+
|
|
714
1745
|
await server.stop();
|
|
715
1746
|
});
|
|
716
1747
|
|
|
717
|
-
it('
|
|
718
|
-
|
|
1748
|
+
it('documents behavior: status remains error after retry without itemKey extractor (fix: register extractor)', async () => {
|
|
1749
|
+
let callCount = 0;
|
|
1750
|
+
const handler = {
|
|
1751
|
+
name: 'RetryNoExtractor',
|
|
1752
|
+
events: ['RetryNoExtractorDone', 'RetryNoExtractorFailed'],
|
|
1753
|
+
handle: async () => {
|
|
1754
|
+
callCount++;
|
|
1755
|
+
if (callCount === 1) {
|
|
1756
|
+
return { type: 'RetryNoExtractorFailed', data: {} };
|
|
1757
|
+
}
|
|
1758
|
+
return { type: 'RetryNoExtractorDone', data: {} };
|
|
1759
|
+
},
|
|
1760
|
+
};
|
|
1761
|
+
const pipeline = define('test').on('Trigger').emit('RetryNoExtractor', {}).build();
|
|
719
1762
|
const server = new PipelineServer({ port: 0 });
|
|
1763
|
+
server.registerCommandHandlers([handler]);
|
|
720
1764
|
server.registerPipeline(pipeline);
|
|
721
1765
|
await server.start();
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1766
|
+
|
|
1767
|
+
const correlationId = `corr-retry-no-extractor-bug`;
|
|
1768
|
+
|
|
1769
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1770
|
+
method: 'POST',
|
|
1771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1772
|
+
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }, correlationId }),
|
|
1773
|
+
});
|
|
1774
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1775
|
+
|
|
1776
|
+
const afterFailure = await fetchAs<PipelineResponse>(
|
|
1777
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1778
|
+
);
|
|
1779
|
+
expect(afterFailure.nodes.find((n) => n.id === 'cmd:RetryNoExtractor')?.status).toBe('error');
|
|
1780
|
+
|
|
1781
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1782
|
+
method: 'POST',
|
|
1783
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1784
|
+
body: JSON.stringify({ type: 'RetryNoExtractor', data: { targetDir: '/slice1' }, correlationId }),
|
|
1785
|
+
});
|
|
1786
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1787
|
+
|
|
1788
|
+
const afterRetry = await fetchAs<PipelineResponse>(
|
|
1789
|
+
`http://localhost:${server.port}/pipeline?correlationId=${correlationId}`,
|
|
1790
|
+
);
|
|
1791
|
+
const node = afterRetry.nodes.find((n) => n.id === 'cmd:RetryNoExtractor');
|
|
1792
|
+
expect(node?.status).toBe('error');
|
|
1793
|
+
expect(node?.endedCount).toBe(2);
|
|
1794
|
+
|
|
727
1795
|
await server.stop();
|
|
728
1796
|
});
|
|
729
1797
|
});
|
|
@@ -758,4 +1826,166 @@ describe('PipelineServer', () => {
|
|
|
758
1826
|
await server.stop();
|
|
759
1827
|
});
|
|
760
1828
|
});
|
|
1829
|
+
|
|
1830
|
+
describe('GET /events', () => {
|
|
1831
|
+
it('should accept SSE connections', async () => {
|
|
1832
|
+
const server = new PipelineServer({ port: 0 });
|
|
1833
|
+
await server.start();
|
|
1834
|
+
|
|
1835
|
+
const controller = new AbortController();
|
|
1836
|
+
const responsePromise = fetch(`http://localhost:${server.port}/events`, {
|
|
1837
|
+
signal: controller.signal,
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1841
|
+
controller.abort();
|
|
1842
|
+
|
|
1843
|
+
try {
|
|
1844
|
+
await responsePromise;
|
|
1845
|
+
} catch {
|
|
1846
|
+
// AbortError expected
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
await server.stop();
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
it('should accept SSE connections with correlationId filter', async () => {
|
|
1853
|
+
const server = new PipelineServer({ port: 0 });
|
|
1854
|
+
await server.start();
|
|
1855
|
+
|
|
1856
|
+
const controller = new AbortController();
|
|
1857
|
+
const responsePromise = fetch(`http://localhost:${server.port}/events?correlationId=test-123`, {
|
|
1858
|
+
signal: controller.signal,
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1862
|
+
controller.abort();
|
|
1863
|
+
|
|
1864
|
+
try {
|
|
1865
|
+
await responsePromise;
|
|
1866
|
+
} catch {
|
|
1867
|
+
// AbortError expected
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
await server.stop();
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
describe('phased execution', () => {
|
|
1875
|
+
it('should emit phased execution events when foreach-phased handler runs', async () => {
|
|
1876
|
+
type Component = { path: string; priority: 'high' | 'medium' | 'low' };
|
|
1877
|
+
type ComponentEvent = { data: { components: Component[] } };
|
|
1878
|
+
type ResultEvent = { data: { componentPath: string } };
|
|
1879
|
+
|
|
1880
|
+
const generateHandler = {
|
|
1881
|
+
name: 'GenerateComponents',
|
|
1882
|
+
events: ['ComponentsGenerated'],
|
|
1883
|
+
handle: async () => ({
|
|
1884
|
+
type: 'ComponentsGenerated',
|
|
1885
|
+
data: { components: [{ path: '/comp/a.tsx', priority: 'high' }] },
|
|
1886
|
+
}),
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
const implementHandler = {
|
|
1890
|
+
name: 'ImplementComponent',
|
|
1891
|
+
events: ['ComponentImplemented'],
|
|
1892
|
+
handle: async (cmd: { data: { componentPath: string } }) => ({
|
|
1893
|
+
type: 'ComponentImplemented',
|
|
1894
|
+
data: { componentPath: cmd.data.componentPath },
|
|
1895
|
+
}),
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
const pipeline = define('test')
|
|
1899
|
+
.on('ComponentsGenerated')
|
|
1900
|
+
.forEach((e: ComponentEvent) => e.data.components)
|
|
1901
|
+
.groupInto(['high', 'medium', 'low'], (c: Component) => c.priority)
|
|
1902
|
+
.process('ImplementComponent', (c: Component) => ({ componentPath: c.path }))
|
|
1903
|
+
.onComplete({
|
|
1904
|
+
success: 'AllComponentsImplemented',
|
|
1905
|
+
failure: 'ComponentImplementationFailed',
|
|
1906
|
+
itemKey: (e: ResultEvent) => e.data.componentPath,
|
|
1907
|
+
})
|
|
1908
|
+
.build();
|
|
1909
|
+
|
|
1910
|
+
const server = new PipelineServer({ port: 0 });
|
|
1911
|
+
server.registerCommandHandlers([generateHandler, implementHandler]);
|
|
1912
|
+
server.registerPipeline(pipeline);
|
|
1913
|
+
await server.start();
|
|
1914
|
+
|
|
1915
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
1916
|
+
method: 'POST',
|
|
1917
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1918
|
+
body: JSON.stringify({ type: 'GenerateComponents', data: {} }),
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1922
|
+
await server.stop();
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
describe('POST /execute', () => {
|
|
1927
|
+
it('should call handler and return event directly', async () => {
|
|
1928
|
+
const handler = {
|
|
1929
|
+
name: 'TestCmd',
|
|
1930
|
+
handle: async () => ({ type: 'TestDone', data: { result: 'success' } }),
|
|
1931
|
+
};
|
|
1932
|
+
const server = new PipelineServer({ port: 0 });
|
|
1933
|
+
server.registerCommandHandlers([handler]);
|
|
1934
|
+
await server.start();
|
|
1935
|
+
|
|
1936
|
+
const response = await fetch(`http://localhost:${server.port}/execute`, {
|
|
1937
|
+
method: 'POST',
|
|
1938
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1939
|
+
body: JSON.stringify({ command: 'TestCmd', payload: { input: 'test' } }),
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
const data = (await response.json()) as { event: string; data: Record<string, unknown> };
|
|
1943
|
+
expect(response.status).toBe(200);
|
|
1944
|
+
expect(data).toEqual({ event: 'TestDone', data: { result: 'success' } });
|
|
1945
|
+
|
|
1946
|
+
await server.stop();
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
it('should return 400 for unknown command', async () => {
|
|
1950
|
+
const server = new PipelineServer({ port: 0 });
|
|
1951
|
+
await server.start();
|
|
1952
|
+
|
|
1953
|
+
const response = await fetch(`http://localhost:${server.port}/execute`, {
|
|
1954
|
+
method: 'POST',
|
|
1955
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1956
|
+
body: JSON.stringify({ command: 'NonExistentCmd', payload: {} }),
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
const data = (await response.json()) as { error: string };
|
|
1960
|
+
expect(response.status).toBe(400);
|
|
1961
|
+
expect(data).toEqual({ error: 'Unknown command: NonExistentCmd' });
|
|
1962
|
+
|
|
1963
|
+
await server.stop();
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
it('should return first event when handler returns array', async () => {
|
|
1967
|
+
const handler = {
|
|
1968
|
+
name: 'MultiEventCmd',
|
|
1969
|
+
handle: async () => [
|
|
1970
|
+
{ type: 'FirstEvent', data: { order: 1 } },
|
|
1971
|
+
{ type: 'SecondEvent', data: { order: 2 } },
|
|
1972
|
+
],
|
|
1973
|
+
};
|
|
1974
|
+
const server = new PipelineServer({ port: 0 });
|
|
1975
|
+
server.registerCommandHandlers([handler]);
|
|
1976
|
+
await server.start();
|
|
1977
|
+
|
|
1978
|
+
const response = await fetch(`http://localhost:${server.port}/execute`, {
|
|
1979
|
+
method: 'POST',
|
|
1980
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1981
|
+
body: JSON.stringify({ command: 'MultiEventCmd', payload: {} }),
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
const data = (await response.json()) as { event: string; data: Record<string, unknown> };
|
|
1985
|
+
expect(response.status).toBe(200);
|
|
1986
|
+
expect(data).toEqual({ event: 'FirstEvent', data: { order: 1 } });
|
|
1987
|
+
|
|
1988
|
+
await server.stop();
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
761
1991
|
});
|