@principal-ai/subsystems-core 0.29.0 → 0.31.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/README.md +15 -110
- package/dist/index.d.ts +7 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -205
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +5 -61
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -131
- package/dist/node.js.map +1 -1
- package/dist/types/index.d.ts +3 -348
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -19
- package/dist/types/index.js.map +1 -1
- package/dist/types/subsystem-model.d.ts +34 -3
- package/dist/types/subsystem-model.d.ts.map +1 -1
- package/dist/types/subsystem-model.js.map +1 -1
- package/package.json +7 -24
- package/schemas/subsystem-model.schema.json +52 -1
- package/src/index.ts +7 -580
- package/src/node.ts +21 -262
- package/src/types/index.ts +3 -456
- package/src/types/subsystem-model.ts +39 -3
- package/dist/ConfigurationLoader.d.ts +0 -76
- package/dist/ConfigurationLoader.d.ts.map +0 -1
- package/dist/ConfigurationLoader.js +0 -148
- package/dist/ConfigurationLoader.js.map +0 -1
- package/dist/ConfigurationValidator.d.ts +0 -31
- package/dist/ConfigurationValidator.d.ts.map +0 -1
- package/dist/ConfigurationValidator.js +0 -239
- package/dist/ConfigurationValidator.js.map +0 -1
- package/dist/EventProcessor.d.ts +0 -49
- package/dist/EventProcessor.d.ts.map +0 -1
- package/dist/EventProcessor.js +0 -220
- package/dist/EventProcessor.js.map +0 -1
- package/dist/EventRecorderService.d.ts +0 -305
- package/dist/EventRecorderService.d.ts.map +0 -1
- package/dist/EventRecorderService.js +0 -467
- package/dist/EventRecorderService.js.map +0 -1
- package/dist/LibraryLoader.d.ts +0 -65
- package/dist/LibraryLoader.d.ts.map +0 -1
- package/dist/LibraryLoader.js +0 -197
- package/dist/LibraryLoader.js.map +0 -1
- package/dist/PathBasedEventProcessor.d.ts +0 -77
- package/dist/PathBasedEventProcessor.d.ts.map +0 -1
- package/dist/PathBasedEventProcessor.js +0 -136
- package/dist/PathBasedEventProcessor.js.map +0 -1
- package/dist/SessionManager.d.ts +0 -194
- package/dist/SessionManager.d.ts.map +0 -1
- package/dist/SessionManager.js +0 -302
- package/dist/SessionManager.js.map +0 -1
- package/dist/ValidationEngine.d.ts +0 -31
- package/dist/ValidationEngine.d.ts.map +0 -1
- package/dist/ValidationEngine.js +0 -162
- package/dist/ValidationEngine.js.map +0 -1
- package/dist/auxiliary/AuxiliaryManifestValidator.d.ts +0 -41
- package/dist/auxiliary/AuxiliaryManifestValidator.d.ts.map +0 -1
- package/dist/auxiliary/AuxiliaryManifestValidator.js +0 -100
- package/dist/auxiliary/AuxiliaryManifestValidator.js.map +0 -1
- package/dist/auxiliary/index.d.ts +0 -9
- package/dist/auxiliary/index.d.ts.map +0 -1
- package/dist/auxiliary/index.js +0 -14
- package/dist/auxiliary/index.js.map +0 -1
- package/dist/auxiliary/validateAreaScopeDisjoint.d.ts +0 -24
- package/dist/auxiliary/validateAreaScopeDisjoint.d.ts.map +0 -1
- package/dist/auxiliary/validateAreaScopeDisjoint.js +0 -61
- package/dist/auxiliary/validateAreaScopeDisjoint.js.map +0 -1
- package/dist/cli/codegen.d.ts +0 -41
- package/dist/cli/codegen.d.ts.map +0 -1
- package/dist/cli/codegen.js +0 -257
- package/dist/cli/codegen.js.map +0 -1
- package/dist/codegen/type-generator.d.ts +0 -123
- package/dist/codegen/type-generator.d.ts.map +0 -1
- package/dist/codegen/type-generator.js +0 -433
- package/dist/codegen/type-generator.js.map +0 -1
- package/dist/codegen/usage-example.d.ts +0 -103
- package/dist/codegen/usage-example.d.ts.map +0 -1
- package/dist/codegen/usage-example.js +0 -258
- package/dist/codegen/usage-example.js.map +0 -1
- package/dist/dashboard/DashboardValidator.d.ts +0 -85
- package/dist/dashboard/DashboardValidator.d.ts.map +0 -1
- package/dist/dashboard/DashboardValidator.js +0 -775
- package/dist/dashboard/DashboardValidator.js.map +0 -1
- package/dist/dashboard/index.d.ts +0 -6
- package/dist/dashboard/index.d.ts.map +0 -1
- package/dist/dashboard/index.js +0 -10
- package/dist/dashboard/index.js.map +0 -1
- package/dist/discovery/CanvasDiscovery.d.ts +0 -160
- package/dist/discovery/CanvasDiscovery.d.ts.map +0 -1
- package/dist/discovery/CanvasDiscovery.js +0 -865
- package/dist/discovery/CanvasDiscovery.js.map +0 -1
- package/dist/discovery/CanvasFileManifest.d.ts +0 -86
- package/dist/discovery/CanvasFileManifest.d.ts.map +0 -1
- package/dist/discovery/CanvasFileManifest.js +0 -338
- package/dist/discovery/CanvasFileManifest.js.map +0 -1
- package/dist/discovery/LibraryDiscovery.d.ts +0 -108
- package/dist/discovery/LibraryDiscovery.d.ts.map +0 -1
- package/dist/discovery/LibraryDiscovery.js +0 -263
- package/dist/discovery/LibraryDiscovery.js.map +0 -1
- package/dist/discovery/types.d.ts +0 -403
- package/dist/discovery/types.d.ts.map +0 -1
- package/dist/discovery/types.js +0 -6
- package/dist/discovery/types.js.map +0 -1
- package/dist/events/EventsCanvasValidator.d.ts +0 -139
- package/dist/events/EventsCanvasValidator.d.ts.map +0 -1
- package/dist/events/EventsCanvasValidator.js +0 -281
- package/dist/events/EventsCanvasValidator.js.map +0 -1
- package/dist/events/NamespacePathIndex.d.ts +0 -61
- package/dist/events/NamespacePathIndex.d.ts.map +0 -1
- package/dist/events/NamespacePathIndex.js +0 -74
- package/dist/events/NamespacePathIndex.js.map +0 -1
- package/dist/events/OtelEventPathsValidator.d.ts +0 -75
- package/dist/events/OtelEventPathsValidator.d.ts.map +0 -1
- package/dist/events/OtelEventPathsValidator.js +0 -126
- package/dist/events/OtelEventPathsValidator.js.map +0 -1
- package/dist/events/ScopeEventsValidator.d.ts +0 -68
- package/dist/events/ScopeEventsValidator.d.ts.map +0 -1
- package/dist/events/ScopeEventsValidator.js +0 -91
- package/dist/events/ScopeEventsValidator.js.map +0 -1
- package/dist/events/index.d.ts +0 -9
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js +0 -12
- package/dist/events/index.js.map +0 -1
- package/dist/events/path-helpers.d.ts +0 -33
- package/dist/events/path-helpers.d.ts.map +0 -1
- package/dist/events/path-helpers.js +0 -59
- package/dist/events/path-helpers.js.map +0 -1
- package/dist/execution/ExecutionLoader.d.ts +0 -135
- package/dist/execution/ExecutionLoader.d.ts.map +0 -1
- package/dist/execution/ExecutionLoader.js +0 -266
- package/dist/execution/ExecutionLoader.js.map +0 -1
- package/dist/execution/ExecutionValidator.d.ts +0 -94
- package/dist/execution/ExecutionValidator.d.ts.map +0 -1
- package/dist/execution/ExecutionValidator.js +0 -503
- package/dist/execution/ExecutionValidator.js.map +0 -1
- package/dist/generated/graph-converter-execution.types.d.ts +0 -177
- package/dist/generated/graph-converter-execution.types.d.ts.map +0 -1
- package/dist/generated/graph-converter-execution.types.js +0 -12
- package/dist/generated/graph-converter-execution.types.js.map +0 -1
- package/dist/helpers/GraphInstrumentationHelper.d.ts +0 -93
- package/dist/helpers/GraphInstrumentationHelper.d.ts.map +0 -1
- package/dist/helpers/GraphInstrumentationHelper.js +0 -252
- package/dist/helpers/GraphInstrumentationHelper.js.map +0 -1
- package/dist/matchers/SpanMatcher.d.ts +0 -107
- package/dist/matchers/SpanMatcher.d.ts.map +0 -1
- package/dist/matchers/SpanMatcher.js +0 -274
- package/dist/matchers/SpanMatcher.js.map +0 -1
- package/dist/orchestration/TraceOrchestrator.d.ts +0 -71
- package/dist/orchestration/TraceOrchestrator.d.ts.map +0 -1
- package/dist/orchestration/TraceOrchestrator.js +0 -416
- package/dist/orchestration/TraceOrchestrator.js.map +0 -1
- package/dist/parsers/OtlpTraceParser.d.ts +0 -131
- package/dist/parsers/OtlpTraceParser.d.ts.map +0 -1
- package/dist/parsers/OtlpTraceParser.js +0 -310
- package/dist/parsers/OtlpTraceParser.js.map +0 -1
- package/dist/registry/CompositeRegistry.d.ts +0 -71
- package/dist/registry/CompositeRegistry.d.ts.map +0 -1
- package/dist/registry/CompositeRegistry.js +0 -113
- package/dist/registry/CompositeRegistry.js.map +0 -1
- package/dist/registry/EventRegistry.d.ts +0 -79
- package/dist/registry/EventRegistry.d.ts.map +0 -1
- package/dist/registry/EventRegistry.js +0 -165
- package/dist/registry/EventRegistry.js.map +0 -1
- package/dist/registry/LocalRegistry.d.ts +0 -111
- package/dist/registry/LocalRegistry.d.ts.map +0 -1
- package/dist/registry/LocalRegistry.js +0 -335
- package/dist/registry/LocalRegistry.js.map +0 -1
- package/dist/registry/RemoteRegistry.d.ts +0 -122
- package/dist/registry/RemoteRegistry.d.ts.map +0 -1
- package/dist/registry/RemoteRegistry.js +0 -399
- package/dist/registry/RemoteRegistry.js.map +0 -1
- package/dist/registry/VersionRegistry.d.ts +0 -51
- package/dist/registry/VersionRegistry.d.ts.map +0 -1
- package/dist/registry/VersionRegistry.js +0 -105
- package/dist/registry/VersionRegistry.js.map +0 -1
- package/dist/rules/config.d.ts +0 -57
- package/dist/rules/config.d.ts.map +0 -1
- package/dist/rules/config.js +0 -382
- package/dist/rules/config.js.map +0 -1
- package/dist/rules/engine.d.ts +0 -70
- package/dist/rules/engine.d.ts.map +0 -1
- package/dist/rules/engine.js +0 -373
- package/dist/rules/engine.js.map +0 -1
- package/dist/rules/implementations/connection-type-references.d.ts +0 -7
- package/dist/rules/implementations/connection-type-references.d.ts.map +0 -1
- package/dist/rules/implementations/connection-type-references.js +0 -107
- package/dist/rules/implementations/connection-type-references.js.map +0 -1
- package/dist/rules/implementations/dead-end-states.d.ts +0 -17
- package/dist/rules/implementations/dead-end-states.d.ts.map +0 -1
- package/dist/rules/implementations/dead-end-states.js +0 -75
- package/dist/rules/implementations/dead-end-states.js.map +0 -1
- package/dist/rules/implementations/index.d.ts +0 -23
- package/dist/rules/implementations/index.d.ts.map +0 -1
- package/dist/rules/implementations/index.js +0 -73
- package/dist/rules/implementations/index.js.map +0 -1
- package/dist/rules/implementations/minimum-node-sources.d.ts +0 -24
- package/dist/rules/implementations/minimum-node-sources.d.ts.map +0 -1
- package/dist/rules/implementations/minimum-node-sources.js +0 -59
- package/dist/rules/implementations/minimum-node-sources.js.map +0 -1
- package/dist/rules/implementations/no-unknown-fields.d.ts +0 -7
- package/dist/rules/implementations/no-unknown-fields.d.ts.map +0 -1
- package/dist/rules/implementations/no-unknown-fields.js +0 -216
- package/dist/rules/implementations/no-unknown-fields.js.map +0 -1
- package/dist/rules/implementations/orphaned-edge-types.d.ts +0 -7
- package/dist/rules/implementations/orphaned-edge-types.d.ts.map +0 -1
- package/dist/rules/implementations/orphaned-edge-types.js +0 -50
- package/dist/rules/implementations/orphaned-edge-types.js.map +0 -1
- package/dist/rules/implementations/orphaned-node-types.d.ts +0 -7
- package/dist/rules/implementations/orphaned-node-types.d.ts.map +0 -1
- package/dist/rules/implementations/orphaned-node-types.js +0 -53
- package/dist/rules/implementations/orphaned-node-types.js.map +0 -1
- package/dist/rules/implementations/required-metadata.d.ts +0 -7
- package/dist/rules/implementations/required-metadata.d.ts.map +0 -1
- package/dist/rules/implementations/required-metadata.js +0 -48
- package/dist/rules/implementations/required-metadata.js.map +0 -1
- package/dist/rules/implementations/state-transition-references.d.ts +0 -7
- package/dist/rules/implementations/state-transition-references.d.ts.map +0 -1
- package/dist/rules/implementations/state-transition-references.js +0 -138
- package/dist/rules/implementations/state-transition-references.js.map +0 -1
- package/dist/rules/implementations/unreachable-states.d.ts +0 -7
- package/dist/rules/implementations/unreachable-states.d.ts.map +0 -1
- package/dist/rules/implementations/unreachable-states.js +0 -83
- package/dist/rules/implementations/unreachable-states.js.map +0 -1
- package/dist/rules/implementations/valid-action-patterns.d.ts +0 -17
- package/dist/rules/implementations/valid-action-patterns.d.ts.map +0 -1
- package/dist/rules/implementations/valid-action-patterns.js +0 -112
- package/dist/rules/implementations/valid-action-patterns.js.map +0 -1
- package/dist/rules/implementations/valid-color-format.d.ts +0 -7
- package/dist/rules/implementations/valid-color-format.d.ts.map +0 -1
- package/dist/rules/implementations/valid-color-format.js +0 -94
- package/dist/rules/implementations/valid-color-format.js.map +0 -1
- package/dist/rules/implementations/valid-edge-types.d.ts +0 -7
- package/dist/rules/implementations/valid-edge-types.d.ts.map +0 -1
- package/dist/rules/implementations/valid-edge-types.js +0 -248
- package/dist/rules/implementations/valid-edge-types.js.map +0 -1
- package/dist/rules/implementations/valid-node-types.d.ts +0 -7
- package/dist/rules/implementations/valid-node-types.d.ts.map +0 -1
- package/dist/rules/implementations/valid-node-types.js +0 -187
- package/dist/rules/implementations/valid-node-types.js.map +0 -1
- package/dist/rules/index.d.ts +0 -28
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -70
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/types.d.ts +0 -309
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -40
- package/dist/rules/types.js.map +0 -1
- package/dist/scopes/ScopePathIndex.d.ts +0 -56
- package/dist/scopes/ScopePathIndex.d.ts.map +0 -1
- package/dist/scopes/ScopePathIndex.js +0 -67
- package/dist/scopes/ScopePathIndex.js.map +0 -1
- package/dist/scopes/ScopesCanvasValidator.d.ts +0 -76
- package/dist/scopes/ScopesCanvasValidator.d.ts.map +0 -1
- package/dist/scopes/ScopesCanvasValidator.js +0 -221
- package/dist/scopes/ScopesCanvasValidator.js.map +0 -1
- package/dist/scopes/index.d.ts +0 -12
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js +0 -30
- package/dist/scopes/index.js.map +0 -1
- package/dist/scopes/utils.d.ts +0 -85
- package/dist/scopes/utils.d.ts.map +0 -1
- package/dist/scopes/utils.js +0 -143
- package/dist/scopes/utils.js.map +0 -1
- package/dist/scopes/validateScopeNamespaceNesting.d.ts +0 -38
- package/dist/scopes/validateScopeNamespaceNesting.d.ts.map +0 -1
- package/dist/scopes/validateScopeNamespaceNesting.js +0 -69
- package/dist/scopes/validateScopeNamespaceNesting.js.map +0 -1
- package/dist/spans/index.d.ts +0 -12
- package/dist/spans/index.d.ts.map +0 -1
- package/dist/spans/index.js +0 -25
- package/dist/spans/index.js.map +0 -1
- package/dist/spans/utils.d.ts +0 -70
- package/dist/spans/utils.d.ts.map +0 -1
- package/dist/spans/utils.js +0 -205
- package/dist/spans/utils.js.map +0 -1
- package/dist/storyboard/builder.d.ts +0 -135
- package/dist/storyboard/builder.d.ts.map +0 -1
- package/dist/storyboard/builder.js +0 -235
- package/dist/storyboard/builder.js.map +0 -1
- package/dist/storyboard/index.d.ts +0 -9
- package/dist/storyboard/index.d.ts.map +0 -1
- package/dist/storyboard/index.js +0 -18
- package/dist/storyboard/index.js.map +0 -1
- package/dist/storyboard/types.d.ts +0 -101
- package/dist/storyboard/types.d.ts.map +0 -1
- package/dist/storyboard/types.js +0 -10
- package/dist/storyboard/types.js.map +0 -1
- package/dist/telemetry/coverage.d.ts +0 -92
- package/dist/telemetry/coverage.d.ts.map +0 -1
- package/dist/telemetry/coverage.js +0 -335
- package/dist/telemetry/coverage.js.map +0 -1
- package/dist/telemetry/event-validator.d.ts +0 -86
- package/dist/telemetry/event-validator.d.ts.map +0 -1
- package/dist/telemetry/event-validator.js +0 -257
- package/dist/telemetry/event-validator.js.map +0 -1
- package/dist/types/audit.d.ts +0 -236
- package/dist/types/audit.d.ts.map +0 -1
- package/dist/types/audit.js +0 -53
- package/dist/types/audit.js.map +0 -1
- package/dist/types/auxiliary.d.ts +0 -54
- package/dist/types/auxiliary.d.ts.map +0 -1
- package/dist/types/auxiliary.js +0 -27
- package/dist/types/auxiliary.js.map +0 -1
- package/dist/types/canvas-scope.d.ts +0 -156
- package/dist/types/canvas-scope.d.ts.map +0 -1
- package/dist/types/canvas-scope.js +0 -62
- package/dist/types/canvas-scope.js.map +0 -1
- package/dist/types/canvas.d.ts +0 -1138
- package/dist/types/canvas.d.ts.map +0 -1
- package/dist/types/canvas.js +0 -178
- package/dist/types/canvas.js.map +0 -1
- package/dist/types/dashboard.d.ts +0 -170
- package/dist/types/dashboard.d.ts.map +0 -1
- package/dist/types/dashboard.js +0 -14
- package/dist/types/dashboard.js.map +0 -1
- package/dist/types/library.d.ts +0 -227
- package/dist/types/library.d.ts.map +0 -1
- package/dist/types/library.js +0 -16
- package/dist/types/library.js.map +0 -1
- package/dist/types/otel.d.ts +0 -465
- package/dist/types/otel.d.ts.map +0 -1
- package/dist/types/otel.js +0 -163
- package/dist/types/otel.js.map +0 -1
- package/dist/types/path-based-config.d.ts +0 -235
- package/dist/types/path-based-config.d.ts.map +0 -1
- package/dist/types/path-based-config.js +0 -11
- package/dist/types/path-based-config.js.map +0 -1
- package/dist/types/registered-trace.d.ts +0 -337
- package/dist/types/registered-trace.d.ts.map +0 -1
- package/dist/types/registered-trace.js +0 -16
- package/dist/types/registered-trace.js.map +0 -1
- package/dist/types/resource-match.d.ts +0 -207
- package/dist/types/resource-match.d.ts.map +0 -1
- package/dist/types/resource-match.js +0 -74
- package/dist/types/resource-match.js.map +0 -1
- package/dist/types/version-registry.d.ts +0 -137
- package/dist/types/version-registry.d.ts.map +0 -1
- package/dist/types/version-registry.js +0 -10
- package/dist/types/version-registry.js.map +0 -1
- package/dist/utils/CanvasConverter.d.ts +0 -148
- package/dist/utils/CanvasConverter.d.ts.map +0 -1
- package/dist/utils/CanvasConverter.js +0 -530
- package/dist/utils/CanvasConverter.js.map +0 -1
- package/dist/utils/GraphConverter.d.ts +0 -18
- package/dist/utils/GraphConverter.d.ts.map +0 -1
- package/dist/utils/GraphConverter.js +0 -80
- package/dist/utils/GraphConverter.js.map +0 -1
- package/dist/utils/PathMatcher.d.ts +0 -55
- package/dist/utils/PathMatcher.d.ts.map +0 -1
- package/dist/utils/PathMatcher.js +0 -178
- package/dist/utils/PathMatcher.js.map +0 -1
- package/dist/utils/TraceToCanvas.d.ts +0 -83
- package/dist/utils/TraceToCanvas.d.ts.map +0 -1
- package/dist/utils/TraceToCanvas.js +0 -336
- package/dist/utils/TraceToCanvas.js.map +0 -1
- package/dist/utils/YamlParser.d.ts +0 -36
- package/dist/utils/YamlParser.d.ts.map +0 -1
- package/dist/utils/YamlParser.js +0 -92
- package/dist/utils/YamlParser.js.map +0 -1
- package/dist/utils/registeredTraceHelpers.d.ts +0 -76
- package/dist/utils/registeredTraceHelpers.d.ts.map +0 -1
- package/dist/utils/registeredTraceHelpers.js +0 -203
- package/dist/utils/registeredTraceHelpers.js.map +0 -1
- package/dist/utils/traceAggregation.d.ts +0 -25
- package/dist/utils/traceAggregation.d.ts.map +0 -1
- package/dist/utils/traceAggregation.js +0 -140
- package/dist/utils/traceAggregation.js.map +0 -1
- package/dist/validation/OtelCanvasValidator.d.ts +0 -40
- package/dist/validation/OtelCanvasValidator.d.ts.map +0 -1
- package/dist/validation/OtelCanvasValidator.js +0 -59
- package/dist/validation/OtelCanvasValidator.js.map +0 -1
- package/dist/validation/index.d.ts +0 -8
- package/dist/validation/index.d.ts.map +0 -1
- package/dist/validation/index.js +0 -13
- package/dist/validation/index.js.map +0 -1
- package/dist/validation/libraryValidation.d.ts +0 -53
- package/dist/validation/libraryValidation.d.ts.map +0 -1
- package/dist/validation/libraryValidation.js +0 -115
- package/dist/validation/libraryValidation.js.map +0 -1
- package/dist/workflow/edge-derivation.d.ts +0 -111
- package/dist/workflow/edge-derivation.d.ts.map +0 -1
- package/dist/workflow/edge-derivation.js +0 -174
- package/dist/workflow/edge-derivation.js.map +0 -1
- package/dist/workflow/edge-validation.d.ts +0 -170
- package/dist/workflow/edge-validation.d.ts.map +0 -1
- package/dist/workflow/edge-validation.js +0 -332
- package/dist/workflow/edge-validation.js.map +0 -1
- package/dist/workflow/example.d.ts +0 -11
- package/dist/workflow/example.d.ts.map +0 -1
- package/dist/workflow/example.js +0 -310
- package/dist/workflow/example.js.map +0 -1
- package/dist/workflow/index.d.ts +0 -20
- package/dist/workflow/index.d.ts.map +0 -1
- package/dist/workflow/index.js +0 -49
- package/dist/workflow/index.js.map +0 -1
- package/dist/workflow/scenario-matcher.d.ts +0 -63
- package/dist/workflow/scenario-matcher.d.ts.map +0 -1
- package/dist/workflow/scenario-matcher.js +0 -215
- package/dist/workflow/scenario-matcher.js.map +0 -1
- package/dist/workflow/template-parser.d.ts +0 -73
- package/dist/workflow/template-parser.d.ts.map +0 -1
- package/dist/workflow/template-parser.js +0 -280
- package/dist/workflow/template-parser.js.map +0 -1
- package/dist/workflow/template-renderer.d.ts +0 -45
- package/dist/workflow/template-renderer.d.ts.map +0 -1
- package/dist/workflow/template-renderer.js +0 -401
- package/dist/workflow/template-renderer.js.map +0 -1
- package/dist/workflow/types.d.ts +0 -393
- package/dist/workflow/types.d.ts.map +0 -1
- package/dist/workflow/types.js +0 -40
- package/dist/workflow/types.js.map +0 -1
- package/dist/workflow/validator.d.ts +0 -363
- package/dist/workflow/validator.d.ts.map +0 -1
- package/dist/workflow/validator.js +0 -2283
- package/dist/workflow/validator.js.map +0 -1
- package/schemas/auxiliary.manifest.schema.json +0 -56
- package/src/ConfigurationLoader.test.ts +0 -466
- package/src/ConfigurationLoader.ts +0 -184
- package/src/ConfigurationValidator.test.ts +0 -200
- package/src/ConfigurationValidator.ts +0 -273
- package/src/EventProcessor.test.ts +0 -405
- package/src/EventProcessor.ts +0 -256
- package/src/EventRecorderService.test.ts +0 -543
- package/src/EventRecorderService.ts +0 -741
- package/src/LibraryLoader.ts +0 -194
- package/src/PathBasedEventProcessor.test.ts +0 -336
- package/src/PathBasedEventProcessor.ts +0 -196
- package/src/SessionManager.test.ts +0 -424
- package/src/SessionManager.ts +0 -471
- package/src/ValidationEngine.test.ts +0 -520
- package/src/ValidationEngine.ts +0 -200
- package/src/auxiliary/AuxiliaryManifestValidator.test.ts +0 -93
- package/src/auxiliary/AuxiliaryManifestValidator.ts +0 -132
- package/src/auxiliary/index.ts +0 -18
- package/src/auxiliary/validateAreaScopeDisjoint.test.ts +0 -85
- package/src/auxiliary/validateAreaScopeDisjoint.ts +0 -72
- package/src/cli/codegen.ts +0 -286
- package/src/codegen/type-generator.test.ts +0 -219
- package/src/codegen/type-generator.ts +0 -560
- package/src/codegen/usage-example.ts +0 -357
- package/src/dashboard/DashboardValidator.ts +0 -904
- package/src/dashboard/index.ts +0 -14
- package/src/discovery/CanvasDiscovery.test.ts +0 -691
- package/src/discovery/CanvasDiscovery.ts +0 -1114
- package/src/discovery/CanvasFileManifest.test.ts +0 -521
- package/src/discovery/CanvasFileManifest.ts +0 -419
- package/src/discovery/LibraryDiscovery.test.ts +0 -329
- package/src/discovery/LibraryDiscovery.ts +0 -386
- package/src/discovery/types.ts +0 -453
- package/src/events/EventsCanvasValidator.test.ts +0 -191
- package/src/events/EventsCanvasValidator.ts +0 -428
- package/src/events/NamespacePathIndex.test.ts +0 -97
- package/src/events/NamespacePathIndex.ts +0 -100
- package/src/events/OtelEventPathsValidator.test.ts +0 -243
- package/src/events/OtelEventPathsValidator.ts +0 -191
- package/src/events/ScopeEventsValidator.test.ts +0 -173
- package/src/events/ScopeEventsValidator.ts +0 -158
- package/src/events/index.ts +0 -27
- package/src/events/path-helpers.ts +0 -53
- package/src/execution/ExecutionLoader.test.ts +0 -378
- package/src/execution/ExecutionLoader.ts +0 -331
- package/src/execution/ExecutionValidator.test.ts +0 -503
- package/src/execution/ExecutionValidator.ts +0 -617
- package/src/generated/graph-converter-execution.types.ts +0 -193
- package/src/helpers/GraphInstrumentationHelper.test.ts +0 -364
- package/src/helpers/GraphInstrumentationHelper.ts +0 -323
- package/src/matchers/SpanMatcher.test.ts +0 -1105
- package/src/matchers/SpanMatcher.ts +0 -381
- package/src/orchestration/TraceOrchestrator.ts +0 -560
- package/src/orchestration/__tests__/TraceOrchestrator.test.ts +0 -1098
- package/src/parsers/OtlpTraceParser.ts +0 -401
- package/src/parsers/__tests__/OtlpTraceParser.test.ts +0 -197
- package/src/registry/CompositeRegistry.ts +0 -140
- package/src/registry/EventRegistry.test.ts +0 -387
- package/src/registry/EventRegistry.ts +0 -191
- package/src/registry/LocalRegistry.ts +0 -416
- package/src/registry/RemoteRegistry.ts +0 -499
- package/src/registry/VersionRegistry.test.ts +0 -223
- package/src/registry/VersionRegistry.ts +0 -121
- package/src/rules/config.test.ts +0 -277
- package/src/rules/config.ts +0 -450
- package/src/rules/engine.test.ts +0 -331
- package/src/rules/engine.ts +0 -448
- package/src/rules/implementations/connection-type-references.ts +0 -117
- package/src/rules/implementations/dead-end-states.ts +0 -101
- package/src/rules/implementations/index.ts +0 -66
- package/src/rules/implementations/minimum-node-sources.ts +0 -84
- package/src/rules/implementations/no-unknown-fields.ts +0 -372
- package/src/rules/implementations/orphaned-edge-types.ts +0 -55
- package/src/rules/implementations/orphaned-node-types.ts +0 -58
- package/src/rules/implementations/required-metadata.ts +0 -52
- package/src/rules/implementations/state-transition-references.ts +0 -155
- package/src/rules/implementations/unreachable-states.ts +0 -94
- package/src/rules/implementations/valid-action-patterns.ts +0 -140
- package/src/rules/implementations/valid-color-format.ts +0 -152
- package/src/rules/implementations/valid-edge-types.ts +0 -273
- package/src/rules/implementations/valid-node-types.ts +0 -200
- package/src/rules/index.ts +0 -92
- package/src/rules/types.ts +0 -426
- package/src/scopes/ScopePathIndex.test.ts +0 -94
- package/src/scopes/ScopePathIndex.ts +0 -89
- package/src/scopes/ScopesCanvasValidator.test.ts +0 -127
- package/src/scopes/ScopesCanvasValidator.ts +0 -305
- package/src/scopes/__tests__/utils.test.ts +0 -383
- package/src/scopes/index.ts +0 -42
- package/src/scopes/utils.ts +0 -182
- package/src/scopes/validateScopeNamespaceNesting.test.ts +0 -127
- package/src/scopes/validateScopeNamespaceNesting.ts +0 -88
- package/src/spans/index.ts +0 -24
- package/src/spans/utils.ts +0 -249
- package/src/storyboard/builder.ts +0 -308
- package/src/storyboard/index.ts +0 -26
- package/src/storyboard/types.ts +0 -117
- package/src/telemetry/coverage.ts +0 -480
- package/src/telemetry/event-validator.ts +0 -312
- package/src/types/audit.ts +0 -314
- package/src/types/auxiliary.ts +0 -63
- package/src/types/canvas-scope.ts +0 -190
- package/src/types/canvas.test.ts +0 -199
- package/src/types/canvas.ts +0 -1395
- package/src/types/dashboard.ts +0 -257
- package/src/types/library.ts +0 -283
- package/src/types/otel.test.ts +0 -219
- package/src/types/otel.ts +0 -620
- package/src/types/path-based-config.ts +0 -285
- package/src/types/registered-trace.ts +0 -432
- package/src/types/resource-match.ts +0 -263
- package/src/types/version-registry.ts +0 -150
- package/src/utils/CanvasConverter.ts +0 -670
- package/src/utils/GraphConverter.test.ts +0 -322
- package/src/utils/GraphConverter.ts +0 -84
- package/src/utils/PathMatcher.test.ts +0 -139
- package/src/utils/PathMatcher.ts +0 -185
- package/src/utils/TraceToCanvas.test.ts +0 -310
- package/src/utils/TraceToCanvas.ts +0 -510
- package/src/utils/YamlParser.ts +0 -77
- package/src/utils/registeredTraceHelpers.ts +0 -251
- package/src/utils/traceAggregation.test.ts +0 -385
- package/src/utils/traceAggregation.ts +0 -173
- package/src/validation/OtelCanvasValidator.ts +0 -86
- package/src/validation/index.ts +0 -17
- package/src/validation/libraryValidation.ts +0 -206
- package/src/workflow/README.md +0 -408
- package/src/workflow/__tests__/edge-derivation.test.ts +0 -449
- package/src/workflow/__tests__/edge-validation.test.ts +0 -417
- package/src/workflow/__tests__/scenario-matcher.test.ts +0 -225
- package/src/workflow/__tests__/template-parser.test.ts +0 -276
- package/src/workflow/__tests__/template-renderer.test.ts +0 -509
- package/src/workflow/__tests__/types.test.ts +0 -187
- package/src/workflow/__tests__/validator.test.ts +0 -3225
- package/src/workflow/edge-derivation.ts +0 -265
- package/src/workflow/edge-validation.ts +0 -503
- package/src/workflow/example.ts +0 -330
- package/src/workflow/index.ts +0 -86
- package/src/workflow/scenario-matcher.ts +0 -253
- package/src/workflow/template-parser.ts +0 -350
- package/src/workflow/template-renderer.ts +0 -453
- package/src/workflow/types.ts +0 -511
- package/src/workflow/validator.ts +0 -2737
|
@@ -1,3225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for workflow template validator
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { WorkflowValidator } from '../validator';
|
|
6
|
-
import type { WorkflowTemplate, WorkflowValidationContext } from '../validator';
|
|
7
|
-
import type { ExtendedCanvas } from '../../types/canvas';
|
|
8
|
-
import type { ComponentLibrary } from '../../types/library';
|
|
9
|
-
import { EventRegistry } from '../../registry/EventRegistry';
|
|
10
|
-
import { NodeFileSystemAdapter } from '@principal-ai/repository-abstraction/node';
|
|
11
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import { tmpdir } from 'os';
|
|
14
|
-
|
|
15
|
-
describe('WorkflowValidator', () => {
|
|
16
|
-
let validator: WorkflowValidator;
|
|
17
|
-
let tempDir: string;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
validator = new WorkflowValidator(new NodeFileSystemAdapter());
|
|
21
|
-
// Create a temporary directory for test files
|
|
22
|
-
tempDir = mkdtempSync(join(tmpdir(), 'workflow-validator-test-'));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
// Clean up temp directory
|
|
27
|
-
if (tempDir) {
|
|
28
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Helper Functions
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
function createValidWorkflow(): WorkflowTemplate {
|
|
37
|
-
return {
|
|
38
|
-
version: '1.0.0',
|
|
39
|
-
canvas: 'test.otel.canvas',
|
|
40
|
-
name: 'Test Workflow',
|
|
41
|
-
description: 'A test workflow template',
|
|
42
|
-
spanPattern: 'test.execution',
|
|
43
|
-
scenarioSelection: 'first-match',
|
|
44
|
-
scenarios: [
|
|
45
|
-
{
|
|
46
|
-
id: 'test-passed',
|
|
47
|
-
priority: 1,
|
|
48
|
-
description: 'Test passed successfully',
|
|
49
|
-
template: {
|
|
50
|
-
introduction: 'Test passed',
|
|
51
|
-
events: {
|
|
52
|
-
'test.started': 'Started test',
|
|
53
|
-
'test.complete': 'Test completed',
|
|
54
|
-
},
|
|
55
|
-
summary: 'Success',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'test-failed',
|
|
60
|
-
priority: 2,
|
|
61
|
-
description: 'Test failed',
|
|
62
|
-
template: {
|
|
63
|
-
introduction: 'Test failed',
|
|
64
|
-
events: {
|
|
65
|
-
'test.started': 'Test started',
|
|
66
|
-
'test.failed': 'Test failed',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createValidCanvas(): ExtendedCanvas {
|
|
75
|
-
return {
|
|
76
|
-
nodes: [
|
|
77
|
-
{
|
|
78
|
-
id: 'test-started',
|
|
79
|
-
type: 'otel-event',
|
|
80
|
-
x: 0,
|
|
81
|
-
y: 0,
|
|
82
|
-
width: 200,
|
|
83
|
-
height: 100,
|
|
84
|
-
eventRef: 'test.started',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'test-complete',
|
|
88
|
-
type: 'otel-event',
|
|
89
|
-
x: 250,
|
|
90
|
-
y: 0,
|
|
91
|
-
width: 200,
|
|
92
|
-
height: 100,
|
|
93
|
-
eventRef: 'test.complete',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'test-error',
|
|
97
|
-
type: 'otel-event',
|
|
98
|
-
x: 500,
|
|
99
|
-
y: 0,
|
|
100
|
-
width: 200,
|
|
101
|
-
height: 100,
|
|
102
|
-
eventRef: 'test.error',
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
edges: [
|
|
106
|
-
{
|
|
107
|
-
id: 'edge1',
|
|
108
|
-
fromNode: 'test-started',
|
|
109
|
-
toNode: 'test-complete',
|
|
110
|
-
fromSide: 'right',
|
|
111
|
-
toSide: 'left',
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
id: 'edge2',
|
|
115
|
-
fromNode: 'test-started',
|
|
116
|
-
toNode: 'test-error',
|
|
117
|
-
fromSide: 'right',
|
|
118
|
-
toSide: 'left',
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
pv: {
|
|
122
|
-
version: '1.0.0',
|
|
123
|
-
name: 'Test Canvas',
|
|
124
|
-
markdown: 'test.md',
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function createContext(
|
|
130
|
-
workflow: Partial<WorkflowTemplate>,
|
|
131
|
-
options: {
|
|
132
|
-
canvas?: ExtendedCanvas;
|
|
133
|
-
canvasPath?: string;
|
|
134
|
-
} = {}
|
|
135
|
-
): WorkflowValidationContext {
|
|
136
|
-
const fullWorkflow = {
|
|
137
|
-
...createValidWorkflow(),
|
|
138
|
-
...workflow,
|
|
139
|
-
} as WorkflowTemplate;
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
workflow: fullWorkflow,
|
|
143
|
-
workflowPath: 'test.workflow.json',
|
|
144
|
-
canvas: options.canvas,
|
|
145
|
-
canvasPath: options.canvasPath,
|
|
146
|
-
basePath: tempDir,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================================================================
|
|
151
|
-
// Schema Validation Tests
|
|
152
|
-
// ============================================================================
|
|
153
|
-
|
|
154
|
-
describe('checkSchema', () => {
|
|
155
|
-
it('should pass for valid workflow template', async () => {
|
|
156
|
-
// Create canvas file so canvas-exists check passes
|
|
157
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
158
|
-
const markdownPath = join(tempDir, 'test.md');
|
|
159
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
160
|
-
writeFileSync(markdownPath, '# Test Documentation');
|
|
161
|
-
|
|
162
|
-
const context = createContext({}, { canvasPath });
|
|
163
|
-
const result = await validator.validate(context);
|
|
164
|
-
|
|
165
|
-
expect(result.errorCount).toBe(0);
|
|
166
|
-
expect(result.warningCount).toBe(0);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should flag missing version', async () => {
|
|
170
|
-
const context = createContext({ version: undefined as unknown });
|
|
171
|
-
const result = await validator.validate(context);
|
|
172
|
-
|
|
173
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
174
|
-
expect(result.violations).toContainEqual(
|
|
175
|
-
expect.objectContaining({
|
|
176
|
-
ruleId: 'workflow-schema-valid',
|
|
177
|
-
path: 'version',
|
|
178
|
-
message: expect.stringContaining('Missing required field "version"'),
|
|
179
|
-
})
|
|
180
|
-
);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should flag invalid semver version', async () => {
|
|
184
|
-
const context = createContext({ version: 'not-a-version' });
|
|
185
|
-
const result = await validator.validate(context);
|
|
186
|
-
|
|
187
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
188
|
-
expect(result.violations).toContainEqual(
|
|
189
|
-
expect.objectContaining({
|
|
190
|
-
ruleId: 'workflow-schema-valid',
|
|
191
|
-
path: 'version',
|
|
192
|
-
message: expect.stringContaining('Invalid version format'),
|
|
193
|
-
})
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should flag missing canvas reference', async () => {
|
|
198
|
-
const context = createContext({ canvas: undefined as unknown });
|
|
199
|
-
const result = await validator.validate(context);
|
|
200
|
-
|
|
201
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
202
|
-
expect(result.violations).toContainEqual(
|
|
203
|
-
expect.objectContaining({
|
|
204
|
-
ruleId: 'workflow-schema-valid',
|
|
205
|
-
path: 'canvas',
|
|
206
|
-
message: expect.stringContaining('Missing required field "canvas"'),
|
|
207
|
-
})
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should flag missing name', async () => {
|
|
212
|
-
const context = createContext({ name: undefined as unknown });
|
|
213
|
-
const result = await validator.validate(context);
|
|
214
|
-
|
|
215
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
216
|
-
expect(result.violations).toContainEqual(
|
|
217
|
-
expect.objectContaining({
|
|
218
|
-
ruleId: 'workflow-schema-valid',
|
|
219
|
-
path: 'name',
|
|
220
|
-
message: expect.stringContaining('Missing required field "name"'),
|
|
221
|
-
})
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should flag missing description', async () => {
|
|
226
|
-
const context = createContext({ description: undefined as unknown });
|
|
227
|
-
const result = await validator.validate(context);
|
|
228
|
-
|
|
229
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
230
|
-
expect(result.violations).toContainEqual(
|
|
231
|
-
expect.objectContaining({
|
|
232
|
-
ruleId: 'workflow-schema-valid',
|
|
233
|
-
path: 'description',
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should flag invalid scenarioSelection', async () => {
|
|
239
|
-
const context = createContext({ scenarioSelection: 'invalid' as unknown });
|
|
240
|
-
const result = await validator.validate(context);
|
|
241
|
-
|
|
242
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
243
|
-
expect(result.violations).toContainEqual(
|
|
244
|
-
expect.objectContaining({
|
|
245
|
-
ruleId: 'workflow-schema-valid',
|
|
246
|
-
path: 'scenarioSelection',
|
|
247
|
-
})
|
|
248
|
-
);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('should flag empty scenarios array', async () => {
|
|
252
|
-
const context = createContext({ scenarios: [] });
|
|
253
|
-
const result = await validator.validate(context);
|
|
254
|
-
|
|
255
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
256
|
-
expect(result.violations).toContainEqual(
|
|
257
|
-
expect.objectContaining({
|
|
258
|
-
ruleId: 'workflow-schema-valid',
|
|
259
|
-
path: 'scenarios',
|
|
260
|
-
message: expect.stringContaining('empty'),
|
|
261
|
-
})
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ============================================================================
|
|
267
|
-
// Canvas Existence Tests
|
|
268
|
-
// ============================================================================
|
|
269
|
-
|
|
270
|
-
describe('checkCanvasExists', () => {
|
|
271
|
-
it('should pass when canvas file exists', async () => {
|
|
272
|
-
// Create a canvas file in temp directory
|
|
273
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
274
|
-
const markdownPath = join(tempDir, 'test.md');
|
|
275
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
276
|
-
writeFileSync(markdownPath, '# Test Documentation');
|
|
277
|
-
|
|
278
|
-
const context = createContext(
|
|
279
|
-
{ canvas: 'test.otel.canvas' },
|
|
280
|
-
{ canvasPath }
|
|
281
|
-
);
|
|
282
|
-
const result = await validator.validate(context);
|
|
283
|
-
|
|
284
|
-
const canvasViolations = result.violations.filter(
|
|
285
|
-
(v) => v.ruleId === 'workflow-canvas-exists'
|
|
286
|
-
);
|
|
287
|
-
expect(canvasViolations).toHaveLength(0);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should flag when canvas file does not exist', async () => {
|
|
291
|
-
const context = createContext(
|
|
292
|
-
{ canvas: 'nonexistent.otel.canvas' },
|
|
293
|
-
{ canvasPath: join(tempDir, 'nonexistent.otel.canvas') }
|
|
294
|
-
);
|
|
295
|
-
const result = await validator.validate(context);
|
|
296
|
-
|
|
297
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
298
|
-
expect(result.violations).toContainEqual(
|
|
299
|
-
expect.objectContaining({
|
|
300
|
-
ruleId: 'workflow-canvas-exists',
|
|
301
|
-
message: expect.stringContaining('does not exist'),
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
// ============================================================================
|
|
308
|
-
// Implementation Files Existence Tests (workflow-files-exist)
|
|
309
|
-
// ============================================================================
|
|
310
|
-
|
|
311
|
-
describe('workflow-files-exist', () => {
|
|
312
|
-
// Create the referenced canvas so unrelated canvas-exists violations stay silent.
|
|
313
|
-
function setupCanvas(): string {
|
|
314
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
315
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
316
|
-
writeFileSync(join(tempDir, 'test.md'), '# Test');
|
|
317
|
-
return canvasPath;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
it('should pass when status is implemented and all files exist', async () => {
|
|
321
|
-
const canvasPath = setupCanvas();
|
|
322
|
-
writeFileSync(join(tempDir, 'impl.ts'), '// impl');
|
|
323
|
-
|
|
324
|
-
const context = createContext(
|
|
325
|
-
{ status: 'implemented', scope: 'test-scope', files: ['impl.ts'] },
|
|
326
|
-
{ canvasPath }
|
|
327
|
-
);
|
|
328
|
-
const result = await validator.validate(context);
|
|
329
|
-
|
|
330
|
-
const fileViolations = result.violations.filter(
|
|
331
|
-
(v) => v.ruleId === 'workflow-files-exist'
|
|
332
|
-
);
|
|
333
|
-
expect(fileViolations).toHaveLength(0);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('should flag each missing file when status is implemented', async () => {
|
|
337
|
-
const canvasPath = setupCanvas();
|
|
338
|
-
|
|
339
|
-
const context = createContext(
|
|
340
|
-
{
|
|
341
|
-
status: 'implemented',
|
|
342
|
-
scope: 'test-scope',
|
|
343
|
-
files: ['missing-one.ts', 'missing-two.ts'],
|
|
344
|
-
},
|
|
345
|
-
{ canvasPath }
|
|
346
|
-
);
|
|
347
|
-
const result = await validator.validate(context);
|
|
348
|
-
|
|
349
|
-
const fileViolations = result.violations.filter(
|
|
350
|
-
(v) => v.ruleId === 'workflow-files-exist'
|
|
351
|
-
);
|
|
352
|
-
expect(fileViolations).toHaveLength(2);
|
|
353
|
-
expect(fileViolations).toContainEqual(
|
|
354
|
-
expect.objectContaining({
|
|
355
|
-
ruleId: 'workflow-files-exist',
|
|
356
|
-
severity: 'error',
|
|
357
|
-
path: 'files',
|
|
358
|
-
message: expect.stringContaining('missing-one.ts'),
|
|
359
|
-
})
|
|
360
|
-
);
|
|
361
|
-
expect(fileViolations).toContainEqual(
|
|
362
|
-
expect.objectContaining({
|
|
363
|
-
ruleId: 'workflow-files-exist',
|
|
364
|
-
message: expect.stringContaining('missing-two.ts'),
|
|
365
|
-
})
|
|
366
|
-
);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('should flag only the missing files when some exist and some do not', async () => {
|
|
370
|
-
const canvasPath = setupCanvas();
|
|
371
|
-
writeFileSync(join(tempDir, 'present.ts'), '// present');
|
|
372
|
-
|
|
373
|
-
const context = createContext(
|
|
374
|
-
{
|
|
375
|
-
status: 'implemented',
|
|
376
|
-
scope: 'test-scope',
|
|
377
|
-
files: ['present.ts', 'absent.ts'],
|
|
378
|
-
},
|
|
379
|
-
{ canvasPath }
|
|
380
|
-
);
|
|
381
|
-
const result = await validator.validate(context);
|
|
382
|
-
|
|
383
|
-
const fileViolations = result.violations.filter(
|
|
384
|
-
(v) => v.ruleId === 'workflow-files-exist'
|
|
385
|
-
);
|
|
386
|
-
expect(fileViolations).toHaveLength(1);
|
|
387
|
-
expect(fileViolations[0].message).toContain('absent.ts');
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('should not check file existence when status is draft', async () => {
|
|
391
|
-
const canvasPath = setupCanvas();
|
|
392
|
-
|
|
393
|
-
const context = createContext(
|
|
394
|
-
{ status: 'draft', files: ['does-not-exist.ts'] },
|
|
395
|
-
{ canvasPath }
|
|
396
|
-
);
|
|
397
|
-
const result = await validator.validate(context);
|
|
398
|
-
|
|
399
|
-
const fileViolations = result.violations.filter(
|
|
400
|
-
(v) => v.ruleId === 'workflow-files-exist'
|
|
401
|
-
);
|
|
402
|
-
expect(fileViolations).toHaveLength(0);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('should not check file existence when status is approved', async () => {
|
|
406
|
-
const canvasPath = setupCanvas();
|
|
407
|
-
|
|
408
|
-
const context = createContext(
|
|
409
|
-
{
|
|
410
|
-
status: 'approved',
|
|
411
|
-
scope: 'test-scope',
|
|
412
|
-
files: ['does-not-exist.ts'],
|
|
413
|
-
},
|
|
414
|
-
{ canvasPath }
|
|
415
|
-
);
|
|
416
|
-
const result = await validator.validate(context);
|
|
417
|
-
|
|
418
|
-
const fileViolations = result.violations.filter(
|
|
419
|
-
(v) => v.ruleId === 'workflow-files-exist'
|
|
420
|
-
);
|
|
421
|
-
expect(fileViolations).toHaveLength(0);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// ============================================================================
|
|
426
|
-
// Scenario Validation Tests
|
|
427
|
-
// ============================================================================
|
|
428
|
-
|
|
429
|
-
describe('checkScenarios', () => {
|
|
430
|
-
it('should pass for valid scenarios', async () => {
|
|
431
|
-
const context = createContext({});
|
|
432
|
-
const result = await validator.validate(context);
|
|
433
|
-
|
|
434
|
-
const scenarioViolations = result.violations.filter(
|
|
435
|
-
(v) => v.ruleId === 'workflow-scenario-valid'
|
|
436
|
-
);
|
|
437
|
-
expect(scenarioViolations).toHaveLength(0);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('should flag missing scenario ID', async () => {
|
|
441
|
-
const context = createContext({
|
|
442
|
-
scenarios: [
|
|
443
|
-
{
|
|
444
|
-
id: undefined as unknown,
|
|
445
|
-
priority: 1,
|
|
446
|
-
description: 'Test',
|
|
447
|
-
template: { introduction: 'Test' },
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
});
|
|
451
|
-
const result = await validator.validate(context);
|
|
452
|
-
|
|
453
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
454
|
-
expect(result.violations).toContainEqual(
|
|
455
|
-
expect.objectContaining({
|
|
456
|
-
ruleId: 'workflow-scenario-valid',
|
|
457
|
-
message: expect.stringContaining('missing required "id"'),
|
|
458
|
-
})
|
|
459
|
-
);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('should flag duplicate scenario IDs', async () => {
|
|
463
|
-
const context = createContext({
|
|
464
|
-
scenarios: [
|
|
465
|
-
{
|
|
466
|
-
id: 'duplicate',
|
|
467
|
-
priority: 1,
|
|
468
|
-
description: 'First',
|
|
469
|
-
template: { introduction: 'First' },
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
id: 'duplicate',
|
|
473
|
-
priority: 2,
|
|
474
|
-
description: 'Second',
|
|
475
|
-
template: { introduction: 'Second' },
|
|
476
|
-
},
|
|
477
|
-
],
|
|
478
|
-
});
|
|
479
|
-
const result = await validator.validate(context);
|
|
480
|
-
|
|
481
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
482
|
-
expect(result.violations).toContainEqual(
|
|
483
|
-
expect.objectContaining({
|
|
484
|
-
ruleId: 'workflow-scenario-valid',
|
|
485
|
-
message: expect.stringContaining('Duplicate scenario ID'),
|
|
486
|
-
})
|
|
487
|
-
);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it('should flag missing priority', async () => {
|
|
491
|
-
const context = createContext({
|
|
492
|
-
scenarios: [
|
|
493
|
-
{
|
|
494
|
-
id: 'test',
|
|
495
|
-
priority: undefined as unknown,
|
|
496
|
-
description: 'Test',
|
|
497
|
-
template: { introduction: 'Test' },
|
|
498
|
-
},
|
|
499
|
-
],
|
|
500
|
-
});
|
|
501
|
-
const result = await validator.validate(context);
|
|
502
|
-
|
|
503
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
504
|
-
expect(result.violations).toContainEqual(
|
|
505
|
-
expect.objectContaining({
|
|
506
|
-
ruleId: 'workflow-scenario-valid',
|
|
507
|
-
message: expect.stringContaining('missing required "priority"'),
|
|
508
|
-
})
|
|
509
|
-
);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it('should flag negative priority', async () => {
|
|
513
|
-
const context = createContext({
|
|
514
|
-
scenarios: [
|
|
515
|
-
{
|
|
516
|
-
id: 'test',
|
|
517
|
-
priority: -1,
|
|
518
|
-
description: 'Test',
|
|
519
|
-
template: { introduction: 'Test' },
|
|
520
|
-
},
|
|
521
|
-
],
|
|
522
|
-
});
|
|
523
|
-
const result = await validator.validate(context);
|
|
524
|
-
|
|
525
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
526
|
-
expect(result.violations).toContainEqual(
|
|
527
|
-
expect.objectContaining({
|
|
528
|
-
ruleId: 'workflow-scenario-valid',
|
|
529
|
-
message: expect.stringContaining('non-negative'),
|
|
530
|
-
})
|
|
531
|
-
);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('should flag duplicate priorities', async () => {
|
|
535
|
-
const context = createContext({
|
|
536
|
-
scenarios: [
|
|
537
|
-
{
|
|
538
|
-
id: 'first',
|
|
539
|
-
priority: 1,
|
|
540
|
-
description: 'First',
|
|
541
|
-
template: { introduction: 'First' },
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
id: 'second',
|
|
545
|
-
priority: 1,
|
|
546
|
-
description: 'Second',
|
|
547
|
-
template: { introduction: 'Second' },
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
});
|
|
551
|
-
const result = await validator.validate(context);
|
|
552
|
-
|
|
553
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
554
|
-
expect(result.violations).toContainEqual(
|
|
555
|
-
expect.objectContaining({
|
|
556
|
-
ruleId: 'workflow-scenario-valid',
|
|
557
|
-
message: expect.stringContaining('Duplicate priority'),
|
|
558
|
-
})
|
|
559
|
-
);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it('should flag missing description', async () => {
|
|
563
|
-
const context = createContext({
|
|
564
|
-
scenarios: [
|
|
565
|
-
{
|
|
566
|
-
id: 'test',
|
|
567
|
-
priority: 1,
|
|
568
|
-
description: undefined as unknown,
|
|
569
|
-
template: { introduction: 'Test' },
|
|
570
|
-
},
|
|
571
|
-
],
|
|
572
|
-
});
|
|
573
|
-
const result = await validator.validate(context);
|
|
574
|
-
|
|
575
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
576
|
-
expect(result.violations).toContainEqual(
|
|
577
|
-
expect.objectContaining({
|
|
578
|
-
ruleId: 'workflow-scenario-valid',
|
|
579
|
-
message: expect.stringContaining('missing required "description"'),
|
|
580
|
-
})
|
|
581
|
-
);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it('should flag missing template', async () => {
|
|
585
|
-
const context = createContext({
|
|
586
|
-
scenarios: [
|
|
587
|
-
{
|
|
588
|
-
id: 'test',
|
|
589
|
-
priority: 1,
|
|
590
|
-
description: 'Test',
|
|
591
|
-
template: undefined as unknown,
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
});
|
|
595
|
-
const result = await validator.validate(context);
|
|
596
|
-
|
|
597
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
598
|
-
expect(result.violations).toContainEqual(
|
|
599
|
-
expect.objectContaining({
|
|
600
|
-
ruleId: 'workflow-scenario-valid',
|
|
601
|
-
message: expect.stringContaining('missing required "template"'),
|
|
602
|
-
})
|
|
603
|
-
);
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
// ============================================================================
|
|
608
|
-
// Template Syntax Tests
|
|
609
|
-
// ============================================================================
|
|
610
|
-
|
|
611
|
-
describe('checkTemplateSyntax', () => {
|
|
612
|
-
it('should pass for valid template strings', async () => {
|
|
613
|
-
const context = createContext({
|
|
614
|
-
scenarios: [
|
|
615
|
-
{
|
|
616
|
-
id: 'test',
|
|
617
|
-
priority: 1,
|
|
618
|
-
description: 'Test',
|
|
619
|
-
template: {
|
|
620
|
-
introduction: 'Started {test.name}',
|
|
621
|
-
events: {
|
|
622
|
-
'test.started': 'Test {test.id} started',
|
|
623
|
-
'test.complete': '{result.status === "passed" ? "✅ Passed" : "❌ Failed"}',
|
|
624
|
-
},
|
|
625
|
-
summary: 'Completed in {duration}ms',
|
|
626
|
-
},
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
});
|
|
630
|
-
const result = await validator.validate(context);
|
|
631
|
-
|
|
632
|
-
const syntaxViolations = result.violations.filter(
|
|
633
|
-
(v) => v.ruleId === 'workflow-template-syntax'
|
|
634
|
-
);
|
|
635
|
-
expect(syntaxViolations).toHaveLength(0);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it('should flag unbalanced braces (missing closing)', async () => {
|
|
639
|
-
const context = createContext({
|
|
640
|
-
scenarios: [
|
|
641
|
-
{
|
|
642
|
-
id: 'test',
|
|
643
|
-
priority: 1,
|
|
644
|
-
description: 'Test',
|
|
645
|
-
template: {
|
|
646
|
-
introduction: 'Test {test.name',
|
|
647
|
-
},
|
|
648
|
-
},
|
|
649
|
-
],
|
|
650
|
-
});
|
|
651
|
-
const result = await validator.validate(context);
|
|
652
|
-
|
|
653
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
654
|
-
expect(result.violations).toContainEqual(
|
|
655
|
-
expect.objectContaining({
|
|
656
|
-
ruleId: 'workflow-template-syntax',
|
|
657
|
-
message: expect.stringContaining('Unbalanced braces'),
|
|
658
|
-
})
|
|
659
|
-
);
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it('should flag unbalanced braces (extra closing)', async () => {
|
|
663
|
-
const context = createContext({
|
|
664
|
-
scenarios: [
|
|
665
|
-
{
|
|
666
|
-
id: 'test',
|
|
667
|
-
priority: 1,
|
|
668
|
-
description: 'Test',
|
|
669
|
-
template: {
|
|
670
|
-
introduction: 'Test {test.name}}',
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
],
|
|
674
|
-
});
|
|
675
|
-
const result = await validator.validate(context);
|
|
676
|
-
|
|
677
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
678
|
-
expect(result.violations).toContainEqual(
|
|
679
|
-
expect.objectContaining({
|
|
680
|
-
ruleId: 'workflow-template-syntax',
|
|
681
|
-
message: expect.stringContaining('Unbalanced braces'),
|
|
682
|
-
})
|
|
683
|
-
);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it('should flag incomplete conditional expression', async () => {
|
|
687
|
-
const context = createContext({
|
|
688
|
-
scenarios: [
|
|
689
|
-
{
|
|
690
|
-
id: 'test',
|
|
691
|
-
priority: 1,
|
|
692
|
-
description: 'Test',
|
|
693
|
-
template: {
|
|
694
|
-
introduction: '{result.passed ? "Success"}',
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
],
|
|
698
|
-
});
|
|
699
|
-
const result = await validator.validate(context);
|
|
700
|
-
|
|
701
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
702
|
-
expect(result.violations).toContainEqual(
|
|
703
|
-
expect.objectContaining({
|
|
704
|
-
ruleId: 'workflow-template-syntax',
|
|
705
|
-
message: expect.stringContaining('Incomplete conditional'),
|
|
706
|
-
})
|
|
707
|
-
);
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it('should handle quotes in template strings', async () => {
|
|
711
|
-
const context = createContext({
|
|
712
|
-
scenarios: [
|
|
713
|
-
{
|
|
714
|
-
id: 'test',
|
|
715
|
-
priority: 1,
|
|
716
|
-
description: 'Test',
|
|
717
|
-
template: {
|
|
718
|
-
introduction: '{test.status === "passed" ? "Success" : "Failed"}',
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
});
|
|
723
|
-
const result = await validator.validate(context);
|
|
724
|
-
|
|
725
|
-
const syntaxViolations = result.violations.filter(
|
|
726
|
-
(v) => v.ruleId === 'workflow-template-syntax'
|
|
727
|
-
);
|
|
728
|
-
expect(syntaxViolations).toHaveLength(0);
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('should validate event templates', async () => {
|
|
732
|
-
const context = createContext({
|
|
733
|
-
scenarios: [
|
|
734
|
-
{
|
|
735
|
-
id: 'test',
|
|
736
|
-
priority: 1,
|
|
737
|
-
description: 'Test',
|
|
738
|
-
template: {
|
|
739
|
-
events: {
|
|
740
|
-
'test.started': 'Started {test.name',
|
|
741
|
-
'test.complete': 'Completed',
|
|
742
|
-
},
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
],
|
|
746
|
-
});
|
|
747
|
-
const result = await validator.validate(context);
|
|
748
|
-
|
|
749
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
750
|
-
expect(result.violations).toContainEqual(
|
|
751
|
-
expect.objectContaining({
|
|
752
|
-
ruleId: 'workflow-template-syntax',
|
|
753
|
-
path: expect.stringContaining('events.test.started'),
|
|
754
|
-
})
|
|
755
|
-
);
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
it('should flag Handlebars {{#if}} conditionals', async () => {
|
|
759
|
-
const context = createContext({
|
|
760
|
-
scenarios: [
|
|
761
|
-
{
|
|
762
|
-
id: 'test',
|
|
763
|
-
priority: 1,
|
|
764
|
-
description: 'Test',
|
|
765
|
-
template: {
|
|
766
|
-
introduction: '{{#if hasError}}Error occurred{{/if}}',
|
|
767
|
-
},
|
|
768
|
-
},
|
|
769
|
-
],
|
|
770
|
-
});
|
|
771
|
-
const result = await validator.validate(context);
|
|
772
|
-
|
|
773
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
774
|
-
expect(result.violations).toContainEqual(
|
|
775
|
-
expect.objectContaining({
|
|
776
|
-
ruleId: 'workflow-template-conditional',
|
|
777
|
-
message: expect.stringContaining('Conditional syntax detected'),
|
|
778
|
-
suggestion: expect.stringContaining('Split into separate scenarios'),
|
|
779
|
-
})
|
|
780
|
-
);
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
it('should flag Handlebars {{#unless}} conditionals', async () => {
|
|
784
|
-
const context = createContext({
|
|
785
|
-
scenarios: [
|
|
786
|
-
{
|
|
787
|
-
id: 'test',
|
|
788
|
-
priority: 1,
|
|
789
|
-
description: 'Test',
|
|
790
|
-
template: {
|
|
791
|
-
summary: '{{#unless success}}Failed{{/unless}}',
|
|
792
|
-
},
|
|
793
|
-
},
|
|
794
|
-
],
|
|
795
|
-
});
|
|
796
|
-
const result = await validator.validate(context);
|
|
797
|
-
|
|
798
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
799
|
-
expect(result.violations).toContainEqual(
|
|
800
|
-
expect.objectContaining({
|
|
801
|
-
ruleId: 'workflow-template-conditional',
|
|
802
|
-
message: expect.stringContaining('{{#unless'),
|
|
803
|
-
})
|
|
804
|
-
);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it('should flag Handlebars {{#each}} iteration', async () => {
|
|
808
|
-
const context = createContext({
|
|
809
|
-
scenarios: [
|
|
810
|
-
{
|
|
811
|
-
id: 'test',
|
|
812
|
-
priority: 1,
|
|
813
|
-
description: 'Test',
|
|
814
|
-
template: {
|
|
815
|
-
introduction: '{{#each items}}{{name}}{{/each}}',
|
|
816
|
-
},
|
|
817
|
-
},
|
|
818
|
-
],
|
|
819
|
-
});
|
|
820
|
-
const result = await validator.validate(context);
|
|
821
|
-
|
|
822
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
823
|
-
expect(result.violations).toContainEqual(
|
|
824
|
-
expect.objectContaining({
|
|
825
|
-
ruleId: 'workflow-template-conditional',
|
|
826
|
-
message: expect.stringContaining('{{#each'),
|
|
827
|
-
})
|
|
828
|
-
);
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
it('should flag Handlebars {{else}} clause', async () => {
|
|
832
|
-
const context = createContext({
|
|
833
|
-
scenarios: [
|
|
834
|
-
{
|
|
835
|
-
id: 'test',
|
|
836
|
-
priority: 1,
|
|
837
|
-
description: 'Test',
|
|
838
|
-
template: {
|
|
839
|
-
events: {
|
|
840
|
-
'test.started': '{{#if fast}}Quick{{else}}Slow{{/if}}',
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
],
|
|
845
|
-
});
|
|
846
|
-
const result = await validator.validate(context);
|
|
847
|
-
|
|
848
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
849
|
-
expect(result.violations).toContainEqual(
|
|
850
|
-
expect.objectContaining({
|
|
851
|
-
ruleId: 'workflow-template-conditional',
|
|
852
|
-
})
|
|
853
|
-
);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it('should allow simple variable interpolation without conditionals', async () => {
|
|
857
|
-
const context = createContext({
|
|
858
|
-
scenarios: [
|
|
859
|
-
{
|
|
860
|
-
id: 'test',
|
|
861
|
-
priority: 1,
|
|
862
|
-
description: 'Test',
|
|
863
|
-
template: {
|
|
864
|
-
introduction: 'Test {{name}} completed in {{duration}}ms',
|
|
865
|
-
events: {
|
|
866
|
-
'test.started': 'Started {{testName}}',
|
|
867
|
-
},
|
|
868
|
-
summary: 'Result: {{status}}',
|
|
869
|
-
},
|
|
870
|
-
},
|
|
871
|
-
],
|
|
872
|
-
});
|
|
873
|
-
const result = await validator.validate(context);
|
|
874
|
-
|
|
875
|
-
const conditionalViolations = result.violations.filter(
|
|
876
|
-
(v) => v.ruleId === 'workflow-template-conditional'
|
|
877
|
-
);
|
|
878
|
-
expect(conditionalViolations).toHaveLength(0);
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
// ============================================================================
|
|
883
|
-
// Emoji Validation Tests
|
|
884
|
-
// ============================================================================
|
|
885
|
-
|
|
886
|
-
describe('checkTemplateEmojis', () => {
|
|
887
|
-
it('should flag emojis in event templates', async () => {
|
|
888
|
-
const context = createContext({
|
|
889
|
-
scenarios: [
|
|
890
|
-
{
|
|
891
|
-
id: 'test',
|
|
892
|
-
priority: 1,
|
|
893
|
-
description: 'Test',
|
|
894
|
-
template: {
|
|
895
|
-
events: {
|
|
896
|
-
'test.started': '🚀 Starting test',
|
|
897
|
-
},
|
|
898
|
-
},
|
|
899
|
-
},
|
|
900
|
-
],
|
|
901
|
-
});
|
|
902
|
-
const result = await validator.validate(context);
|
|
903
|
-
|
|
904
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
905
|
-
expect(result.violations).toContainEqual(
|
|
906
|
-
expect.objectContaining({
|
|
907
|
-
ruleId: 'workflow-template-emoji',
|
|
908
|
-
message: expect.stringContaining('emoji'),
|
|
909
|
-
suggestion: expect.stringContaining('Remove emoji'),
|
|
910
|
-
})
|
|
911
|
-
);
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it('should flag emojis in introduction', async () => {
|
|
915
|
-
const context = createContext({
|
|
916
|
-
scenarios: [
|
|
917
|
-
{
|
|
918
|
-
id: 'test',
|
|
919
|
-
priority: 1,
|
|
920
|
-
description: 'Test',
|
|
921
|
-
template: {
|
|
922
|
-
introduction: '✅ Test Started',
|
|
923
|
-
events: {
|
|
924
|
-
'test.started': 'Started',
|
|
925
|
-
},
|
|
926
|
-
},
|
|
927
|
-
},
|
|
928
|
-
],
|
|
929
|
-
});
|
|
930
|
-
const result = await validator.validate(context);
|
|
931
|
-
|
|
932
|
-
expect(result.violations).toContainEqual(
|
|
933
|
-
expect.objectContaining({
|
|
934
|
-
ruleId: 'workflow-template-emoji',
|
|
935
|
-
})
|
|
936
|
-
);
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
it('should flag emojis in summary', async () => {
|
|
940
|
-
const context = createContext({
|
|
941
|
-
scenarios: [
|
|
942
|
-
{
|
|
943
|
-
id: 'test',
|
|
944
|
-
priority: 1,
|
|
945
|
-
description: 'Test',
|
|
946
|
-
template: {
|
|
947
|
-
events: {
|
|
948
|
-
'test.started': 'Started',
|
|
949
|
-
},
|
|
950
|
-
summary: '❌ Failed',
|
|
951
|
-
},
|
|
952
|
-
},
|
|
953
|
-
],
|
|
954
|
-
});
|
|
955
|
-
const result = await validator.validate(context);
|
|
956
|
-
|
|
957
|
-
expect(result.violations).toContainEqual(
|
|
958
|
-
expect.objectContaining({
|
|
959
|
-
ruleId: 'workflow-template-emoji',
|
|
960
|
-
})
|
|
961
|
-
);
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
it('should flag multiple different emojis', async () => {
|
|
965
|
-
const context = createContext({
|
|
966
|
-
scenarios: [
|
|
967
|
-
{
|
|
968
|
-
id: 'test',
|
|
969
|
-
priority: 1,
|
|
970
|
-
description: 'Test',
|
|
971
|
-
template: {
|
|
972
|
-
introduction: '🔄 Loading...',
|
|
973
|
-
events: {
|
|
974
|
-
'test.started': '▶️ Started',
|
|
975
|
-
'test.complete': '✅ Done',
|
|
976
|
-
},
|
|
977
|
-
summary: '🎉 Success!',
|
|
978
|
-
},
|
|
979
|
-
},
|
|
980
|
-
],
|
|
981
|
-
});
|
|
982
|
-
const result = await validator.validate(context);
|
|
983
|
-
|
|
984
|
-
const emojiViolations = result.violations.filter(
|
|
985
|
-
(v) => v.ruleId === 'workflow-template-emoji'
|
|
986
|
-
);
|
|
987
|
-
expect(emojiViolations.length).toBeGreaterThan(0);
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
it('should allow templates without emojis', async () => {
|
|
991
|
-
const context = createContext({
|
|
992
|
-
scenarios: [
|
|
993
|
-
{
|
|
994
|
-
id: 'test',
|
|
995
|
-
priority: 1,
|
|
996
|
-
description: 'Test',
|
|
997
|
-
template: {
|
|
998
|
-
introduction: '[START] Test Beginning',
|
|
999
|
-
events: {
|
|
1000
|
-
'test.started': 'Started: {{name}}',
|
|
1001
|
-
'test.complete': 'Completed in {{duration}}ms',
|
|
1002
|
-
},
|
|
1003
|
-
summary: '[DONE] Test finished successfully',
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1006
|
-
],
|
|
1007
|
-
});
|
|
1008
|
-
const result = await validator.validate(context);
|
|
1009
|
-
|
|
1010
|
-
const emojiViolations = result.violations.filter(
|
|
1011
|
-
(v) => v.ruleId === 'workflow-template-emoji'
|
|
1012
|
-
);
|
|
1013
|
-
expect(emojiViolations).toHaveLength(0);
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
it('should allow special characters that are not emojis', async () => {
|
|
1017
|
-
const context = createContext({
|
|
1018
|
-
scenarios: [
|
|
1019
|
-
{
|
|
1020
|
-
id: 'test',
|
|
1021
|
-
priority: 1,
|
|
1022
|
-
description: 'Test',
|
|
1023
|
-
template: {
|
|
1024
|
-
introduction: '>>> Test Output <<<',
|
|
1025
|
-
events: {
|
|
1026
|
-
'test.started': '* Started @ {{timestamp}}',
|
|
1027
|
-
'test.complete': '# Complete - Duration: {{duration}}ms',
|
|
1028
|
-
},
|
|
1029
|
-
summary: '=== Summary ===',
|
|
1030
|
-
},
|
|
1031
|
-
},
|
|
1032
|
-
],
|
|
1033
|
-
});
|
|
1034
|
-
const result = await validator.validate(context);
|
|
1035
|
-
|
|
1036
|
-
const emojiViolations = result.violations.filter(
|
|
1037
|
-
(v) => v.ruleId === 'workflow-template-emoji'
|
|
1038
|
-
);
|
|
1039
|
-
expect(emojiViolations).toHaveLength(0);
|
|
1040
|
-
});
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// ============================================================================
|
|
1044
|
-
// Formatting Options Tests
|
|
1045
|
-
// ============================================================================
|
|
1046
|
-
|
|
1047
|
-
describe('checkFormattingOptions', () => {
|
|
1048
|
-
it('should pass for valid formatting options', async () => {
|
|
1049
|
-
const context = createContext({
|
|
1050
|
-
formatting: {
|
|
1051
|
-
indentPerLevel: ' ',
|
|
1052
|
-
timestampFormat: 'HH:mm:ss.SSS',
|
|
1053
|
-
showTimestamps: true,
|
|
1054
|
-
showDuration: true,
|
|
1055
|
-
showSpanIds: false,
|
|
1056
|
-
showAttributes: 'matched',
|
|
1057
|
-
},
|
|
1058
|
-
});
|
|
1059
|
-
const result = await validator.validate(context);
|
|
1060
|
-
|
|
1061
|
-
const formattingViolations = result.violations.filter(
|
|
1062
|
-
(v) => v.ruleId === 'workflow-formatting-options'
|
|
1063
|
-
);
|
|
1064
|
-
expect(formattingViolations).toHaveLength(0);
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
it('should flag invalid showAttributes value', async () => {
|
|
1068
|
-
const context = createContext({
|
|
1069
|
-
formatting: {
|
|
1070
|
-
showAttributes: 'invalid' as unknown,
|
|
1071
|
-
},
|
|
1072
|
-
});
|
|
1073
|
-
const result = await validator.validate(context);
|
|
1074
|
-
|
|
1075
|
-
expect(result.warningCount).toBeGreaterThan(0);
|
|
1076
|
-
expect(result.violations).toContainEqual(
|
|
1077
|
-
expect.objectContaining({
|
|
1078
|
-
ruleId: 'workflow-formatting-options',
|
|
1079
|
-
severity: 'warn',
|
|
1080
|
-
message: expect.stringContaining('Invalid showAttributes'),
|
|
1081
|
-
})
|
|
1082
|
-
);
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
it('should accept valid showAttributes values', async () => {
|
|
1086
|
-
const values = ['none', 'matched', 'all'];
|
|
1087
|
-
|
|
1088
|
-
for (const value of values) {
|
|
1089
|
-
const context = createContext({
|
|
1090
|
-
formatting: {
|
|
1091
|
-
showAttributes: value as unknown,
|
|
1092
|
-
},
|
|
1093
|
-
});
|
|
1094
|
-
const result = await validator.validate(context);
|
|
1095
|
-
|
|
1096
|
-
const formattingViolations = result.violations.filter(
|
|
1097
|
-
(v) => v.ruleId === 'workflow-formatting-options' && v.path === 'formatting.showAttributes'
|
|
1098
|
-
);
|
|
1099
|
-
expect(formattingViolations).toHaveLength(0);
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
// ============================================================================
|
|
1105
|
-
// Aggregate Results Tests
|
|
1106
|
-
// ============================================================================
|
|
1107
|
-
|
|
1108
|
-
describe('aggregateResults', () => {
|
|
1109
|
-
it('should correctly count errors and warnings', async () => {
|
|
1110
|
-
const context = createContext({
|
|
1111
|
-
version: undefined as unknown,
|
|
1112
|
-
name: undefined as unknown,
|
|
1113
|
-
formatting: {
|
|
1114
|
-
showAttributes: 'invalid' as unknown,
|
|
1115
|
-
},
|
|
1116
|
-
});
|
|
1117
|
-
const result = await validator.validate(context);
|
|
1118
|
-
|
|
1119
|
-
expect(result.errorCount).toBeGreaterThan(0);
|
|
1120
|
-
expect(result.warningCount).toBeGreaterThan(0);
|
|
1121
|
-
expect(result.violations.length).toBe(result.errorCount + result.warningCount);
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
it('should count fixable violations', async () => {
|
|
1125
|
-
// Currently no violations are fixable, but test the infrastructure
|
|
1126
|
-
const context = createContext({});
|
|
1127
|
-
const result = await validator.validate(context);
|
|
1128
|
-
|
|
1129
|
-
const fixable = result.violations.filter((v) => v.fixable);
|
|
1130
|
-
expect(result.fixableCount).toBe(fixable.length);
|
|
1131
|
-
});
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
// ============================================================================
|
|
1135
|
-
// Integration Tests
|
|
1136
|
-
// ============================================================================
|
|
1137
|
-
|
|
1138
|
-
describe('integration tests', () => {
|
|
1139
|
-
it('should validate a complete valid workflow template', async () => {
|
|
1140
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
1141
|
-
const markdownPath = join(tempDir, 'test.md');
|
|
1142
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
1143
|
-
writeFileSync(markdownPath, '# Test Documentation');
|
|
1144
|
-
|
|
1145
|
-
const workflow: WorkflowTemplate = {
|
|
1146
|
-
version: '1.0.0',
|
|
1147
|
-
canvas: 'test.otel.canvas',
|
|
1148
|
-
name: 'Complete Test Workflow',
|
|
1149
|
-
description: 'A complete workflow template for testing',
|
|
1150
|
-
spanPattern: 'test.integration',
|
|
1151
|
-
scenarioSelection: 'first-match',
|
|
1152
|
-
scenarios: [
|
|
1153
|
-
{
|
|
1154
|
-
id: 'error',
|
|
1155
|
-
priority: 1,
|
|
1156
|
-
description: 'Error scenario',
|
|
1157
|
-
template: {
|
|
1158
|
-
introduction: '[ERROR] Error Occurred',
|
|
1159
|
-
events: {
|
|
1160
|
-
'test.started': 'Started: {test.name}',
|
|
1161
|
-
'test.error': 'Error: {error.message}',
|
|
1162
|
-
},
|
|
1163
|
-
summary: '[FAILED]',
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
{
|
|
1167
|
-
id: 'success',
|
|
1168
|
-
priority: 2,
|
|
1169
|
-
description: 'Success scenario',
|
|
1170
|
-
template: {
|
|
1171
|
-
introduction: '[SUCCESS] Test Passed',
|
|
1172
|
-
events: {
|
|
1173
|
-
'test.started': 'Started: {test.name}',
|
|
1174
|
-
'test.complete': 'Completed in {duration}ms',
|
|
1175
|
-
},
|
|
1176
|
-
summary: '[SUCCESS]',
|
|
1177
|
-
},
|
|
1178
|
-
},
|
|
1179
|
-
],
|
|
1180
|
-
formatting: {
|
|
1181
|
-
indentPerLevel: ' ',
|
|
1182
|
-
showTimestamps: false,
|
|
1183
|
-
showDuration: true,
|
|
1184
|
-
showAttributes: 'matched',
|
|
1185
|
-
},
|
|
1186
|
-
};
|
|
1187
|
-
|
|
1188
|
-
const context: WorkflowValidationContext = {
|
|
1189
|
-
workflow,
|
|
1190
|
-
workflowPath: 'test.workflow.json',
|
|
1191
|
-
canvas: createValidCanvas(),
|
|
1192
|
-
canvasPath,
|
|
1193
|
-
basePath: tempDir,
|
|
1194
|
-
};
|
|
1195
|
-
|
|
1196
|
-
const result = await validator.validate(context);
|
|
1197
|
-
|
|
1198
|
-
expect(result.errorCount).toBe(0);
|
|
1199
|
-
expect(result.warningCount).toBe(0);
|
|
1200
|
-
expect(result.violations).toHaveLength(0);
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
it('should collect multiple violations from different rules', async () => {
|
|
1204
|
-
const workflow = {
|
|
1205
|
-
version: 'invalid-version',
|
|
1206
|
-
canvas: 'nonexistent.canvas',
|
|
1207
|
-
name: '',
|
|
1208
|
-
scenarios: [
|
|
1209
|
-
{
|
|
1210
|
-
id: 'test',
|
|
1211
|
-
priority: -1,
|
|
1212
|
-
template: {
|
|
1213
|
-
introduction: '{unclosed',
|
|
1214
|
-
},
|
|
1215
|
-
},
|
|
1216
|
-
],
|
|
1217
|
-
formatting: {
|
|
1218
|
-
showAttributes: 'invalid',
|
|
1219
|
-
},
|
|
1220
|
-
} as unknown as WorkflowTemplate;
|
|
1221
|
-
|
|
1222
|
-
const context: WorkflowValidationContext = {
|
|
1223
|
-
workflow,
|
|
1224
|
-
workflowPath: 'test.workflow.json',
|
|
1225
|
-
basePath: tempDir,
|
|
1226
|
-
};
|
|
1227
|
-
|
|
1228
|
-
const result = await validator.validate(context);
|
|
1229
|
-
|
|
1230
|
-
expect(result.violations.length).toBeGreaterThan(5);
|
|
1231
|
-
|
|
1232
|
-
const ruleIds = new Set(result.violations.map((v) => v.ruleId));
|
|
1233
|
-
expect(ruleIds.size).toBeGreaterThan(3); // Multiple different rules triggered
|
|
1234
|
-
});
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
// ============================================================================
|
|
1238
|
-
// EventRegistry Enhanced Error Messages Tests
|
|
1239
|
-
// ============================================================================
|
|
1240
|
-
|
|
1241
|
-
describe('checkEventReferences with EventRegistry', () => {
|
|
1242
|
-
function createLibrary(eventSchemas: Record<string, { description: string; attributes: Record<string, unknown> }>): ComponentLibrary {
|
|
1243
|
-
return {
|
|
1244
|
-
version: '1.0.0',
|
|
1245
|
-
name: 'Test Library',
|
|
1246
|
-
nodeComponents: {},
|
|
1247
|
-
edgeComponents: {},
|
|
1248
|
-
eventSchemas,
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function createCanvasWithEvents(events: string[]): ExtendedCanvas {
|
|
1253
|
-
return {
|
|
1254
|
-
nodes: events.map((eventName, i) => ({
|
|
1255
|
-
id: `node-${i}`,
|
|
1256
|
-
type: 'text' as const,
|
|
1257
|
-
x: i * 100,
|
|
1258
|
-
y: 0,
|
|
1259
|
-
width: 100,
|
|
1260
|
-
height: 50,
|
|
1261
|
-
pv: {
|
|
1262
|
-
eventRef: eventName,
|
|
1263
|
-
},
|
|
1264
|
-
})),
|
|
1265
|
-
edges: [],
|
|
1266
|
-
pv: {
|
|
1267
|
-
version: '1.0.0',
|
|
1268
|
-
name: 'Test Canvas',
|
|
1269
|
-
markdown: 'test.md',
|
|
1270
|
-
},
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
it('should show library suggestion when event exists in library but not canvas', async () => {
|
|
1275
|
-
// Create library with the event
|
|
1276
|
-
const library = createLibrary({
|
|
1277
|
-
'cleanup.started': { description: 'Cleanup started', attributes: {} },
|
|
1278
|
-
});
|
|
1279
|
-
|
|
1280
|
-
// Create a canvas WITHOUT the event
|
|
1281
|
-
const canvas = createCanvasWithEvents(['other.event']);
|
|
1282
|
-
|
|
1283
|
-
// Create another canvas that HAS the event (for the registry)
|
|
1284
|
-
const otherCanvas = createCanvasWithEvents(['cleanup.started']);
|
|
1285
|
-
|
|
1286
|
-
// Build registry with library
|
|
1287
|
-
const canvases = new Map<string, ExtendedCanvas>();
|
|
1288
|
-
canvases.set('other.otel.canvas', otherCanvas);
|
|
1289
|
-
const eventRegistry = EventRegistry.build(library, canvases, 'library.yaml');
|
|
1290
|
-
|
|
1291
|
-
// Workflow references an event not in its canvas
|
|
1292
|
-
const workflow: WorkflowTemplate = {
|
|
1293
|
-
version: '1.0.0',
|
|
1294
|
-
canvas: 'test.otel.canvas',
|
|
1295
|
-
name: 'Test Workflow',
|
|
1296
|
-
description: 'Test',
|
|
1297
|
-
mode: 'span-tree',
|
|
1298
|
-
scenarioSelection: 'first-match',
|
|
1299
|
-
scenarios: [
|
|
1300
|
-
{
|
|
1301
|
-
id: 'default',
|
|
1302
|
-
priority: 1,
|
|
1303
|
-
description: 'Default',
|
|
1304
|
-
template: {
|
|
1305
|
-
events: {
|
|
1306
|
-
'cleanup.started': 'Cleanup started', // Not in canvas
|
|
1307
|
-
},
|
|
1308
|
-
},
|
|
1309
|
-
},
|
|
1310
|
-
],
|
|
1311
|
-
};
|
|
1312
|
-
|
|
1313
|
-
const context: WorkflowValidationContext = {
|
|
1314
|
-
workflow,
|
|
1315
|
-
workflowPath: 'test.workflow.json',
|
|
1316
|
-
canvas,
|
|
1317
|
-
canvasPath: join(tempDir, 'test.otel.canvas'),
|
|
1318
|
-
basePath: tempDir,
|
|
1319
|
-
eventRegistry,
|
|
1320
|
-
};
|
|
1321
|
-
|
|
1322
|
-
const result = await validator.validate(context);
|
|
1323
|
-
|
|
1324
|
-
// Should have an error about the missing event
|
|
1325
|
-
const eventViolation = result.violations.find(
|
|
1326
|
-
v => v.ruleId === 'workflow-event-sync' && v.message.includes('cleanup.started')
|
|
1327
|
-
);
|
|
1328
|
-
|
|
1329
|
-
expect(eventViolation).toBeDefined();
|
|
1330
|
-
// Should suggest using eventRef since it's in the library
|
|
1331
|
-
expect(eventViolation!.message).toContain('available in library');
|
|
1332
|
-
expect(eventViolation!.suggestion).toContain('eventRef');
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
it('should show canvas location when event exists in another canvas but not library', async () => {
|
|
1336
|
-
// No library events
|
|
1337
|
-
const canvases = new Map<string, ExtendedCanvas>();
|
|
1338
|
-
|
|
1339
|
-
// Create another canvas that has the event we're looking for
|
|
1340
|
-
const cleanupCanvas = createCanvasWithEvents(['cleanup.started']);
|
|
1341
|
-
canvases.set('cleanup-operations.otel.canvas', cleanupCanvas);
|
|
1342
|
-
|
|
1343
|
-
// Build registry without library
|
|
1344
|
-
const eventRegistry = EventRegistry.build(undefined, canvases);
|
|
1345
|
-
|
|
1346
|
-
// Create a canvas WITHOUT the event
|
|
1347
|
-
const canvas = createCanvasWithEvents(['other.event']);
|
|
1348
|
-
|
|
1349
|
-
// Workflow references an event not in its canvas
|
|
1350
|
-
const workflow: WorkflowTemplate = {
|
|
1351
|
-
version: '1.0.0',
|
|
1352
|
-
canvas: 'test.otel.canvas',
|
|
1353
|
-
name: 'Test Workflow',
|
|
1354
|
-
description: 'Test',
|
|
1355
|
-
mode: 'span-tree',
|
|
1356
|
-
scenarioSelection: 'first-match',
|
|
1357
|
-
scenarios: [
|
|
1358
|
-
{
|
|
1359
|
-
id: 'default',
|
|
1360
|
-
priority: 1,
|
|
1361
|
-
description: 'Default',
|
|
1362
|
-
template: {
|
|
1363
|
-
events: {
|
|
1364
|
-
'cleanup.started': 'Cleanup started', // Not in canvas but in another canvas
|
|
1365
|
-
},
|
|
1366
|
-
},
|
|
1367
|
-
},
|
|
1368
|
-
],
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
const context: WorkflowValidationContext = {
|
|
1372
|
-
workflow,
|
|
1373
|
-
workflowPath: 'test.workflow.json',
|
|
1374
|
-
canvas,
|
|
1375
|
-
canvasPath: join(tempDir, 'test.otel.canvas'),
|
|
1376
|
-
basePath: tempDir,
|
|
1377
|
-
eventRegistry,
|
|
1378
|
-
};
|
|
1379
|
-
|
|
1380
|
-
const result = await validator.validate(context);
|
|
1381
|
-
|
|
1382
|
-
// Should have an error about the missing event
|
|
1383
|
-
const eventViolation = result.violations.find(
|
|
1384
|
-
v => v.ruleId === 'workflow-event-sync' && v.message.includes('cleanup.started')
|
|
1385
|
-
);
|
|
1386
|
-
|
|
1387
|
-
expect(eventViolation).toBeDefined();
|
|
1388
|
-
// Should mention where the event was found
|
|
1389
|
-
expect(eventViolation!.message).toContain('cleanup-operations.otel.canvas');
|
|
1390
|
-
// Should suggest adding to library
|
|
1391
|
-
expect(eventViolation!.suggestion).toContain('library.yaml');
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
it('should show generic error when event not found anywhere', async () => {
|
|
1395
|
-
// Empty registry
|
|
1396
|
-
const eventRegistry = EventRegistry.build(undefined, new Map());
|
|
1397
|
-
|
|
1398
|
-
// Create a canvas without the event
|
|
1399
|
-
const canvas = createCanvasWithEvents(['other.event']);
|
|
1400
|
-
|
|
1401
|
-
// Workflow references an event that doesn't exist anywhere
|
|
1402
|
-
const workflow: WorkflowTemplate = {
|
|
1403
|
-
version: '1.0.0',
|
|
1404
|
-
canvas: 'test.otel.canvas',
|
|
1405
|
-
name: 'Test Workflow',
|
|
1406
|
-
description: 'Test',
|
|
1407
|
-
mode: 'span-tree',
|
|
1408
|
-
scenarioSelection: 'first-match',
|
|
1409
|
-
scenarios: [
|
|
1410
|
-
{
|
|
1411
|
-
id: 'default',
|
|
1412
|
-
priority: 1,
|
|
1413
|
-
description: 'Default',
|
|
1414
|
-
template: {
|
|
1415
|
-
events: {
|
|
1416
|
-
'nonexistent.event': 'Does not exist anywhere',
|
|
1417
|
-
},
|
|
1418
|
-
},
|
|
1419
|
-
},
|
|
1420
|
-
],
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
|
-
const context: WorkflowValidationContext = {
|
|
1424
|
-
workflow,
|
|
1425
|
-
workflowPath: 'test.workflow.json',
|
|
1426
|
-
canvas,
|
|
1427
|
-
canvasPath: join(tempDir, 'test.otel.canvas'),
|
|
1428
|
-
basePath: tempDir,
|
|
1429
|
-
eventRegistry,
|
|
1430
|
-
};
|
|
1431
|
-
|
|
1432
|
-
const result = await validator.validate(context);
|
|
1433
|
-
|
|
1434
|
-
// Should have an error about the missing event
|
|
1435
|
-
const eventViolation = result.violations.find(
|
|
1436
|
-
v => v.ruleId === 'workflow-event-sync' && v.message.includes('nonexistent.event')
|
|
1437
|
-
);
|
|
1438
|
-
|
|
1439
|
-
expect(eventViolation).toBeDefined();
|
|
1440
|
-
// Should show the standard error message
|
|
1441
|
-
expect(eventViolation!.message).toContain('not defined in canvas');
|
|
1442
|
-
});
|
|
1443
|
-
|
|
1444
|
-
it('should work without eventRegistry (backward compatible)', async () => {
|
|
1445
|
-
// Create a canvas without the event
|
|
1446
|
-
const canvas = createCanvasWithEvents(['other.event']);
|
|
1447
|
-
|
|
1448
|
-
// Workflow references an event not in canvas
|
|
1449
|
-
const workflow: WorkflowTemplate = {
|
|
1450
|
-
version: '1.0.0',
|
|
1451
|
-
canvas: 'test.otel.canvas',
|
|
1452
|
-
name: 'Test Workflow',
|
|
1453
|
-
description: 'Test',
|
|
1454
|
-
mode: 'span-tree',
|
|
1455
|
-
scenarioSelection: 'first-match',
|
|
1456
|
-
scenarios: [
|
|
1457
|
-
{
|
|
1458
|
-
id: 'default',
|
|
1459
|
-
priority: 1,
|
|
1460
|
-
description: 'Default',
|
|
1461
|
-
template: {
|
|
1462
|
-
events: {
|
|
1463
|
-
'missing.event': 'Not in canvas',
|
|
1464
|
-
},
|
|
1465
|
-
},
|
|
1466
|
-
},
|
|
1467
|
-
],
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
// No eventRegistry provided
|
|
1471
|
-
const context: WorkflowValidationContext = {
|
|
1472
|
-
workflow,
|
|
1473
|
-
workflowPath: 'test.workflow.json',
|
|
1474
|
-
canvas,
|
|
1475
|
-
canvasPath: join(tempDir, 'test.otel.canvas'),
|
|
1476
|
-
basePath: tempDir,
|
|
1477
|
-
// eventRegistry: undefined
|
|
1478
|
-
};
|
|
1479
|
-
|
|
1480
|
-
const result = await validator.validate(context);
|
|
1481
|
-
|
|
1482
|
-
// Should still report the error, just without enhanced suggestions
|
|
1483
|
-
const eventViolation = result.violations.find(
|
|
1484
|
-
v => v.ruleId === 'workflow-event-sync' && v.message.includes('missing.event')
|
|
1485
|
-
);
|
|
1486
|
-
|
|
1487
|
-
expect(eventViolation).toBeDefined();
|
|
1488
|
-
expect(eventViolation!.message).toContain('not defined in canvas');
|
|
1489
|
-
});
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// ============================================================================
|
|
1493
|
-
// Event Connectivity Validation Tests
|
|
1494
|
-
// ============================================================================
|
|
1495
|
-
|
|
1496
|
-
describe('checkEventConnectivity', () => {
|
|
1497
|
-
it('should pass when all events are connected', async () => {
|
|
1498
|
-
const workflow: WorkflowTemplate = {
|
|
1499
|
-
version: '1.0.0',
|
|
1500
|
-
name: 'Test',
|
|
1501
|
-
description: 'Test workflow',
|
|
1502
|
-
mode: 'span-tree',
|
|
1503
|
-
canvas: 'test.otel.canvas',
|
|
1504
|
-
scenarios: [{
|
|
1505
|
-
id: 'test',
|
|
1506
|
-
priority: 1,
|
|
1507
|
-
description: 'Test scenario',
|
|
1508
|
-
template: {
|
|
1509
|
-
events: {
|
|
1510
|
-
'event.started': 'Started',
|
|
1511
|
-
'event.processing': 'Processing',
|
|
1512
|
-
'event.complete': 'Complete'
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}]
|
|
1516
|
-
};
|
|
1517
|
-
|
|
1518
|
-
const canvas: ExtendedCanvas = {
|
|
1519
|
-
nodes: [
|
|
1520
|
-
{ id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
|
|
1521
|
-
{ id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.processing', attributes: {} } },
|
|
1522
|
-
{ id: 'n3', type: 'otel-event', x: 300, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } }
|
|
1523
|
-
],
|
|
1524
|
-
edges: [
|
|
1525
|
-
{ id: 'e1', fromNode: 'n1', toNode: 'n2' },
|
|
1526
|
-
{ id: 'e2', fromNode: 'n2', toNode: 'n3' }
|
|
1527
|
-
],
|
|
1528
|
-
pv: {
|
|
1529
|
-
version: '1.0.0',
|
|
1530
|
-
name: 'Test Canvas',
|
|
1531
|
-
markdown: 'test.md'
|
|
1532
|
-
}
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
|
-
const context: WorkflowValidationContext = {
|
|
1536
|
-
workflow,
|
|
1537
|
-
workflowPath: 'test.workflow.json',
|
|
1538
|
-
canvas,
|
|
1539
|
-
basePath: tempDir
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
const result = await validator.validate(context);
|
|
1543
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
1544
|
-
expect(connectivityViolations).toHaveLength(0);
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
it('should error when events are disconnected', async () => {
|
|
1548
|
-
const workflow: WorkflowTemplate = {
|
|
1549
|
-
version: '1.0.0',
|
|
1550
|
-
name: 'Test',
|
|
1551
|
-
description: 'Test workflow',
|
|
1552
|
-
mode: 'span-tree',
|
|
1553
|
-
canvas: 'test.otel.canvas',
|
|
1554
|
-
scenarios: [{
|
|
1555
|
-
id: 'test',
|
|
1556
|
-
priority: 1,
|
|
1557
|
-
description: 'Test scenario',
|
|
1558
|
-
template: {
|
|
1559
|
-
events: {
|
|
1560
|
-
'event.started': 'Started',
|
|
1561
|
-
'event.complete': 'Complete',
|
|
1562
|
-
'event.error': 'Error' // Disconnected - no path from started/complete to error
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}]
|
|
1566
|
-
};
|
|
1567
|
-
|
|
1568
|
-
const canvas: ExtendedCanvas = {
|
|
1569
|
-
nodes: [
|
|
1570
|
-
{ id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
|
|
1571
|
-
{ id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } },
|
|
1572
|
-
{ id: 'n3', type: 'otel-event', x: 0, y: 100, width: 100, height: 50, event: { name: 'event.error', attributes: {} } }
|
|
1573
|
-
],
|
|
1574
|
-
edges: [
|
|
1575
|
-
{ id: 'e1', fromNode: 'n1', toNode: 'n2' }
|
|
1576
|
-
// n3 is isolated - no edges connecting it to n1 or n2
|
|
1577
|
-
],
|
|
1578
|
-
pv: {
|
|
1579
|
-
version: '1.0.0',
|
|
1580
|
-
name: 'Test Canvas',
|
|
1581
|
-
markdown: 'test.md'
|
|
1582
|
-
}
|
|
1583
|
-
};
|
|
1584
|
-
|
|
1585
|
-
const context: WorkflowValidationContext = {
|
|
1586
|
-
workflow,
|
|
1587
|
-
workflowPath: 'test.workflow.json',
|
|
1588
|
-
canvas,
|
|
1589
|
-
basePath: tempDir
|
|
1590
|
-
};
|
|
1591
|
-
|
|
1592
|
-
const result = await validator.validate(context);
|
|
1593
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
1594
|
-
expect(connectivityViolations).toHaveLength(1);
|
|
1595
|
-
expect(connectivityViolations[0].severity).toBe('error');
|
|
1596
|
-
expect(connectivityViolations[0].message).toContain('disconnected');
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
it('should error when canvas has no edges but workflow has multi-event scenarios', async () => {
|
|
1600
|
-
const workflow: WorkflowTemplate = {
|
|
1601
|
-
version: '1.0.0',
|
|
1602
|
-
name: 'Test',
|
|
1603
|
-
description: 'Test workflow',
|
|
1604
|
-
mode: 'span-tree',
|
|
1605
|
-
canvas: 'test.otel.canvas',
|
|
1606
|
-
scenarios: [{
|
|
1607
|
-
id: 'test',
|
|
1608
|
-
priority: 1,
|
|
1609
|
-
description: 'Test scenario',
|
|
1610
|
-
template: {
|
|
1611
|
-
events: {
|
|
1612
|
-
'event.started': 'Started',
|
|
1613
|
-
'event.complete': 'Complete'
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}]
|
|
1617
|
-
};
|
|
1618
|
-
|
|
1619
|
-
const canvas: ExtendedCanvas = {
|
|
1620
|
-
nodes: [
|
|
1621
|
-
{ id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
|
|
1622
|
-
{ id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } }
|
|
1623
|
-
],
|
|
1624
|
-
edges: [], // No edges defined
|
|
1625
|
-
pv: {
|
|
1626
|
-
version: '1.0.0',
|
|
1627
|
-
name: 'Test Canvas',
|
|
1628
|
-
markdown: 'test.md'
|
|
1629
|
-
}
|
|
1630
|
-
};
|
|
1631
|
-
|
|
1632
|
-
const context: WorkflowValidationContext = {
|
|
1633
|
-
workflow,
|
|
1634
|
-
workflowPath: 'test.workflow.json',
|
|
1635
|
-
canvas,
|
|
1636
|
-
basePath: tempDir
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
const result = await validator.validate(context);
|
|
1640
|
-
// Should produce connectivity violation when canvas has no edges but scenario has multiple events
|
|
1641
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
1642
|
-
expect(connectivityViolations).toHaveLength(1);
|
|
1643
|
-
expect(connectivityViolations[0].severity).toBe('error');
|
|
1644
|
-
expect(connectivityViolations[0].message).toContain('has no edges');
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
|
-
it('should skip check when scenario has less than 2 events', async () => {
|
|
1648
|
-
const workflow: WorkflowTemplate = {
|
|
1649
|
-
version: '1.0.0',
|
|
1650
|
-
name: 'Test',
|
|
1651
|
-
description: 'Test workflow',
|
|
1652
|
-
mode: 'span-tree',
|
|
1653
|
-
canvas: 'test.otel.canvas',
|
|
1654
|
-
scenarios: [{
|
|
1655
|
-
id: 'test',
|
|
1656
|
-
priority: 1,
|
|
1657
|
-
description: 'Test scenario',
|
|
1658
|
-
template: {
|
|
1659
|
-
events: {
|
|
1660
|
-
'event.started': 'Started'
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
}]
|
|
1664
|
-
};
|
|
1665
|
-
|
|
1666
|
-
const canvas: ExtendedCanvas = {
|
|
1667
|
-
nodes: [
|
|
1668
|
-
{ id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } }
|
|
1669
|
-
],
|
|
1670
|
-
edges: [],
|
|
1671
|
-
pv: {
|
|
1672
|
-
version: '1.0.0',
|
|
1673
|
-
name: 'Test Canvas',
|
|
1674
|
-
markdown: 'test.md'
|
|
1675
|
-
}
|
|
1676
|
-
};
|
|
1677
|
-
|
|
1678
|
-
const context: WorkflowValidationContext = {
|
|
1679
|
-
workflow,
|
|
1680
|
-
workflowPath: 'test.workflow.json',
|
|
1681
|
-
canvas,
|
|
1682
|
-
basePath: tempDir
|
|
1683
|
-
};
|
|
1684
|
-
|
|
1685
|
-
const result = await validator.validate(context);
|
|
1686
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
1687
|
-
expect(connectivityViolations).toHaveLength(0);
|
|
1688
|
-
});
|
|
1689
|
-
|
|
1690
|
-
it('should handle events connected through intermediate nodes', async () => {
|
|
1691
|
-
const workflow: WorkflowTemplate = {
|
|
1692
|
-
version: '1.0.0',
|
|
1693
|
-
name: 'Test',
|
|
1694
|
-
description: 'Test workflow',
|
|
1695
|
-
mode: 'span-tree',
|
|
1696
|
-
canvas: 'test.otel.canvas',
|
|
1697
|
-
scenarios: [{
|
|
1698
|
-
id: 'test',
|
|
1699
|
-
priority: 1,
|
|
1700
|
-
description: 'Test scenario',
|
|
1701
|
-
template: {
|
|
1702
|
-
events: {
|
|
1703
|
-
'event.start': 'Start',
|
|
1704
|
-
'event.end': 'End'
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}]
|
|
1708
|
-
};
|
|
1709
|
-
|
|
1710
|
-
const canvas: ExtendedCanvas = {
|
|
1711
|
-
nodes: [
|
|
1712
|
-
{ id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.start', attributes: {} } },
|
|
1713
|
-
{ id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.middle1', attributes: {} } },
|
|
1714
|
-
{ id: 'n3', type: 'otel-event', x: 300, y: 0, width: 100, height: 50, event: { name: 'event.middle2', attributes: {} } },
|
|
1715
|
-
{ id: 'n4', type: 'otel-event', x: 450, y: 0, width: 100, height: 50, event: { name: 'event.end', attributes: {} } }
|
|
1716
|
-
],
|
|
1717
|
-
edges: [
|
|
1718
|
-
{ id: 'e1', fromNode: 'n1', toNode: 'n2' },
|
|
1719
|
-
{ id: 'e2', fromNode: 'n2', toNode: 'n3' },
|
|
1720
|
-
{ id: 'e3', fromNode: 'n3', toNode: 'n4' }
|
|
1721
|
-
],
|
|
1722
|
-
pv: {
|
|
1723
|
-
version: '1.0.0',
|
|
1724
|
-
name: 'Test Canvas',
|
|
1725
|
-
markdown: 'test.md'
|
|
1726
|
-
}
|
|
1727
|
-
};
|
|
1728
|
-
|
|
1729
|
-
const context: WorkflowValidationContext = {
|
|
1730
|
-
workflow,
|
|
1731
|
-
workflowPath: 'test.workflow.json',
|
|
1732
|
-
canvas,
|
|
1733
|
-
basePath: tempDir
|
|
1734
|
-
};
|
|
1735
|
-
|
|
1736
|
-
const result = await validator.validate(context);
|
|
1737
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
1738
|
-
// Should pass because events are connected through intermediate nodes
|
|
1739
|
-
expect(connectivityViolations).toHaveLength(0);
|
|
1740
|
-
});
|
|
1741
|
-
});
|
|
1742
|
-
|
|
1743
|
-
// ============================================================================
|
|
1744
|
-
// Subset Validation Tests
|
|
1745
|
-
// ============================================================================
|
|
1746
|
-
|
|
1747
|
-
describe('checkScenarioSubsets', () => {
|
|
1748
|
-
it('should pass when scenarios have mutually exclusive event sets', async () => {
|
|
1749
|
-
const workflow: WorkflowTemplate = {
|
|
1750
|
-
version: '1.0.0',
|
|
1751
|
-
name: 'Test',
|
|
1752
|
-
description: 'Test workflow',
|
|
1753
|
-
mode: 'span-tree',
|
|
1754
|
-
canvas: 'test.otel.canvas',
|
|
1755
|
-
scenarioSelection: 'first-match',
|
|
1756
|
-
scenarios: [
|
|
1757
|
-
{
|
|
1758
|
-
id: 'success',
|
|
1759
|
-
priority: 1,
|
|
1760
|
-
description: 'Success scenario',
|
|
1761
|
-
template: {
|
|
1762
|
-
events: {
|
|
1763
|
-
'payment.authorized': 'Payment authorized',
|
|
1764
|
-
'order.confirmed': 'Order confirmed'
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
},
|
|
1768
|
-
{
|
|
1769
|
-
id: 'failure',
|
|
1770
|
-
priority: 2,
|
|
1771
|
-
description: 'Failure scenario',
|
|
1772
|
-
template: {
|
|
1773
|
-
events: {
|
|
1774
|
-
'payment.declined': 'Payment declined',
|
|
1775
|
-
'error.logged': 'Error logged'
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
]
|
|
1780
|
-
};
|
|
1781
|
-
|
|
1782
|
-
const context: WorkflowValidationContext = {
|
|
1783
|
-
workflow,
|
|
1784
|
-
workflowPath: 'test.workflow.json',
|
|
1785
|
-
basePath: tempDir
|
|
1786
|
-
};
|
|
1787
|
-
|
|
1788
|
-
const result = await validator.validate(context);
|
|
1789
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
1790
|
-
expect(subsetViolations).toHaveLength(0);
|
|
1791
|
-
});
|
|
1792
|
-
|
|
1793
|
-
it('should pass when scenarios have overlapping but not subset event sets', async () => {
|
|
1794
|
-
const workflow: WorkflowTemplate = {
|
|
1795
|
-
version: '1.0.0',
|
|
1796
|
-
name: 'Test',
|
|
1797
|
-
description: 'Test workflow',
|
|
1798
|
-
mode: 'span-tree',
|
|
1799
|
-
canvas: 'test.otel.canvas',
|
|
1800
|
-
scenarioSelection: 'first-match',
|
|
1801
|
-
scenarios: [
|
|
1802
|
-
{
|
|
1803
|
-
id: 'google-oauth',
|
|
1804
|
-
priority: 1,
|
|
1805
|
-
description: 'Google OAuth',
|
|
1806
|
-
template: {
|
|
1807
|
-
events: {
|
|
1808
|
-
'oauth.google': 'Google auth',
|
|
1809
|
-
'oauth.complete': 'OAuth complete',
|
|
1810
|
-
'user.created': 'User created'
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
},
|
|
1814
|
-
{
|
|
1815
|
-
id: 'email-password',
|
|
1816
|
-
priority: 2,
|
|
1817
|
-
description: 'Email/password signup',
|
|
1818
|
-
template: {
|
|
1819
|
-
events: {
|
|
1820
|
-
'email.verified': 'Email verified',
|
|
1821
|
-
'password.set': 'Password set',
|
|
1822
|
-
'user.created': 'User created'
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
]
|
|
1827
|
-
};
|
|
1828
|
-
|
|
1829
|
-
const context: WorkflowValidationContext = {
|
|
1830
|
-
workflow,
|
|
1831
|
-
workflowPath: 'test.workflow.json',
|
|
1832
|
-
basePath: tempDir
|
|
1833
|
-
};
|
|
1834
|
-
|
|
1835
|
-
const result = await validator.validate(context);
|
|
1836
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
1837
|
-
expect(subsetViolations).toHaveLength(0);
|
|
1838
|
-
});
|
|
1839
|
-
|
|
1840
|
-
it('should error when one scenario is a strict subset of another', async () => {
|
|
1841
|
-
const workflow: WorkflowTemplate = {
|
|
1842
|
-
version: '1.0.0',
|
|
1843
|
-
name: 'Test',
|
|
1844
|
-
description: 'Test workflow',
|
|
1845
|
-
mode: 'span-tree',
|
|
1846
|
-
canvas: 'test.otel.canvas',
|
|
1847
|
-
scenarioSelection: 'first-match',
|
|
1848
|
-
scenarios: [
|
|
1849
|
-
{
|
|
1850
|
-
id: 'basic-checkout',
|
|
1851
|
-
priority: 1,
|
|
1852
|
-
description: 'Basic checkout',
|
|
1853
|
-
template: {
|
|
1854
|
-
events: {
|
|
1855
|
-
'cart.viewed': 'Cart viewed',
|
|
1856
|
-
'checkout.started': 'Checkout started'
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
},
|
|
1860
|
-
{
|
|
1861
|
-
id: 'complete-checkout',
|
|
1862
|
-
priority: 2,
|
|
1863
|
-
description: 'Complete checkout',
|
|
1864
|
-
template: {
|
|
1865
|
-
events: {
|
|
1866
|
-
'cart.viewed': 'Cart viewed',
|
|
1867
|
-
'checkout.started': 'Checkout started',
|
|
1868
|
-
'payment.complete': 'Payment complete'
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
]
|
|
1873
|
-
};
|
|
1874
|
-
|
|
1875
|
-
const context: WorkflowValidationContext = {
|
|
1876
|
-
workflow,
|
|
1877
|
-
workflowPath: 'test.workflow.json',
|
|
1878
|
-
basePath: tempDir
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
const result = await validator.validate(context);
|
|
1882
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
1883
|
-
|
|
1884
|
-
expect(subsetViolations).toHaveLength(1);
|
|
1885
|
-
expect(subsetViolations[0].severity).toBe('error');
|
|
1886
|
-
expect(subsetViolations[0].message).toContain('basic-checkout');
|
|
1887
|
-
expect(subsetViolations[0].message).toContain('strict subset');
|
|
1888
|
-
expect(subsetViolations[0].message).toContain('complete-checkout');
|
|
1889
|
-
expect(subsetViolations[0].impact).toContain('ambiguous');
|
|
1890
|
-
expect(subsetViolations[0].suggestion).toContain('Merge into one scenario');
|
|
1891
|
-
expect(subsetViolations[0].suggestion).toContain('mutually exclusive');
|
|
1892
|
-
});
|
|
1893
|
-
|
|
1894
|
-
it('should detect reverse subset relationship', async () => {
|
|
1895
|
-
const workflow: WorkflowTemplate = {
|
|
1896
|
-
version: '1.0.0',
|
|
1897
|
-
name: 'Test',
|
|
1898
|
-
description: 'Test workflow',
|
|
1899
|
-
mode: 'span-tree',
|
|
1900
|
-
canvas: 'test.otel.canvas',
|
|
1901
|
-
scenarioSelection: 'first-match',
|
|
1902
|
-
scenarios: [
|
|
1903
|
-
{
|
|
1904
|
-
id: 'complete-flow',
|
|
1905
|
-
priority: 1,
|
|
1906
|
-
description: 'Complete flow',
|
|
1907
|
-
template: {
|
|
1908
|
-
events: {
|
|
1909
|
-
'step.one': 'Step 1',
|
|
1910
|
-
'step.two': 'Step 2',
|
|
1911
|
-
'step.three': 'Step 3'
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
},
|
|
1915
|
-
{
|
|
1916
|
-
id: 'partial-flow',
|
|
1917
|
-
priority: 2,
|
|
1918
|
-
description: 'Partial flow',
|
|
1919
|
-
template: {
|
|
1920
|
-
events: {
|
|
1921
|
-
'step.one': 'Step 1',
|
|
1922
|
-
'step.two': 'Step 2'
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
]
|
|
1927
|
-
};
|
|
1928
|
-
|
|
1929
|
-
const context: WorkflowValidationContext = {
|
|
1930
|
-
workflow,
|
|
1931
|
-
workflowPath: 'test.workflow.json',
|
|
1932
|
-
basePath: tempDir
|
|
1933
|
-
};
|
|
1934
|
-
|
|
1935
|
-
const result = await validator.validate(context);
|
|
1936
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
1937
|
-
|
|
1938
|
-
expect(subsetViolations).toHaveLength(1);
|
|
1939
|
-
expect(subsetViolations[0].message).toContain('partial-flow');
|
|
1940
|
-
expect(subsetViolations[0].message).toContain('strict subset');
|
|
1941
|
-
expect(subsetViolations[0].message).toContain('complete-flow');
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
|
-
it('should pass when scenarios have same number of events but different events', async () => {
|
|
1945
|
-
const workflow: WorkflowTemplate = {
|
|
1946
|
-
version: '1.0.0',
|
|
1947
|
-
name: 'Test',
|
|
1948
|
-
description: 'Test workflow',
|
|
1949
|
-
mode: 'span-tree',
|
|
1950
|
-
canvas: 'test.otel.canvas',
|
|
1951
|
-
scenarioSelection: 'first-match',
|
|
1952
|
-
scenarios: [
|
|
1953
|
-
{
|
|
1954
|
-
id: 'scenario-a',
|
|
1955
|
-
priority: 1,
|
|
1956
|
-
description: 'Scenario A',
|
|
1957
|
-
template: {
|
|
1958
|
-
events: {
|
|
1959
|
-
'event.a': 'Event A',
|
|
1960
|
-
'event.b': 'Event B'
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
},
|
|
1964
|
-
{
|
|
1965
|
-
id: 'scenario-b',
|
|
1966
|
-
priority: 2,
|
|
1967
|
-
description: 'Scenario B',
|
|
1968
|
-
template: {
|
|
1969
|
-
events: {
|
|
1970
|
-
'event.c': 'Event C',
|
|
1971
|
-
'event.d': 'Event D'
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
]
|
|
1976
|
-
};
|
|
1977
|
-
|
|
1978
|
-
const context: WorkflowValidationContext = {
|
|
1979
|
-
workflow,
|
|
1980
|
-
workflowPath: 'test.workflow.json',
|
|
1981
|
-
basePath: tempDir
|
|
1982
|
-
};
|
|
1983
|
-
|
|
1984
|
-
const result = await validator.validate(context);
|
|
1985
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
1986
|
-
expect(subsetViolations).toHaveLength(0);
|
|
1987
|
-
});
|
|
1988
|
-
|
|
1989
|
-
it('should pass when workflow has only one scenario', async () => {
|
|
1990
|
-
const workflow: WorkflowTemplate = {
|
|
1991
|
-
version: '1.0.0',
|
|
1992
|
-
name: 'Test',
|
|
1993
|
-
description: 'Test workflow',
|
|
1994
|
-
mode: 'span-tree',
|
|
1995
|
-
canvas: 'test.otel.canvas',
|
|
1996
|
-
scenarioSelection: 'first-match',
|
|
1997
|
-
scenarios: [
|
|
1998
|
-
{
|
|
1999
|
-
id: 'only-scenario',
|
|
2000
|
-
priority: 1,
|
|
2001
|
-
description: 'Only scenario',
|
|
2002
|
-
template: {
|
|
2003
|
-
events: {
|
|
2004
|
-
'event.one': 'Event 1'
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
]
|
|
2009
|
-
};
|
|
2010
|
-
|
|
2011
|
-
const context: WorkflowValidationContext = {
|
|
2012
|
-
workflow,
|
|
2013
|
-
workflowPath: 'test.workflow.json',
|
|
2014
|
-
basePath: tempDir
|
|
2015
|
-
};
|
|
2016
|
-
|
|
2017
|
-
const result = await validator.validate(context);
|
|
2018
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
2019
|
-
expect(subsetViolations).toHaveLength(0);
|
|
2020
|
-
});
|
|
2021
|
-
|
|
2022
|
-
it('should handle scenarios with empty event sets', async () => {
|
|
2023
|
-
const workflow: WorkflowTemplate = {
|
|
2024
|
-
version: '1.0.0',
|
|
2025
|
-
name: 'Test',
|
|
2026
|
-
description: 'Test workflow',
|
|
2027
|
-
mode: 'span-tree',
|
|
2028
|
-
canvas: 'test.otel.canvas',
|
|
2029
|
-
scenarioSelection: 'first-match',
|
|
2030
|
-
scenarios: [
|
|
2031
|
-
{
|
|
2032
|
-
id: 'empty-events',
|
|
2033
|
-
priority: 1,
|
|
2034
|
-
description: 'Empty events',
|
|
2035
|
-
template: {
|
|
2036
|
-
introduction: 'Test',
|
|
2037
|
-
events: {}
|
|
2038
|
-
}
|
|
2039
|
-
},
|
|
2040
|
-
{
|
|
2041
|
-
id: 'with-events',
|
|
2042
|
-
priority: 2,
|
|
2043
|
-
description: 'With events',
|
|
2044
|
-
template: {
|
|
2045
|
-
events: {
|
|
2046
|
-
'event.one': 'Event 1'
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
]
|
|
2051
|
-
};
|
|
2052
|
-
|
|
2053
|
-
const context: WorkflowValidationContext = {
|
|
2054
|
-
workflow,
|
|
2055
|
-
workflowPath: 'test.workflow.json',
|
|
2056
|
-
basePath: tempDir
|
|
2057
|
-
};
|
|
2058
|
-
|
|
2059
|
-
const result = await validator.validate(context);
|
|
2060
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
2061
|
-
|
|
2062
|
-
// Empty set is a strict subset of any non-empty set
|
|
2063
|
-
expect(subsetViolations).toHaveLength(1);
|
|
2064
|
-
expect(subsetViolations[0].message).toContain('empty-events');
|
|
2065
|
-
expect(subsetViolations[0].message).toContain('strict subset');
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
it('should detect multiple subset violations in a workflow', async () => {
|
|
2069
|
-
const workflow: WorkflowTemplate = {
|
|
2070
|
-
version: '1.0.0',
|
|
2071
|
-
name: 'Test',
|
|
2072
|
-
description: 'Test workflow',
|
|
2073
|
-
mode: 'span-tree',
|
|
2074
|
-
canvas: 'test.otel.canvas',
|
|
2075
|
-
scenarioSelection: 'first-match',
|
|
2076
|
-
scenarios: [
|
|
2077
|
-
{
|
|
2078
|
-
id: 'minimal',
|
|
2079
|
-
priority: 1,
|
|
2080
|
-
description: 'Minimal',
|
|
2081
|
-
template: {
|
|
2082
|
-
events: {
|
|
2083
|
-
'event.a': 'A'
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
},
|
|
2087
|
-
{
|
|
2088
|
-
id: 'medium',
|
|
2089
|
-
priority: 2,
|
|
2090
|
-
description: 'Medium',
|
|
2091
|
-
template: {
|
|
2092
|
-
events: {
|
|
2093
|
-
'event.a': 'A',
|
|
2094
|
-
'event.b': 'B'
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
},
|
|
2098
|
-
{
|
|
2099
|
-
id: 'complete',
|
|
2100
|
-
priority: 3,
|
|
2101
|
-
description: 'Complete',
|
|
2102
|
-
template: {
|
|
2103
|
-
events: {
|
|
2104
|
-
'event.a': 'A',
|
|
2105
|
-
'event.b': 'B',
|
|
2106
|
-
'event.c': 'C'
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
]
|
|
2111
|
-
};
|
|
2112
|
-
|
|
2113
|
-
const context: WorkflowValidationContext = {
|
|
2114
|
-
workflow,
|
|
2115
|
-
workflowPath: 'test.workflow.json',
|
|
2116
|
-
basePath: tempDir
|
|
2117
|
-
};
|
|
2118
|
-
|
|
2119
|
-
const result = await validator.validate(context);
|
|
2120
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
2121
|
-
|
|
2122
|
-
// Should detect:
|
|
2123
|
-
// 1. minimal ⊂ medium
|
|
2124
|
-
// 2. minimal ⊂ complete
|
|
2125
|
-
// 3. medium ⊂ complete
|
|
2126
|
-
expect(subsetViolations.length).toBeGreaterThanOrEqual(3);
|
|
2127
|
-
});
|
|
2128
|
-
|
|
2129
|
-
it('should provide helpful fix suggestions in error messages', async () => {
|
|
2130
|
-
const workflow: WorkflowTemplate = {
|
|
2131
|
-
version: '1.0.0',
|
|
2132
|
-
name: 'Test',
|
|
2133
|
-
description: 'Test workflow',
|
|
2134
|
-
mode: 'span-tree',
|
|
2135
|
-
canvas: 'test.otel.canvas',
|
|
2136
|
-
scenarioSelection: 'first-match',
|
|
2137
|
-
scenarios: [
|
|
2138
|
-
{
|
|
2139
|
-
id: 'abandoned',
|
|
2140
|
-
priority: 1,
|
|
2141
|
-
description: 'Abandoned cart',
|
|
2142
|
-
template: {
|
|
2143
|
-
events: {
|
|
2144
|
-
'cart.created': 'Cart created',
|
|
2145
|
-
'items.added': 'Items added'
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
},
|
|
2149
|
-
{
|
|
2150
|
-
id: 'completed',
|
|
2151
|
-
priority: 2,
|
|
2152
|
-
description: 'Completed purchase',
|
|
2153
|
-
template: {
|
|
2154
|
-
events: {
|
|
2155
|
-
'cart.created': 'Cart created',
|
|
2156
|
-
'items.added': 'Items added',
|
|
2157
|
-
'checkout.complete': 'Checkout complete'
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
]
|
|
2162
|
-
};
|
|
2163
|
-
|
|
2164
|
-
const context: WorkflowValidationContext = {
|
|
2165
|
-
workflow,
|
|
2166
|
-
workflowPath: 'test.workflow.json',
|
|
2167
|
-
basePath: tempDir
|
|
2168
|
-
};
|
|
2169
|
-
|
|
2170
|
-
const result = await validator.validate(context);
|
|
2171
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
2172
|
-
|
|
2173
|
-
expect(subsetViolations).toHaveLength(1);
|
|
2174
|
-
|
|
2175
|
-
const violation = subsetViolations[0];
|
|
2176
|
-
expect(violation.suggestion).toContain('cart.created');
|
|
2177
|
-
expect(violation.suggestion).toContain('items.added');
|
|
2178
|
-
expect(violation.suggestion).toContain('checkout.complete');
|
|
2179
|
-
expect(violation.suggestion).toContain('Merge into one scenario');
|
|
2180
|
-
expect(violation.suggestion).toContain('template conditionals');
|
|
2181
|
-
expect(violation.suggestion).toContain('mutually exclusive');
|
|
2182
|
-
expect(violation.suggestion).toContain('distinguishing events');
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
it('should error when two scenarios have identical event sets', async () => {
|
|
2186
|
-
const workflow: WorkflowTemplate = {
|
|
2187
|
-
version: '1.0.0',
|
|
2188
|
-
name: 'Test',
|
|
2189
|
-
description: 'Test workflow',
|
|
2190
|
-
mode: 'span-tree',
|
|
2191
|
-
canvas: 'test.otel.canvas',
|
|
2192
|
-
spanPattern: 'test.*',
|
|
2193
|
-
scenarios: [
|
|
2194
|
-
{
|
|
2195
|
-
id: 'scenario-a',
|
|
2196
|
-
priority: 1,
|
|
2197
|
-
description: 'First scenario',
|
|
2198
|
-
template: {
|
|
2199
|
-
summary: 'Test A',
|
|
2200
|
-
events: {
|
|
2201
|
-
'event.start': 'Started',
|
|
2202
|
-
'event.complete': 'Completed'
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
},
|
|
2206
|
-
{
|
|
2207
|
-
id: 'scenario-b',
|
|
2208
|
-
priority: 2,
|
|
2209
|
-
description: 'Second scenario with same events',
|
|
2210
|
-
template: {
|
|
2211
|
-
summary: 'Test B',
|
|
2212
|
-
events: {
|
|
2213
|
-
'event.start': 'Started',
|
|
2214
|
-
'event.complete': 'Completed'
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
]
|
|
2219
|
-
};
|
|
2220
|
-
|
|
2221
|
-
const context: WorkflowValidationContext = {
|
|
2222
|
-
workflow,
|
|
2223
|
-
workflowPath: 'test.workflow.json',
|
|
2224
|
-
basePath: tempDir
|
|
2225
|
-
};
|
|
2226
|
-
|
|
2227
|
-
const result = await validator.validate(context);
|
|
2228
|
-
const identicalViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-identical');
|
|
2229
|
-
|
|
2230
|
-
expect(identicalViolations).toHaveLength(1);
|
|
2231
|
-
expect(identicalViolations[0].severity).toBe('error');
|
|
2232
|
-
expect(identicalViolations[0].message).toContain('scenario-a');
|
|
2233
|
-
expect(identicalViolations[0].message).toContain('scenario-b');
|
|
2234
|
-
expect(identicalViolations[0].message).toContain('identical event sets');
|
|
2235
|
-
expect(identicalViolations[0].impact).toContain('arbitrary');
|
|
2236
|
-
expect(identicalViolations[0].suggestion).toContain('Merge into a single scenario');
|
|
2237
|
-
});
|
|
2238
|
-
|
|
2239
|
-
it('should pass when scenarios have same size but different events', async () => {
|
|
2240
|
-
const workflow: WorkflowTemplate = {
|
|
2241
|
-
version: '1.0.0',
|
|
2242
|
-
name: 'Test',
|
|
2243
|
-
description: 'Test workflow',
|
|
2244
|
-
mode: 'span-tree',
|
|
2245
|
-
canvas: 'test.otel.canvas',
|
|
2246
|
-
spanPattern: 'test.*',
|
|
2247
|
-
scenarios: [
|
|
2248
|
-
{
|
|
2249
|
-
id: 'oauth-flow',
|
|
2250
|
-
priority: 1,
|
|
2251
|
-
description: 'OAuth login',
|
|
2252
|
-
template: {
|
|
2253
|
-
summary: 'OAuth',
|
|
2254
|
-
events: {
|
|
2255
|
-
'oauth.start': 'Started',
|
|
2256
|
-
'oauth.complete': 'Completed'
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
},
|
|
2260
|
-
{
|
|
2261
|
-
id: 'password-flow',
|
|
2262
|
-
priority: 2,
|
|
2263
|
-
description: 'Password login',
|
|
2264
|
-
template: {
|
|
2265
|
-
summary: 'Password',
|
|
2266
|
-
events: {
|
|
2267
|
-
'password.start': 'Started',
|
|
2268
|
-
'password.complete': 'Completed'
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
]
|
|
2273
|
-
};
|
|
2274
|
-
|
|
2275
|
-
const context: WorkflowValidationContext = {
|
|
2276
|
-
workflow,
|
|
2277
|
-
workflowPath: 'test.workflow.json',
|
|
2278
|
-
basePath: tempDir
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
const result = await validator.validate(context);
|
|
2282
|
-
const identicalViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-identical');
|
|
2283
|
-
const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
|
|
2284
|
-
|
|
2285
|
-
expect(identicalViolations).toHaveLength(0);
|
|
2286
|
-
expect(subsetViolations).toHaveLength(0);
|
|
2287
|
-
});
|
|
2288
|
-
});
|
|
2289
|
-
|
|
2290
|
-
// ============================================================================
|
|
2291
|
-
// Span Pattern Validation Tests
|
|
2292
|
-
// ============================================================================
|
|
2293
|
-
|
|
2294
|
-
describe('validateSpanPatterns (CLI-level validation)', () => {
|
|
2295
|
-
it('should pass when all workflows have unique spanPatterns', () => {
|
|
2296
|
-
const workflows = [
|
|
2297
|
-
{
|
|
2298
|
-
workflow: {
|
|
2299
|
-
version: '1.0.0',
|
|
2300
|
-
canvas: 'test.otel.canvas',
|
|
2301
|
-
name: 'Payment Workflow',
|
|
2302
|
-
description: 'Payment processing',
|
|
2303
|
-
spanPattern: 'payment.authorize',
|
|
2304
|
-
scenarioSelection: 'first-match' as const,
|
|
2305
|
-
scenarios: [],
|
|
2306
|
-
},
|
|
2307
|
-
workflowPath: 'workflows/payment.workflow.json',
|
|
2308
|
-
},
|
|
2309
|
-
{
|
|
2310
|
-
workflow: {
|
|
2311
|
-
version: '1.0.0',
|
|
2312
|
-
canvas: 'test.otel.canvas',
|
|
2313
|
-
name: 'Inventory Workflow',
|
|
2314
|
-
description: 'Inventory management',
|
|
2315
|
-
spanPattern: 'inventory.reserve',
|
|
2316
|
-
scenarioSelection: 'first-match' as const,
|
|
2317
|
-
scenarios: [],
|
|
2318
|
-
},
|
|
2319
|
-
workflowPath: 'workflows/inventory.workflow.json',
|
|
2320
|
-
},
|
|
2321
|
-
];
|
|
2322
|
-
|
|
2323
|
-
const violations = WorkflowValidator.validateSpanPatterns(workflows);
|
|
2324
|
-
expect(violations).toHaveLength(0);
|
|
2325
|
-
});
|
|
2326
|
-
|
|
2327
|
-
it('should error when workflows have duplicate spanPatterns', () => {
|
|
2328
|
-
const workflows = [
|
|
2329
|
-
{
|
|
2330
|
-
workflow: {
|
|
2331
|
-
version: '1.0.0',
|
|
2332
|
-
canvas: 'test1.otel.canvas',
|
|
2333
|
-
name: 'Payment Workflow 1',
|
|
2334
|
-
description: 'Payment processing',
|
|
2335
|
-
spanPattern: 'payment.authorize',
|
|
2336
|
-
scenarioSelection: 'first-match' as const,
|
|
2337
|
-
scenarios: [],
|
|
2338
|
-
},
|
|
2339
|
-
workflowPath: 'workflows/payment1.workflow.json',
|
|
2340
|
-
},
|
|
2341
|
-
{
|
|
2342
|
-
workflow: {
|
|
2343
|
-
version: '1.0.0',
|
|
2344
|
-
canvas: 'test2.otel.canvas',
|
|
2345
|
-
name: 'Payment Workflow 2',
|
|
2346
|
-
description: 'Payment processing alternative',
|
|
2347
|
-
spanPattern: 'payment.authorize',
|
|
2348
|
-
scenarioSelection: 'first-match' as const,
|
|
2349
|
-
scenarios: [],
|
|
2350
|
-
},
|
|
2351
|
-
workflowPath: 'workflows/payment2.workflow.json',
|
|
2352
|
-
},
|
|
2353
|
-
];
|
|
2354
|
-
|
|
2355
|
-
const violations = WorkflowValidator.validateSpanPatterns(workflows);
|
|
2356
|
-
|
|
2357
|
-
expect(violations).toHaveLength(2); // One for each workflow
|
|
2358
|
-
expect(violations[0].ruleId).toBe('workflow-span-pattern-duplicate');
|
|
2359
|
-
expect(violations[0].severity).toBe('error');
|
|
2360
|
-
expect(violations[0].message).toContain('payment.authorize');
|
|
2361
|
-
expect(violations[0].message).toContain('Duplicate spanPattern');
|
|
2362
|
-
expect(violations[0].suggestion).toContain('payment2.workflow.json');
|
|
2363
|
-
});
|
|
2364
|
-
|
|
2365
|
-
it('should detect multiple duplicates across many workflows', () => {
|
|
2366
|
-
const workflows = [
|
|
2367
|
-
{
|
|
2368
|
-
workflow: {
|
|
2369
|
-
version: '1.0.0',
|
|
2370
|
-
canvas: 'test.otel.canvas',
|
|
2371
|
-
name: 'Workflow A1',
|
|
2372
|
-
description: 'Test',
|
|
2373
|
-
spanPattern: 'span.a',
|
|
2374
|
-
scenarioSelection: 'first-match' as const,
|
|
2375
|
-
scenarios: [],
|
|
2376
|
-
},
|
|
2377
|
-
workflowPath: 'a1.workflow.json',
|
|
2378
|
-
},
|
|
2379
|
-
{
|
|
2380
|
-
workflow: {
|
|
2381
|
-
version: '1.0.0',
|
|
2382
|
-
canvas: 'test.otel.canvas',
|
|
2383
|
-
name: 'Workflow A2',
|
|
2384
|
-
description: 'Test',
|
|
2385
|
-
spanPattern: 'span.a',
|
|
2386
|
-
scenarioSelection: 'first-match' as const,
|
|
2387
|
-
scenarios: [],
|
|
2388
|
-
},
|
|
2389
|
-
workflowPath: 'a2.workflow.json',
|
|
2390
|
-
},
|
|
2391
|
-
{
|
|
2392
|
-
workflow: {
|
|
2393
|
-
version: '1.0.0',
|
|
2394
|
-
canvas: 'test.otel.canvas',
|
|
2395
|
-
name: 'Workflow B',
|
|
2396
|
-
description: 'Test',
|
|
2397
|
-
spanPattern: 'span.b',
|
|
2398
|
-
scenarioSelection: 'first-match' as const,
|
|
2399
|
-
scenarios: [],
|
|
2400
|
-
},
|
|
2401
|
-
workflowPath: 'b.workflow.json',
|
|
2402
|
-
},
|
|
2403
|
-
{
|
|
2404
|
-
workflow: {
|
|
2405
|
-
version: '1.0.0',
|
|
2406
|
-
canvas: 'test.otel.canvas',
|
|
2407
|
-
name: 'Workflow C1',
|
|
2408
|
-
description: 'Test',
|
|
2409
|
-
spanPattern: 'span.c',
|
|
2410
|
-
scenarioSelection: 'first-match' as const,
|
|
2411
|
-
scenarios: [],
|
|
2412
|
-
},
|
|
2413
|
-
workflowPath: 'c1.workflow.json',
|
|
2414
|
-
},
|
|
2415
|
-
{
|
|
2416
|
-
workflow: {
|
|
2417
|
-
version: '1.0.0',
|
|
2418
|
-
canvas: 'test.otel.canvas',
|
|
2419
|
-
name: 'Workflow C2',
|
|
2420
|
-
description: 'Test',
|
|
2421
|
-
spanPattern: 'span.c',
|
|
2422
|
-
scenarioSelection: 'first-match' as const,
|
|
2423
|
-
scenarios: [],
|
|
2424
|
-
},
|
|
2425
|
-
workflowPath: 'c2.workflow.json',
|
|
2426
|
-
},
|
|
2427
|
-
];
|
|
2428
|
-
|
|
2429
|
-
const violations = WorkflowValidator.validateSpanPatterns(workflows);
|
|
2430
|
-
|
|
2431
|
-
// Should have violations for: a1, a2, c1, c2 (4 workflows with duplicates)
|
|
2432
|
-
expect(violations.length).toBe(4);
|
|
2433
|
-
|
|
2434
|
-
// Check that span.b has no violations
|
|
2435
|
-
const spanBViolations = violations.filter(v => v.file === 'b.workflow.json');
|
|
2436
|
-
expect(spanBViolations).toHaveLength(0);
|
|
2437
|
-
});
|
|
2438
|
-
|
|
2439
|
-
it('should handle workflows without spanPattern gracefully', () => {
|
|
2440
|
-
const workflows = [
|
|
2441
|
-
{
|
|
2442
|
-
workflow: {
|
|
2443
|
-
version: '1.0.0',
|
|
2444
|
-
canvas: 'test.otel.canvas',
|
|
2445
|
-
name: 'Valid Workflow',
|
|
2446
|
-
description: 'Test',
|
|
2447
|
-
spanPattern: 'payment.authorize',
|
|
2448
|
-
scenarioSelection: 'first-match' as const,
|
|
2449
|
-
scenarios: [],
|
|
2450
|
-
},
|
|
2451
|
-
workflowPath: 'valid.workflow.json',
|
|
2452
|
-
},
|
|
2453
|
-
{
|
|
2454
|
-
workflow: {
|
|
2455
|
-
version: '1.0.0',
|
|
2456
|
-
canvas: 'test.otel.canvas',
|
|
2457
|
-
name: 'Invalid Workflow',
|
|
2458
|
-
description: 'Test',
|
|
2459
|
-
// spanPattern missing - will be caught by individual validation
|
|
2460
|
-
scenarioSelection: 'first-match' as const,
|
|
2461
|
-
scenarios: [],
|
|
2462
|
-
} as Partial<WorkflowTemplate> as WorkflowTemplate,
|
|
2463
|
-
workflowPath: 'invalid.workflow.json',
|
|
2464
|
-
},
|
|
2465
|
-
];
|
|
2466
|
-
|
|
2467
|
-
const violations = WorkflowValidator.validateSpanPatterns(workflows);
|
|
2468
|
-
|
|
2469
|
-
// Should not error - just skip the workflow without spanPattern
|
|
2470
|
-
expect(violations).toHaveLength(0);
|
|
2471
|
-
});
|
|
2472
|
-
});
|
|
2473
|
-
|
|
2474
|
-
// ============================================================================
|
|
2475
|
-
// Span Pattern Schema Validation Tests
|
|
2476
|
-
// ============================================================================
|
|
2477
|
-
|
|
2478
|
-
describe('checkSchema - spanPattern', () => {
|
|
2479
|
-
it('should error when spanPattern is missing', async () => {
|
|
2480
|
-
const context = createContext({ spanPattern: undefined });
|
|
2481
|
-
const result = await validator.validate(context);
|
|
2482
|
-
|
|
2483
|
-
const spanPatternViolations = result.violations.filter(
|
|
2484
|
-
v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
|
|
2485
|
-
);
|
|
2486
|
-
|
|
2487
|
-
expect(spanPatternViolations).toHaveLength(1);
|
|
2488
|
-
expect(spanPatternViolations[0].message).toContain('Missing required field "spanPattern"');
|
|
2489
|
-
expect(spanPatternViolations[0].suggestion).toContain('exact span name');
|
|
2490
|
-
});
|
|
2491
|
-
|
|
2492
|
-
it('should error when spanPattern is empty string', async () => {
|
|
2493
|
-
const context = createContext({ spanPattern: '' });
|
|
2494
|
-
const result = await validator.validate(context);
|
|
2495
|
-
|
|
2496
|
-
const spanPatternViolations = result.violations.filter(
|
|
2497
|
-
v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
|
|
2498
|
-
);
|
|
2499
|
-
|
|
2500
|
-
expect(spanPatternViolations).toHaveLength(1);
|
|
2501
|
-
expect(spanPatternViolations[0].message).toContain('Missing required field');
|
|
2502
|
-
});
|
|
2503
|
-
|
|
2504
|
-
it('should pass when spanPattern is a valid string', async () => {
|
|
2505
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
2506
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
2507
|
-
|
|
2508
|
-
const context = createContext({ spanPattern: 'payment.authorize' }, { canvasPath });
|
|
2509
|
-
const result = await validator.validate(context);
|
|
2510
|
-
|
|
2511
|
-
const spanPatternViolations = result.violations.filter(
|
|
2512
|
-
v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
|
|
2513
|
-
);
|
|
2514
|
-
|
|
2515
|
-
expect(spanPatternViolations).toHaveLength(0);
|
|
2516
|
-
});
|
|
2517
|
-
|
|
2518
|
-
it('should allow parentheses in spanPattern (Next.js style span names)', async () => {
|
|
2519
|
-
const canvasPath = join(tempDir, 'test.otel.canvas');
|
|
2520
|
-
writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
|
|
2521
|
-
|
|
2522
|
-
// Next.js creates span names like "executing api route (app) /api/auth/me/route"
|
|
2523
|
-
const context = createContext({
|
|
2524
|
-
spanPattern: 'executing api route (app) /api/auth/me/route'
|
|
2525
|
-
}, { canvasPath });
|
|
2526
|
-
const result = await validator.validate(context);
|
|
2527
|
-
|
|
2528
|
-
const spanPatternViolations = result.violations.filter(
|
|
2529
|
-
v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
|
|
2530
|
-
);
|
|
2531
|
-
|
|
2532
|
-
expect(spanPatternViolations).toHaveLength(0);
|
|
2533
|
-
});
|
|
2534
|
-
|
|
2535
|
-
it('should error when spanPattern contains wildcard *', async () => {
|
|
2536
|
-
const context = createContext({ spanPattern: 'payment.*' });
|
|
2537
|
-
const result = await validator.validate(context);
|
|
2538
|
-
|
|
2539
|
-
const spanPatternViolations = result.violations.filter(
|
|
2540
|
-
v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
|
|
2541
|
-
);
|
|
2542
|
-
|
|
2543
|
-
expect(spanPatternViolations).toHaveLength(1);
|
|
2544
|
-
expect(spanPatternViolations[0].message).toContain('wildcard');
|
|
2545
|
-
});
|
|
2546
|
-
|
|
2547
|
-
it('should error when spanPattern contains regex special characters', async () => {
|
|
2548
|
-
const regexPatterns = [
|
|
2549
|
-
'payment[type]', // square brackets
|
|
2550
|
-
'payment{1,2}', // curly braces (quantifier)
|
|
2551
|
-
'payment^start', // caret
|
|
2552
|
-
'payment$end', // dollar
|
|
2553
|
-
'payment|refund', // pipe (alternation)
|
|
2554
|
-
'payment\\d+', // backslash
|
|
2555
|
-
'payment+', // plus
|
|
2556
|
-
'payment?', // question mark
|
|
2557
|
-
];
|
|
2558
|
-
|
|
2559
|
-
for (const pattern of regexPatterns) {
|
|
2560
|
-
const context = createContext({ spanPattern: pattern });
|
|
2561
|
-
const result = await validator.validate(context);
|
|
2562
|
-
|
|
2563
|
-
const spanPatternViolations = result.violations.filter(
|
|
2564
|
-
v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
|
|
2565
|
-
);
|
|
2566
|
-
|
|
2567
|
-
expect(spanPatternViolations).toHaveLength(1);
|
|
2568
|
-
expect(spanPatternViolations[0].message).toContain('regex special characters');
|
|
2569
|
-
}
|
|
2570
|
-
});
|
|
2571
|
-
});
|
|
2572
|
-
|
|
2573
|
-
// ============================================================================
|
|
2574
|
-
// Event Attribute Requirements
|
|
2575
|
-
// ============================================================================
|
|
2576
|
-
|
|
2577
|
-
describe('Event Attribute Requirements', () => {
|
|
2578
|
-
it('should error when attribute is not used in any template', async () => {
|
|
2579
|
-
const workflow: WorkflowTemplate = {
|
|
2580
|
-
version: '1.0.0',
|
|
2581
|
-
name: 'Test',
|
|
2582
|
-
description: 'Test workflow',
|
|
2583
|
-
mode: 'span-tree',
|
|
2584
|
-
canvas: 'test.otel.canvas',
|
|
2585
|
-
scenarios: [{
|
|
2586
|
-
id: 'test',
|
|
2587
|
-
priority: 1,
|
|
2588
|
-
description: 'Test scenario',
|
|
2589
|
-
template: {
|
|
2590
|
-
events: {
|
|
2591
|
-
'order.completed': 'Order {{orderId}} completed' // total is not used
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
}]
|
|
2595
|
-
};
|
|
2596
|
-
|
|
2597
|
-
const canvas: ExtendedCanvas = {
|
|
2598
|
-
nodes: [
|
|
2599
|
-
{
|
|
2600
|
-
id: 'n1',
|
|
2601
|
-
type: 'otel-event',
|
|
2602
|
-
x: 0,
|
|
2603
|
-
y: 0,
|
|
2604
|
-
width: 100,
|
|
2605
|
-
height: 50,
|
|
2606
|
-
event: {
|
|
2607
|
-
name: 'order.completed',
|
|
2608
|
-
description: 'Order completion event',
|
|
2609
|
-
attributes: {
|
|
2610
|
-
orderId: { type: 'string' },
|
|
2611
|
-
total: { type: 'number' } // not used in template
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
],
|
|
2616
|
-
edges: [],
|
|
2617
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
2618
|
-
};
|
|
2619
|
-
|
|
2620
|
-
const context: WorkflowValidationContext = {
|
|
2621
|
-
workflow,
|
|
2622
|
-
workflowPath: 'test.workflow.json',
|
|
2623
|
-
canvas,
|
|
2624
|
-
basePath: tempDir
|
|
2625
|
-
};
|
|
2626
|
-
|
|
2627
|
-
const result = await validator.validate(context);
|
|
2628
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
|
|
2629
|
-
|
|
2630
|
-
expect(violations).toHaveLength(1);
|
|
2631
|
-
expect(violations[0].message).toContain('order.completed');
|
|
2632
|
-
expect(violations[0].message).toContain('total');
|
|
2633
|
-
expect(violations[0].message).toContain('not used in any template');
|
|
2634
|
-
});
|
|
2635
|
-
|
|
2636
|
-
it('should pass when all attributes are used', async () => {
|
|
2637
|
-
const workflow: WorkflowTemplate = {
|
|
2638
|
-
version: '1.0.0',
|
|
2639
|
-
name: 'Test',
|
|
2640
|
-
description: 'Test workflow',
|
|
2641
|
-
mode: 'span-tree',
|
|
2642
|
-
canvas: 'test.otel.canvas',
|
|
2643
|
-
scenarios: [{
|
|
2644
|
-
id: 'test',
|
|
2645
|
-
priority: 1,
|
|
2646
|
-
description: 'Test scenario',
|
|
2647
|
-
template: {
|
|
2648
|
-
events: {
|
|
2649
|
-
'order.completed': 'Order {{orderId}} for {{total}}'
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
}]
|
|
2653
|
-
};
|
|
2654
|
-
|
|
2655
|
-
const canvas: ExtendedCanvas = {
|
|
2656
|
-
nodes: [
|
|
2657
|
-
{
|
|
2658
|
-
id: 'n1',
|
|
2659
|
-
type: 'otel-event',
|
|
2660
|
-
x: 0,
|
|
2661
|
-
y: 0,
|
|
2662
|
-
width: 100,
|
|
2663
|
-
height: 50,
|
|
2664
|
-
event: {
|
|
2665
|
-
name: 'order.completed',
|
|
2666
|
-
description: 'Order completion event',
|
|
2667
|
-
attributes: {
|
|
2668
|
-
orderId: { type: 'string' },
|
|
2669
|
-
total: { type: 'number' }
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
],
|
|
2674
|
-
edges: [],
|
|
2675
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
2676
|
-
};
|
|
2677
|
-
|
|
2678
|
-
const context: WorkflowValidationContext = {
|
|
2679
|
-
workflow,
|
|
2680
|
-
workflowPath: 'test.workflow.json',
|
|
2681
|
-
canvas,
|
|
2682
|
-
basePath: tempDir
|
|
2683
|
-
};
|
|
2684
|
-
|
|
2685
|
-
const result = await validator.validate(context);
|
|
2686
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
|
|
2687
|
-
|
|
2688
|
-
expect(violations).toHaveLength(0);
|
|
2689
|
-
});
|
|
2690
|
-
|
|
2691
|
-
it('should consider nested property access as using the parent attribute', async () => {
|
|
2692
|
-
const workflow: WorkflowTemplate = {
|
|
2693
|
-
version: '1.0.0',
|
|
2694
|
-
name: 'Test',
|
|
2695
|
-
description: 'Test workflow',
|
|
2696
|
-
mode: 'span-tree',
|
|
2697
|
-
canvas: 'test.otel.canvas',
|
|
2698
|
-
scenarios: [{
|
|
2699
|
-
id: 'test',
|
|
2700
|
-
priority: 1,
|
|
2701
|
-
description: 'Test scenario',
|
|
2702
|
-
template: {
|
|
2703
|
-
events: {
|
|
2704
|
-
'config.loaded': 'Loaded {{config.timeout}} ms timeout'
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
}]
|
|
2708
|
-
};
|
|
2709
|
-
|
|
2710
|
-
const canvas: ExtendedCanvas = {
|
|
2711
|
-
nodes: [
|
|
2712
|
-
{
|
|
2713
|
-
id: 'n1',
|
|
2714
|
-
type: 'otel-event',
|
|
2715
|
-
x: 0,
|
|
2716
|
-
y: 0,
|
|
2717
|
-
width: 100,
|
|
2718
|
-
height: 50,
|
|
2719
|
-
event: {
|
|
2720
|
-
name: 'config.loaded',
|
|
2721
|
-
description: 'Configuration loaded',
|
|
2722
|
-
attributes: {
|
|
2723
|
-
config: { type: 'object' } // used via nested path {{config.timeout}}
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
],
|
|
2728
|
-
edges: [],
|
|
2729
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
2730
|
-
};
|
|
2731
|
-
|
|
2732
|
-
const context: WorkflowValidationContext = {
|
|
2733
|
-
workflow,
|
|
2734
|
-
workflowPath: 'test.workflow.json',
|
|
2735
|
-
canvas,
|
|
2736
|
-
basePath: tempDir
|
|
2737
|
-
};
|
|
2738
|
-
|
|
2739
|
-
const result = await validator.validate(context);
|
|
2740
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
|
|
2741
|
-
|
|
2742
|
-
expect(violations).toHaveLength(0);
|
|
2743
|
-
});
|
|
2744
|
-
|
|
2745
|
-
it('should check library event schemas via eventRef', async () => {
|
|
2746
|
-
const workflow: WorkflowTemplate = {
|
|
2747
|
-
version: '1.0.0',
|
|
2748
|
-
name: 'Test',
|
|
2749
|
-
description: 'Test workflow',
|
|
2750
|
-
mode: 'span-tree',
|
|
2751
|
-
canvas: 'test.otel.canvas',
|
|
2752
|
-
scenarios: [{
|
|
2753
|
-
id: 'test',
|
|
2754
|
-
priority: 1,
|
|
2755
|
-
description: 'Test scenario',
|
|
2756
|
-
template: {
|
|
2757
|
-
events: {
|
|
2758
|
-
'user.signup': 'User {{email}} signed up' // name is not used
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}]
|
|
2762
|
-
};
|
|
2763
|
-
|
|
2764
|
-
const canvas: ExtendedCanvas = {
|
|
2765
|
-
nodes: [
|
|
2766
|
-
{
|
|
2767
|
-
id: 'n1',
|
|
2768
|
-
type: 'otel-event',
|
|
2769
|
-
x: 0,
|
|
2770
|
-
y: 0,
|
|
2771
|
-
width: 100,
|
|
2772
|
-
height: 50,
|
|
2773
|
-
eventRef: 'user.signup'
|
|
2774
|
-
}
|
|
2775
|
-
],
|
|
2776
|
-
edges: [],
|
|
2777
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
2778
|
-
};
|
|
2779
|
-
|
|
2780
|
-
const library: ComponentLibrary = {
|
|
2781
|
-
version: '1.0.0',
|
|
2782
|
-
eventSchemas: {
|
|
2783
|
-
'user.signup': {
|
|
2784
|
-
description: 'User signup event',
|
|
2785
|
-
attributes: {
|
|
2786
|
-
email: { type: 'string' },
|
|
2787
|
-
name: { type: 'string' } // not used in template
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
};
|
|
2792
|
-
|
|
2793
|
-
const canvases = new Map<string, ExtendedCanvas>();
|
|
2794
|
-
canvases.set('test.otel.canvas', canvas);
|
|
2795
|
-
const eventRegistry = EventRegistry.build(library, canvases, 'library.yaml');
|
|
2796
|
-
|
|
2797
|
-
const context: WorkflowValidationContext = {
|
|
2798
|
-
workflow,
|
|
2799
|
-
workflowPath: 'test.workflow.json',
|
|
2800
|
-
canvas,
|
|
2801
|
-
basePath: tempDir,
|
|
2802
|
-
eventRegistry
|
|
2803
|
-
};
|
|
2804
|
-
|
|
2805
|
-
const result = await validator.validate(context);
|
|
2806
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
|
|
2807
|
-
|
|
2808
|
-
expect(violations).toHaveLength(1);
|
|
2809
|
-
expect(violations[0].message).toContain('user.signup');
|
|
2810
|
-
expect(violations[0].message).toContain('name');
|
|
2811
|
-
});
|
|
2812
|
-
|
|
2813
|
-
it('should not validate attributes for events not included in workflow (disconnected subgraph support)', async () => {
|
|
2814
|
-
// This test verifies the fix for Issue 3: Conflicting Validation Rules for Shared Canvases with Disconnected Subgraphs
|
|
2815
|
-
// A canvas may have multiple disconnected flows, and workflows may focus on only one flow.
|
|
2816
|
-
// We should NOT report "attribute not used" errors for events that aren't even part of the workflow.
|
|
2817
|
-
|
|
2818
|
-
const workflow: WorkflowTemplate = {
|
|
2819
|
-
version: '1.0.0',
|
|
2820
|
-
name: 'Navigation Flow',
|
|
2821
|
-
description: 'Workflow focusing only on navigation events (subgraph 1)',
|
|
2822
|
-
mode: 'span-tree',
|
|
2823
|
-
canvas: 'multi-flow.otel.canvas',
|
|
2824
|
-
scenarios: [{
|
|
2825
|
-
id: 'navigation',
|
|
2826
|
-
priority: 1,
|
|
2827
|
-
description: 'Navigation flow',
|
|
2828
|
-
template: {
|
|
2829
|
-
events: {
|
|
2830
|
-
// Only uses events from the navigation subgraph
|
|
2831
|
-
'panel.opened': 'Panel {{panelName}} opened',
|
|
2832
|
-
'panel.closed': 'Panel {{panelName}} closed'
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
}]
|
|
2836
|
-
};
|
|
2837
|
-
|
|
2838
|
-
// Canvas with TWO disconnected subgraphs:
|
|
2839
|
-
// Subgraph 1 (navigation): panel.opened -> panel.closed
|
|
2840
|
-
// Subgraph 2 (tool execution): tool.started -> tool.completed
|
|
2841
|
-
const canvas: ExtendedCanvas = {
|
|
2842
|
-
nodes: [
|
|
2843
|
-
// Subgraph 1: Navigation (included in workflow)
|
|
2844
|
-
{
|
|
2845
|
-
id: 'nav1',
|
|
2846
|
-
type: 'otel-event',
|
|
2847
|
-
x: 0,
|
|
2848
|
-
y: 0,
|
|
2849
|
-
width: 100,
|
|
2850
|
-
height: 50,
|
|
2851
|
-
event: {
|
|
2852
|
-
name: 'panel.opened',
|
|
2853
|
-
description: 'Panel opened event',
|
|
2854
|
-
attributes: {
|
|
2855
|
-
panelName: { type: 'string' }
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
},
|
|
2859
|
-
{
|
|
2860
|
-
id: 'nav2',
|
|
2861
|
-
type: 'otel-event',
|
|
2862
|
-
x: 200,
|
|
2863
|
-
y: 0,
|
|
2864
|
-
width: 100,
|
|
2865
|
-
height: 50,
|
|
2866
|
-
event: {
|
|
2867
|
-
name: 'panel.closed',
|
|
2868
|
-
description: 'Panel closed event',
|
|
2869
|
-
attributes: {
|
|
2870
|
-
panelName: { type: 'string' }
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
},
|
|
2874
|
-
// Subgraph 2: Tool execution (NOT included in workflow)
|
|
2875
|
-
{
|
|
2876
|
-
id: 'tool1',
|
|
2877
|
-
type: 'otel-event',
|
|
2878
|
-
x: 0,
|
|
2879
|
-
y: 200,
|
|
2880
|
-
width: 100,
|
|
2881
|
-
height: 50,
|
|
2882
|
-
event: {
|
|
2883
|
-
name: 'tool.started',
|
|
2884
|
-
description: 'Tool started event',
|
|
2885
|
-
attributes: {
|
|
2886
|
-
toolName: { type: 'string' },
|
|
2887
|
-
toolArgs: { type: 'string' } // These attributes are NOT used in the workflow
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
},
|
|
2891
|
-
{
|
|
2892
|
-
id: 'tool2',
|
|
2893
|
-
type: 'otel-event',
|
|
2894
|
-
x: 200,
|
|
2895
|
-
y: 200,
|
|
2896
|
-
width: 100,
|
|
2897
|
-
height: 50,
|
|
2898
|
-
event: {
|
|
2899
|
-
name: 'tool.completed',
|
|
2900
|
-
description: 'Tool completed event',
|
|
2901
|
-
attributes: {
|
|
2902
|
-
toolName: { type: 'string' },
|
|
2903
|
-
result: { type: 'string' } // These attributes are NOT used in the workflow
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
],
|
|
2908
|
-
edges: [
|
|
2909
|
-
// Subgraph 1 edges
|
|
2910
|
-
{ id: 'e1', fromNode: 'nav1', fromSide: 'right', toNode: 'nav2', toSide: 'left' },
|
|
2911
|
-
// Subgraph 2 edges (disconnected from subgraph 1)
|
|
2912
|
-
{ id: 'e2', fromNode: 'tool1', fromSide: 'right', toNode: 'tool2', toSide: 'left' }
|
|
2913
|
-
],
|
|
2914
|
-
pv: { version: '1.0.0', name: 'Multi-Flow Canvas', markdown: 'test.md' }
|
|
2915
|
-
};
|
|
2916
|
-
|
|
2917
|
-
const context: WorkflowValidationContext = {
|
|
2918
|
-
workflow,
|
|
2919
|
-
workflowPath: 'navigation.workflow.json',
|
|
2920
|
-
canvas,
|
|
2921
|
-
basePath: tempDir
|
|
2922
|
-
};
|
|
2923
|
-
|
|
2924
|
-
const result = await validator.validate(context);
|
|
2925
|
-
const attributeViolations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
|
|
2926
|
-
const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
|
|
2927
|
-
|
|
2928
|
-
// Should NOT have any "attribute not used" errors for tool.started or tool.completed
|
|
2929
|
-
// because those events are not part of this workflow
|
|
2930
|
-
expect(attributeViolations).toHaveLength(0);
|
|
2931
|
-
|
|
2932
|
-
// Should NOT have connectivity errors because the workflow only uses connected events
|
|
2933
|
-
expect(connectivityViolations).toHaveLength(0);
|
|
2934
|
-
});
|
|
2935
|
-
});
|
|
2936
|
-
|
|
2937
|
-
describe('workflow-template-attribute-not-in-schema', () => {
|
|
2938
|
-
it('should error when template references attribute not in event schema', async () => {
|
|
2939
|
-
const workflow: WorkflowTemplate = {
|
|
2940
|
-
version: '1.0.0',
|
|
2941
|
-
name: 'Test',
|
|
2942
|
-
description: 'Test workflow',
|
|
2943
|
-
mode: 'span-tree',
|
|
2944
|
-
canvas: 'test.otel.canvas',
|
|
2945
|
-
scenarios: [{
|
|
2946
|
-
id: 'test',
|
|
2947
|
-
priority: 1,
|
|
2948
|
-
description: 'Test scenario',
|
|
2949
|
-
template: {
|
|
2950
|
-
events: {
|
|
2951
|
-
// Template references context.projectRoot which is NOT in the schema
|
|
2952
|
-
'task.delete.error': 'Task {{input.taskId}} not found in {{context.projectRoot}}'
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}]
|
|
2956
|
-
};
|
|
2957
|
-
|
|
2958
|
-
const canvas: ExtendedCanvas = {
|
|
2959
|
-
nodes: [
|
|
2960
|
-
{
|
|
2961
|
-
id: 'n1',
|
|
2962
|
-
type: 'otel-event',
|
|
2963
|
-
x: 0,
|
|
2964
|
-
y: 0,
|
|
2965
|
-
width: 100,
|
|
2966
|
-
height: 50,
|
|
2967
|
-
event: {
|
|
2968
|
-
name: 'task.delete.error',
|
|
2969
|
-
description: 'Task deletion error',
|
|
2970
|
-
attributes: {
|
|
2971
|
-
'error.type': { type: 'string' },
|
|
2972
|
-
'error.message': { type: 'string' },
|
|
2973
|
-
'input.taskId': { type: 'string' }
|
|
2974
|
-
// Note: context.projectRoot is NOT defined here
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
],
|
|
2979
|
-
edges: [],
|
|
2980
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
2981
|
-
};
|
|
2982
|
-
|
|
2983
|
-
const context: WorkflowValidationContext = {
|
|
2984
|
-
workflow,
|
|
2985
|
-
workflowPath: 'test.workflow.json',
|
|
2986
|
-
canvas,
|
|
2987
|
-
basePath: tempDir
|
|
2988
|
-
};
|
|
2989
|
-
|
|
2990
|
-
const result = await validator.validate(context);
|
|
2991
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
|
|
2992
|
-
|
|
2993
|
-
expect(violations).toHaveLength(1);
|
|
2994
|
-
expect(violations[0].message).toContain('context.projectRoot');
|
|
2995
|
-
expect(violations[0].message).toContain('not defined in the event schema');
|
|
2996
|
-
expect(violations[0].message).toContain('task.delete.error');
|
|
2997
|
-
});
|
|
2998
|
-
|
|
2999
|
-
it('should pass when all template attributes are in event schema', async () => {
|
|
3000
|
-
const workflow: WorkflowTemplate = {
|
|
3001
|
-
version: '1.0.0',
|
|
3002
|
-
name: 'Test',
|
|
3003
|
-
description: 'Test workflow',
|
|
3004
|
-
mode: 'span-tree',
|
|
3005
|
-
canvas: 'test.otel.canvas',
|
|
3006
|
-
scenarios: [{
|
|
3007
|
-
id: 'test',
|
|
3008
|
-
priority: 1,
|
|
3009
|
-
description: 'Test scenario',
|
|
3010
|
-
template: {
|
|
3011
|
-
events: {
|
|
3012
|
-
'task.create.complete': 'Created task {{output.taskId}} in {{duration.ms}}ms'
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
}]
|
|
3016
|
-
};
|
|
3017
|
-
|
|
3018
|
-
const canvas: ExtendedCanvas = {
|
|
3019
|
-
nodes: [
|
|
3020
|
-
{
|
|
3021
|
-
id: 'n1',
|
|
3022
|
-
type: 'otel-event',
|
|
3023
|
-
x: 0,
|
|
3024
|
-
y: 0,
|
|
3025
|
-
width: 100,
|
|
3026
|
-
height: 50,
|
|
3027
|
-
event: {
|
|
3028
|
-
name: 'task.create.complete',
|
|
3029
|
-
description: 'Task creation complete',
|
|
3030
|
-
attributes: {
|
|
3031
|
-
'output.taskId': { type: 'string' },
|
|
3032
|
-
'duration.ms': { type: 'number' }
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
],
|
|
3037
|
-
edges: [],
|
|
3038
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
3039
|
-
};
|
|
3040
|
-
|
|
3041
|
-
const context: WorkflowValidationContext = {
|
|
3042
|
-
workflow,
|
|
3043
|
-
workflowPath: 'test.workflow.json',
|
|
3044
|
-
canvas,
|
|
3045
|
-
basePath: tempDir
|
|
3046
|
-
};
|
|
3047
|
-
|
|
3048
|
-
const result = await validator.validate(context);
|
|
3049
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
|
|
3050
|
-
|
|
3051
|
-
expect(violations).toHaveLength(0);
|
|
3052
|
-
});
|
|
3053
|
-
|
|
3054
|
-
it('should error for multiple undefined attributes', async () => {
|
|
3055
|
-
const workflow: WorkflowTemplate = {
|
|
3056
|
-
version: '1.0.0',
|
|
3057
|
-
name: 'Test',
|
|
3058
|
-
description: 'Test workflow',
|
|
3059
|
-
mode: 'span-tree',
|
|
3060
|
-
canvas: 'test.otel.canvas',
|
|
3061
|
-
scenarios: [{
|
|
3062
|
-
id: 'test',
|
|
3063
|
-
priority: 1,
|
|
3064
|
-
description: 'Test scenario',
|
|
3065
|
-
template: {
|
|
3066
|
-
events: {
|
|
3067
|
-
'task.delete.error': 'Task {{input.taskId}} not found in {{context.projectRoot}} ({{context.taskCount}} tasks)'
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
}]
|
|
3071
|
-
};
|
|
3072
|
-
|
|
3073
|
-
const canvas: ExtendedCanvas = {
|
|
3074
|
-
nodes: [
|
|
3075
|
-
{
|
|
3076
|
-
id: 'n1',
|
|
3077
|
-
type: 'otel-event',
|
|
3078
|
-
x: 0,
|
|
3079
|
-
y: 0,
|
|
3080
|
-
width: 100,
|
|
3081
|
-
height: 50,
|
|
3082
|
-
event: {
|
|
3083
|
-
name: 'task.delete.error',
|
|
3084
|
-
description: 'Task deletion error',
|
|
3085
|
-
attributes: {
|
|
3086
|
-
'input.taskId': { type: 'string' }
|
|
3087
|
-
// context.projectRoot and context.taskCount are NOT defined
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
],
|
|
3092
|
-
edges: [],
|
|
3093
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
3094
|
-
};
|
|
3095
|
-
|
|
3096
|
-
const context: WorkflowValidationContext = {
|
|
3097
|
-
workflow,
|
|
3098
|
-
workflowPath: 'test.workflow.json',
|
|
3099
|
-
canvas,
|
|
3100
|
-
basePath: tempDir
|
|
3101
|
-
};
|
|
3102
|
-
|
|
3103
|
-
const result = await validator.validate(context);
|
|
3104
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
|
|
3105
|
-
|
|
3106
|
-
expect(violations).toHaveLength(2);
|
|
3107
|
-
expect(violations.some(v => v.message.includes('context.projectRoot'))).toBe(true);
|
|
3108
|
-
expect(violations.some(v => v.message.includes('context.taskCount'))).toBe(true);
|
|
3109
|
-
});
|
|
3110
|
-
|
|
3111
|
-
it('should handle nested attribute access when parent is defined', async () => {
|
|
3112
|
-
const workflow: WorkflowTemplate = {
|
|
3113
|
-
version: '1.0.0',
|
|
3114
|
-
name: 'Test',
|
|
3115
|
-
description: 'Test workflow',
|
|
3116
|
-
mode: 'span-tree',
|
|
3117
|
-
canvas: 'test.otel.canvas',
|
|
3118
|
-
scenarios: [{
|
|
3119
|
-
id: 'test',
|
|
3120
|
-
priority: 1,
|
|
3121
|
-
description: 'Test scenario',
|
|
3122
|
-
template: {
|
|
3123
|
-
events: {
|
|
3124
|
-
// Accessing nested path input.taskId when input.taskId is defined
|
|
3125
|
-
'task.update.started': 'Updating task {{input.taskId}}'
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
}]
|
|
3129
|
-
};
|
|
3130
|
-
|
|
3131
|
-
const canvas: ExtendedCanvas = {
|
|
3132
|
-
nodes: [
|
|
3133
|
-
{
|
|
3134
|
-
id: 'n1',
|
|
3135
|
-
type: 'otel-event',
|
|
3136
|
-
x: 0,
|
|
3137
|
-
y: 0,
|
|
3138
|
-
width: 100,
|
|
3139
|
-
height: 50,
|
|
3140
|
-
event: {
|
|
3141
|
-
name: 'task.update.started',
|
|
3142
|
-
description: 'Task update started',
|
|
3143
|
-
attributes: {
|
|
3144
|
-
'input.taskId': { type: 'string' },
|
|
3145
|
-
'input.hasTitle': { type: 'boolean' }
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
],
|
|
3150
|
-
edges: [],
|
|
3151
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
3152
|
-
};
|
|
3153
|
-
|
|
3154
|
-
const context: WorkflowValidationContext = {
|
|
3155
|
-
workflow,
|
|
3156
|
-
workflowPath: 'test.workflow.json',
|
|
3157
|
-
canvas,
|
|
3158
|
-
basePath: tempDir
|
|
3159
|
-
};
|
|
3160
|
-
|
|
3161
|
-
const result = await validator.validate(context);
|
|
3162
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
|
|
3163
|
-
|
|
3164
|
-
expect(violations).toHaveLength(0);
|
|
3165
|
-
});
|
|
3166
|
-
|
|
3167
|
-
it('should suggest similar attributes when typo detected', async () => {
|
|
3168
|
-
const workflow: WorkflowTemplate = {
|
|
3169
|
-
version: '1.0.0',
|
|
3170
|
-
name: 'Test',
|
|
3171
|
-
description: 'Test workflow',
|
|
3172
|
-
mode: 'span-tree',
|
|
3173
|
-
canvas: 'test.otel.canvas',
|
|
3174
|
-
scenarios: [{
|
|
3175
|
-
id: 'test',
|
|
3176
|
-
priority: 1,
|
|
3177
|
-
description: 'Test scenario',
|
|
3178
|
-
template: {
|
|
3179
|
-
events: {
|
|
3180
|
-
// Typo: input.takId instead of input.taskId
|
|
3181
|
-
'task.delete.started': 'Deleting task {{input.takId}}'
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
}]
|
|
3185
|
-
};
|
|
3186
|
-
|
|
3187
|
-
const canvas: ExtendedCanvas = {
|
|
3188
|
-
nodes: [
|
|
3189
|
-
{
|
|
3190
|
-
id: 'n1',
|
|
3191
|
-
type: 'otel-event',
|
|
3192
|
-
x: 0,
|
|
3193
|
-
y: 0,
|
|
3194
|
-
width: 100,
|
|
3195
|
-
height: 50,
|
|
3196
|
-
event: {
|
|
3197
|
-
name: 'task.delete.started',
|
|
3198
|
-
description: 'Task deletion started',
|
|
3199
|
-
attributes: {
|
|
3200
|
-
'input.taskId': { type: 'string' }
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
],
|
|
3205
|
-
edges: [],
|
|
3206
|
-
pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
|
|
3207
|
-
};
|
|
3208
|
-
|
|
3209
|
-
const context: WorkflowValidationContext = {
|
|
3210
|
-
workflow,
|
|
3211
|
-
workflowPath: 'test.workflow.json',
|
|
3212
|
-
canvas,
|
|
3213
|
-
basePath: tempDir
|
|
3214
|
-
};
|
|
3215
|
-
|
|
3216
|
-
const result = await validator.validate(context);
|
|
3217
|
-
const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
|
|
3218
|
-
|
|
3219
|
-
expect(violations).toHaveLength(1);
|
|
3220
|
-
expect(violations[0].message).toContain('input.takId');
|
|
3221
|
-
// Should suggest the similar attribute
|
|
3222
|
-
expect(violations[0].suggestion).toContain('input.taskId');
|
|
3223
|
-
});
|
|
3224
|
-
});
|
|
3225
|
-
});
|