diff --git a/specs/012-output-converters/checklists/completion.md b/specs/012-output-converters/checklists/completion.md index 2622ca1d1..d51e6dfae 100644 --- a/specs/012-output-converters/checklists/completion.md +++ b/specs/012-output-converters/checklists/completion.md @@ -64,12 +64,12 @@ This checklist maps the approved requirements to implementation and automated ev ## Verification log -- Core output-converter and serialization tests: 45 passed on `net10.0`. +- Core output-converter and serialization tests: 47 passed on `net10.0`. - Management definition-validation tests: 3 passed on `net10.0`. - API endpoint tests: 6 passed on `net10.0`. - API-client component tests: 2 passed on `net10.0`. - Runtime component tests: 3 passed on `net10.0` (workflow-output scenario plus the corrected variable and missing-registration scenarios). -- Studio output-converter tests: 8 passed on `net10.0` against this Core worktree. +- Studio output-converter tests: 9 passed on `net10.0` against this Core worktree. - Core project build: passed on `net10.0` with zero warnings and errors. - Core, Management, API, and API-client projects built successfully for `net8.0`, `net9.0`, and `net10.0` during the solution build. - The complete solution build could not finish with `--no-restore` because unrelated projects had no `project.assets.json` in this worktree (`Elsa.Workflows.IntegrationTests`, `Elsa.Hosting.Management`, and `Elsa.Workflows.Runtime.UnitTests`). diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ExceptionState.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ExceptionState.cs index ef29bf328..a8ec0b67a 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ExceptionState.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ExceptionState.cs @@ -9,14 +9,18 @@ public record ExceptionState( string Type, string Message, string? StackTrace, - ExceptionState? InnerException, - IReadOnlyDictionary? Metadata = null) + ExceptionState? InnerException) { + /// + /// Gets privacy-safe structured exception metadata. + /// + public IReadOnlyDictionary? Metadata { get; init; } + /// /// Initializes a new instance of the class. /// [JsonConstructor] - public ExceptionState() : this(null!, null!, null, null, null) + public ExceptionState() : this(null!, null!, null, null) { } diff --git a/src/modules/Elsa.Workflows.Core/Services/OutputBindingDestinationResolver.cs b/src/modules/Elsa.Workflows.Core/Services/OutputBindingDestinationResolver.cs index adcf7187c..c624d5020 100644 --- a/src/modules/Elsa.Workflows.Core/Services/OutputBindingDestinationResolver.cs +++ b/src/modules/Elsa.Workflows.Core/Services/OutputBindingDestinationResolver.cs @@ -32,8 +32,8 @@ public class OutputBindingDestinationResolver : IOutputBindingDestinationResolve if (referenceId == null) return null; - var variable = activityNode - .Ancestors() + var variable = new[] { activityNode } + .Concat(activityNode.Ancestors()) .Select(x => x.Activity) .OfType() .SelectMany(x => x.Variables) diff --git a/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs b/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs index d0733db27..20de8fddd 100644 --- a/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs +++ b/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs @@ -11,14 +11,18 @@ public record ExceptionState( Type Type, string Message, string? StackTrace, - ExceptionState? InnerException, - IReadOnlyDictionary? Metadata = null) + ExceptionState? InnerException) { + /// + /// Gets privacy-safe structured exception metadata. + /// + public IReadOnlyDictionary? Metadata { get; init; } + /// /// Constructor /// [JsonConstructor] - public ExceptionState() : this(default!, default!, default, default, default) + public ExceptionState() : this(default!, default!, default, default) { } @@ -35,6 +39,9 @@ public record ExceptionState( var metadata = metadataProvider?.GetSafeMetadata(); var innerException = metadataProvider == null ? FromException(ex.InnerException) : null; - return new ExceptionState(ex.GetType(), ex.Message, ex.StackTrace, innerException, metadata); + return new ExceptionState(ex.GetType(), ex.Message, ex.StackTrace, innerException) + { + Metadata = metadata + }; } } diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputBindingDestinationResolverTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputBindingDestinationResolverTests.cs index 41cf8f233..6994b58ee 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputBindingDestinationResolverTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputBindingDestinationResolverTests.cs @@ -80,6 +80,29 @@ public class OutputBindingDestinationResolverTests Assert.Equal(OutputBindingDestinationKind.Variable, destination.Kind); } + [Fact] + public void Resolve_StaticVariable_IncludesCurrentVariableContainer() + { + var referenceId = "local-destination"; + var activity = new Sequence + { + Id = "activity", + Variables = [new Variable("LocalDestination", "", referenceId)] + }; + var node = new ActivityNode(activity, "Body"); + var workflow = new Workflow { Root = activity }; + var graph = new WorkflowGraph(workflow, node, [node]); + var output = new Output(new MemoryBlockReference(referenceId)); + + var destination = _resolver.Resolve(graph, node, output); + + Assert.NotNull(destination); + Assert.Equal(referenceId, destination.Id); + Assert.Equal(typeof(string), destination.Type); + Assert.True(destination.AllowsNull); + Assert.Equal(OutputBindingDestinationKind.Variable, destination.Kind); + } + [Theory] [InlineData(typeof(string), true)] [InlineData(typeof(int?), true)] diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputConversionExceptionStateTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputConversionExceptionStateTests.cs index f5f2595ab..0aadb559f 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputConversionExceptionStateTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/OutputConverters/OutputConversionExceptionStateTests.cs @@ -43,4 +43,21 @@ public class OutputConversionExceptionStateTests Assert.Null(state.Metadata); Assert.DoesNotContain("do not persist", state.Message); } + + [Fact] + public void Metadata_PreservesTheOriginalFourPositionRecordContract() + { + var state = new ExceptionState(typeof(InvalidOperationException), "failure", "stack", null) + { + Metadata = new Dictionary { ["stage"] = "Invocation" } + }; + + var (type, message, stackTrace, innerException) = state; + + Assert.Equal(typeof(InvalidOperationException), type); + Assert.Equal("failure", message); + Assert.Equal("stack", stackTrace); + Assert.Null(innerException); + Assert.Equal("Invocation", state.Metadata!["stage"]); + } }