From d4c00f13ed397165c4e06e1ee3e92db4b316808b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 13 Sep 2026 23:09:36 +0200 Subject: [PATCH] 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 * 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 * 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 --------- Co-authored-by: Cursor Agent --- ...ActivityPropertyLogPersistenceEvaluator.cs | 41 +++- .../Elsa.Workflows.ComponentTests.csproj | 6 + .../InternalStateLoggingTests.cs | 68 +++++++ .../internal-state-logging-activity.json | 129 +++++++++++++ .../internal-state-logging-workflow.json | 122 ++++++++++++ ...ityPropertyLogPersistenceEvaluatorTests.cs | 180 ++++++++++++++++++ .../DefaultActivityExecutionMapperTests.cs | 71 +++++++ 7 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/InternalStateLoggingTests.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-activity.json create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-workflow.json create mode 100644 test/unit/Elsa.Workflows.Runtime.UnitTests/LogPersistence/ActivityPropertyLogPersistenceEvaluatorTests.cs diff --git a/src/modules/Elsa.Workflows.Runtime/LogPersistence/Services/ActivityPropertyLogPersistenceEvaluator.cs b/src/modules/Elsa.Workflows.Runtime/LogPersistence/Services/ActivityPropertyLogPersistenceEvaluator.cs index 2113e290b..81876ca7a 100644 --- a/src/modules/Elsa.Workflows.Runtime/LogPersistence/Services/ActivityPropertyLogPersistenceEvaluator.cs +++ b/src/modules/Elsa.Workflows.Runtime/LogPersistence/Services/ActivityPropertyLogPersistenceEvaluator.cs @@ -69,12 +69,18 @@ public class ActivityPropertyLogPersistenceEvaluator : IActivityPropertyLogPersi public async Task 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> 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 legacyProps, IDictionary configProps, LogPersistenceMode defaultMode)> GetPersistenceDefaultsAsync(ActivityExecutionContext context, CancellationToken cancellationToken) + private async Task<(IDictionary legacyProps, IDictionary configProps, LogPersistenceMode defaultMode, Workflow workflow, ActivityExecutionContext rootContext)> GetPersistenceDefaultsAsync(ActivityExecutionContext context, CancellationToken cancellationToken) { var legacyProps = context.Activity.CustomProperties.GetValueOrDefault>(LegacyKey, () => new Dictionary())!; 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>(ConfigKey, () => new Dictionary())!; - 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 EvaluateInternalStateModeAsync( - ExpressionExecutionContext executionContext, - IDictionary currentConfig, + ExpressionExecutionContext activityExecutionContext, + ExpressionExecutionContext workflowExecutionContext, + IDictionary activityConfig, + IDictionary workflowProperties, LogPersistenceMode defaultMode, CancellationToken cancellationToken) { - var configObject = currentConfig.GetValueOrDefault("internalState", () => new Dictionary())!; + var workflowConfig = workflowProperties.GetValueOrDefault>(ConfigKey, () => new Dictionary())!; + var workflowInternalState = await EvaluateInternalStateConfigAsync(workflowExecutionContext, workflowConfig, () => defaultMode, cancellationToken); + return await EvaluateInternalStateConfigAsync(activityExecutionContext, activityConfig, () => workflowInternalState, cancellationToken); + } + + private async Task EvaluateInternalStateConfigAsync( + ExpressionExecutionContext executionContext, + IDictionary configProps, + Func 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> GetPersistablePropertiesAsync( diff --git a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj index db0b44d36..0516d100b 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj +++ b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj @@ -54,6 +54,12 @@ Always + + Always + + + Always + Always diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/InternalStateLoggingTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/InternalStateLoggingTests.cs new file mode 100644 index 000000000..0ccff3cd6 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/InternalStateLoggingTests.cs @@ -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 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> ExecuteAndGetWriteLinesAsync(string workflowDefinitionId) + { + var client = WorkflowServer.CreateApiClient(); + using var response = await client.ExecuteAsync(workflowDefinitionId); + var model = await response.ReadAsJsonAsync(WorkflowServer.Services); + var writeLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + var store = Scope.ServiceProvider.GetRequiredService(); + var records = await store.FindManyAsync(new ActivityExecutionRecordFilter + { + WorkflowInstanceId = model.WorkflowState.Id + }); + + return records.Where(x => x.ActivityType == writeLineActivityTypeName).ToList(); + } +} diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-activity.json b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-activity.json new file mode 100644 index 000000000..1bb51e937 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-activity.json @@ -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" + } + } + ] + } +} diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-workflow.json b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-workflow.json new file mode 100644 index 000000000..3b635af22 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/LogPersistenceModes/internal-state-logging-workflow.json @@ -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" + } + } + ] + } +} diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/LogPersistence/ActivityPropertyLogPersistenceEvaluatorTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/LogPersistence/ActivityPropertyLogPersistenceEvaluatorTests.cs new file mode 100644 index 000000000..459eb0d54 --- /dev/null +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/LogPersistence/ActivityPropertyLogPersistenceEvaluatorTests.cs @@ -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 + { + ["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 + { + ["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 + { + ["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 + { + ["default"] = Strategy(defaultMode) + }; + + if (internalState != null) + config["internalState"] = Strategy(internalState); + + var activity = new WriteLine("text"); + activity.CustomProperties["logPersistenceConfig"] = config; + return activity; + } + + private static Dictionary Strategy(Type strategyType) => new() + { + ["evaluationMode"] = "Strategy", + ["strategyType"] = strategyType.GetSimpleAssemblyQualifiedName() + }; + + private static async Task EvaluateAsync(IActivity activity, Action? 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(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + + private static ActivityPropertyLogPersistenceEvaluator CreateEvaluator(ActivityExecutionContext context) => + new( + context.GetRequiredService(), + context.GetRequiredService(), + context.GetRequiredService(), + Microsoft.Extensions.Options.Options.Create(new ManagementOptions + { + LogPersistenceMode = LogPersistenceMode.Include + }), + NullLogger.Instance); +} diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultActivityExecutionMapperTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultActivityExecutionMapperTests.cs index 3d20c75fc..9e850b734 100644 --- a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultActivityExecutionMapperTests.cs +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultActivityExecutionMapperTests.cs @@ -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 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(); + safeSerializer.Serialize(Arg.Any()).Returns("serialized"); + + var payloadSerializer = Substitute.For(); + payloadSerializer.Serialize(Arg.Any()).Returns("serialized"); + + var compressionCodecResolver = Substitute.For(); + var compressionCodec = Substitute.For(); + compressionCodecResolver.Resolve(Arg.Any()).Returns(compressionCodec); + compressionCodec.CompressAsync(Arg.Any(), Arg.Any()).Returns(new ValueTask("compressed")); + + var managementOptions = Substitute.For>(); + managementOptions.Value.Returns(new ManagementOptions()); + + return new DefaultActivityExecutionMapper( + safeSerializer, + payloadSerializer, + compressionCodecResolver, + managementOptions); + } }