Harden output converter contracts
This commit is contained in:
parent
d698e6b005
commit
0737def83d
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -9,14 +9,18 @@ public record ExceptionState(
|
|||
string Type,
|
||||
string Message,
|
||||
string? StackTrace,
|
||||
ExceptionState? InnerException,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null)
|
||||
ExceptionState? InnerException)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets privacy-safe structured exception metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExceptionState"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
public ExceptionState() : this(null!, null!, null, null, null)
|
||||
public ExceptionState() : this(null!, null!, null, null)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IVariableContainer>()
|
||||
.SelectMany(x => x.Variables)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,18 @@ public record ExceptionState(
|
|||
Type Type,
|
||||
string Message,
|
||||
string? StackTrace,
|
||||
ExceptionState? InnerException,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null)
|
||||
ExceptionState? InnerException)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets privacy-safe structured exception metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
[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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>("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<int>(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)]
|
||||
|
|
|
|||
|
|
@ -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<string, string> { ["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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue