Harden output converter contracts
(cherry picked from commit 0737def83d)
This commit is contained in:
parent
61fc376dac
commit
dff7d9f987
|
|
@ -64,12 +64,12 @@ This checklist maps the approved requirements to implementation and automated ev
|
||||||
|
|
||||||
## Verification log
|
## 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`.
|
- Management definition-validation tests: 3 passed on `net10.0`.
|
||||||
- API endpoint tests: 6 passed on `net10.0`.
|
- API endpoint tests: 6 passed on `net10.0`.
|
||||||
- API-client component tests: 2 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).
|
- 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 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.
|
- 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`).
|
- 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 Type,
|
||||||
string Message,
|
string Message,
|
||||||
string? StackTrace,
|
string? StackTrace,
|
||||||
ExceptionState? InnerException,
|
ExceptionState? InnerException)
|
||||||
IReadOnlyDictionary<string, string>? Metadata = null)
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets privacy-safe structured exception metadata.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ExceptionState"/> class.
|
/// Initializes a new instance of the <see cref="ExceptionState"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConstructor]
|
[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)
|
if (referenceId == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var variable = activityNode
|
var variable = new[] { activityNode }
|
||||||
.Ancestors()
|
.Concat(activityNode.Ancestors())
|
||||||
.Select(x => x.Activity)
|
.Select(x => x.Activity)
|
||||||
.OfType<IVariableContainer>()
|
.OfType<IVariableContainer>()
|
||||||
.SelectMany(x => x.Variables)
|
.SelectMany(x => x.Variables)
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,18 @@ public record ExceptionState(
|
||||||
Type Type,
|
Type Type,
|
||||||
string Message,
|
string Message,
|
||||||
string? StackTrace,
|
string? StackTrace,
|
||||||
ExceptionState? InnerException,
|
ExceptionState? InnerException)
|
||||||
IReadOnlyDictionary<string, string>? Metadata = null)
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets privacy-safe structured exception metadata.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConstructor]
|
[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 metadata = metadataProvider?.GetSafeMetadata();
|
||||||
var innerException = metadataProvider == null ? FromException(ex.InnerException) : null;
|
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);
|
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]
|
[Theory]
|
||||||
[InlineData(typeof(string), true)]
|
[InlineData(typeof(string), true)]
|
||||||
[InlineData(typeof(int?), true)]
|
[InlineData(typeof(int?), true)]
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,21 @@ public class OutputConversionExceptionStateTests
|
||||||
Assert.Null(state.Metadata);
|
Assert.Null(state.Metadata);
|
||||||
Assert.DoesNotContain("do not persist", state.Message);
|
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