fix(runtime): read InternalState from logPersistenceConfig (#8126)
* fix(runtime): read InternalState from logPersistenceConfig Honor Studio-written customProperties.logPersistenceConfig.internalState and fall back to workflow internalState, then default/ResolveMode. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * test(runtime): register activity context in InternalState evaluator tests ActivityTestFixture.BuildAsync does not add the context to WorkflowExecutionContext.ActivityExecutionContexts, which GetPersistenceDefaultsAsync requires. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(runtime): evaluate workflow InternalState with root context Use the workflow/root ExpressionExecutionContext for workflow-level internalState (parity with default), and keep the activity context for the activity override. Look up component records by activity name. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
ab93c67623
commit
d4c00f13ed
|
|
@ -69,12 +69,18 @@ public class ActivityPropertyLogPersistenceEvaluator : IActivityPropertyLogPersi
|
|||
public async Task<ActivityLogPersistenceModeMap> EvaluateLogPersistenceModesAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var (legacyProps, configProps, defaultMode) = await GetPersistenceDefaultsAsync(context, cancellationToken);
|
||||
var (legacyProps, configProps, defaultMode, workflow, rootContext) = await GetPersistenceDefaultsAsync(context, cancellationToken);
|
||||
var map = new ActivityLogPersistenceModeMap();
|
||||
|
||||
await EvaluatePropertiesAsync(context, "inputs", context.ActivityDescriptor.Inputs, legacyProps, configProps, defaultMode, map.Inputs, cancellationToken);
|
||||
await EvaluatePropertiesAsync(context, "outputs", context.ActivityDescriptor.Outputs, legacyProps, configProps, defaultMode, map.Outputs, cancellationToken);
|
||||
map.InternalState = await EvaluateInternalStateModeAsync(context.ExpressionExecutionContext, context.Activity.CustomProperties, defaultMode, cancellationToken);
|
||||
map.InternalState = await EvaluateInternalStateModeAsync(
|
||||
context.ExpressionExecutionContext,
|
||||
rootContext.ExpressionExecutionContext,
|
||||
configProps,
|
||||
workflow.CustomProperties,
|
||||
defaultMode,
|
||||
cancellationToken);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
|
@ -82,12 +88,12 @@ public class ActivityPropertyLogPersistenceEvaluator : IActivityPropertyLogPersi
|
|||
public async Task<Dictionary<string, object>> GetPersistableOutputAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var cancellationToken = context.WorkflowExecutionContext.CancellationToken;
|
||||
var (legacyProps, configProps, defaultMode) = await GetPersistenceDefaultsAsync(context, cancellationToken);
|
||||
var (legacyProps, configProps, defaultMode, _, _) = await GetPersistenceDefaultsAsync(context, cancellationToken);
|
||||
var outputs = context.GetOutputs();
|
||||
return await GetPersistablePropertiesAsync(context, outputs, "outputs", legacyProps, configProps, defaultMode, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(IDictionary<string, object> legacyProps, IDictionary<string, object> configProps, LogPersistenceMode defaultMode)> GetPersistenceDefaultsAsync(ActivityExecutionContext context, CancellationToken cancellationToken)
|
||||
private async Task<(IDictionary<string, object> legacyProps, IDictionary<string, object> configProps, LogPersistenceMode defaultMode, Workflow workflow, ActivityExecutionContext rootContext)> GetPersistenceDefaultsAsync(ActivityExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var legacyProps = context.Activity.CustomProperties.GetValueOrDefault<IDictionary<string, object>>(LegacyKey, () => new Dictionary<string, object>())!;
|
||||
var rootContext = context.WorkflowExecutionContext.ActivityExecutionContexts.First(x => x.ParentActivityExecutionContext == null);
|
||||
|
|
@ -95,7 +101,7 @@ public class ActivityPropertyLogPersistenceEvaluator : IActivityPropertyLogPersi
|
|||
var workflowDefault = await GetDefaultPersistenceModeAsync(rootContext.ExpressionExecutionContext, workflow.CustomProperties, () => _options.Value.LogPersistenceMode, cancellationToken);
|
||||
var activityDefault = await GetDefaultPersistenceModeAsync(context.ExpressionExecutionContext, context.Activity.CustomProperties, () => workflowDefault, cancellationToken);
|
||||
var configProps = context.Activity.CustomProperties.GetValueOrDefault<IDictionary<string, object>>(ConfigKey, () => new Dictionary<string, object>())!;
|
||||
return (legacyProps, configProps, activityDefault);
|
||||
return (legacyProps, configProps, activityDefault, workflow, rootContext);
|
||||
}
|
||||
|
||||
private async Task EvaluatePropertiesAsync(
|
||||
|
|
@ -136,15 +142,30 @@ public class ActivityPropertyLogPersistenceEvaluator : IActivityPropertyLogPersi
|
|||
}
|
||||
|
||||
private async Task<LogPersistenceMode> EvaluateInternalStateModeAsync(
|
||||
ExpressionExecutionContext executionContext,
|
||||
IDictionary<string, object> currentConfig,
|
||||
ExpressionExecutionContext activityExecutionContext,
|
||||
ExpressionExecutionContext workflowExecutionContext,
|
||||
IDictionary<string, object> activityConfig,
|
||||
IDictionary<string, object> workflowProperties,
|
||||
LogPersistenceMode defaultMode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var configObject = currentConfig.GetValueOrDefault("internalState", () => new Dictionary<string, object>())!;
|
||||
var workflowConfig = workflowProperties.GetValueOrDefault<IDictionary<string, object>>(ConfigKey, () => new Dictionary<string, object>())!;
|
||||
var workflowInternalState = await EvaluateInternalStateConfigAsync(workflowExecutionContext, workflowConfig, () => defaultMode, cancellationToken);
|
||||
return await EvaluateInternalStateConfigAsync(activityExecutionContext, activityConfig, () => workflowInternalState, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<LogPersistenceMode> EvaluateInternalStateConfigAsync(
|
||||
ExpressionExecutionContext executionContext,
|
||||
IDictionary<string, object> configProps,
|
||||
Func<LogPersistenceMode> defaultFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var configObject = configProps.GetValueOrDefault("internalState", () => null);
|
||||
var config = ConvertToConfig(configObject);
|
||||
if (config != null) return await EvaluateConfigAsync(config, executionContext, () => defaultMode, cancellationToken);
|
||||
return LogPersistenceMode.Inherit;
|
||||
if (config != null)
|
||||
return await EvaluateConfigAsync(config, executionContext, defaultFactory, cancellationToken);
|
||||
|
||||
return ResolveMode(LogPersistenceMode.Inherit, defaultFactory);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> GetPersistablePropertiesAsync(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@
|
|||
<None Update="Scenarios\LogPersistenceModes\input-output-logging-3-child.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\LogPersistenceModes\internal-state-logging-activity.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\LogPersistenceModes\internal-state-logging-workflow.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowCompletion\hello-world.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
|
||||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses;
|
||||
using Elsa.Testing.Shared.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.LogPersistenceModes;
|
||||
|
||||
public class InternalStateLoggingTests(App app) : AppComponentTest(app)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ActivityInternalState_ShouldBeIndependentOfDefaultMode()
|
||||
{
|
||||
var records = await ExecuteAndGetWriteLinesAsync("internal-state-logging-activity");
|
||||
|
||||
AssertInternalState(GetRecord(records, "WriteLine1"), textIncluded: false, internalStateIncluded: true);
|
||||
AssertInternalState(GetRecord(records, "WriteLine2"), textIncluded: true, internalStateIncluded: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkflowInternalState_ShouldApplyWhenActivityInternalStateIsMissing()
|
||||
{
|
||||
var records = await ExecuteAndGetWriteLinesAsync("internal-state-logging-workflow");
|
||||
|
||||
AssertInternalState(GetRecord(records, "WriteLine1"), textIncluded: false, internalStateIncluded: true);
|
||||
AssertInternalState(GetRecord(records, "WriteLine2"), textIncluded: false, internalStateIncluded: false);
|
||||
}
|
||||
|
||||
private static ActivityExecutionRecord GetRecord(IReadOnlyList<ActivityExecutionRecord> records, string activityName)
|
||||
{
|
||||
return records.Single(x => x.ActivityName == activityName);
|
||||
}
|
||||
|
||||
private static void AssertInternalState(ActivityExecutionRecord record, bool textIncluded, bool internalStateIncluded)
|
||||
{
|
||||
Assert.Equal(textIncluded, record.ActivityState?.ContainsKey(nameof(WriteLine.Text)) == true);
|
||||
|
||||
if (internalStateIncluded)
|
||||
{
|
||||
Assert.NotNull(record.Properties);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Null(record.Properties);
|
||||
Assert.Null(record.Payload);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<ActivityExecutionRecord>> ExecuteAndGetWriteLinesAsync(string workflowDefinitionId)
|
||||
{
|
||||
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
|
||||
using var response = await client.ExecuteAsync(workflowDefinitionId);
|
||||
var model = await response.ReadAsJsonAsync<ExecuteWorkflowDefinitionResponse>(WorkflowServer.Services);
|
||||
var writeLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<WriteLine>();
|
||||
var store = Scope.ServiceProvider.GetRequiredService<IActivityExecutionStore>();
|
||||
var records = await store.FindManyAsync(new ActivityExecutionRecordFilter
|
||||
{
|
||||
WorkflowInstanceId = model.WorkflowState.Id
|
||||
});
|
||||
|
||||
return records.Where(x => x.ActivityType == writeLineActivityTypeName).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"id": "c1a8e6d04f7b2a91",
|
||||
"definitionId": "internal-state-logging-activity",
|
||||
"name": "Internal State Logging Activity",
|
||||
"createdAt": "2026-09-13T20:00:00+00:00",
|
||||
"version": 1,
|
||||
"toolVersion": "3.9.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {
|
||||
"Elsa:WorkflowContextProviderTypes": []
|
||||
},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "f4b21c8e9a6d3057",
|
||||
"nodeId": "Workflow1:f4b21c8e9a6d3057",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Default exclude, internal state include"
|
||||
}
|
||||
},
|
||||
"id": "a91c3e7b5d204f18",
|
||||
"nodeId": "Workflow1:f4b21c8e9a6d3057:a91c3e7b5d204f18",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"logPersistenceConfig": {
|
||||
"default": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Exclude, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"internalState": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Include, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Default include, internal state exclude"
|
||||
}
|
||||
},
|
||||
"id": "b02d4f8c6e315a29",
|
||||
"nodeId": "Workflow1:f4b21c8e9a6d3057:b02d4f8c6e315a29",
|
||||
"name": "WriteLine2",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"logPersistenceConfig": {
|
||||
"default": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Include, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"internalState": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Exclude, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 220,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "a91c3e7b5d204f18",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "b02d4f8c6e315a29",
|
||||
"port": "In"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"id": "d2b9f7e15a8c3b04",
|
||||
"definitionId": "internal-state-logging-workflow",
|
||||
"name": "Internal State Logging Workflow",
|
||||
"createdAt": "2026-09-13T20:00:00+00:00",
|
||||
"version": 1,
|
||||
"toolVersion": "3.9.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {
|
||||
"Elsa:WorkflowContextProviderTypes": [],
|
||||
"logPersistenceConfig": {
|
||||
"default": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Exclude, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"internalState": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Include, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "e5c32d9f0b7e4168",
|
||||
"nodeId": "Workflow1:e5c32d9f0b7e4168",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Inherits workflow internal state include"
|
||||
}
|
||||
},
|
||||
"id": "c13e5a9d7f426b30",
|
||||
"nodeId": "Workflow1:e5c32d9f0b7e4168:c13e5a9d7f426b30",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Overrides workflow internal state to exclude"
|
||||
}
|
||||
},
|
||||
"id": "d24f6b0e8a537c41",
|
||||
"nodeId": "Workflow1:e5c32d9f0b7e4168:d24f6b0e8a537c41",
|
||||
"name": "WriteLine2",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"logPersistenceConfig": {
|
||||
"internalState": {
|
||||
"evaluationMode": "Strategy",
|
||||
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Exclude, Elsa.Workflows.Core",
|
||||
"expression": null
|
||||
},
|
||||
"inputs": {},
|
||||
"outputs": {}
|
||||
},
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 220,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "c13e5a9d7f426b30",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "d24f6b0e8a537c41",
|
||||
"port": "In"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.LogPersistence;
|
||||
using Elsa.Workflows.LogPersistence.Strategies;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.UnitTests.LogPersistence;
|
||||
|
||||
public class ActivityPropertyLogPersistenceEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Evaluate_HonorsActivityInternalStateInclude_WhenDefaultIsExclude()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude), internalState: typeof(Include));
|
||||
|
||||
var map = await EvaluateAsync(activity);
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Include, map.InternalState);
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.Inputs[nameof(WriteLine.Text)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_HonorsActivityInternalStateExclude_WhenDefaultIsInclude()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Include), internalState: typeof(Exclude));
|
||||
|
||||
var map = await EvaluateAsync(activity);
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.InternalState);
|
||||
Assert.Equal(LogPersistenceMode.Include, map.Inputs[nameof(WriteLine.Text)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_FallsBackToWorkflowInternalState_WhenActivityInternalStateIsMissing()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude));
|
||||
|
||||
var map = await EvaluateAsync(activity, workflow =>
|
||||
{
|
||||
workflow.CustomProperties["logPersistenceConfig"] = new Dictionary<string, object>
|
||||
{
|
||||
["default"] = Strategy(typeof(Exclude)),
|
||||
["internalState"] = Strategy(typeof(Include))
|
||||
};
|
||||
});
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Include, map.InternalState);
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.Inputs[nameof(WriteLine.Text)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_ActivityInternalStateOverridesWorkflowInternalState()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude), internalState: typeof(Exclude));
|
||||
|
||||
var map = await EvaluateAsync(activity, workflow =>
|
||||
{
|
||||
workflow.CustomProperties["logPersistenceConfig"] = new Dictionary<string, object>
|
||||
{
|
||||
["default"] = Strategy(typeof(Exclude)),
|
||||
["internalState"] = Strategy(typeof(Include))
|
||||
};
|
||||
});
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.InternalState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_FallsBackToDefault_WhenInternalStateIsMissing()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude));
|
||||
|
||||
var map = await EvaluateAsync(activity);
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.InternalState);
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.Inputs[nameof(WriteLine.Text)]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_IgnoresTopLevelCustomPropertiesInternalState()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude));
|
||||
activity.CustomProperties["internalState"] = Strategy(typeof(Include));
|
||||
|
||||
var map = await EvaluateAsync(activity);
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Exclude, map.InternalState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Evaluate_UsesWorkflowRootContext_ForWorkflowInternalStateExpression()
|
||||
{
|
||||
var activity = CreateWriteLine(defaultMode: typeof(Exclude));
|
||||
var fixture = CreateFixture(activity);
|
||||
var seed = await fixture.BuildAsync();
|
||||
var workflowExecutionContext = seed.WorkflowExecutionContext;
|
||||
var rootContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(workflowExecutionContext.Workflow);
|
||||
var childContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity, new ActivityInvocationOptions
|
||||
{
|
||||
Owner = rootContext
|
||||
});
|
||||
workflowExecutionContext.AddActivityExecutionContext(rootContext);
|
||||
workflowExecutionContext.AddActivityExecutionContext(childContext);
|
||||
|
||||
workflowExecutionContext.Workflow.CustomProperties["logPersistenceConfig"] = new Dictionary<string, object>
|
||||
{
|
||||
["internalState"] = new LogPersistenceConfiguration
|
||||
{
|
||||
EvaluationMode = LogPersistenceEvaluationMode.Expression,
|
||||
Expression = Expression.DelegateExpression(ctx =>
|
||||
{
|
||||
ctx.TryGetActivityExecutionContext(out var evaluated);
|
||||
return evaluated.ParentActivityExecutionContext == null
|
||||
? LogPersistenceMode.Include
|
||||
: LogPersistenceMode.Exclude;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var map = await CreateEvaluator(childContext).EvaluateLogPersistenceModesAsync(childContext);
|
||||
|
||||
Assert.Equal(LogPersistenceMode.Include, map.InternalState);
|
||||
}
|
||||
|
||||
private static WriteLine CreateWriteLine(Type defaultMode, Type? internalState = null)
|
||||
{
|
||||
var config = new Dictionary<string, object>
|
||||
{
|
||||
["default"] = Strategy(defaultMode)
|
||||
};
|
||||
|
||||
if (internalState != null)
|
||||
config["internalState"] = Strategy(internalState);
|
||||
|
||||
var activity = new WriteLine("text");
|
||||
activity.CustomProperties["logPersistenceConfig"] = config;
|
||||
return activity;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> Strategy(Type strategyType) => new()
|
||||
{
|
||||
["evaluationMode"] = "Strategy",
|
||||
["strategyType"] = strategyType.GetSimpleAssemblyQualifiedName()
|
||||
};
|
||||
|
||||
private static async Task<ActivityLogPersistenceModeMap> EvaluateAsync(IActivity activity, Action<Workflow>? configureWorkflow = null)
|
||||
{
|
||||
var fixture = CreateFixture(activity);
|
||||
var context = await fixture.BuildAsync();
|
||||
context.WorkflowExecutionContext.AddActivityExecutionContext(context);
|
||||
configureWorkflow?.Invoke(context.WorkflowExecutionContext.Workflow);
|
||||
return await CreateEvaluator(context).EvaluateLogPersistenceModesAsync(context);
|
||||
}
|
||||
|
||||
private static ActivityTestFixture CreateFixture(IActivity activity) =>
|
||||
new ActivityTestFixture(activity).ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILogPersistenceStrategy, Include>();
|
||||
services.AddSingleton<ILogPersistenceStrategy, Exclude>();
|
||||
services.AddSingleton<ILogPersistenceStrategy, Inherit>();
|
||||
services.AddSingleton<ILogPersistenceStrategyService, DefaultLogPersistenceStrategyService>();
|
||||
});
|
||||
|
||||
private static ActivityPropertyLogPersistenceEvaluator CreateEvaluator(ActivityExecutionContext context) =>
|
||||
new(
|
||||
context.GetRequiredService<ILogPersistenceStrategyService>(),
|
||||
context.GetRequiredService<IExpressionDescriptorRegistry>(),
|
||||
context.GetRequiredService<IExpressionEvaluator>(),
|
||||
Microsoft.Extensions.Options.Options.Create(new ManagementOptions
|
||||
{
|
||||
LogPersistenceMode = LogPersistenceMode.Include
|
||||
}),
|
||||
NullLogger<ActivityPropertyLogPersistenceEvaluator>.Instance);
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.LogPersistence;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
|
||||
|
|
@ -59,4 +61,73 @@ public class DefaultActivityExecutionMapperTests
|
|||
Assert.Equal("context-b", record.SchedulingActivityExecutionId);
|
||||
Assert.Equal("activity-b", record.SchedulingActivityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_IncludesPropertiesAndPayload_WhenInternalStateIsInclude_EvenIfInputsAreExcluded()
|
||||
{
|
||||
var record = await MapWithPersistenceAsync(
|
||||
LogPersistenceMode.Include,
|
||||
inputs: LogPersistenceMode.Exclude);
|
||||
|
||||
Assert.NotNull(record.Properties);
|
||||
Assert.Equal("property-value", record.Properties["InternalKey"]);
|
||||
Assert.NotNull(record.Payload);
|
||||
Assert.Equal("journal-value", record.Payload["JournalKey"]);
|
||||
Assert.False(record.ActivityState?.ContainsKey("Text"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_ExcludesPropertiesAndPayload_WhenInternalStateIsExclude_EvenIfInputsAreIncluded()
|
||||
{
|
||||
var record = await MapWithPersistenceAsync(
|
||||
LogPersistenceMode.Exclude,
|
||||
inputs: LogPersistenceMode.Include);
|
||||
|
||||
Assert.Null(record.Properties);
|
||||
Assert.Null(record.Payload);
|
||||
Assert.True(record.ActivityState?.ContainsKey("Text"));
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionRecord> MapWithPersistenceAsync(
|
||||
LogPersistenceMode internalState,
|
||||
LogPersistenceMode inputs)
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
var activity = new WriteLine("Test");
|
||||
var context = await new ActivityTestFixture(activity).BuildAsync();
|
||||
|
||||
context.ActivityState["Text"] = "Test";
|
||||
context.Properties["InternalKey"] = "property-value";
|
||||
context.JournalData["JournalKey"] = "journal-value";
|
||||
context.SetLogPersistenceModeMap(new ActivityLogPersistenceModeMap
|
||||
{
|
||||
InternalState = internalState,
|
||||
Inputs = { [nameof(WriteLine.Text)] = inputs }
|
||||
});
|
||||
|
||||
return await mapper.MapAsync(context);
|
||||
}
|
||||
|
||||
private static DefaultActivityExecutionMapper CreateMapper()
|
||||
{
|
||||
var safeSerializer = Substitute.For<ISafeSerializer>();
|
||||
safeSerializer.Serialize(Arg.Any<object>()).Returns("serialized");
|
||||
|
||||
var payloadSerializer = Substitute.For<IPayloadSerializer>();
|
||||
payloadSerializer.Serialize(Arg.Any<object>()).Returns("serialized");
|
||||
|
||||
var compressionCodecResolver = Substitute.For<ICompressionCodecResolver>();
|
||||
var compressionCodec = Substitute.For<ICompressionCodec>();
|
||||
compressionCodecResolver.Resolve(Arg.Any<string>()).Returns(compressionCodec);
|
||||
compressionCodec.CompressAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(new ValueTask<string>("compressed"));
|
||||
|
||||
var managementOptions = Substitute.For<IOptions<ManagementOptions>>();
|
||||
managementOptions.Value.Returns(new ManagementOptions());
|
||||
|
||||
return new DefaultActivityExecutionMapper(
|
||||
safeSerializer,
|
||||
payloadSerializer,
|
||||
compressionCodecResolver,
|
||||
managementOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue