From 5e137e634c4daa5029594591f76daeec063817c2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 15 May 2026 22:20:38 -0500 Subject: [PATCH 01/17] Fix for the workflow propagation integration tests not working as expected Signed-off-by: Whit Waldo --- .../HistoryPropagationWorkflowTests.cs | 178 +++++++++++++----- 1 file changed, 127 insertions(+), 51 deletions(-) diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index 82acce252..6f275e60b 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -11,17 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Testing; using Dapr.Testcontainers.Harnesses; using Dapr.Testcontainers.Xunit.Attributes; using Dapr.Workflow; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Dapr.IntegrationTest.Workflow; @@ -49,12 +44,34 @@ public sealed class HistoryPropagationWorkflowTests public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() { var instanceId = Guid.NewGuid().ToString(); - await using var testApp = await BuildTestAppAsync( - opt => + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }); + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); using var scope = testApp.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); @@ -79,14 +96,36 @@ public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() { var instanceId = Guid.NewGuid().ToString(); - await using var testApp = await BuildTestAppAsync( - opt => + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }); + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterActivity(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); using var scope = testApp.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); @@ -106,13 +145,35 @@ public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() { var instanceId = Guid.NewGuid().ToString(); - await using var testApp = await BuildTestAppAsync( - opt => + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }); + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); using var scope = testApp.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); @@ -132,12 +193,34 @@ public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() { var instanceId = Guid.NewGuid().ToString(); - await using var testApp = await BuildTestAppAsync( - opt => + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }); + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); using var scope = testApp.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); @@ -160,28 +243,9 @@ public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() { var instanceId = Guid.NewGuid().ToString(); - await using var testApp = await BuildTestAppAsync( - opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - } - - private static async Task BuildTestAppAsync(Action configureRuntime) - { var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( needsActorState: true, cancellationToken: TestContext.Current.CancellationToken); await environment.StartAsync(TestContext.Current.CancellationToken); @@ -189,12 +253,15 @@ private static async Task BuildTestAppAsync(Action { builder.Services.AddDaprWorkflowBuilder( - configureRuntime: configureRuntime, + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, configureClient: (sp, clientBuilder) => { var config = sp.GetRequiredService(); @@ -204,6 +271,15 @@ private static async Task BuildTestAppAsync(Action(); + + await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); } private sealed record PropagationTestResult( From edcc53eb4578974ba5a68dc89a706bb988873e9a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 16 May 2026 14:01:03 -0500 Subject: [PATCH 02/17] Forcing a round trip in tests to properly record completion event Signed-off-by: Whit Waldo --- .../HistoryPropagationWorkflowTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index 6f275e60b..c8c1655e9 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -386,13 +386,18 @@ public override async Task RunAsync(WorkflowContext context, object? input /// private sealed class PropagatedHistoryReceiver : Workflow { - public override Task RunAsync(WorkflowContext context, object? input) + public override async Task RunAsync(WorkflowContext context, object? input) { var propagated = context.GetPropagatedHistory(); - var result = new PropagationTestResult( + // Yield via a zero-duration timer so this sub-orchestration completes through + // a proper async round-trip with the sidecar rather than synchronously on its + // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous + // first-turn completion causes the parent to hang waiting for the completion + // event that the sidecar never delivers for same-turn completions. + await context.CreateTimer(TimeSpan.Zero); + return new PropagationTestResult( ChildReceivedPropagatedHistory: propagated is not null, PropagatedEntryCount: propagated?.Entries.Count ?? 0); - return Task.FromResult(result); } } From 44f9f1951539ef845a58ba00dd8e9fda621c76d9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 16 May 2026 23:12:19 -0500 Subject: [PATCH 03/17] Fixing timestamp precision issue impacting SDK and causing tests to timeout Signed-off-by: Whit Waldo --- .../Worker/Internal/WorkflowOrchestrationContext.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index d62d9ce25..7a1835208 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -201,6 +201,19 @@ private async Task CallActivityInternalAsync(string name, object? input, W /// public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) { + // DateTime has 100ns (tick) precision but google.protobuf.Timestamp has nanosecond + // precision. When OrchestratorStarted.Timestamp is converted to DateTime via + // ToDateTime(), sub-100ns nanoseconds are truncated (e.g. nanos=N becomes nanos=(N/100)*100). + // If fireAt <= _currentUtcDateTime (i.e. zero or negative delay), Timestamp.FromDateTime + // may produce a Timestamp up to 99ns BEFORE OrchestratorStarted.Timestamp. + // Some Dapr runtime versions (e.g. 1.17) validate that CreateTimerAction.fireAt >= + // orchestrationStartTime and reject the CompleteOrchestratorTask call when this is + // violated, causing a silent infinite retry loop. Adding 1 tick (100ns) ensures the + // resulting Timestamp is always strictly after OrchestratorStarted.Timestamp, satisfying + // the runtime's constraint while still providing "fire as soon as possible" semantics. + if (fireAt <= _currentUtcDateTime) + fireAt = _currentUtcDateTime.AddTicks(1); + return CreateTimerInternal( Timestamp.FromDateTime(fireAt), new TimerOriginCreateTimer(), cancellationToken); } From 8935b09901e7863609d710552969ccaa1a1061f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 05:10:03 +0000 Subject: [PATCH 04/17] Add unit tests verifying CreateTimer zero-delay adjustment behavior Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/bf784a02-c358-4b4d-8de1-046dc403f7d8 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../WorkflowOrchestrationContextTests.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index 90bd0a142..64737660d 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -887,6 +887,108 @@ public void IsReplaying_ShouldBeTrue_WhenHistoryHasUnconsumedEvents_AndFalseAfte Assert.False(context.IsReplaying); } + /// + /// Verifies that CreateTimer with a zero (or past) fireAt always emits a timer + /// strictly AFTER the orchestration-started timestamp. Dapr runtimes validate that + /// CreateTimerAction.fireAt >= orchestrationStartTime; passing a timestamp equal to + /// or before the start time causes silent rejection, which hangs the workflow. + /// + [Fact] + public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneTick() + { + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + + var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: startTime, + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + tracker); + + context.InitializeNewTurn(startTime); + + // Simulate CreateTimer(TimeSpan.Zero): fireAt == _currentUtcDateTime + _ = context.CreateTimer(startTime, CancellationToken.None); + + var action = context.PendingActions.Single(); + Assert.NotNull(action.CreateTimer); + + var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); + Assert.True(emittedFireAt > startTime, + $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); + Assert.Equal(startTime.AddTicks(1), emittedFireAt); + } + + /// + /// Verifies that CreateTimer with a past fireAt (before currentUtcDateTime) also + /// emits a timer strictly after the orchestration-started timestamp. + /// + [Fact] + public void CreateTimer_WhenFireAtIsBeforeCurrentUtcDateTime_AdjustsFireAtByOneTick() + { + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + + var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: startTime, + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + tracker); + + context.InitializeNewTurn(startTime); + + // fireAt is in the past relative to startTime + var pastTime = startTime.AddSeconds(-10); + _ = context.CreateTimer(pastTime, CancellationToken.None); + + var action = context.PendingActions.Single(); + Assert.NotNull(action.CreateTimer); + + var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); + Assert.True(emittedFireAt > startTime, + $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); + Assert.Equal(startTime.AddTicks(1), emittedFireAt); + } + + /// + /// Verifies that CreateTimer with a future fireAt is not adjusted. + /// + [Fact] + public void CreateTimer_WhenFireAtIsAfterCurrentUtcDateTime_IsNotAdjusted() + { + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + + var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var futureTime = startTime.AddSeconds(60); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: startTime, + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + tracker); + + context.InitializeNewTurn(startTime); + + _ = context.CreateTimer(futureTime, CancellationToken.None); + + var action = context.PendingActions.Single(); + Assert.NotNull(action.CreateTimer); + + var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); + Assert.Equal(futureTime, emittedFireAt); + } + [Fact] public async Task CreateTimer_ShouldReturnCompletedTask_WhenTimerFiredInHistory() { From 0688edba9f5f7a442b5f0d521287a70e3abe2552 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 05:11:15 +0000 Subject: [PATCH 05/17] Fix HTML entity in XML doc comment Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/bf784a02-c358-4b4d-8de1-046dc403f7d8 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../Worker/Internal/WorkflowOrchestrationContextTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index 64737660d..b36aee4a2 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -890,7 +890,7 @@ public void IsReplaying_ShouldBeTrue_WhenHistoryHasUnconsumedEvents_AndFalseAfte /// /// Verifies that CreateTimer with a zero (or past) fireAt always emits a timer /// strictly AFTER the orchestration-started timestamp. Dapr runtimes validate that - /// CreateTimerAction.fireAt >= orchestrationStartTime; passing a timestamp equal to + /// CreateTimerAction.fireAt >= orchestrationStartTime; passing a timestamp equal to /// or before the start time causes silent rejection, which hangs the workflow. /// [Fact] From d16cb5af7386f5b36aaf655a112817061d61f8fa Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 17 May 2026 00:56:02 -0500 Subject: [PATCH 06/17] Fix CreateTimer to clamp fire time above current wall-clock time Dapr 1.18+ validates that CreateTimerAction.fireAt is strictly greater than the current sidecar time when CompleteOrchestratorTask is called. The previous fix added only 1 tick (100 ns) over the orchestration start timestamp, which arrives already past-due by the time the sidecar processes the response. GrpcProtocolHandler silently swallows the rejection error, causing the child workflow to enter an infinite replay loop and the integration tests to time out after 30 minutes. Replace the orchestration-timestamp floor with DateTime.UtcNow.AddSeconds(1). This ensures the fire time remains strictly in the future regardless of SDK-to-sidecar network latency or minor clock skew, while still providing "fire as soon as possible" semantics for zero-delay timers. Fixes all five HistoryPropagationWorkflowTests failing on Dapr 1.18 RC. Co-Authored-By: Claude Sonnet 4.6 --- .../Internal/WorkflowOrchestrationContext.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index 7a1835208..cb0f593ad 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -201,18 +201,17 @@ private async Task CallActivityInternalAsync(string name, object? input, W /// public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) { - // DateTime has 100ns (tick) precision but google.protobuf.Timestamp has nanosecond - // precision. When OrchestratorStarted.Timestamp is converted to DateTime via - // ToDateTime(), sub-100ns nanoseconds are truncated (e.g. nanos=N becomes nanos=(N/100)*100). - // If fireAt <= _currentUtcDateTime (i.e. zero or negative delay), Timestamp.FromDateTime - // may produce a Timestamp up to 99ns BEFORE OrchestratorStarted.Timestamp. - // Some Dapr runtime versions (e.g. 1.17) validate that CreateTimerAction.fireAt >= - // orchestrationStartTime and reject the CompleteOrchestratorTask call when this is - // violated, causing a silent infinite retry loop. Adding 1 tick (100ns) ensures the - // resulting Timestamp is always strictly after OrchestratorStarted.Timestamp, satisfying - // the runtime's constraint while still providing "fire as soon as possible" semantics. - if (fireAt <= _currentUtcDateTime) - fireAt = _currentUtcDateTime.AddTicks(1); + // Clamp fireAt to be at least 1 second ahead of the current wall-clock time. + // Dapr 1.18+ validates that CreateTimerAction.fireAt is strictly greater than + // the current sidecar time. Without a positive offset, a zero-delay or past-due + // timer arrives at the sidecar already expired, causing the sidecar to reject + // the CompleteOrchestratorTask call. GrpcProtocolHandler only logs that rejection, + // producing a silent infinite replay loop until the test times out. A 1-second + // floor absorbs SDK-to-sidecar network latency and any minor clock skew between + // hosts, ensuring the fire time is still in the future when the sidecar processes it. + var floor = DateTime.UtcNow.AddSeconds(1); + if (fireAt < floor) + fireAt = floor; return CreateTimerInternal( Timestamp.FromDateTime(fireAt), new TimerOriginCreateTimer(), cancellationToken); From 8122d151fd22539a371639d349e47046a40bbdb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 06:35:08 +0000 Subject: [PATCH 07/17] Adjust non-positive workflow timers to +1ms for runtime compatibility Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/e8ae6d84-6379-4988-8dc3-c176ade33b1d Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../Internal/WorkflowOrchestrationContext.cs | 14 +++++++------- .../Internal/WorkflowOrchestrationContextTests.cs | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index 7a1835208..fe6e1f177 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -205,14 +205,14 @@ public override Task CreateTimer(DateTime fireAt, CancellationToken cancellation // precision. When OrchestratorStarted.Timestamp is converted to DateTime via // ToDateTime(), sub-100ns nanoseconds are truncated (e.g. nanos=N becomes nanos=(N/100)*100). // If fireAt <= _currentUtcDateTime (i.e. zero or negative delay), Timestamp.FromDateTime - // may produce a Timestamp up to 99ns BEFORE OrchestratorStarted.Timestamp. - // Some Dapr runtime versions (e.g. 1.17) validate that CreateTimerAction.fireAt >= - // orchestrationStartTime and reject the CompleteOrchestratorTask call when this is - // violated, causing a silent infinite retry loop. Adding 1 tick (100ns) ensures the - // resulting Timestamp is always strictly after OrchestratorStarted.Timestamp, satisfying - // the runtime's constraint while still providing "fire as soon as possible" semantics. + // may produce a Timestamp before OrchestratorStarted.Timestamp. + // + // Some Dapr runtime versions validate that CreateTimerAction.fireAt >= orchestrationStartTime + // and can reject near-zero timers. Shift non-positive timers to +1ms so the timer is + // safely in the future across runtime timestamp precision/rounding behavior while keeping + // "fire as soon as possible" semantics. if (fireAt <= _currentUtcDateTime) - fireAt = _currentUtcDateTime.AddTicks(1); + fireAt = _currentUtcDateTime.AddMilliseconds(1); return CreateTimerInternal( Timestamp.FromDateTime(fireAt), new TimerOriginCreateTimer(), cancellationToken); diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index b36aee4a2..04690e2dd 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -894,7 +894,7 @@ public void IsReplaying_ShouldBeTrue_WhenHistoryHasUnconsumedEvents_AndFalseAfte /// or before the start time causes silent rejection, which hangs the workflow. /// [Fact] - public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneTick() + public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneMillisecond() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -920,7 +920,7 @@ public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneTic var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); Assert.True(emittedFireAt > startTime, $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); - Assert.Equal(startTime.AddTicks(1), emittedFireAt); + Assert.Equal(startTime.AddMilliseconds(1), emittedFireAt); } /// @@ -928,7 +928,7 @@ public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneTic /// emits a timer strictly after the orchestration-started timestamp. /// [Fact] - public void CreateTimer_WhenFireAtIsBeforeCurrentUtcDateTime_AdjustsFireAtByOneTick() + public void CreateTimer_WhenFireAtIsBeforeCurrentUtcDateTime_AdjustsFireAtByOneMillisecond() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -955,7 +955,7 @@ public void CreateTimer_WhenFireAtIsBeforeCurrentUtcDateTime_AdjustsFireAtByOneT var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); Assert.True(emittedFireAt > startTime, $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); - Assert.Equal(startTime.AddTicks(1), emittedFireAt); + Assert.Equal(startTime.AddMilliseconds(1), emittedFireAt); } /// From b0b96e2d3d663bbca8c54b45e11f5ec716ec1f11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:35:23 +0000 Subject: [PATCH 08/17] Revert timer clamp; add activity-yield variant of history propagation test Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/b6fcdbac-9f69-4dbe-80d2-b7ceaa04cf5b Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../Internal/WorkflowOrchestrationContext.cs | 13 --- .../HistoryPropagationWorkflowTests.cs | 92 ++++++++++++++++ .../WorkflowOrchestrationContextTests.cs | 102 ------------------ 3 files changed, 92 insertions(+), 115 deletions(-) diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index fe6e1f177..d62d9ce25 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -201,19 +201,6 @@ private async Task CallActivityInternalAsync(string name, object? input, W /// public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) { - // DateTime has 100ns (tick) precision but google.protobuf.Timestamp has nanosecond - // precision. When OrchestratorStarted.Timestamp is converted to DateTime via - // ToDateTime(), sub-100ns nanoseconds are truncated (e.g. nanos=N becomes nanos=(N/100)*100). - // If fireAt <= _currentUtcDateTime (i.e. zero or negative delay), Timestamp.FromDateTime - // may produce a Timestamp before OrchestratorStarted.Timestamp. - // - // Some Dapr runtime versions validate that CreateTimerAction.fireAt >= orchestrationStartTime - // and can reject near-zero timers. Shift non-positive timers to +1ms so the timer is - // safely in the future across runtime timestamp precision/rounding behavior while keeping - // "fire as soon as possible" semantics. - if (fireAt <= _currentUtcDateTime) - fireAt = _currentUtcDateTime.AddMilliseconds(1); - return CreateTimerInternal( Timestamp.FromDateTime(fireAt), new TimerOriginCreateTimer(), cancellationToken); } diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index c8c1655e9..ac342ad1c 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -88,6 +88,61 @@ public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() Assert.Equal(0, output.PropagatedEntryCount); } + /// + /// Variant of where the child workflow + /// yields via an activity round-trip instead of a zero-duration timer. This exercises the same + /// "child sub-orchestration must perform at least one async operation" requirement but through + /// a different awaitable so both yield mechanisms are covered. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ShouldCompleteSuccessfully_WithNoPropagationScope_ChildYieldsViaActivity() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterActivity(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParentWithActivityChild), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + var output = result.ReadOutputAs(); + Assert.NotNull(output); + // No scope set → child should report no propagated history + Assert.False(output.ChildReceivedPropagatedHistory); + Assert.Equal(0, output.PropagatedEntryCount); + } + /// /// Verifies that scheduling a child workflow with /// does not produce any errors and both workflows complete. @@ -401,6 +456,43 @@ public override async Task RunAsync(WorkflowContext conte } } + /// + /// Variant of that yields via an activity round-trip + /// instead of a zero-duration timer. Used to verify that the activity-based async yield works + /// equivalently for satisfying the "sub-orchestration must perform at least one async operation" + /// requirement. + /// + private sealed class PropagatedHistoryReceiverWithActivity : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var propagated = context.GetPropagatedHistory(); + // Yield via an activity round-trip so this sub-orchestration completes through + // a proper async round-trip with the sidecar rather than synchronously on its + // first turn. + await context.CallActivityAsync(nameof(EchoActivity), "yield"); + return new PropagationTestResult( + ChildReceivedPropagatedHistory: propagated is not null, + PropagatedEntryCount: propagated?.Entries.Count ?? 0); + } + } + + /// + /// Parent workflow paired with . Schedules + /// the activity-yielding child with no propagation scope (default). + /// + private sealed class NoPropagationParentWithActivityChild : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var childId = $"{context.InstanceId}-child"; + return await context.CallChildWorkflowAsync( + nameof(PropagatedHistoryReceiverWithActivity), + input: null, + options: new ChildWorkflowTaskOptions(InstanceId: childId)); + } + } + private sealed class EchoActivity : WorkflowActivity { public override Task RunAsync(WorkflowActivityContext context, string input) diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index 04690e2dd..90bd0a142 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -887,108 +887,6 @@ public void IsReplaying_ShouldBeTrue_WhenHistoryHasUnconsumedEvents_AndFalseAfte Assert.False(context.IsReplaying); } - /// - /// Verifies that CreateTimer with a zero (or past) fireAt always emits a timer - /// strictly AFTER the orchestration-started timestamp. Dapr runtimes validate that - /// CreateTimerAction.fireAt >= orchestrationStartTime; passing a timestamp equal to - /// or before the start time causes silent rejection, which hangs the workflow. - /// - [Fact] - public void CreateTimer_WhenFireAtEqualsCurrentUtcDateTime_AdjustsFireAtByOneMillisecond() - { - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var tracker = new WorkflowVersionTracker([]); - - var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); - - var context = new WorkflowOrchestrationContext( - name: "wf", - instanceId: "i", - currentUtcDateTime: startTime, - workflowSerializer: serializer, - loggerFactory: NullLoggerFactory.Instance, - tracker); - - context.InitializeNewTurn(startTime); - - // Simulate CreateTimer(TimeSpan.Zero): fireAt == _currentUtcDateTime - _ = context.CreateTimer(startTime, CancellationToken.None); - - var action = context.PendingActions.Single(); - Assert.NotNull(action.CreateTimer); - - var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); - Assert.True(emittedFireAt > startTime, - $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); - Assert.Equal(startTime.AddMilliseconds(1), emittedFireAt); - } - - /// - /// Verifies that CreateTimer with a past fireAt (before currentUtcDateTime) also - /// emits a timer strictly after the orchestration-started timestamp. - /// - [Fact] - public void CreateTimer_WhenFireAtIsBeforeCurrentUtcDateTime_AdjustsFireAtByOneMillisecond() - { - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var tracker = new WorkflowVersionTracker([]); - - var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); - - var context = new WorkflowOrchestrationContext( - name: "wf", - instanceId: "i", - currentUtcDateTime: startTime, - workflowSerializer: serializer, - loggerFactory: NullLoggerFactory.Instance, - tracker); - - context.InitializeNewTurn(startTime); - - // fireAt is in the past relative to startTime - var pastTime = startTime.AddSeconds(-10); - _ = context.CreateTimer(pastTime, CancellationToken.None); - - var action = context.PendingActions.Single(); - Assert.NotNull(action.CreateTimer); - - var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); - Assert.True(emittedFireAt > startTime, - $"Expected fireAt ({emittedFireAt:O}) to be strictly after orchestrationStartTime ({startTime:O})"); - Assert.Equal(startTime.AddMilliseconds(1), emittedFireAt); - } - - /// - /// Verifies that CreateTimer with a future fireAt is not adjusted. - /// - [Fact] - public void CreateTimer_WhenFireAtIsAfterCurrentUtcDateTime_IsNotAdjusted() - { - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var tracker = new WorkflowVersionTracker([]); - - var startTime = new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var futureTime = startTime.AddSeconds(60); - - var context = new WorkflowOrchestrationContext( - name: "wf", - instanceId: "i", - currentUtcDateTime: startTime, - workflowSerializer: serializer, - loggerFactory: NullLoggerFactory.Instance, - tracker); - - context.InitializeNewTurn(startTime); - - _ = context.CreateTimer(futureTime, CancellationToken.None); - - var action = context.PendingActions.Single(); - Assert.NotNull(action.CreateTimer); - - var emittedFireAt = action.CreateTimer.FireAt.ToDateTime(); - Assert.Equal(futureTime, emittedFireAt); - } - [Fact] public async Task CreateTimer_ShouldReturnCompletedTask_WhenTimerFiredInHistory() { From 27a27deda1a49083d861ae053410664a14c8e09d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 17 May 2026 22:15:52 -0500 Subject: [PATCH 09/17] Updated workflow prototypes Signed-off-by: Whit Waldo --- .../Dapr.Workflow.Grpc.csproj | 5 + src/Dapr.Workflow.Grpc/attestation.proto | 292 ++++++ src/Dapr.Workflow.Grpc/backend_service.proto | 193 ++++ src/Dapr.Workflow.Grpc/history_events.proto | 304 +++++++ src/Dapr.Workflow.Grpc/orchestration.proto | 115 +++ .../orchestrator_actions.proto | 118 +++ .../orchestrator_service.proto | 845 ++---------------- src/Dapr.Workflow.Grpc/runtime_state.proto | 21 +- 8 files changed, 1128 insertions(+), 765 deletions(-) create mode 100644 src/Dapr.Workflow.Grpc/attestation.proto create mode 100644 src/Dapr.Workflow.Grpc/backend_service.proto create mode 100644 src/Dapr.Workflow.Grpc/history_events.proto create mode 100644 src/Dapr.Workflow.Grpc/orchestration.proto create mode 100644 src/Dapr.Workflow.Grpc/orchestrator_actions.proto diff --git a/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj b/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj index bfd5b76f2..79597b2a5 100644 --- a/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj +++ b/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj @@ -20,6 +20,11 @@ + + + + + diff --git a/src/Dapr.Workflow.Grpc/attestation.proto b/src/Dapr.Workflow.Grpc/attestation.proto new file mode 100644 index 000000000..dd3030597 --- /dev/null +++ b/src/Dapr.Workflow.Grpc/attestation.proto @@ -0,0 +1,292 @@ +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +option csharp_namespace = "Dapr.DurableTask.Protobuf"; +option java_package = "io.dapr.durabletask.implementation.protobuf"; +option go_package = "/api/protos"; + +// ============================================================================ +// Cross-workflow completion attestations +// ============================================================================ +// +// An attestation is a compact, portable proof that a child workflow or +// activity task executed a specific invocation, was executed by a specific +// SPIFFE identity, reached a specific terminal state, and produced a +// specific (input, output) pair. Attestations let sibling or downstream +// workflows verify these claims without trusting the parent that forwarded +// them. +// +// Each attestation is a (payload, signature) wrapper. The payload is a +// deterministically serialized protobuf encoding of the inner ...Payload +// message (same approach as HistorySignature in backend_service.proto), +// produced once by the signer and thereafter treated as opaque bytes. +// Receivers, storage layers, and verifiers never re-marshal the payload; +// signatures are validated against the exact bytes the signer produced. +// Cross-language interoperability does not depend on protobuf having a +// universal canonical encoding (it does not): the bytes that get signed +// are the bytes that get verified, and they are never reconstructed by +// any peer. This makes the inner payload proto safely evolvable: old +// attestations with older field sets remain verifiable because their +// stored bytes are preserved. +// +// The signature covers sha256(payload). Algorithm is determined by the +// signer certificate's key type (Ed25519, ECDSA, RSA), same rules as +// HistorySignature.signature in backend_service.proto. + +// ============================================================================ +// Canonical byte serialization for ioDigest +// ============================================================================ +// +// ioDigest fields commit to an invocation's input and output. To stay stable +// across protobuf versions, implementations, and languages, the bytes fed +// into the digest are defined directly in terms of user-visible data — the +// protobuf marshaler's output is never used. +// +// The digest is: +// sha256( u64be(len(inputBytes)) || inputBytes || +// u64be(len(outputBytes)) || outputBytes ) +// where inputBytes and outputBytes come from the rules below. Length +// prefixes are big-endian uint64 to prevent concatenation ambiguity (same +// pattern as HistorySignature.eventsDigest). +// +// --- String normalization --- +// All UTF-8 string bytes used below are first normalized to Unicode +// Normalization Form C (NFC). Different-language SDKs (Go, .NET, Python, +// Java, JS) may default to different Unicode normalization forms; NFC is +// the web-standard canonical form and ensures that semantically equal +// strings produce identical bytes regardless of which SDK emitted them. +// Verifiers MUST NFC-normalize before hashing to compare against a +// signer-produced digest. +// +// --- Input bytes (child workflows and activities) --- +// Inputs are carried as google.protobuf.StringValue in the source event. +// Canonical bytes: +// wrapper unset: zero-length +// wrapper set: nfc_utf8(string_value.value) +// The StringValue envelope is not included — only the NFC-normalized +// UTF-8 content of the value field. +// +// --- Output bytes, COMPLETED (child and activity) --- +// Same rule as input: NFC-normalized UTF-8 bytes of the result +// StringValue's value field, or zero-length if unset. +// +// --- Output bytes, FAILED (child and activity) --- +// Spec-defined canonical serialization of TaskFailureDetails, independent +// of protobuf wire format: +// +// u32be(len(errorType)) || nfc_utf8(errorType) +// u32be(len(errorMessage)) || nfc_utf8(errorMessage) +// u32be(len(stackTrace)) || nfc_utf8(stackTrace) // StringValue.value, +// // zero-length if unset +// u8(0 if innerFailure is unset, 1 if set) +// if innerFailure is set: +// +// +// Fields appear in this fixed order regardless of proto field numbers. +// Unset string fields serialize as zero-length. No protobuf tags, varints, +// or envelopes are emitted. The TaskFailureDetails.isNonRetriable field is +// intentionally excluded — it is a framework retry-policy hint, not a +// description of the failure, and committing to it would couple attestation +// verification to retry-semantics evolution with no security benefit. +// +// The innerFailure recursion depth is capped (implementation-defined, at +// least 32 levels). Chains exceeding the cap are treated as malformed: +// the signer refuses to produce an attestation and the verifier refuses +// to verify one. This bounds the work performed on attacker-controlled +// input. +// +// --- Versioning --- +// The canonicalSpecVersion field on each payload identifies which revision +// of the rules above produced the attestation's ioDigest. Verifiers that +// don't recognize the value reject the attestation rather than risk a +// silent digest mismatch. Current value: 1. When TaskFailureDetails or +// another canonicalized input grows an attestation-relevant field, the +// spec is revised and canonicalSpecVersion is incremented — the rules +// above are never silently changed. +// +// --- Certificate validity --- +// Verifiers check that the signer certificate is valid at a specific +// point in time. The correct choice of time depends on whether the +// attestation is being verified at ingestion or from stored history: +// +// * Ingestion (parent absorbs an inbound attestation from a child/ +// activity): use a trusted wallclock (time.Now()). The enclosing +// HistoryEvent.timestamp is set by the sender and is not yet covered +// by a signature at this point, so it cannot be trusted. Wallclock +// gives "is the cert still valid right now" which is the right +// freshness guarantee at the trust boundary. +// +// * Stored / propagated history (re-verification after the enclosing +// event has been signed): use the enclosing HistoryEvent.timestamp. +// Once the event is covered by a HistorySignature, the timestamp is +// tamper-evident and provides a stable historical point-in-time for +// cert validity — the cert was valid at signing time, even if it has +// since expired. This mirrors how HistorySignature itself is checked +// against the last event in its signed range. + +// Terminal state of a child workflow at the moment of attestation. +enum TerminalStatus { + TERMINAL_STATUS_UNSPECIFIED = 0; + TERMINAL_STATUS_COMPLETED = 1; + TERMINAL_STATUS_FAILED = 2; +} + +// Terminal state of an activity task at the moment of attestation. +// Activities have no "terminate" operation, so the space is smaller than +// TerminalStatus. +enum ActivityTerminalStatus { + ACTIVITY_TERMINAL_STATUS_UNSPECIFIED = 0; + ACTIVITY_TERMINAL_STATUS_COMPLETED = 1; + ACTIVITY_TERMINAL_STATUS_FAILED = 2; +} + +// Inner signed payload for a child workflow completion attestation. The +// deterministically serialized form of this message is what the signer +// signs over and what receivers verify against; the bytes are produced +// once and never re-marshaled. +message ChildCompletionAttestationPayload { + // Parent workflow instance ID. Binds the attestation to a single parent + // run, preventing replay by other instances of the same parent workflow + // that share a signing key. + string parentInstanceId = 1; + + // taskScheduledId from the parent's ChildWorkflowInstanceCreatedEvent. + // Unique within the parent instance; distinguishes multiple invocations + // of the same child workflow. + int32 parentTaskScheduledId = 2; + + // sha256 commitment to this invocation's input and output. The bytes + // fed into the digest are produced by the canonical byte serialization + // spec at the top of this file — not by any protobuf marshaler — so + // the digest is stable across proto versions, implementations, and + // languages. Use terminalStatus to select the output serialization rule + // (COMPLETED or FAILED). + bytes ioDigest = 3; + + // sha256 of the DER-encoded X.509 certificate chain bytes of the + // signer (leaf first, intermediates concatenated; same byte format + // as the `certificate` field of SigningCertificate). Computed directly + // over the DER bytes rather than any protobuf envelope, so the digest + // is stable across protobuf version changes. The certificate itself + // is carried as a companion field on the enclosing event on first + // delivery and stored once in the receiver's external certificate + // table (ext-sigcert-NNNNNN), looked up by this digest. + bytes signerCertDigest = 4; + + // Terminal state of the child workflow at the moment of attestation. + // Signed so that a verifier reading the attestation from propagated + // history can tell whether the child succeeded without relying on the + // enclosing event type (Completed vs Failed), which may not be + // visible or trustworthy when the attestation is inspected in + // isolation. + TerminalStatus terminalStatus = 5; + + // Version of the canonical byte serialization spec used to compute + // ioDigest. See the "Versioning" section of the spec block at the top + // of this file. Verifiers that don't recognize the value reject the + // attestation rather than risk a silent digest mismatch. Current + // value: 1. + uint32 canonicalSpecVersion = 6; +} + +// Signed wrapper around ChildCompletionAttestationPayload. +message ChildCompletionAttestation { + // Deterministically serialized form of ChildCompletionAttestationPayload + // produced once by the signer. Opaque bytes thereafter; receivers, + // storage layers, and verifiers never re-marshal. + bytes payload = 1; + + // Cryptographic signature over sha256(payload) using the private key + // corresponding to the certificate whose digest is in the payload's + // signerCertDigest field. Signature format follows the same rules as + // HistorySignature.signature. + bytes signature = 2; +} + +// Inner signed payload for an activity completion attestation. Activities +// have no signed history chain of their own (unlike child workflows), so +// there is no finalSignatureDigest field. Activity identity is the hosting +// app's SPIFFE identity; a compromised app can attest only to activities +// it hosts, not to activities hosted on other apps. +message ActivityCompletionAttestationPayload { + // Parent workflow instance ID that scheduled the activity. + string parentInstanceId = 1; + + // taskScheduledId from the parent's TaskScheduledEvent. Unique within + // the parent instance. + int32 parentTaskScheduledId = 2; + + // Activity name from the parent's TaskScheduledEvent. Explicit because + // no separate creation event binds it in the parent's history the way + // ChildWorkflowInstanceCreatedEvent does for child workflows. + string activityName = 3; + + // sha256 commitment to this invocation's input and output. See the + // canonical byte serialization spec at the top of this file. Use + // terminalStatus (ACTIVITY_TERMINAL_STATUS_COMPLETED or _FAILED) to + // select the output serialization rule. + bytes ioDigest = 4; + + // sha256 of the DER-encoded X.509 certificate chain bytes of the + // activity executor's signer. Same semantics and storage behavior as + // ChildCompletionAttestationPayload.signerCertDigest. + bytes signerCertDigest = 5; + + // Terminal state of the activity at the moment of attestation. + ActivityTerminalStatus terminalStatus = 6; + + // Version of the canonical byte serialization spec used to compute + // ioDigest. See the "Versioning" section of the spec block at the top + // of this file. Verifiers that don't recognize the value reject the + // attestation rather than risk a silent digest mismatch. Current + // value: 1. + uint32 canonicalSpecVersion = 7; +} + +// Signed wrapper around ActivityCompletionAttestationPayload. +message ActivityCompletionAttestation { + // Deterministically serialized form of + // ActivityCompletionAttestationPayload produced once by the signer. + // Opaque bytes thereafter; receivers, storage layers, and verifiers + // never re-marshal. + bytes payload = 1; + + // Cryptographic signature over sha256(payload). + bytes signature = 2; +} + +// A foreign signer's X.509 certificate; one belonging to another workflow +// instance or activity executor whose attestations this workflow has +// received. Stored once per unique digest and referenced by digest from +// any attestation embedded in history. Stored as individual actor state +// keys: ext-sigcert-000000, ext-sigcert-000001, etc. +// +// Lifecycle mirrors SigningCertificate (monotonically appended within a +// run, cleared on ContinueAsNew and instance purge, tracked by +// BackendWorkflowStateMetadata.externalSigningCertificateLength). Dedup +// within a run is performed by in-memory digest→index lookup built at +// load time. +message ExternalSigningCertificate { + // sha256 of the DER-encoded X.509 certificate chain bytes (the value + // in `certificate` below). Also the primary lookup key used by + // attestations' signerCertDigest fields. Stored explicitly so + // load-time index construction and post-load integrity checks do not + // have to re-hash every entry. + bytes digest = 1; + + // Same byte format as SigningCertificate.certificate: DER-encoded + // X.509 chain, leaf first, intermediates concatenated. + bytes certificate = 2; +} \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/backend_service.proto b/src/Dapr.Workflow.Grpc/backend_service.proto new file mode 100644 index 000000000..f3676e330 --- /dev/null +++ b/src/Dapr.Workflow.Grpc/backend_service.proto @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +syntax = "proto3"; + +package durabletask.protos.backend.v1; + +option csharp_namespace = "Dapr.DurableTask.Protobuf"; +option java_package = "io.dapr.durabletask.implementation.protobuf"; +option go_package = "/api/protos"; + +import "orchestration.proto"; +import "history_events.proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +// Request payload for adding new workflow events. +message AddEventRequest { + // The ID of the workflow to send an event to. + WorkflowInstance instance = 1; + // The event to send to the workflow. + HistoryEvent event = 2; +} + +// Response payload for adding new workflow events. +message AddEventResponse { + // No fields +} + +// Request payload for completing an activity work item. +message CompleteActivityWorkItemRequest { + // The completion token that was provided when the work item was fetched. + string completionToken = 1; + + // The response event that will be sent to the workflow. + // This must be either a TaskCompleted event or a TaskFailed event. + HistoryEvent responseEvent = 2; +} + +// Response payload for completing an activity work item. +message CompleteActivityWorkItemResponse { + // No fields +} + +// Request payload for completing a workflow work item. +message CompleteWorkflowWorkItemRequest { + // The completion token that was provided when the work item was fetched. + string completionToken = 1; + WorkflowInstance instance = 2; + OrchestrationStatus runtimeStatus = 3; + google.protobuf.StringValue customStatus = 4; + repeated HistoryEvent newHistory = 5; + repeated HistoryEvent newTasks = 6; + repeated HistoryEvent newTimers = 7; + repeated WorkflowMessage newMessages = 8; + + // The number of work item events that were processed by the workflow. + // This field is optional. If not set, the service should assume that the workflow processed all events. + google.protobuf.Int32Value numEventsProcessed = 9; +} + +// Response payload for completing a workflow work item. +message CompleteWorkflowWorkItemResponse { + // No fields +} + +// A message to be delivered to a workflow by the backend. +message WorkflowMessage { + // The ID of the workflow instance to receive the message. + WorkflowInstance instance = 1; + // The event payload to be received by the target workflow. + HistoryEvent event = 2; +} + +message BackendWorkflowState { + repeated HistoryEvent inbox = 1; + repeated HistoryEvent history = 2; + google.protobuf.StringValue customStatus = 3; + uint64 generation = 4; +} + +// ActivityInvocation wraps a TaskScheduled HistoryEvent with optional +// propagated history for delivery to an activity actor. +message ActivityInvocation { + HistoryEvent historyEvent = 1; + + // Propagated history from the calling workflow. + optional PropagatedHistory propagatedHistory = 2; +} + +message CreateWorkflowInstanceRequest { + HistoryEvent startEvent = 1; + reserved 2; + reserved "policy"; + + // Propagated history from the parent workflow. + optional PropagatedHistory propagatedHistory = 3; +} + +message WorkflowMetadata { + string instanceId = 1; + string name = 2; + OrchestrationStatus runtimeStatus = 3; + google.protobuf.Timestamp createdAt = 4; + google.protobuf.Timestamp lastUpdatedAt = 5; + google.protobuf.StringValue input = 6; + google.protobuf.StringValue output = 7; + google.protobuf.StringValue customStatus = 8; + TaskFailureDetails failureDetails = 9; + google.protobuf.Timestamp completedAt = 10; + string parentInstanceId = 11; + optional google.protobuf.StringValue version = 12; + optional google.protobuf.StringValue parentAppId = 13; + optional google.protobuf.Timestamp startedAt = 14; +} + +message BackendWorkflowStateMetadata { + uint64 inboxLength = 1; + uint64 historyLength = 2; + uint64 generation = 3; + + // Number of HistorySignature entries stored (signature-NNNNNN keys). + uint64 signatureLength = 4; + + // Number of SigningCertificate entries stored (sigcert-NNNNNN keys). + uint64 signingCertificateLength = 5; + + // Number of ExternalSigningCertificate entries stored + // (ext-sigcert-NNNNNN keys). Same lifecycle as signingCertificateLength: + // monotonically grows within a run as new foreign signer certificates + // are absorbed from incoming attestations, zeroed on ContinueAsNew, + // cleared on instance purge. Subject to the same maxStateEntries + // tampering bound. + uint64 externalSigningCertificateLength = 6; +} + +// A signing identity's X.509 certificate, stored once and referenced by index +// from HistorySignature entries. This avoids duplicating the certificate +// across every signature from the same identity. Stored as individual actor +// state keys: sigcert-000000, sigcert-000001, etc. +message SigningCertificate { + // X.509 certificate chain of the signing identity. Certificates are + // DER-encoded and concatenated directly in order: leaf first, followed by + // intermediates. Each certificate is a self-delimiting ASN.1 SEQUENCE, + // so the chain can be parsed by reading consecutive DER structures. + bytes certificate = 1; +} + +// Signing metadata for a contiguous range of history events. +// This is metadata-only — it does NOT contain the events themselves. +// Events are stored once in history-NNNNNN keys; this message references them +// by index range and stores only the signing artifacts. +// Stored as individual actor state keys: signature-000000, signature-000001, +// etc. +message HistorySignature { + // Index of the first event covered by this signature (inclusive). + uint64 startEventIndex = 1; + + // Number of events covered by this signature. + uint64 eventCount = 2; + + // SHA-256 digest of the previous HistorySignature message (the entire + // deterministically serialized protobuf message). Absent for the first + // signature in the chain (no predecessor). When computing the signature + // input for the root case, this value is treated as empty (zero-length). + optional bytes previousSignatureDigest = 3; + + // SHA-256 digest over the concatenation of the raw serialized bytes of each + // history event in this range, in order. The bytes are the exact values + // persisted to the state store (one per history-NNNNNN key). + bytes eventsDigest = 4; + + // Index into the SigningCertificate table (sigcert-NNNNNN keys). + // Multiple signatures from the same identity share the same index. + // A new entry is appended only when the certificate rotates. + uint64 certificateIndex = 5; + + // Cryptographic signature over + // SHA-256(previousSignatureDigest || eventsDigest) + // using the private key corresponding to the referenced certificate. + // The algorithm is determined by the certificate's key type: + // Ed25519: raw Ed25519 signature over the input bytes + // ECDSA: fixed-size r||s over SHA-256(input), each component + // zero-padded to the curve byte length + // RSA: PKCS#1 v1.5 with SHA-256 + bytes signature = 6; +} + +message DurableTimer { + HistoryEvent timerEvent = 1; + uint64 generation = 2; +} \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/history_events.proto b/src/Dapr.Workflow.Grpc/history_events.proto new file mode 100644 index 000000000..59ae017df --- /dev/null +++ b/src/Dapr.Workflow.Grpc/history_events.proto @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +syntax = "proto3"; + +option csharp_namespace = "Dapr.DurableTask.Protobuf"; +option java_package = "io.dapr.durabletask.implementation.protobuf"; +option go_package = "/api/protos"; + +import "orchestration.proto"; +import "attestation.proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +message ExecutionStartedEvent { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + WorkflowInstance workflowInstance = 4; + ParentInstanceInfo parentInstance = 5; + google.protobuf.Timestamp scheduledStartTimestamp = 6; + TraceContext parentTraceContext = 7; + google.protobuf.StringValue workflowSpanID = 8; + map tags = 9; +} + +message ExecutionCompletedEvent { + OrchestrationStatus workflowStatus = 1; + google.protobuf.StringValue result = 2; + TaskFailureDetails failureDetails = 3; +} + +message ExecutionTerminatedEvent { + google.protobuf.StringValue input = 1; + bool recurse = 2; +} + +message TaskScheduledEvent { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + TraceContext parentTraceContext = 4; + string taskExecutionId = 5; + + // If defined, indicates that this task was the starting point of a new + // workflow execution as the result of a rerun operation. + optional RerunParentInstanceInfo rerunParentInstanceInfo = 6; + + // History propagation scope used when this task was originally scheduled. + // Persisted on the event so rerun can re-issue the task with the same + // scope after the action has been discarded. + optional HistoryPropagationScope historyPropagationScope = 7; +} + +message TaskCompletedEvent { + int32 taskScheduledId = 1; + google.protobuf.StringValue result = 2; + string taskExecutionId = 3; + + // Attestation signed by the activity executor's SPIFFE identity. + // Present when the activity was executed under a signing-enabled + // configuration. Verified on inbox ingestion against the companion + // signerCertificate and preserved in stored history for future audit + // and forwarding via provenance bundles. + optional ActivityCompletionAttestation attestation = 4; + + // Companion: DER-encoded X.509 certificate chain of the executor's + // signing identity (leaf first, intermediates concatenated; same + // format as SigningCertificate.certificate in backend_service.proto). + // Wire-only; stripped by the receiver before the event is written to + // history-NNNNNN. The certificate lives once in ext-sigcert-NNNNNN, + // referenced by attestation payload's signerCertDigest. + optional bytes signerCertificate = 5; +} + +message TaskFailedEvent { + int32 taskScheduledId = 1; + TaskFailureDetails failureDetails = 2; + string taskExecutionId = 3; + + // See TaskCompletedEvent.attestation. + optional ActivityCompletionAttestation attestation = 4; + + // Wire-only companion; see TaskCompletedEvent.signerCertificate. + optional bytes signerCertificate = 5; +} + +message ChildWorkflowInstanceCreatedEvent { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + TraceContext parentTraceContext = 5; + + // If defined, indicates that this task was the starting point of a new + // workflow execution as the result of a rerun operation. + optional RerunParentInstanceInfo rerunParentInstanceInfo = 6; + + // History propagation scope used when this child workflow was originally + // scheduled. Persisted on the event so rerun can re-issue the child with + // the same scope after the action has been discarded. + optional HistoryPropagationScope historyPropagationScope = 7; +} + +message ChildWorkflowInstanceCompletedEvent { + int32 taskScheduledId = 1; + google.protobuf.StringValue result = 2; + + // Attestation signed by the completing child workflow's SPIFFE + // identity. Present when the child was executed under a signing-enabled + // configuration. Verified on inbox ingestion against the companion + // signerCertificate and preserved in stored history for future audit + // and forwarding via provenance bundles. + optional ChildCompletionAttestation attestation = 3; + + // Companion: DER-encoded X.509 certificate chain of the child's signing + // identity (leaf first, intermediates concatenated; same format as + // SigningCertificate.certificate in backend_service.proto). Wire-only; + // stripped by the receiver before the event is written to + // history-NNNNNN. The certificate lives once in ext-sigcert-NNNNNN, + // referenced by attestation payload's signerCertDigest. + optional bytes signerCertificate = 4; +} + +message ChildWorkflowInstanceFailedEvent { + int32 taskScheduledId = 1; + TaskFailureDetails failureDetails = 2; + + // See ChildWorkflowInstanceCompletedEvent.attestation. + optional ChildCompletionAttestation attestation = 3; + + // Wire-only companion; see + // ChildWorkflowInstanceCompletedEvent.signerCertificate. + optional bytes signerCertificate = 4; +} + +// DetachedWorkflowInstanceCreatedEvent records that a running workflow +// created a new, detached workflow instance via CreateDetachedWorkflowAction. +// The new workflow has no parent linkage (no completion or failure flows +// back), so this event only stores a pointer to the spawned instance — the +// inputs themselves are consumed directly from the action when scheduling. +// Replay matches on instanceId, so it is the same value the action carried. +message DetachedWorkflowInstanceCreatedEvent { + string instanceId = 1; +} + +// Indicates the timer was created by a createTimer call with no special origin. +message TimerOriginCreateTimer {} + +// Indicates the timer was created as a timeout for a waitForExternalEvent call. +message TimerOriginExternalEvent { + // The name of the external event being waited on, matching EventRaisedEvent.name. + string name = 1; +} + +// Indicates the timer was created as a retry delay for an activity execution. +message TimerOriginActivityRetry { + // The task execution ID of the activity being retried. + string taskExecutionId = 1; +} + +// Indicates the timer was created as a retry delay for a child workflow execution. +message TimerOriginChildWorkflowRetry { + // The instance ID of the workflow being retried. + string instanceId = 1; +} + +message TimerCreatedEvent { + google.protobuf.Timestamp fireAt = 1; + optional string name = 2; + + // If defined, indicates that this task was the starting point of a new + // workflow execution as the result of a rerun operation. + optional RerunParentInstanceInfo rerunParentInstanceInfo = 3; + + // If set, provides additional context attached to this timer. + oneof origin { + TimerOriginCreateTimer createTimer = 4; + TimerOriginExternalEvent externalEvent = 5; + TimerOriginActivityRetry activityRetry = 6; + TimerOriginChildWorkflowRetry childWorkflowRetry = 7; + } +} + +message TimerFiredEvent { + google.protobuf.Timestamp fireAt = 1; + int32 timerId = 2; +} + +message WorkflowStartedEvent { + optional WorkflowVersion version = 1; +} + +message WorkflowCompletedEvent { + // No payload data +} + +message EventSentEvent { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue input = 3; +} + +message EventRaisedEvent { + string name = 1; + google.protobuf.StringValue input = 2; +} + +message ContinueAsNewEvent { + google.protobuf.StringValue input = 1; +} + +message ExecutionSuspendedEvent { + google.protobuf.StringValue input = 1; +} + +message ExecutionResumedEvent { + google.protobuf.StringValue input = 1; +} + +message ExecutionStalledEvent { + StalledReason reason = 1; + optional string description = 2; +} + +message HistoryEvent { + int32 eventId = 1; + google.protobuf.Timestamp timestamp = 2; + oneof eventType { + ExecutionStartedEvent executionStarted = 3; + ExecutionCompletedEvent executionCompleted = 4; + ExecutionTerminatedEvent executionTerminated = 5; + TaskScheduledEvent taskScheduled = 6; + TaskCompletedEvent taskCompleted = 7; + TaskFailedEvent taskFailed = 8; + ChildWorkflowInstanceCreatedEvent childWorkflowInstanceCreated = 9; + ChildWorkflowInstanceCompletedEvent childWorkflowInstanceCompleted = 10; + ChildWorkflowInstanceFailedEvent childWorkflowInstanceFailed = 11; + TimerCreatedEvent timerCreated = 12; + TimerFiredEvent timerFired = 13; + WorkflowStartedEvent workflowStarted = 14; + WorkflowCompletedEvent workflowCompleted = 15; + EventSentEvent eventSent = 16; + EventRaisedEvent eventRaised = 17; + ContinueAsNewEvent continueAsNew = 20; + ExecutionSuspendedEvent executionSuspended = 21; + ExecutionResumedEvent executionResumed = 22; + ExecutionStalledEvent executionStalled = 31; + DetachedWorkflowInstanceCreatedEvent detachedWorkflowInstanceCreated = 32; + } + reserved 18, 19, 23, 24, 25, 26, 27, 28, 29; + optional TaskRouter router = 30; +} + +// A self-contained range of events produced by a single app, used when +// history from multiple workflows is propagated to a downstream workflow +// or activity. Each chunk owns the raw event bytes its producer signed; +// receivers digest those bytes directly and decode them into typed +// HistoryEvents on demand. +message PropagatedHistoryChunk { + // Raw deterministic bytes of each HistoryEvent in this chunk, in execution + // order. The producer marshals each event once and signs over these exact + // bytes; receivers digest them directly and never re-marshal, so chunk + // verification is independent of protobuf marshaler-version stability + // across producer and receiver. This mirrors the approach attestations use + // for ioDigest: signed bytes travel verbatim end-to-end. The chunk's + // length is len(rawEvents). + repeated bytes rawEvents = 1; + + string appId = 2; + + // The workflow instance ID/name that produced the events in this chunk. + string instanceId = 3; + string workflowName = 4; + + // Raw deterministic bytes of each HistorySignature message produced by the + // chunk's app at dispatch time, covering rawEvents in order. Receivers + // unmarshal these on demand to verify the chain. Raw bytes are required + // because HistorySignature.previousSignatureDigest commits to the exact + // persisted serialization; re-marshaling on the wire would break chain + // linkage. See backend_service.proto: HistorySignature. + repeated bytes rawSignatures = 5; + + // X.509 certificate chains of the chunk app's signing identities, + // DER-concatenated leaf-first then intermediates (same encoding as + // backend_service.proto: SigningCertificate.certificate). Each + // HistorySignature in rawSignatures has a certificateIndex that indexes + // into this list, scoped to the chunk's producer app. Raw bytes here avoid + // a circular import on backend_service.proto's SigningCertificate type. + repeated bytes signingCertChains = 6; +} + +message PropagatedHistory { + // The propagation scope that was used to produce this history. + HistoryPropagationScope scope = 1; + + // Per-app history chunks. Each chunk owns the raw event bytes its producer + // signed (PropagatedHistoryChunk.rawEvents); receivers digest those bytes + // directly and decode them into typed HistoryEvents on demand. Chunks are + // ordered, non-overlapping, and together describe the full propagated + // event sequence. + repeated PropagatedHistoryChunk chunks = 2; +} \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/orchestration.proto b/src/Dapr.Workflow.Grpc/orchestration.proto new file mode 100644 index 000000000..c8f954bf0 --- /dev/null +++ b/src/Dapr.Workflow.Grpc/orchestration.proto @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +syntax = "proto3"; + +option csharp_namespace = "Dapr.DurableTask.Protobuf"; +option java_package = "io.dapr.durabletask.implementation.protobuf"; +option go_package = "/api/protos"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +enum StalledReason { + PATCH_MISMATCH = 0; + VERSION_NOT_AVAILABLE = 1; + PAYLOAD_SIZE_EXCEEDED = 2; +} + +enum OrchestrationStatus { + ORCHESTRATION_STATUS_RUNNING = 0; + ORCHESTRATION_STATUS_COMPLETED = 1; + ORCHESTRATION_STATUS_CONTINUED_AS_NEW = 2; + ORCHESTRATION_STATUS_FAILED = 3; + ORCHESTRATION_STATUS_CANCELED = 4; + ORCHESTRATION_STATUS_TERMINATED = 5; + ORCHESTRATION_STATUS_PENDING = 6; + ORCHESTRATION_STATUS_SUSPENDED = 7; + ORCHESTRATION_STATUS_STALLED = 8; +} + +message TaskRouter { + string sourceAppID = 1; + optional string targetAppID = 2; + optional string targetAppNamespace = 3; +} + +message WorkflowVersion { + repeated string patches = 1; + + // The name of the executed workflow + optional string name = 2; +} + +message WorkflowInstance { + string instanceId = 1; + google.protobuf.StringValue executionId = 2; +} + +message TaskFailureDetails { + string errorType = 1; + string errorMessage = 2; + google.protobuf.StringValue stackTrace = 3; + TaskFailureDetails innerFailure = 4; + bool isNonRetriable = 5; +} + +message ParentInstanceInfo { + int32 taskScheduledId = 1; + google.protobuf.StringValue name = 2; + google.protobuf.StringValue version = 3; + WorkflowInstance workflowInstance = 4; + optional string appID = 5; + optional string appNamespace = 6; +} + +// RerunParentInstanceInfo is used to indicate that this workflow was +// started as part of a rerun operation. Contains information about the parent +// workflow instance which was rerun. +message RerunParentInstanceInfo { + // instanceID is the workflow instance ID this workflow has been + // rerun from. + string instanceID = 1; +} + +message TraceContext { + string traceParent = 1; + string spanID = 2 [deprecated=true]; + google.protobuf.StringValue traceState = 3; +} + +// HistoryPropagationScope controls how history is propagated to a child +// workflow or activity +enum HistoryPropagationScope { + // No propagation. This is the default for an unset/missing field; the + // child receives no history from the caller. + HISTORY_PROPAGATION_SCOPE_NONE = 0; + // Propagate the caller's own history events only. The child does + // not see any ancestral history (trust boundary). + HISTORY_PROPAGATION_SCOPE_OWN_HISTORY = 1; + // Propagate the caller's own history events AND the full ancestral + // chain. Any propagated history this workflow received from its + // parent is forwarded to the child. + HISTORY_PROPAGATION_SCOPE_LINEAGE = 2; +} + +message WorkflowState { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + OrchestrationStatus workflowStatus = 4; + google.protobuf.Timestamp scheduledStartTimestamp = 5; + google.protobuf.Timestamp createdTimestamp = 6; + google.protobuf.Timestamp lastUpdatedTimestamp = 7; + google.protobuf.StringValue input = 8; + google.protobuf.StringValue output = 9; + google.protobuf.StringValue customStatus = 10; + TaskFailureDetails failureDetails = 11; + google.protobuf.StringValue executionId = 12; + google.protobuf.Timestamp completedTimestamp = 13; + google.protobuf.StringValue parentInstanceId = 14; + map tags = 15; + google.protobuf.StringValue parentAppId = 16; + optional google.protobuf.Timestamp startedAt = 17; + +} \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/orchestrator_actions.proto b/src/Dapr.Workflow.Grpc/orchestrator_actions.proto new file mode 100644 index 000000000..fedae5631 --- /dev/null +++ b/src/Dapr.Workflow.Grpc/orchestrator_actions.proto @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +syntax = "proto3"; + +option csharp_namespace = "Dapr.DurableTask.Protobuf"; +option java_package = "io.dapr.durabletask.implementation.protobuf"; +option go_package = "/api/protos"; + +import "orchestration.proto"; +import "history_events.proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +message ScheduleTaskAction { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + optional TaskRouter router = 4; + string taskExecutionId = 5; + + // History propagation scope. Absent/SCOPE_NONE = no propagation. + optional HistoryPropagationScope historyPropagationScope = 6; +} + +message CreateChildWorkflowAction { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + optional TaskRouter router = 5; + + // History propagation scope. Absent/SCOPE_NONE = no propagation. + optional HistoryPropagationScope historyPropagationScope = 6; +} + +// CreateDetachedWorkflowAction creates a new, detached workflow instance from +// a running workflow. Mirrors the fields of CreateInstanceRequest (the client +// scheduling API) so the runtime has all the information needed to schedule +// the new instance directly from this action. The spawned workflow is fully +// decoupled from the caller: no parent pointer is recorded on the new +// workflow, no completion is awaited, and no failure propagation flows back. +// The creation is recorded once in the caller's history as a +// DetachedWorkflowInstanceCreatedEvent referencing the new instance ID. +message CreateDetachedWorkflowAction { + // instanceId is the ID assigned to the new workflow. It is mandatory: + // implementors must set a stable, deterministic ID so that on replay the + // call resolves to the same DetachedWorkflowInstanceCreatedEvent in + // history. + string instanceId = 1; + // name of the workflow to schedule. Mandatory. + string name = 2; + + // The remaining fields mirror the optional inputs of + // CreateInstanceRequest. Wrapper types (StringValue) carry presence via + // the wrapper; bare message fields are explicitly marked optional. + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + optional google.protobuf.Timestamp scheduledStartTimestamp = 5; + google.protobuf.StringValue executionId = 6; + map tags = 7; + optional TraceContext parentTraceContext = 8; + optional TaskRouter router = 9; +} + +message CreateTimerAction { + google.protobuf.Timestamp fireAt = 1; + optional string name = 2; + + // If set, provides additional context attached to this timer. + oneof origin { + TimerOriginCreateTimer createTimer = 3; + TimerOriginExternalEvent externalEvent = 4; + TimerOriginActivityRetry activityRetry = 5; + TimerOriginChildWorkflowRetry childWorkflowRetry = 6; + } +} + +message SendEventAction { + WorkflowInstance instance = 1; + string name = 2; + google.protobuf.StringValue data = 3; +} + +message CompleteWorkflowAction { + OrchestrationStatus workflowStatus = 1; + google.protobuf.StringValue result = 2; + google.protobuf.StringValue details = 3; + google.protobuf.StringValue newVersion = 4; + repeated HistoryEvent carryoverEvents = 5; + TaskFailureDetails failureDetails = 6; +} + +message TerminateWorkflowAction { + string instanceId = 1; + google.protobuf.StringValue reason = 2; + bool recurse = 3; +} + +message WorkflowVersionNotAvailableAction { +} + +message WorkflowAction { + int32 id = 1; + oneof workflowActionType { + ScheduleTaskAction scheduleTask = 2; + CreateChildWorkflowAction createChildWorkflow = 3; + CreateTimerAction createTimer = 4; + SendEventAction sendEvent = 5; + CompleteWorkflowAction completeWorkflow = 6; + TerminateWorkflowAction terminateWorkflow = 7; + WorkflowVersionNotAvailableAction workflowVersionNotAvailable = 10; + CreateDetachedWorkflowAction createDetachedWorkflow = 11; + } + reserved 8; + optional TaskRouter router = 9; +} \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/orchestrator_service.proto b/src/Dapr.Workflow.Grpc/orchestrator_service.proto index dafd57c21..d593204b1 100644 --- a/src/Dapr.Workflow.Grpc/orchestrator_service.proto +++ b/src/Dapr.Workflow.Grpc/orchestrator_service.proto @@ -7,68 +7,27 @@ option csharp_namespace = "Dapr.DurableTask.Protobuf"; option java_package = "io.dapr.durabletask.implementation.protobuf"; option go_package = "/api/protos"; +import "orchestration.proto"; +import "history_events.proto"; +import "orchestrator_actions.proto"; + import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/empty.proto"; -enum StalledReason { - PATCH_MISMATCH = 0; - VERSION_NAME_MISMATCH = 1; -} - -// HistoryPropagationScope determines which ancestor workflow history events are -// propagated to a child workflow when it is scheduled. -enum HistoryPropagationScope { - // No history is propagated. This is the default behavior. - HISTORY_PROPAGATION_SCOPE_NONE = 0; - - // Only the calling workflow's own history events are propagated to the child. - // Ancestor history is excluded, acting as a trust boundary. - HISTORY_PROPAGATION_SCOPE_OWN_HISTORY = 1; - - // The calling workflow's history and all ancestor history (the full lineage) is propagated. - HISTORY_PROPAGATION_SCOPE_LINEAGE = 2; -} - -// PropagatedHistorySegment represents the history of a single ancestor workflow instance -// that has been propagated to a child workflow. -message PropagatedHistorySegment { - // The Dapr App ID of the application that ran the ancestor workflow. - string app_id = 1; - // The orchestration instance ID of the ancestor workflow. - string instance_id = 2; - // The name of the ancestor workflow. - string workflow_name = 3; - // The ordered list of history events from the ancestor workflow. - repeated HistoryEvent events = 4; -} - -message TaskRouter { - string sourceAppID = 1; - optional string targetAppID = 2; -} - -message OrchestrationVersion { - repeated string patches = 1; - - // The name of the executed workflow - optional string name = 2; -} - -message OrchestrationInstance { - string instanceId = 1; - google.protobuf.StringValue executionId = 2; -} - message ActivityRequest { string name = 1; google.protobuf.StringValue version = 2; google.protobuf.StringValue input = 3; - OrchestrationInstance orchestrationInstance = 4; + WorkflowInstance workflowInstance = 4; int32 taskId = 5; TraceContext parentTraceContext = 6; string taskExecutionId = 7; + + // Propagated history from the calling workflow. + // Delivered via the work item stream to the SDK, so that the + // activity function can access it via ctx. + optional PropagatedHistory propagatedHistory = 8; } message ActivityResponse { @@ -79,382 +38,32 @@ message ActivityResponse { string completionToken = 5; } -message TaskFailureDetails { - string errorType = 1; - string errorMessage = 2; - google.protobuf.StringValue stackTrace = 3; - TaskFailureDetails innerFailure = 4; - bool isNonRetriable = 5; -} - -enum OrchestrationStatus { - ORCHESTRATION_STATUS_RUNNING = 0; - ORCHESTRATION_STATUS_COMPLETED = 1; - ORCHESTRATION_STATUS_CONTINUED_AS_NEW = 2; - ORCHESTRATION_STATUS_FAILED = 3; - ORCHESTRATION_STATUS_CANCELED = 4; - ORCHESTRATION_STATUS_TERMINATED = 5; - ORCHESTRATION_STATUS_PENDING = 6; - ORCHESTRATION_STATUS_SUSPENDED = 7; - ORCHESTRATION_STATUS_STALLED = 8; -} - -message ParentInstanceInfo { - int32 taskScheduledId = 1; - google.protobuf.StringValue name = 2; - google.protobuf.StringValue version = 3; - OrchestrationInstance orchestrationInstance = 4; - optional string appID = 5; -} - -// RerunParentInstanceInfo is used to indicate that this orchestration was -// started as part of a rerun operation. Contains information about the parent -// orchestration instance which was rerun. -message RerunParentInstanceInfo { - // instanceID is the orchestration instance ID this orchestration has been - // rerun from. - string instanceID = 1; -} - -message TraceContext { - string traceParent = 1; - string spanID = 2 [deprecated=true]; - google.protobuf.StringValue traceState = 3; -} - -message ExecutionStartedEvent { - string name = 1; - google.protobuf.StringValue version = 2; - google.protobuf.StringValue input = 3; - OrchestrationInstance orchestrationInstance = 4; - ParentInstanceInfo parentInstance = 5; - google.protobuf.Timestamp scheduledStartTimestamp = 6; - TraceContext parentTraceContext = 7; - google.protobuf.StringValue orchestrationSpanID = 8; - map tags = 9; -} - -message ExecutionCompletedEvent { - OrchestrationStatus orchestrationStatus = 1; - google.protobuf.StringValue result = 2; - TaskFailureDetails failureDetails = 3; -} - -message ExecutionTerminatedEvent { - google.protobuf.StringValue input = 1; - bool recurse = 2; -} - -message TaskScheduledEvent { - string name = 1; - google.protobuf.StringValue version = 2; - google.protobuf.StringValue input = 3; - TraceContext parentTraceContext = 4; - string taskExecutionId = 5; - - // If defined, indicates that this task was the starting point of a new - // workflow execution as the result of a rerun operation. - optional RerunParentInstanceInfo rerunParentInstanceInfo = 6; -} - -message TaskCompletedEvent { - int32 taskScheduledId = 1; - google.protobuf.StringValue result = 2; - string taskExecutionId = 3; -} - -message TaskFailedEvent { - int32 taskScheduledId = 1; - TaskFailureDetails failureDetails = 2; - string taskExecutionId = 3; -} - -message SubOrchestrationInstanceCreatedEvent { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue version = 3; - google.protobuf.StringValue input = 4; - TraceContext parentTraceContext = 5; -} - -message SubOrchestrationInstanceCompletedEvent { - int32 taskScheduledId = 1; - google.protobuf.StringValue result = 2; -} - -message SubOrchestrationInstanceFailedEvent { - int32 taskScheduledId = 1; - TaskFailureDetails failureDetails = 2; -} - -message TimerCreatedEvent { - google.protobuf.Timestamp fireAt = 1; - optional string name = 2; - - // If defined, indicates that this task was the starting point of a new - // workflow execution as the result of a rerun operation. - optional RerunParentInstanceInfo rerunParentInstanceInfo = 3; - - // Indicates the reason this timer was created. - oneof origin { - TimerOriginCreateTimer originCreateTimer = 4; - TimerOriginExternalEvent originExternalEvent = 5; - TimerOriginActivityRetry originActivityRetry = 6; - TimerOriginChildWorkflowRetry originChildWorkflowRetry = 7; - } -} - -message TimerFiredEvent { - google.protobuf.Timestamp fireAt = 1; - int32 timerId = 2; -} - -message OrchestratorStartedEvent { - optional OrchestrationVersion version = 1; -} - -message OrchestratorCompletedEvent { - // No payload data -} - -message EventSentEvent { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue input = 3; -} - -message EventRaisedEvent { - string name = 1; - google.protobuf.StringValue input = 2; -} - -message GenericEvent { - google.protobuf.StringValue data = 1; -} - -message HistoryStateEvent { - OrchestrationState orchestrationState = 1; -} - -message ContinueAsNewEvent { - google.protobuf.StringValue input = 1; -} - -message ExecutionSuspendedEvent { - google.protobuf.StringValue input = 1; -} - -message ExecutionResumedEvent { - google.protobuf.StringValue input = 1; -} - -message ExecutionStalledEvent { - StalledReason reason = 1; - optional string description = 2; -} - -message EntityOperationSignaledEvent { - string requestId = 1; - string operation = 2; - google.protobuf.Timestamp scheduledTime = 3; - google.protobuf.StringValue input = 4; - google.protobuf.StringValue targetInstanceId = 5; // used only within histories, null in messages -} - -message EntityOperationCalledEvent { - string requestId = 1; - string operation = 2; - google.protobuf.Timestamp scheduledTime = 3; - google.protobuf.StringValue input = 4; - google.protobuf.StringValue parentInstanceId = 5; // used only within messages, null in histories - google.protobuf.StringValue parentExecutionId = 6; // used only within messages, null in histories - google.protobuf.StringValue targetInstanceId = 7; // used only within histories, null in messages -} - -message EntityLockRequestedEvent { - string criticalSectionId = 1; - repeated string lockSet = 2; - int32 position = 3; - google.protobuf.StringValue parentInstanceId = 4; // used only within messages, null in histories -} - -message EntityOperationCompletedEvent { - string requestId = 1; - google.protobuf.StringValue output = 2; -} - -message EntityOperationFailedEvent { - string requestId = 1; - TaskFailureDetails failureDetails = 2; -} - -message EntityUnlockSentEvent { - string criticalSectionId = 1; - google.protobuf.StringValue parentInstanceId = 2; // used only within messages, null in histories - google.protobuf.StringValue targetInstanceId = 3; // used only within histories, null in messages -} - -message EntityLockGrantedEvent { - string criticalSectionId = 1; -} - -message HistoryEvent { - int32 eventId = 1; - google.protobuf.Timestamp timestamp = 2; - oneof eventType { - ExecutionStartedEvent executionStarted = 3; - ExecutionCompletedEvent executionCompleted = 4; - ExecutionTerminatedEvent executionTerminated = 5; - TaskScheduledEvent taskScheduled = 6; - TaskCompletedEvent taskCompleted = 7; - TaskFailedEvent taskFailed = 8; - SubOrchestrationInstanceCreatedEvent subOrchestrationInstanceCreated = 9; - SubOrchestrationInstanceCompletedEvent subOrchestrationInstanceCompleted = 10; - SubOrchestrationInstanceFailedEvent subOrchestrationInstanceFailed = 11; - TimerCreatedEvent timerCreated = 12; - TimerFiredEvent timerFired = 13; - OrchestratorStartedEvent orchestratorStarted = 14; - OrchestratorCompletedEvent orchestratorCompleted = 15; - EventSentEvent eventSent = 16; - EventRaisedEvent eventRaised = 17; - GenericEvent genericEvent = 18; - HistoryStateEvent historyState = 19; - ContinueAsNewEvent continueAsNew = 20; - ExecutionSuspendedEvent executionSuspended = 21; - ExecutionResumedEvent executionResumed = 22; - EntityOperationSignaledEvent entityOperationSignaled = 23; - EntityOperationCalledEvent entityOperationCalled = 24; - EntityOperationCompletedEvent entityOperationCompleted = 25; - EntityOperationFailedEvent entityOperationFailed = 26; - EntityLockRequestedEvent entityLockRequested = 27; - EntityLockGrantedEvent entityLockGranted = 28; - EntityUnlockSentEvent entityUnlockSent = 29; - ExecutionStalledEvent executionStalled = 31; - } - optional TaskRouter router = 30; -} - -message ScheduleTaskAction { - string name = 1; - google.protobuf.StringValue version = 2; - google.protobuf.StringValue input = 3; - optional TaskRouter router = 4; - string taskExecutionId = 5; -} - -message CreateSubOrchestrationAction { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue version = 3; - google.protobuf.StringValue input = 4; - optional TaskRouter router = 5; - // Specifies which ancestor history events should be propagated to the child workflow. - optional HistoryPropagationScope history_propagation_scope = 6; - // The history segments to propagate to the child workflow. - // Populated by the SDK based on the history_propagation_scope. - repeated PropagatedHistorySegment propagated_history = 7; -} - -// Timer created explicitly by the workflow via CreateTimer(). -message TimerOriginCreateTimer { -} - -// Timer created to track the timeout of a WaitForExternalEvent call. -message TimerOriginExternalEvent { - string name = 1; -} - -// Timer created to manage the retry delay of an activity. -message TimerOriginActivityRetry { - string taskExecutionId = 1; -} - -// Timer created to manage the retry delay of a child workflow. -message TimerOriginChildWorkflowRetry { - string instanceId = 1; -} - -message CreateTimerAction { - google.protobuf.Timestamp fireAt = 1; - optional string name = 2; - - // Indicates the reason this timer is being created. - oneof origin { - TimerOriginCreateTimer originCreateTimer = 3; - TimerOriginExternalEvent originExternalEvent = 4; - TimerOriginActivityRetry originActivityRetry = 5; - TimerOriginChildWorkflowRetry originChildWorkflowRetry = 6; - } -} - -message SendEventAction { - OrchestrationInstance instance = 1; - string name = 2; - google.protobuf.StringValue data = 3; -} - -message CompleteOrchestrationAction { - OrchestrationStatus orchestrationStatus = 1; - google.protobuf.StringValue result = 2; - google.protobuf.StringValue details = 3; - google.protobuf.StringValue newVersion = 4; - repeated HistoryEvent carryoverEvents = 5; - TaskFailureDetails failureDetails = 6; -} - -message TerminateOrchestrationAction { - string instanceId = 1; - google.protobuf.StringValue reason = 2; - bool recurse = 3; -} - -message SendEntityMessageAction { - oneof EntityMessageType { - EntityOperationSignaledEvent entityOperationSignaled = 1; - EntityOperationCalledEvent entityOperationCalled = 2; - EntityLockRequestedEvent entityLockRequested = 3; - EntityUnlockSentEvent entityUnlockSent = 4; - } -} - -message OrchestratorAction { - int32 id = 1; - oneof orchestratorActionType { - ScheduleTaskAction scheduleTask = 2; - CreateSubOrchestrationAction createSubOrchestration = 3; - CreateTimerAction createTimer = 4; - SendEventAction sendEvent = 5; - CompleteOrchestrationAction completeOrchestration = 6; - TerminateOrchestrationAction terminateOrchestration = 7; - SendEntityMessageAction sendEntityMessage = 8; - } - optional TaskRouter router = 9; -} - -message OrchestratorRequest { +message WorkflowRequest { string instanceId = 1; google.protobuf.StringValue executionId = 2; repeated HistoryEvent pastEvents = 3; repeated HistoryEvent newEvents = 4; - OrchestratorEntityParameters entityParameters = 5; + reserved 5; bool requiresHistoryStreaming = 6; optional TaskRouter router = 7; - // Workflow history propagated from ancestor workflow instances. - // Populated when the parent scheduled this workflow with a non-None HistoryPropagationScope. - repeated PropagatedHistorySegment propagated_history = 8; + + // Propagated history from a parent workflow. + // Delivered via the work item stream to the SDK, so that the + // workflow function can access it via ctx. + optional PropagatedHistory propagatedHistory = 8; } -message OrchestratorResponse { +message WorkflowResponse { string instanceId = 1; - repeated OrchestratorAction actions = 2; + repeated WorkflowAction actions = 2; google.protobuf.StringValue customStatus = 3; string completionToken = 4; - // The number of work item events that were processed by the orchestrator. - // This field is optional. If not set, the service should assume that the orchestrator processed all events. + // The number of work item events that were processed by the workflow. + // This field is optional. If not set, the service should assume that the workflow processed all events. google.protobuf.Int32Value numEventsProcessed = 5; - optional OrchestrationVersion version = 6; + optional WorkflowVersion version = 6; } message CreateInstanceRequest { @@ -463,23 +72,13 @@ message CreateInstanceRequest { google.protobuf.StringValue version = 3; google.protobuf.StringValue input = 4; google.protobuf.Timestamp scheduledStartTimestamp = 5; - OrchestrationIdReusePolicy orchestrationIdReusePolicy = 6; + reserved 6; + reserved "orchestrationIdReusePolicy"; google.protobuf.StringValue executionId = 7; map tags = 8; TraceContext parentTraceContext = 9; } -message OrchestrationIdReusePolicy { - repeated OrchestrationStatus operationStatus = 1; - CreateOrchestrationAction action = 2; -} - -enum CreateOrchestrationAction { - ERROR = 0; - IGNORE = 1; - TERMINATE = 2; -} - message CreateInstanceResponse { string instanceId = 1; } @@ -491,34 +90,7 @@ message GetInstanceRequest { message GetInstanceResponse { bool exists = 1; - OrchestrationState orchestrationState = 2; -} - -message RewindInstanceRequest { - string instanceId = 1; - google.protobuf.StringValue reason = 2; -} - -message RewindInstanceResponse { - // Empty for now. Using explicit type incase we want to add content later. -} - -message OrchestrationState { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue version = 3; - OrchestrationStatus orchestrationStatus = 4; - google.protobuf.Timestamp scheduledStartTimestamp = 5; - google.protobuf.Timestamp createdTimestamp = 6; - google.protobuf.Timestamp lastUpdatedTimestamp = 7; - google.protobuf.StringValue input = 8; - google.protobuf.StringValue output = 9; - google.protobuf.StringValue customStatus = 10; - TaskFailureDetails failureDetails = 11; - google.protobuf.StringValue executionId = 12; - google.protobuf.Timestamp completedTimestamp = 13; - google.protobuf.StringValue parentInstanceId = 14; - map tags = 15; + WorkflowState workflowState = 2; } message RaiseEventRequest { @@ -559,26 +131,6 @@ message ResumeResponse { // No payload } -message QueryInstancesRequest { - InstanceQuery query = 1; -} - -message InstanceQuery{ - repeated OrchestrationStatus runtimeStatus = 1; - google.protobuf.Timestamp createdTimeFrom = 2; - google.protobuf.Timestamp createdTimeTo = 3; - repeated google.protobuf.StringValue taskHubNames = 4; - int32 maxInstanceCount = 5; - google.protobuf.StringValue continuationToken = 6; - google.protobuf.StringValue instanceIdPrefix = 7; - bool fetchInputsAndOutputs = 8; -} - -message QueryInstancesResponse { - repeated OrchestrationState orchestrationState = 1; - google.protobuf.StringValue continuationToken = 2; -} - message PurgeInstancesRequest { oneof request { string instanceId = 1; @@ -609,273 +161,15 @@ message PurgeInstancesResponse { google.protobuf.BoolValue isComplete = 2; } -message CreateTaskHubRequest { - bool recreateIfExists = 1; -} - -message CreateTaskHubResponse { - //no playload -} - -message DeleteTaskHubRequest { - //no playload -} - -message DeleteTaskHubResponse { - //no playload -} - -message SignalEntityRequest { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue input = 3; - string requestId = 4; - google.protobuf.Timestamp scheduledTime = 5; -} - -message SignalEntityResponse { - // no payload -} - -message GetEntityRequest { - string instanceId = 1; - bool includeState = 2; -} - -message GetEntityResponse { - bool exists = 1; - EntityMetadata entity = 2; -} - -message EntityQuery { - google.protobuf.StringValue instanceIdStartsWith = 1; - google.protobuf.Timestamp lastModifiedFrom = 2; - google.protobuf.Timestamp lastModifiedTo = 3; - bool includeState = 4; - bool includeTransient = 5; - google.protobuf.Int32Value pageSize = 6; - google.protobuf.StringValue continuationToken = 7; -} - -message QueryEntitiesRequest { - EntityQuery query = 1; -} - -message QueryEntitiesResponse { - repeated EntityMetadata entities = 1; - google.protobuf.StringValue continuationToken = 2; -} - -message EntityMetadata { - string instanceId = 1; - google.protobuf.Timestamp lastModifiedTime = 2; - int32 backlogQueueSize = 3; - google.protobuf.StringValue lockedBy = 4; - google.protobuf.StringValue serializedState = 5; -} - -message CleanEntityStorageRequest { - google.protobuf.StringValue continuationToken = 1; - bool removeEmptyEntities = 2; - bool releaseOrphanedLocks = 3; -} - -message CleanEntityStorageResponse { - google.protobuf.StringValue continuationToken = 1; - int32 emptyEntitiesRemoved = 2; - int32 orphanedLocksReleased = 3; -} - -message OrchestratorEntityParameters { - google.protobuf.Duration entityMessageReorderWindow = 1; -} - -message EntityBatchRequest { - string instanceId = 1; - google.protobuf.StringValue entityState = 2; - repeated OperationRequest operations = 3; -} - -message EntityBatchResult { - repeated OperationResult results = 1; - repeated OperationAction actions = 2; - google.protobuf.StringValue entityState = 3; - TaskFailureDetails failureDetails = 4; - string completionToken = 5; - repeated OperationInfo operationInfos = 6; // used only with DTS -} - -message EntityRequest { - string instanceId = 1; - string executionId = 2; - google.protobuf.StringValue entityState = 3; // null if entity does not exist - repeated HistoryEvent operationRequests = 4; -} - -message OperationRequest { - string operation = 1; - string requestId = 2; - google.protobuf.StringValue input = 3; -} - -message OperationResult { - oneof resultType { - OperationResultSuccess success = 1; - OperationResultFailure failure = 2; - } -} - -message OperationInfo { - string requestId = 1; - OrchestrationInstance responseDestination = 2; // null for signals -} - -message OperationResultSuccess { - google.protobuf.StringValue result = 1; -} - -message OperationResultFailure { - TaskFailureDetails failureDetails = 1; -} - -message OperationAction { - int32 id = 1; - oneof operationActionType { - SendSignalAction sendSignal = 2; - StartNewOrchestrationAction startNewOrchestration = 3; - } -} - -message SendSignalAction { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue input = 3; - google.protobuf.Timestamp scheduledTime = 4; -} - -message StartNewOrchestrationAction { - string instanceId = 1; - string name = 2; - google.protobuf.StringValue version = 3; - google.protobuf.StringValue input = 4; - google.protobuf.Timestamp scheduledTime = 5; -} - -message AbandonActivityTaskRequest { - string completionToken = 1; -} - -message AbandonActivityTaskResponse { - // Empty. -} - -message AbandonOrchestrationTaskRequest { - string completionToken = 1; -} - -message AbandonOrchestrationTaskResponse { - // Empty. -} - -message AbandonEntityTaskRequest { - string completionToken = 1; -} - -message AbandonEntityTaskResponse { - // Empty. -} - -service TaskHubSidecarService { - // Sends a hello request to the sidecar service. - rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); - - // Starts a new orchestration instance. - rpc StartInstance(CreateInstanceRequest) returns (CreateInstanceResponse); - - // Gets the status of an existing orchestration instance. - rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse); - - // Rewinds an orchestration instance to last known good state and replays from there. - rpc RewindInstance(RewindInstanceRequest) returns (RewindInstanceResponse); - - // Waits for an orchestration instance to reach a running or completion state. - rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse); - - // Waits for an orchestration instance to reach a completion state (completed, failed, terminated, etc.). - rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse); - - // Raises an event to a running orchestration instance. - rpc RaiseEvent(RaiseEventRequest) returns (RaiseEventResponse); - - // Terminates a running orchestration instance. - rpc TerminateInstance(TerminateRequest) returns (TerminateResponse); - - // Suspends a running orchestration instance. - rpc SuspendInstance(SuspendRequest) returns (SuspendResponse); - - // Resumes a suspended orchestration instance. - rpc ResumeInstance(ResumeRequest) returns (ResumeResponse); - - // rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse); - - rpc QueryInstances(QueryInstancesRequest) returns (QueryInstancesResponse); - rpc PurgeInstances(PurgeInstancesRequest) returns (PurgeInstancesResponse); - - rpc GetWorkItems(GetWorkItemsRequest) returns (stream WorkItem); - rpc CompleteActivityTask(ActivityResponse) returns (CompleteTaskResponse); - rpc CompleteOrchestratorTask(OrchestratorResponse) returns (CompleteTaskResponse); - rpc CompleteEntityTask(EntityBatchResult) returns (CompleteTaskResponse); - - // Gets the history of an orchestration instance as a stream of events. - rpc StreamInstanceHistory(StreamInstanceHistoryRequest) returns (stream HistoryChunk); - - // Deletes and Creates the necessary resources for the orchestration service and the instance store - rpc CreateTaskHub(CreateTaskHubRequest) returns (CreateTaskHubResponse); - - // Deletes the resources for the orchestration service and optionally the instance store - rpc DeleteTaskHub(DeleteTaskHubRequest) returns (DeleteTaskHubResponse); - - // sends a signal to an entity - rpc SignalEntity(SignalEntityRequest) returns (SignalEntityResponse); - - // get information about a specific entity - rpc GetEntity(GetEntityRequest) returns (GetEntityResponse); - - // query entities - rpc QueryEntities(QueryEntitiesRequest) returns (QueryEntitiesResponse); - - // clean entity storage - rpc CleanEntityStorage(CleanEntityStorageRequest) returns (CleanEntityStorageResponse); - - // Abandons a single work item - rpc AbandonTaskActivityWorkItem(AbandonActivityTaskRequest) returns (AbandonActivityTaskResponse); - - // Abandon an orchestration work item - rpc AbandonTaskOrchestratorWorkItem(AbandonOrchestrationTaskRequest) returns (AbandonOrchestrationTaskResponse); - - // Abandon an entity work item - rpc AbandonTaskEntityWorkItem(AbandonEntityTaskRequest) returns (AbandonEntityTaskResponse); - - // Rerun a Workflow from a specific event ID of a workflow instance. - rpc RerunWorkflowFromEvent(RerunWorkflowFromEventRequest) returns (RerunWorkflowFromEventResponse); - - rpc ListInstanceIDs (ListInstanceIDsRequest) returns (ListInstanceIDsResponse); - rpc GetInstanceHistory (GetInstanceHistoryRequest) returns (GetInstanceHistoryResponse); -} - message GetWorkItemsRequest { - int32 maxConcurrentOrchestrationWorkItems = 1; - int32 maxConcurrentActivityWorkItems = 2; - int32 maxConcurrentEntityWorkItems = 3; - - repeated WorkerCapability capabilities = 10; + reserved 1, 2, 3, 10; } enum WorkerCapability { WORKER_CAPABILITY_UNSPECIFIED = 0; // Indicates that the worker is capable of streaming instance history as a more optimized - // alternative to receiving the full history embedded in the orchestrator work-item. + // alternative to receiving the full history embedded in the workflow work-item. // When set, the service may return work items without any history events as an optimization. // It is strongly recommended that all SDKs support this capability. WORKER_CAPABILITY_HISTORY_STREAMING = 1; @@ -883,12 +177,10 @@ enum WorkerCapability { message WorkItem { oneof request { - OrchestratorRequest orchestratorRequest = 1; + WorkflowRequest workflowRequest = 1; ActivityRequest activityRequest = 2; - EntityBatchRequest entityRequest = 3; // (older) used by orchestration services implementations - HealthPing healthPing = 4; - EntityRequest entityRequestV2 = 5; // (newer) used by backend service implementations } + reserved 3, 4, 5; string completionToken = 10; } @@ -896,27 +188,11 @@ message CompleteTaskResponse { // No payload } -message HealthPing { - // No payload -} - -message StreamInstanceHistoryRequest { - string instanceId = 1; - google.protobuf.StringValue executionId = 2; - - // When set to true, the service may return a more optimized response suitable for workers. - bool forWorkItemProcessing = 3; -} - -message HistoryChunk { - repeated HistoryEvent events = 1; -} - // RerunWorkflowFromEventRequest is used to rerun a workflow instance from a // specific event ID. message RerunWorkflowFromEventRequest { - // sourceInstanceID is the orchestration instance ID to rerun. Can be a top - // level instance, or sub-orchestration instance. + // sourceInstanceID is the workflow instance ID to rerun. Can be a top + // level instance, or child workflow instance. string sourceInstanceID = 1; // the event id to start the new workflow instance from. @@ -935,6 +211,11 @@ message RerunWorkflowFromEventRequest { // inputs being `StringValue` which cannot be optional, and therefore no nil // value can be signalled or overwritten. bool overwriteInput = 5; + + // newChildWorkflowInstanceID is an optional instance ID to use when + // rerunning from a child workflow. Only accepted if the event ID given is + // targeting a child workflow creation event. + optional string newChildWorkflowInstanceID = 6; } // RerunWorkflowFromEventResponse is the response to executing @@ -943,7 +224,7 @@ message RerunWorkflowFromEventResponse { string newInstanceID = 1; } -// ListInstanceIDsRequest is used to list all orchestration instances. +// ListInstanceIDsRequest is used to list all workflow instances. message ListInstanceIDsRequest { // continuationToken is the continuation token to use for pagination. This // is the token which the next page should start from. If not given, the @@ -965,8 +246,8 @@ message ListInstanceIDsResponse { optional string continuationToken = 2; } -// GetInstanceHistoryRequest is used to get the full history of an -// orchestration instance. +// GetInstanceHistoryRequest is used to get the full history of a +// workflow instance. message GetInstanceHistoryRequest { string instanceId = 1; } @@ -974,4 +255,52 @@ message GetInstanceHistoryRequest { // GetInstanceHistoryResponse is the response to executing GetInstanceHistory. message GetInstanceHistoryResponse { repeated HistoryEvent events = 1; +} + +service TaskHubSidecarService { + // Sends a hello request to the sidecar service. + rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); + + // Starts a new workflow instance. + rpc StartInstance(CreateInstanceRequest) returns (CreateInstanceResponse); + + // Gets the status of an existing workflow instance. + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse); + + // Waits for a workflow instance to reach a running or completion state. + rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse); + + // Waits for a workflow instance to reach a completion state (completed, failed, terminated, etc.). + rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse); + + // Raises an event to a running workflow instance. + rpc RaiseEvent(RaiseEventRequest) returns (RaiseEventResponse); + + // Terminates a running workflow instance. + rpc TerminateInstance(TerminateRequest) returns (TerminateResponse); + + // Suspends a running workflow instance. + rpc SuspendInstance(SuspendRequest) returns (SuspendResponse); + + // Resumes a suspended workflow instance. + rpc ResumeInstance(ResumeRequest) returns (ResumeResponse); + + rpc PurgeInstances(PurgeInstancesRequest) returns (PurgeInstancesResponse); + + rpc GetWorkItems(GetWorkItemsRequest) returns (stream WorkItem); + rpc CompleteActivityTask(ActivityResponse) returns (CompleteTaskResponse); + + // Deprecated: Use CompleteWorkflowTask instead. + rpc CompleteOrchestratorTask(WorkflowResponse) returns (CompleteTaskResponse) { + option deprecated = true; + } + + // Completes a workflow work item. + rpc CompleteWorkflowTask(WorkflowResponse) returns (CompleteTaskResponse); + + // Rerun a Workflow from a specific event ID of a workflow instance. + rpc RerunWorkflowFromEvent(RerunWorkflowFromEventRequest) returns (RerunWorkflowFromEventResponse); + + rpc ListInstanceIDs (ListInstanceIDsRequest) returns (ListInstanceIDsResponse); + rpc GetInstanceHistory (GetInstanceHistoryRequest) returns (GetInstanceHistoryResponse); } \ No newline at end of file diff --git a/src/Dapr.Workflow.Grpc/runtime_state.proto b/src/Dapr.Workflow.Grpc/runtime_state.proto index e47ee0615..8cedef156 100644 --- a/src/Dapr.Workflow.Grpc/runtime_state.proto +++ b/src/Dapr.Workflow.Grpc/runtime_state.proto @@ -19,7 +19,8 @@ option csharp_namespace = "Dapr.DurableTask.Protobuf"; option java_package = "io.dapr.durabletask.implementation.protobuf"; option go_package = "/api/protos"; -import "orchestrator_service.proto"; +import "orchestration.proto"; +import "history_events.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; @@ -29,14 +30,14 @@ message RuntimeStateStalled { optional string description = 2; } -// OrchestrationRuntimeState holds the current state for an orchestration. -message OrchestrationRuntimeState { +// WorkflowRuntimeState holds the current state for a workflow. +message WorkflowRuntimeState { string instanceId = 1; repeated HistoryEvent newEvents = 2; repeated HistoryEvent oldEvents = 3; repeated HistoryEvent pendingTasks = 4; repeated HistoryEvent pendingTimers = 5; - repeated OrchestrationRuntimeStateMessage pendingMessages = 6; + repeated WorkflowRuntimeStateMessage pendingMessages = 6; ExecutionStartedEvent startEvent = 7; ExecutionCompletedEvent completedEvent = 8; @@ -50,8 +51,14 @@ message OrchestrationRuntimeState { optional RuntimeStateStalled stalled = 15; } -// OrchestrationRuntimeStateMessage holds an OrchestratorMessage and the target instance ID. -message OrchestrationRuntimeStateMessage { +// WorkflowRuntimeStateMessage holds a HistoryEvent payload and the target instance ID. +message WorkflowRuntimeStateMessage { HistoryEvent historyEvent = 1; - string TargetInstanceID = 2; + string targetInstanceId = 2; + + // Propagated history to deliver to the child workflow. + // This is a transport field used when creating child workflows with + // history propagation enabled. It is NOT stored as part of any + // workflow's history events. + optional PropagatedHistory propagatedHistory = 3; } \ No newline at end of file From df2a5665e39956a783e46516c553610898e05859 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 18 May 2026 00:57:09 -0500 Subject: [PATCH 10/17] Manually updated all references to old "Orchestrator" types and updated to new "Workflow" types Signed-off-by: Whit Waldo --- .../PropagatedHistory.cs | 170 +-- .../WorkflowContext.cs | 23 - .../Dapr.Workflow.Grpc.csproj | 8 +- src/Dapr.Workflow/Client/ProtoConverters.cs | 21 +- .../Client/WorkflowGrpcClient.cs | 2 +- .../Versioning/WorkflowVersionTracker.cs | 8 +- .../Worker/Grpc/GrpcProtocolHandler.cs | 51 +- .../Worker/Internal/TimerOriginHelpers.cs | 16 +- .../Internal/WorkflowOrchestrationContext.cs | 198 +--- src/Dapr.Workflow/Worker/WorkflowWorker.cs | 149 ++- src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 2 + ...alEventDoesNotBlockConcurrencySlotTests.cs | 161 --- .../HistoryPropagationWorkflowTests.cs | 834 +++++++-------- .../MaxConcurrentActivitiesTests.cs | 204 ---- .../MaxConcurrentWorkflowsTests.cs | 157 --- .../WorkflowContextDelegationTests.cs | 1 - ...orkflowContextWaitForExternalEventTests.cs | 1 - .../WorkflowTests.cs | 1 - .../Client/ProtoConvertersTests.cs | 21 +- .../Client/WorkflowGrpcClientTests.cs | 26 +- .../ParallelExtensionsTest.cs | 1 - .../Versioning/WorkflowVersionTrackerTests.cs | 22 +- .../Worker/Grpc/GrpcProtocolHandlerTests.cs | 302 +----- .../Worker/Internal/TimerOriginTests.cs | 117 ++- .../WorkflowHistoryPropagationTests.cs | 966 +++++++++--------- .../WorkflowOrchestrationContextTests.cs | 68 +- .../Worker/WorkflowWorkerTests.cs | 828 +++++++-------- .../Worker/WorkflowsFactoryTests.cs | 1 - .../WorkflowRuntimeOptionsTests.cs | 46 +- ...orkflowServiceCollectionExtensionsTests.cs | 1 - 30 files changed, 1683 insertions(+), 2723 deletions(-) delete mode 100644 test/Dapr.IntegrationTest.Workflow/ExternalEventDoesNotBlockConcurrencySlotTests.cs delete mode 100644 test/Dapr.IntegrationTest.Workflow/MaxConcurrentActivitiesTests.cs delete mode 100644 test/Dapr.IntegrationTest.Workflow/MaxConcurrentWorkflowsTests.cs diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs index eda643b7d..711563e8f 100644 --- a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs +++ b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs @@ -1,85 +1,85 @@ -// ------------------------------------------------------------------------ -// Copyright 2026 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -namespace Dapr.Workflow; - -using System; -using System.Collections.Generic; -using System.Linq; - -/// -/// Contains the workflow history that was propagated from ancestor workflow instances. -/// Each entry corresponds to a single ancestor's history. -/// -/// -/// A workflow receives propagated history when it is scheduled with a -/// other than . -/// Use to retrieve the propagated history -/// inside a workflow implementation. -/// -public sealed class PropagatedHistory -{ - private readonly IReadOnlyList _entries; - - /// - /// Initializes a new instance of with the given entries. - /// - /// The propagated history entries from ancestor workflows. - public PropagatedHistory(IReadOnlyList entries) - { - _entries = entries ?? throw new ArgumentNullException(nameof(entries)); - } - - /// - /// Gets the ordered list of propagated history entries. - /// The first entry corresponds to the immediate parent workflow; subsequent entries - /// correspond to progressively older ancestors when is used. - /// - public IReadOnlyList Entries => _entries; - - /// - /// Returns a new containing only entries from the specified App ID. - /// - /// The Dapr App ID to filter by. - /// A filtered instance. - public PropagatedHistory FilterByAppId(string appId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(appId); - return new PropagatedHistory( - _entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList()); - } - - /// - /// Returns a new containing only the entry with the specified instance ID. - /// - /// The workflow instance ID to filter by. - /// A filtered instance. - public PropagatedHistory FilterByInstanceId(string instanceId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); - return new PropagatedHistory( - _entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList()); - } - - /// - /// Returns a new containing only entries for the specified workflow name. - /// - /// The workflow name to filter by. - /// A filtered instance. - public PropagatedHistory FilterByWorkflowName(string workflowName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(workflowName); - return new PropagatedHistory( - _entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList()); - } -} +// // ------------------------------------------------------------------------ +// // Copyright 2026 The Dapr Authors +// // Licensed under the Apache License, Version 2.0 (the "License"); +// // you may not use this file except in compliance with the License. +// // You may obtain a copy of the License at +// // http://www.apache.org/licenses/LICENSE-2.0 +// // Unless required by applicable law or agreed to in writing, software +// // distributed under the License is distributed on an "AS IS" BASIS, +// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// // See the License for the specific language governing permissions and +// // limitations under the License. +// // ------------------------------------------------------------------------ +// +// namespace Dapr.Workflow; +// +// using System; +// using System.Collections.Generic; +// using System.Linq; +// +// /// +// /// Contains the workflow history that was propagated from ancestor workflow instances. +// /// Each entry corresponds to a single ancestor's history. +// /// +// /// +// /// A workflow receives propagated history when it is scheduled with a +// /// other than . +// /// Use to retrieve the propagated history +// /// inside a workflow implementation. +// /// +// public sealed class PropagatedHistory +// { +// private readonly IReadOnlyList _entries; +// +// /// +// /// Initializes a new instance of with the given entries. +// /// +// /// The propagated history entries from ancestor workflows. +// public PropagatedHistory(IReadOnlyList entries) +// { +// _entries = entries ?? throw new ArgumentNullException(nameof(entries)); +// } +// +// /// +// /// Gets the ordered list of propagated history entries. +// /// The first entry corresponds to the immediate parent workflow; subsequent entries +// /// correspond to progressively older ancestors when is used. +// /// +// public IReadOnlyList Entries => _entries; +// +// /// +// /// Returns a new containing only entries from the specified App ID. +// /// +// /// The Dapr App ID to filter by. +// /// A filtered instance. +// public PropagatedHistory FilterByAppId(string appId) +// { +// ArgumentException.ThrowIfNullOrWhiteSpace(appId); +// return new PropagatedHistory( +// _entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList()); +// } +// +// /// +// /// Returns a new containing only the entry with the specified instance ID. +// /// +// /// The workflow instance ID to filter by. +// /// A filtered instance. +// public PropagatedHistory FilterByInstanceId(string instanceId) +// { +// ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); +// return new PropagatedHistory( +// _entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList()); +// } +// +// /// +// /// Returns a new containing only entries for the specified workflow name. +// /// +// /// The workflow name to filter by. +// /// A filtered instance. +// public PropagatedHistory FilterByWorkflowName(string workflowName) +// { +// ArgumentException.ThrowIfNullOrWhiteSpace(workflowName); +// return new PropagatedHistory( +// _entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList()); +// } +// } diff --git a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs index de355eaf5..c2071f3b3 100644 --- a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs +++ b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs @@ -330,27 +330,4 @@ public virtual Task CallChildWorkflowAsync( /// /// The new value. public abstract Guid NewGuid(); - - /// - /// Gets the workflow history that was propagated from ancestor workflow instances, or null - /// if no history was propagated to this workflow. - /// - /// - /// - /// A workflow receives propagated history when it is scheduled as a child workflow and the parent - /// specified a other than . - /// - /// - /// Use , , - /// or to narrow down the returned entries. - /// - /// - /// This method always returns the same value regardless of whether the workflow is replaying. - /// - /// - /// - /// A containing entries from ancestor workflows, - /// or null if no history was propagated to this workflow instance. - /// - public abstract PropagatedHistory? GetPropagatedHistory(); } diff --git a/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj b/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj index 79597b2a5..679cede27 100644 --- a/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj +++ b/src/Dapr.Workflow.Grpc/Dapr.Workflow.Grpc.csproj @@ -22,11 +22,11 @@ - - - + + + - + diff --git a/src/Dapr.Workflow/Client/ProtoConverters.cs b/src/Dapr.Workflow/Client/ProtoConverters.cs index 91aac5d4f..7941828c4 100644 --- a/src/Dapr.Workflow/Client/ProtoConverters.cs +++ b/src/Dapr.Workflow/Client/ProtoConverters.cs @@ -23,10 +23,10 @@ namespace Dapr.Workflow.Client; internal static class ProtoConverters { /// - /// Converts proto to . + /// Converts proto to . /// - public static WorkflowMetadata ToWorkflowMetadata(OrchestrationState state, IDaprSerializer serializer) => - new(state.InstanceId, state.Name, ToRuntimeStatus(state.OrchestrationStatus), + public static WorkflowMetadata ToWorkflowMetadata(Dapr.DurableTask.Protobuf.WorkflowState state, IDaprSerializer serializer) => + new(state.InstanceId, state.Name, ToRuntimeStatus(state.WorkflowStatus), state.CreatedTimestamp?.ToDateTime() ?? DateTime.MinValue, state.LastUpdatedTimestamp?.ToDateTime() ?? DateTime.MinValue, serializer) { @@ -73,21 +73,22 @@ public static WorkflowHistoryEventType ToHistoryEventType(HistoryEvent.EventType HistoryEvent.EventTypeOneofCase.TaskScheduled => WorkflowHistoryEventType.TaskScheduled, HistoryEvent.EventTypeOneofCase.TaskCompleted => WorkflowHistoryEventType.TaskCompleted, HistoryEvent.EventTypeOneofCase.TaskFailed => WorkflowHistoryEventType.TaskFailed, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated => WorkflowHistoryEventType.SubOrchestrationInstanceCreated, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted => WorkflowHistoryEventType.SubOrchestrationInstanceCompleted, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed => WorkflowHistoryEventType.SubOrchestrationInstanceFailed, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceCreated => WorkflowHistoryEventType.SubOrchestrationInstanceCreated, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceCompleted => WorkflowHistoryEventType.SubOrchestrationInstanceCompleted, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceFailed => WorkflowHistoryEventType.SubOrchestrationInstanceFailed, HistoryEvent.EventTypeOneofCase.TimerCreated => WorkflowHistoryEventType.TimerCreated, HistoryEvent.EventTypeOneofCase.TimerFired => WorkflowHistoryEventType.TimerFired, - HistoryEvent.EventTypeOneofCase.OrchestratorStarted => WorkflowHistoryEventType.OrchestratorStarted, - HistoryEvent.EventTypeOneofCase.OrchestratorCompleted => WorkflowHistoryEventType.OrchestratorCompleted, + HistoryEvent.EventTypeOneofCase.WorkflowStarted => WorkflowHistoryEventType.OrchestratorStarted, + HistoryEvent.EventTypeOneofCase.WorkflowCompleted => WorkflowHistoryEventType.OrchestratorCompleted, HistoryEvent.EventTypeOneofCase.EventSent => WorkflowHistoryEventType.EventSent, HistoryEvent.EventTypeOneofCase.EventRaised => WorkflowHistoryEventType.EventRaised, - HistoryEvent.EventTypeOneofCase.GenericEvent => WorkflowHistoryEventType.GenericEvent, - HistoryEvent.EventTypeOneofCase.HistoryState => WorkflowHistoryEventType.HistoryState, + // HistoryEvent.EventTypeOneofCase.GenericEvent => WorkflowHistoryEventType.GenericEvent, + // HistoryEvent.EventTypeOneofCase.HistoryState => WorkflowHistoryEventType.HistoryState, HistoryEvent.EventTypeOneofCase.ContinueAsNew => WorkflowHistoryEventType.ContinueAsNew, HistoryEvent.EventTypeOneofCase.ExecutionSuspended => WorkflowHistoryEventType.ExecutionSuspended, HistoryEvent.EventTypeOneofCase.ExecutionResumed => WorkflowHistoryEventType.ExecutionResumed, HistoryEvent.EventTypeOneofCase.ExecutionStalled => WorkflowHistoryEventType.ExecutionStalled, + HistoryEvent.EventTypeOneofCase.DetachedWorkflowInstanceCreated => WorkflowHistoryEventType.SubOrchestrationInstanceCreated, _ => WorkflowHistoryEventType.Unknown }; } diff --git a/src/Dapr.Workflow/Client/WorkflowGrpcClient.cs b/src/Dapr.Workflow/Client/WorkflowGrpcClient.cs index d76522cee..7971bb531 100644 --- a/src/Dapr.Workflow/Client/WorkflowGrpcClient.cs +++ b/src/Dapr.Workflow/Client/WorkflowGrpcClient.cs @@ -79,7 +79,7 @@ public override async Task ScheduleNewWorkflowAsync(string workflowName, return null; } - return ProtoConverters.ToWorkflowMetadata(response.OrchestrationState, serializer); + return ProtoConverters.ToWorkflowMetadata(response.WorkflowState, serializer); } catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) { diff --git a/src/Dapr.Workflow/Versioning/WorkflowVersionTracker.cs b/src/Dapr.Workflow/Versioning/WorkflowVersionTracker.cs index bcf5360b9..be3b572be 100644 --- a/src/Dapr.Workflow/Versioning/WorkflowVersionTracker.cs +++ b/src/Dapr.Workflow/Versioning/WorkflowVersionTracker.cs @@ -123,11 +123,11 @@ public bool RequestPatch(string patchName, bool isReplaying) } /// - /// Produces the to stamp into the . + /// Produces the to stamp into the . /// /// The name of the current workflow. - /// An instance of a . - public OrchestrationVersion BuildResponseVersion(string workflowName) => new() + /// An instance of a . + public WorkflowVersion BuildResponseVersion(string workflowName) => new() { Name = workflowName, Patches = { _patchesThisTurn } @@ -139,7 +139,7 @@ private static List ListAllVersioningPatches(IReadOnlyList foreach (var ev in events) { - var version = ev.OrchestratorStarted?.Version; + var version = ev.WorkflowStarted?.Version; if (version is not null) { result.AddRange(version.Patches); diff --git a/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs b/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs index 9123f2652..097fc8641 100644 --- a/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs +++ b/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs @@ -29,21 +29,16 @@ namespace Dapr.Workflow.Worker.Grpc; internal sealed class GrpcProtocolHandler( TaskHubSidecarService.TaskHubSidecarServiceClient grpcClient, ILoggerFactory loggerFactory, - int maxConcurrentWorkItems = 100, - int maxConcurrentActivities = 100, - string? daprApiToken = null) : IAsyncDisposable -{ + string? daprApiToken = null) : IAsyncDisposable { private static readonly TimeSpan ReconnectDelay = TimeSpan.FromSeconds(5); private static readonly TimeSpan KeepaliveInterval = TimeSpan.FromSeconds(30); private readonly CancellationTokenSource _disposalCts = new(); - private readonly ILogger _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); + private readonly ILogger _logger = loggerFactory.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); private readonly TaskHubSidecarService.TaskHubSidecarServiceClient _grpcClient = grpcClient ?? throw new ArgumentNullException(nameof(grpcClient)); - private readonly int _maxConcurrentWorkItems = maxConcurrentWorkItems > 0 ? maxConcurrentWorkItems : throw new ArgumentOutOfRangeException(nameof(maxConcurrentWorkItems)); - private readonly int _maxConcurrentActivities = maxConcurrentActivities > 0 ? maxConcurrentActivities : throw new ArgumentOutOfRangeException(nameof(maxConcurrentActivities)); - private readonly SemaphoreSlim _orchestrationSemaphore = new(maxConcurrentWorkItems, maxConcurrentWorkItems); - private readonly SemaphoreSlim _activitySemaphore = new(maxConcurrentActivities, maxConcurrentActivities); + private readonly SemaphoreSlim _orchestrationSemaphore = new(100); + private readonly SemaphoreSlim _activitySemaphore = new(100); private AsyncServerStreamingCall? _streamingCall; private int _activeWorkItemCount; @@ -56,7 +51,7 @@ internal sealed class GrpcProtocolHandler( /// Handler for activity work items. /// Cancellation token. public async Task StartAsync( - Func> workflowHandler, + Func> workflowHandler, Func> activityHandler, CancellationToken cancellationToken) { @@ -64,11 +59,7 @@ public async Task StartAsync( var token = linkedCts.Token; // Establish the bidirectional stream - var request = new GetWorkItemsRequest - { - MaxConcurrentOrchestrationWorkItems = _maxConcurrentWorkItems, - MaxConcurrentActivityWorkItems = _maxConcurrentActivities - }; + var request = new GetWorkItemsRequest(); while (!token.IsCancellationRequested) { @@ -157,7 +148,7 @@ private static async Task DelayOrStopAsync(TimeSpan delay, CancellationToken tok /// private async Task ReceiveLoopAsync( IAsyncStreamReader workItemsStream, - Func> orchestratorHandler, + Func> workflowHandler, Func> activityHandler, CancellationToken cancellationToken) { @@ -173,8 +164,8 @@ private async Task ReceiveLoopAsync( // Dispatch based on work item type var workItemTask = workItem.RequestCase switch { - WorkItem.RequestOneofCase.OrchestratorRequest => Task.Run( - () => ProcessWorkflowAsync(workItem.OrchestratorRequest, completionToken, orchestratorHandler, cancellationToken), + WorkItem.RequestOneofCase.WorkflowRequest => Task.Run( + () => ProcessWorkflowAsync(workItem.WorkflowRequest, completionToken, workflowHandler, cancellationToken), cancellationToken), WorkItem.RequestOneofCase.ActivityRequest => Task.Run( () => ProcessActivityAsync(workItem.ActivityRequest, completionToken, activityHandler, cancellationToken), @@ -187,7 +178,7 @@ private async Task ReceiveLoopAsync( activeWorkItems.Add(workItemTask); // Clean up completed tasks periodically - if (activeWorkItems.Count > _maxConcurrentWorkItems * 2) + if (activeWorkItems.Count > 200) { activeWorkItems.RemoveAll(t => t.IsCompleted); } @@ -221,8 +212,8 @@ private async Task ReceiveLoopAsync( /// /// Processes a workflow request work item. /// - private async Task ProcessWorkflowAsync(OrchestratorRequest request, string completionToken, - Func> handler, CancellationToken cancellationToken) + private async Task ProcessWorkflowAsync(WorkflowRequest request, string completionToken, + Func> handler, CancellationToken cancellationToken) { await _orchestrationSemaphore.WaitAsync(cancellationToken); var activeCount = Interlocked.Increment(ref _activeWorkItemCount); @@ -235,7 +226,7 @@ private async Task ProcessWorkflowAsync(OrchestratorRequest request, string comp // This try/catch must NOT include the CompleteOrchestratorTaskAsync call below — a transport // failure during delivery must not be converted into an orchestrator-level failure, as that // would incorrectly mark a healthy workflow turn as failed. - OrchestratorResponse result; + WorkflowResponse result; try { result = await handler(request, completionToken); @@ -256,7 +247,7 @@ private async Task ProcessWorkflowAsync(OrchestratorRequest request, string comp try { var grpcCallOptions = CreateCallOptions(cancellationToken); - await _grpcClient.CompleteOrchestratorTaskAsync(result, grpcCallOptions); + await _grpcClient.CompleteWorkflowTaskAsync(result, grpcCallOptions); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -285,7 +276,7 @@ private async Task ProcessActivityAsync(ActivityRequest request, string completi try { - _logger.LogGrpcProtocolHandlerActivityProcessorStart(request.OrchestrationInstance.InstanceId, request.Name, + _logger.LogGrpcProtocolHandlerActivityProcessorStart(request.WorkflowInstance.InstanceId, request.Name, request.TaskId, activeCount); // Execute the activity and determine the result (success or application failure). @@ -305,7 +296,7 @@ private async Task ProcessActivityAsync(ActivityRequest request, string completi catch (Exception ex) { _logger.LogGrpcProtocolHandlerActivityProcessorError(ex, request.Name, - request.OrchestrationInstance?.InstanceId); + request.WorkflowInstance?.InstanceId); result = CreateActivityFailureResult(request, completionToken, ex); } @@ -339,7 +330,7 @@ private async Task ProcessActivityAsync(ActivityRequest request, string completi private static ActivityResponse CreateActivityFailureResult(ActivityRequest request, string completionToken, Exception ex) => new() { - InstanceId = request.OrchestrationInstance.InstanceId, + InstanceId = request.WorkflowInstance.InstanceId, TaskId = request.TaskId, CompletionToken = completionToken, FailureDetails = new() @@ -353,18 +344,18 @@ private static ActivityResponse CreateActivityFailureResult(ActivityRequest requ /// /// Creates a failure result for an orchestrator exception. /// - private static OrchestratorResponse CreateWorkflowFailureResult(OrchestratorRequest request, string completionToken, Exception ex) => + private static WorkflowResponse CreateWorkflowFailureResult(WorkflowRequest request, string completionToken, Exception ex) => new() { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { ErrorType = ex.GetType().FullName ?? "Exception", diff --git a/src/Dapr.Workflow/Worker/Internal/TimerOriginHelpers.cs b/src/Dapr.Workflow/Worker/Internal/TimerOriginHelpers.cs index 4109e5332..94397701f 100644 --- a/src/Dapr.Workflow/Worker/Internal/TimerOriginHelpers.cs +++ b/src/Dapr.Workflow/Worker/Internal/TimerOriginHelpers.cs @@ -44,28 +44,28 @@ internal static void SetTimerOrigin(CreateTimerAction action, IMessage origin) switch (origin) { case TimerOriginCreateTimer createTimer: - action.OriginCreateTimer = createTimer; + action.CreateTimer = createTimer; break; case TimerOriginExternalEvent externalEvent: - action.OriginExternalEvent = externalEvent; + action.ExternalEvent = externalEvent; break; case TimerOriginActivityRetry activityRetry: - action.OriginActivityRetry = activityRetry; + action.ActivityRetry = activityRetry; break; case TimerOriginChildWorkflowRetry childWorkflowRetry: - action.OriginChildWorkflowRetry = childWorkflowRetry; + action.ChildWorkflowRetry = childWorkflowRetry; break; } } /// - /// Determines whether a is an optional external event timer + /// Determines whether a is an optional external event timer /// (sentinel fireAt + ExternalEvent origin). /// - internal static bool IsOptionalExternalEventTimerAction(OrchestratorAction action) + internal static bool IsOptionalExternalEventTimerAction(WorkflowAction action) { return action.CreateTimer is { } timer - && timer.OriginCase == CreateTimerAction.OriginOneofCase.OriginExternalEvent + && timer.OriginCase == CreateTimerAction.OriginOneofCase.ExternalEvent && timer.FireAt != null && timer.FireAt.Equals(ExternalEventIndefiniteFireAt); } @@ -76,7 +76,7 @@ internal static bool IsOptionalExternalEventTimerAction(OrchestratorAction actio /// internal static bool IsOptionalExternalEventTimerCreatedEvent(TimerCreatedEvent timerCreated) { - return timerCreated.OriginCase == TimerCreatedEvent.OriginOneofCase.OriginExternalEvent + return timerCreated.OriginCase == TimerCreatedEvent.OriginOneofCase.ExternalEvent && timerCreated.FireAt != null && timerCreated.FireAt.Equals(ExternalEventIndefiniteFireAt); } diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index fe6e1f177..8084b4389 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -21,7 +21,6 @@ using Dapr.Common.Serialization; using Dapr.DurableTask.Protobuf; using Dapr.Workflow.Client; -using Dapr.Workflow.Serialization; using Dapr.Workflow.Versioning; using Google.Protobuf; using Microsoft.Extensions.Logging; @@ -50,7 +49,7 @@ internal sealed class WorkflowOrchestrationContext : WorkflowContext private readonly Dictionary> _openTasks = []; private readonly Dictionary _taskIdToExecutionId = []; private readonly Dictionary _executionIdToTaskId = new(StringComparer.Ordinal); - private readonly SortedDictionary _pendingActions = []; + private readonly SortedDictionary _pendingActions = []; private readonly IDaprSerializer _workflowSerializer; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -72,8 +71,6 @@ internal sealed class WorkflowOrchestrationContext : WorkflowContext private static readonly Guid InstanceIdNamespace = new("6f927a2e-9c7e-4a1d-9b8d-7a86f2e7f62f"); private readonly string? _appId; - private readonly PropagatedHistory? _propagatedHistory; - private readonly IReadOnlyList _ownHistory; private int _sequenceNumber; private int _guidCounter; private object? _customStatus; @@ -86,7 +83,7 @@ public WorkflowOrchestrationContext(string name, string instanceId, DateTime cur IDaprSerializer workflowSerializer, ILoggerFactory loggerFactory, WorkflowVersionTracker versionTracker, string? appId = null, string? executionId = null, IReadOnlyList? ownHistory = null, - IEnumerable? incomingPropagatedHistory = null) + IEnumerable? incomingPropagatedHistory = null) { _workflowSerializer = workflowSerializer; _loggerFactory = loggerFactory; @@ -101,11 +98,6 @@ public WorkflowOrchestrationContext(string name, string instanceId, DateTime cur _currentUtcDateTime = currentUtcDateTime; _appId = appId; // Necessary for setting the source app ID value on the task router _versionTracker = versionTracker; - _ownHistory = ownHistory ?? Array.Empty(); - var propagatedSegments = incomingPropagatedHistory?.ToList(); - _propagatedHistory = propagatedSegments is { Count: > 0 } - ? new PropagatedHistory(propagatedSegments.Select(ConvertSegment).ToList()) - : null; _logger.LogWorkflowContextConstructorSetup(name, instanceId); } @@ -132,7 +124,7 @@ public override bool IsPatched(string patchName) /// /// Gets the list of pending orchestrator actions to be sent to the Dapr sidecar. /// - internal IReadOnlyCollection PendingActions => _pendingActions.Values; + internal IReadOnlyCollection PendingActions => _pendingActions.Values; /// /// Gets the custom status set by the workflow, if any. @@ -176,7 +168,7 @@ private async Task CallActivityInternalAsync(string name, object? input, W return await HandleHistoryMatch(name, earlyCompletion, taskId); } - _pendingActions.Add(taskId, new OrchestratorAction + _pendingActions.Add(taskId, new WorkflowAction { Id = taskId, ScheduleTask = new ScheduleTaskAction @@ -229,7 +221,7 @@ private async Task CreateTimerInternal( var createTimerAction = new CreateTimerAction { FireAt = fireAt }; SetTimerOrigin(createTimerAction, origin); - _pendingActions.Add(taskId, new OrchestratorAction + _pendingActions.Add(taskId, new WorkflowAction { Id = taskId, CreateTimer = createTimerAction @@ -388,12 +380,12 @@ public override void SendEvent(string instanceId, string eventName, object paylo ArgumentException.ThrowIfNullOrWhiteSpace(eventName); var taskId = _sequenceNumber++; - _pendingActions.Add(taskId, new OrchestratorAction + _pendingActions.Add(taskId, new WorkflowAction { Id = taskId, SendEvent = new SendEventAction { - Instance = new OrchestrationInstance { InstanceId = instanceId }, + Instance = new WorkflowInstance { InstanceId = instanceId }, Name = eventName, Data = _workflowSerializer.Serialize(payload) } @@ -435,7 +427,7 @@ private async Task CallChildWorkflowInternalAsync( var router = CreateRouter(options?.TargetAppId); - var createSubOrchestrationAction = new CreateSubOrchestrationAction + var createChildWorkflowAction = new CreateChildWorkflowAction { Name = workflowName, InstanceId = childInstanceId, @@ -447,21 +439,18 @@ private async Task CallChildWorkflowInternalAsync( var propagationScope = options?.PropagationScope ?? HistoryPropagationScope.None; if (propagationScope != HistoryPropagationScope.None) { - createSubOrchestrationAction.HistoryPropagationScope = propagationScope switch + createChildWorkflowAction.HistoryPropagationScope = propagationScope switch { HistoryPropagationScope.OwnHistory => Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, HistoryPropagationScope.Lineage => Dapr.DurableTask.Protobuf.HistoryPropagationScope.Lineage, _ => Dapr.DurableTask.Protobuf.HistoryPropagationScope.None }; - - var segments = BuildPropagatedSegments(propagationScope); - createSubOrchestrationAction.PropagatedHistory.AddRange(segments); } - _pendingActions.Add(taskId, new OrchestratorAction + _pendingActions.Add(taskId, new WorkflowAction { Id = taskId, - CreateSubOrchestration = createSubOrchestrationAction, + CreateChildWorkflow = createChildWorkflowAction, Router = router }); @@ -485,12 +474,12 @@ private async Task CallChildWorkflowInternalAsync( /// public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { - var action = new OrchestratorAction + var action = new WorkflowAction { Id = _sequenceNumber++, - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.ContinuedAsNew, + WorkflowStatus = OrchestrationStatus.ContinuedAsNew, Result = _workflowSerializer.Serialize(newInput), } }; @@ -516,9 +505,9 @@ internal void FinalizeCarryoverEvents() foreach (var action in _pendingActions.Values) { - if (action.CompleteOrchestration?.OrchestrationStatus == OrchestrationStatus.ContinuedAsNew) + if (action.CompleteWorkflow?.WorkflowStatus == OrchestrationStatus.ContinuedAsNew) { - action.CompleteOrchestration.CarryoverEvents.AddRange(_externalEventBuffer); + action.CompleteWorkflow.CarryoverEvents.AddRange(_externalEventBuffer); return; } } @@ -532,9 +521,6 @@ public override Guid NewGuid() return CreateGuidFromName(_instanceGuid, Encoding.UTF8.GetBytes(name)); } - /// - public override PropagatedHistory? GetPropagatedHistory() => _propagatedHistory; - /// public override ILogger CreateReplaySafeLogger(string categoryName) => new ReplaySafeLogger(_loggerFactory.CreateLogger(categoryName), () => IsReplaying); @@ -555,8 +541,8 @@ private Task HandleHistoryMatch(string name, HistoryEvent e, int taskId) { { TaskCompleted: { } completed } => HandleCompletedActivityFromHistory(name, completed), { TaskFailed: { } failed } => HandleFailedActivityFromHistory(name, failed), - { SubOrchestrationInstanceCompleted: { } completed } => HandleCompletedChildWorkflowFromHistory(name, completed), - { SubOrchestrationInstanceFailed: { } failed } => HandleFailedChildWorkflowFromHistory(name, failed), + { ChildWorkflowInstanceCompleted: { } completed } => HandleCompletedChildWorkflowFromHistory(name, completed), + { ChildWorkflowInstanceFailed: { } failed } => HandleFailedChildWorkflowFromHistory(name, failed), _ => throw new InvalidOperationException($"Unexpected history event type for task ID {taskId}") }; } @@ -569,7 +555,7 @@ internal void ProcessEvents(IEnumerable events, bool isReplaying) { switch (historyEvent) { - case { OrchestratorStarted: { } started }: + case { WorkflowStarted: { } started }: HandleOrchestratorStarted(historyEvent, started); break; @@ -585,15 +571,15 @@ internal void ProcessEvents(IEnumerable events, bool isReplaying) HandleActionCompleted(historyEvent, failed.TaskScheduledId); break; - case { SubOrchestrationInstanceCreated: {} created }: + case { ChildWorkflowInstanceCreated: {} created }: OnSubOrchestrationCreated(historyEvent, created); break; - case { SubOrchestrationInstanceCompleted: { } completed }: + case { ChildWorkflowInstanceCompleted: { } completed }: HandleActionCompleted(historyEvent, completed.TaskScheduledId); break; - case { SubOrchestrationInstanceFailed: { } failed }: + case { ChildWorkflowInstanceFailed: { } failed }: HandleActionCompleted(historyEvent, failed.TaskScheduledId); break; @@ -616,7 +602,7 @@ internal void ProcessEvents(IEnumerable events, bool isReplaying) } } - private void HandleOrchestratorStarted(HistoryEvent historyEvent, OrchestratorStartedEvent _) + private void HandleOrchestratorStarted(HistoryEvent historyEvent, WorkflowStartedEvent _) { InitializeNewTurn(historyEvent.Timestamp.ToDateTime()); } @@ -651,15 +637,14 @@ private void HandleActionCreated(HistoryEvent historyEvent) /// true if an optional timer was dropped; false otherwise. private bool TryDropOptionalTimerAt(int eventId) { - if (_pendingActions.TryGetValue(eventId, out var pendingAction) - && pendingAction.CreateTimer != null - && IsOptionalExternalEventTimerAction(pendingAction)) - { - DropOptionalExternalEventTimerAt(eventId); - return true; - } + if (!_pendingActions.TryGetValue(eventId, out var pendingAction) + || pendingAction.CreateTimer == null + || !IsOptionalExternalEventTimerAction(pendingAction)) + return false; + + DropOptionalExternalEventTimerAt(eventId); + return true; - return false; } /// @@ -676,19 +661,19 @@ private void OnTaskScheduled(HistoryEvent historyEvent) /// Handles a SubOrchestrationInstanceCreated history event, dropping an optional timer if needed. /// private void OnSubOrchestrationCreated(HistoryEvent historyEvent, - SubOrchestrationInstanceCreatedEvent created) + ChildWorkflowInstanceCreatedEvent created) { TryDropOptionalTimerAt(historyEvent.EventId); HandleSubOrchestrationCreated(historyEvent, created); } - + private void HandleSubOrchestrationCreated(HistoryEvent historyEvent, - SubOrchestrationInstanceCreatedEvent created) + ChildWorkflowInstanceCreatedEvent created) { // The runtime may assign an EventId that does not match our local taskId. // Try to correlate using the child instance id (which we control). var createdEventId = historyEvent.EventId; - + // SubOrchestrationInstanceCreatedEvent should carry the child instance id. // If it's missing/empty, we can't build a mapping. if (!string.IsNullOrWhiteSpace(created.InstanceId) && @@ -698,16 +683,16 @@ private void HandleSubOrchestrationCreated(HistoryEvent historyEvent, { _subOrchestrationCreatedEventIdToParentTaskId[createdEventId] = parentTaskId; } - + // The "created" history event means the schedule action is no longer pending. // Remove by parentTaskId (our action id), not by createdEventId. _pendingActions.Remove(parentTaskId); - + // Optional: prevent unbounded growth _subOrchestrationInstanceIdToParentTaskId.Remove(created.InstanceId); return; } - + // Fallback to old behavior if we can't correlate _pendingActions.Remove(createdEventId); } @@ -748,14 +733,7 @@ private void DropOptionalExternalEventTimerAt(int atId) } // Shift all pending actions with id > atId down by one - var actionsToShift = new List>(); - foreach (var kvp in _pendingActions) - { - if (kvp.Key > atId) - { - actionsToShift.Add(kvp); - } - } + var actionsToShift = _pendingActions.Where(kvp => kvp.Key > atId).ToList(); foreach (var kvp in actionsToShift) { @@ -770,14 +748,7 @@ private void DropOptionalExternalEventTimerAt(int atId) } // Shift open tasks with id > atId down by one - var tasksToShift = new List>>(); - foreach (var kvp in _openTasks) - { - if (kvp.Key > atId) - { - tasksToShift.Add(kvp); - } - } + var tasksToShift = _openTasks.Where(kvp => kvp.Key > atId).ToList(); foreach (var kvp in tasksToShift) { @@ -940,7 +911,7 @@ private void RemoveTaskExecutionMapping(int taskId) /// Handles a child workflow that completed in the workflow history. /// private Task HandleCompletedChildWorkflowFromHistory(string workflowName, - SubOrchestrationInstanceCompletedEvent completed) + ChildWorkflowInstanceCompletedEvent completed) { _logger.LogChildWorkflowCompletedFromHistory(workflowName, InstanceId); return Task.FromResult(DeserializeResult(completed.Result ?? string.Empty)); @@ -950,7 +921,7 @@ private Task HandleCompletedChildWorkflowFromHistory(string wo /// Handles a child workflow that failed in the workflow history. /// private Task HandleFailedChildWorkflowFromHistory(string workflowName, - SubOrchestrationInstanceFailedEvent failed) + ChildWorkflowInstanceFailedEvent failed) { _logger.LogChildWorkflowFailedFromHistory(workflowName, InstanceId); throw new WorkflowTaskFailedException($"Child workflow '{workflowName}' failed", @@ -1024,91 +995,4 @@ private static WorkflowTaskFailedException CreateTaskFailedException(TaskFailedE return new WorkflowTaskFailedException($"Task failed: {failureDetails.ErrorMessage}", failureDetails); } - - /// - /// Builds the list of proto objects to attach to a - /// based on the requested propagation scope. - /// - private IEnumerable BuildPropagatedSegments(HistoryPropagationScope scope) - { - // Always include the current workflow's own history as the first segment - var ownSegment = new PropagatedHistorySegment - { - AppId = _appId ?? string.Empty, - InstanceId = InstanceId, - WorkflowName = Name - }; - ownSegment.Events.AddRange(_ownHistory); - yield return ownSegment; - - // For Lineage, also include the history this workflow received from its ancestors - if (scope == HistoryPropagationScope.Lineage && _propagatedHistory is not null) - { - foreach (var entry in _propagatedHistory.Entries) - { - var ancestorSegment = new PropagatedHistorySegment - { - AppId = entry.AppId, - InstanceId = entry.InstanceId, - WorkflowName = entry.WorkflowName - }; - // Re-encode the domain events back to proto events for forwarding - // The forwarded events are already proto-sourced; they were stored as domain events - // so we cannot round-trip them here without the original proto. - // Instead, forward empty event lists — the metadata (appId/instanceId/workflowName) - // is the primary useful content for filtering. - yield return ancestorSegment; - } - } - } - - /// - /// Converts a proto message to a domain . - /// - private static PropagatedHistoryEntry ConvertSegment(PropagatedHistorySegment segment) - { - var events = segment.Events - .Select(e => new PropagatedHistoryEvent(e.EventId, MapEventKind(e), MapTimestamp(e.Timestamp))) - .ToList(); - - return new PropagatedHistoryEntry( - segment.AppId, - segment.InstanceId, - segment.WorkflowName, - events); - } - - /// - /// Maps a proto to a . - /// - private static HistoryEventKind MapEventKind(HistoryEvent e) => e.EventTypeCase switch - { - HistoryEvent.EventTypeOneofCase.ExecutionStarted => HistoryEventKind.ExecutionStarted, - HistoryEvent.EventTypeOneofCase.ExecutionCompleted => HistoryEventKind.ExecutionCompleted, - HistoryEvent.EventTypeOneofCase.ExecutionTerminated => HistoryEventKind.ExecutionTerminated, - HistoryEvent.EventTypeOneofCase.TaskScheduled => HistoryEventKind.TaskScheduled, - HistoryEvent.EventTypeOneofCase.TaskCompleted => HistoryEventKind.TaskCompleted, - HistoryEvent.EventTypeOneofCase.TaskFailed => HistoryEventKind.TaskFailed, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated => HistoryEventKind.SubOrchestrationInstanceCreated, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted => HistoryEventKind.SubOrchestrationInstanceCompleted, - HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed => HistoryEventKind.SubOrchestrationInstanceFailed, - HistoryEvent.EventTypeOneofCase.TimerCreated => HistoryEventKind.TimerCreated, - HistoryEvent.EventTypeOneofCase.TimerFired => HistoryEventKind.TimerFired, - HistoryEvent.EventTypeOneofCase.OrchestratorStarted => HistoryEventKind.OrchestratorStarted, - HistoryEvent.EventTypeOneofCase.OrchestratorCompleted => HistoryEventKind.OrchestratorCompleted, - HistoryEvent.EventTypeOneofCase.EventSent => HistoryEventKind.EventSent, - HistoryEvent.EventTypeOneofCase.EventRaised => HistoryEventKind.EventRaised, - HistoryEvent.EventTypeOneofCase.ContinueAsNew => HistoryEventKind.ContinueAsNew, - HistoryEvent.EventTypeOneofCase.ExecutionSuspended => HistoryEventKind.ExecutionSuspended, - HistoryEvent.EventTypeOneofCase.ExecutionResumed => HistoryEventKind.ExecutionResumed, - _ => HistoryEventKind.Unknown - }; - - /// - /// Converts a proto Timestamp to a . - /// - private static DateTimeOffset MapTimestamp(Google.Protobuf.WellKnownTypes.Timestamp? timestamp) => - timestamp is not null - ? new DateTimeOffset(timestamp.ToDateTime(), TimeSpan.Zero) - : DateTimeOffset.MinValue; } diff --git a/src/Dapr.Workflow/Worker/WorkflowWorker.cs b/src/Dapr.Workflow/Worker/WorkflowWorker.cs index 0d3d82158..8e9048c07 100644 --- a/src/Dapr.Workflow/Worker/WorkflowWorker.cs +++ b/src/Dapr.Workflow/Worker/WorkflowWorker.cs @@ -20,7 +20,6 @@ using Dapr.Common.Serialization; using Dapr.DurableTask.Protobuf; using Dapr.Workflow.Abstractions; -using Dapr.Workflow.Serialization; using Dapr.Workflow.Versioning; using Dapr.Workflow.Worker.Grpc; using Dapr.Workflow.Worker.Internal; @@ -41,14 +40,12 @@ internal sealed class WorkflowWorker( ILoggerFactory loggerFactory, IDaprSerializer workflowSerializer, IServiceProvider serviceProvider, - WorkflowRuntimeOptions options, IConfiguration? configuration = null) : BackgroundService { private readonly TaskHubSidecarService.TaskHubSidecarServiceClient _grpcClient = grpcClient ?? throw new ArgumentNullException(nameof(grpcClient)); private readonly IWorkflowsFactory _workflowsFactory = workflowsFactory ?? throw new ArgumentNullException(nameof(workflowsFactory)); private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); private readonly ILogger _logger = loggerFactory.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); - private readonly WorkflowRuntimeOptions _options = options ?? throw new ArgumentNullException(nameof(options)); private readonly IDaprSerializer _serializer = workflowSerializer ?? throw new ArgumentNullException(nameof(workflowSerializer)); private readonly string? _daprApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); @@ -64,10 +61,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { // Create the protocol handler - _protocolHandler = new GrpcProtocolHandler(_grpcClient, loggerFactory, _options.MaxConcurrentWorkflows, _options.MaxConcurrentActivities, _daprApiToken); + _protocolHandler = new GrpcProtocolHandler(_grpcClient, loggerFactory, _daprApiToken); // Start processing work items - await _protocolHandler.StartAsync(HandleOrchestratorResponseAsync, HandleActivityResponseAsync, stoppingToken); + await _protocolHandler.StartAsync(HandleWorkflowResponseAsync, HandleActivityResponseAsync, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { @@ -80,7 +77,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - private async Task HandleOrchestratorResponseAsync(OrchestratorRequest request, string completionToken) + private async Task HandleWorkflowResponseAsync(WorkflowRequest request, string completionToken) { _logger.LogWorkerWorkflowHandleOrchestratorRequestStart(request.InstanceId); @@ -100,19 +97,11 @@ private async Task HandleOrchestratorResponseAsync(Orchest if (request.RequiresHistoryStreaming) { - var streamRequest = new StreamInstanceHistoryRequest - { - InstanceId = request.InstanceId, - ExecutionId = request.ExecutionId, - ForWorkItemProcessing = true - }; - - using var call = _grpcClient.StreamInstanceHistory(streamRequest, CreateCallOptions(CancellationToken.None)); - while (await call.ResponseStream.MoveNext(CancellationToken.None).ConfigureAwait(false)) - { - var chunk = call.ResponseStream.Current.Events; - allPastEvents.AddRange(chunk); - } + var streamRequest = new GetInstanceHistoryRequest { InstanceId = request.InstanceId }; + + var result = await _grpcClient.GetInstanceHistoryAsync(streamRequest, CreateCallOptions()) + .ConfigureAwait(false); + allPastEvents.AddRange(result.Events); } // If the most recent event is `ExecutionTerminated`, acknowledge termination immediately. @@ -121,17 +110,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest if (latestEvent?.ExecutionTerminated != null) { - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Terminated + WorkflowStatus = OrchestrationStatus.Terminated } } } @@ -142,7 +131,7 @@ private async Task HandleOrchestratorResponseAsync(Orchest // This keeps the workflow paused while still committing the suspension event. if (latestEvent?.ExecutionSuspended != null) { - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken @@ -178,29 +167,19 @@ private async Task HandleOrchestratorResponseAsync(Orchest } } - if (string.IsNullOrEmpty(workflowName)) - { - foreach (var e in allPastEvents.Concat(request.NewEvents).Reverse()) - { - var state = e.HistoryState?.OrchestrationState; - if (state is null || string.IsNullOrEmpty(state.Name)) - continue; - - workflowName = state.Name; - if (string.IsNullOrEmpty(serializedInput) && !string.IsNullOrEmpty(state.Input)) - { - serializedInput = state.Input; - } - - break; - } - } + // if (string.IsNullOrEmpty(workflowName)) + // { + // foreach (var e in allPastEvents.Concat(request.NewEvents).Reverse()) + // { + // var state = e. + // } + // } if (string.IsNullOrEmpty(workflowName)) { foreach (var e in allPastEvents.Concat(request.NewEvents).Reverse()) { - var versionName = e.OrchestratorStarted?.Version?.Name; + var versionName = e.WorkflowStarted?.Version?.Name; if (!string.IsNullOrEmpty(versionName)) { workflowName = versionName; @@ -218,17 +197,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest else { _logger.LogWorkerWorkflowHandleOrchestratorRequestNotInRegistry(""); - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { IsNonRetriable = true, @@ -246,17 +225,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest if (routerRegistry is not null && !routerRegistry.Contains(workflowName)) { _logger.LogWorkerWorkflowHandleOrchestratorRequestNotInRegistry(workflowName); - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { IsNonRetriable = true, @@ -278,17 +257,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest { _logger.LogWorkerWorkflowHandleOrchestratorRequestActivationFailed(workflowActivationException, workflowName); - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { IsNonRetriable = true, @@ -304,17 +283,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest _logger.LogWorkerWorkflowHandleOrchestratorRequestNotInRegistry(workflowName); - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { IsNonRetriable = true, @@ -333,14 +312,14 @@ private async Task HandleOrchestratorResponseAsync(Orchest : DateTime.UtcNow; var currentTurnStartedEvent = request.NewEvents.Reverse() - .FirstOrDefault(e => e.OrchestratorStarted != null); + .FirstOrDefault(e => e.WorkflowStarted != null); var currentTurnTimestamp = currentTurnStartedEvent?.Timestamp?.ToDateTime() ?? currentUtcDateTime; // Initialize the context with the FULL history - var incomingPropagatedHistory = request.PropagatedHistory.Count > 0 - ? request.PropagatedHistory + var incomingPropagatedHistory = request.PropagatedHistory.Chunks.Count > 0 + ? request.PropagatedHistory.Chunks : null; var context = new WorkflowOrchestrationContext(workflowName, request.InstanceId, currentUtcDateTime, _serializer, loggerFactory, versionTracker, appId, request.ExecutionId, @@ -388,17 +367,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest // If the history processing caused a stall (e.g. via OnOrchestratorStarted), return immediately if (versionTracker.IsStalled) { - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Stalled, + WorkflowStatus = OrchestrationStatus.Stalled, Details = versionTracker.StalledEvent?.Description ?? "Workflow stalled due to patch mismatch." } @@ -408,7 +387,7 @@ private async Task HandleOrchestratorResponseAsync(Orchest } // Get all pending actions from the context - var response = new OrchestratorResponse + var response = new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken @@ -429,7 +408,7 @@ private async Task HandleOrchestratorResponseAsync(Orchest // If the workflow issued ContinueAsNew, it already queued a completion action; just return it. if (context.PendingActions.Any(a => - a.CompleteOrchestration?.OrchestrationStatus == OrchestrationStatus.ContinuedAsNew)) + a.CompleteWorkflow?.WorkflowStatus == OrchestrationStatus.ContinuedAsNew)) { _logger.LogWorkerWorkflowHandleOrchestratorRequestCompleted(workflowName, request.InstanceId); return response; @@ -454,23 +433,23 @@ private async Task HandleOrchestratorResponseAsync(Orchest var output = await runTask.ConfigureAwait(false); var outputJson = output != null ? _serializer.Serialize(output) : string.Empty; - response.Actions.Add(new OrchestratorAction + response.Actions.Add(new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { Result = outputJson, - OrchestrationStatus = OrchestrationStatus.Completed + WorkflowStatus = OrchestrationStatus.Completed } }); } catch (Exception ex) { // Report the failure as an action so Dapr records the workflow as FAILED - response.Actions.Add(new OrchestratorAction + response.Actions.Add(new WorkflowAction { - CompleteOrchestration = new CompleteOrchestrationAction + CompleteWorkflow = new CompleteWorkflowAction { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { IsNonRetriable = true, @@ -489,17 +468,17 @@ private async Task HandleOrchestratorResponseAsync(Orchest { _logger.LogWorkerWorkflowHandleOrchestratorRequestFailed(ex, request.InstanceId); - return new OrchestratorResponse + return new WorkflowResponse { InstanceId = request.InstanceId, CompletionToken = completionToken, Actions = { - new OrchestratorAction + new WorkflowAction { - CompleteOrchestration = new() + CompleteWorkflow = new() { - OrchestrationStatus = OrchestrationStatus.Failed, + WorkflowStatus = OrchestrationStatus.Failed, FailureDetails = new() { ErrorType = ex.GetType().FullName ?? "Exception", @@ -515,7 +494,7 @@ private async Task HandleOrchestratorResponseAsync(Orchest private async Task HandleActivityResponseAsync(ActivityRequest request, string completionToken) { - _logger.LogWorkerWorkflowHandleActivityRequestStart(request.Name, request.OrchestrationInstance?.InstanceId, request.TaskId); + _logger.LogWorkerWorkflowHandleActivityRequestStart(request.Name, request.WorkflowInstance?.InstanceId, request.TaskId); try { @@ -532,7 +511,7 @@ private async Task HandleActivityResponseAsync(ActivityRequest return new ActivityResponse { - InstanceId = request.OrchestrationInstance?.InstanceId ?? string.Empty, + InstanceId = request.WorkflowInstance?.InstanceId ?? string.Empty, TaskId = request.TaskId, CompletionToken = completionToken, FailureDetails = new() @@ -548,7 +527,7 @@ private async Task HandleActivityResponseAsync(ActivityRequest return new ActivityResponse { - InstanceId = request.OrchestrationInstance?.InstanceId ?? string.Empty, + InstanceId = request.WorkflowInstance?.InstanceId ?? string.Empty, TaskId = request.TaskId, CompletionToken = completionToken, FailureDetails = new() @@ -566,7 +545,7 @@ private async Task HandleActivityResponseAsync(ActivityRequest : request.TaskId.ToString(); var context = new WorkflowActivityContextImpl(activityIdentifier, - request.OrchestrationInstance?.InstanceId ?? string.Empty, taskExecutionKey); + request.WorkflowInstance?.InstanceId ?? string.Empty, taskExecutionKey); // Restore the trace context provided by the sidecar so Activity.Current is non-null using var traceActivity = StartActivityFromRequest(request); @@ -590,7 +569,7 @@ private async Task HandleActivityResponseAsync(ActivityRequest return new ActivityResponse { - InstanceId = request.OrchestrationInstance?.InstanceId ?? string.Empty, + InstanceId = request.WorkflowInstance?.InstanceId ?? string.Empty, TaskId = request.TaskId, Result = outputJson, CompletionToken = completionToken @@ -598,11 +577,11 @@ private async Task HandleActivityResponseAsync(ActivityRequest } catch (Exception ex) { - _logger.LogWorkerWorkflowHandleActivityRequestFailed(ex, request.Name, request.OrchestrationInstance?.InstanceId); + _logger.LogWorkerWorkflowHandleActivityRequestFailed(ex, request.Name, request.WorkflowInstance?.InstanceId); return new ActivityResponse { - InstanceId = request.OrchestrationInstance?.InstanceId ?? string.Empty, + InstanceId = request.WorkflowInstance?.InstanceId ?? string.Empty, TaskId = request.TaskId, CompletionToken = completionToken, FailureDetails = new() @@ -663,6 +642,6 @@ public override async Task StopAsync(CancellationToken cancellationToken) return act; } - private CallOptions CreateCallOptions(CancellationToken cancellationToken) => + private CallOptions CreateCallOptions(CancellationToken cancellationToken = default) => DaprClientUtilities.ConfigureGrpcCallOptions(typeof(WorkflowWorker).Assembly, _daprApiToken, cancellationToken); } diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index eb43d19a1..15f31f425 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -38,6 +38,7 @@ public sealed class WorkflowRuntimeOptions /// usage. /// /// Thrown when value is less than 1. + [Obsolete("This property is obsolete and no longer does anything - please use the options on the Dapr runtime instead. This property will be removed in a future SDK release.")] public int MaxConcurrentWorkflows { get => _maxConcurrentWorkflows; @@ -56,6 +57,7 @@ public int MaxConcurrentWorkflows /// memory usage. /// /// Thrown when value is less than 1. + [Obsolete("This property is obsolete and no longer does anything - please use the options on the Dapr runtime instead. This property will be removed in a future SDK release.")] public int MaxConcurrentActivities { get => _maxConcurrentActivities; diff --git a/test/Dapr.IntegrationTest.Workflow/ExternalEventDoesNotBlockConcurrencySlotTests.cs b/test/Dapr.IntegrationTest.Workflow/ExternalEventDoesNotBlockConcurrencySlotTests.cs deleted file mode 100644 index 8e01aa5a3..000000000 --- a/test/Dapr.IntegrationTest.Workflow/ExternalEventDoesNotBlockConcurrencySlotTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Harnesses; -using Dapr.Testcontainers.Xunit.Attributes; -using Dapr.Workflow; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Dapr.IntegrationTest.Workflow; - -/// -/// Verifies that workflows suspended on -/// do not occupy a concurrency slot, allowing additional workflows to run while the first -/// batch is waiting. -/// -public sealed class ExternalEventDoesNotBlockConcurrencySlotTests -{ - private const string WaitingStatus = "WaitingForEvent"; - private const string EventName = "ContinueSignal"; - - /// - /// With set to 3, schedule - /// 3 workflows that each wait on an external event, then schedule a 4th workflow and - /// confirm it completes before releasing the waiting ones. - /// - [MinimumDaprRuntimeFact("1.17")] - public async Task FourthWorkflow_ShouldComplete_WhileFirstThreeAreWaitingOnExternalEvent() - { - const int concurrencyLimit = 3; - - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - // Three workflows that will block on an external event. - var waitingIds = Enumerable.Range(0, concurrencyLimit) - .Select(_ => Guid.NewGuid().ToString()) - .ToArray(); - - // One workflow that should run immediately even though the limit is 3. - var fourthId = Guid.NewGuid().ToString(); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.MaxConcurrentWorkflows = concurrencyLimit; - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrEmpty(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); - - // Schedule all three waiting workflows and let them reach their suspended state. - await Task.WhenAll(waitingIds.Select(id => - daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(WaitForEventWorkflow), id, id))); - - using var waitCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - await Task.WhenAll(waitingIds.Select(id => - WaitForCustomStatusAsync(daprWorkflowClient, id, WaitingStatus, waitCts.Token))); - - // All three are now suspended. Schedule the fourth, which should not be blocked. - await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(EchoWorkflow), fourthId, fourthId); - - using var fourthCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - var fourthResult = await daprWorkflowClient.WaitForWorkflowCompletionAsync( - fourthId, getInputsAndOutputs: true, cancellation: fourthCts.Token); - - Assert.Equal(WorkflowRuntimeStatus.Completed, fourthResult.RuntimeStatus); - Assert.Equal(fourthId, fourthResult.ReadOutputAs()); - - // Release all three waiting workflows. - await Task.WhenAll(waitingIds.Select(id => - daprWorkflowClient.RaiseEventAsync(id, EventName, "released", - TestContext.Current.CancellationToken))); - - using var completionCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - var waitingResults = await Task.WhenAll(waitingIds.Select(id => - daprWorkflowClient.WaitForWorkflowCompletionAsync( - id, getInputsAndOutputs: true, cancellation: completionCts.Token))); - - foreach (var result in waitingResults) - { - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - Assert.Equal("released", result.ReadOutputAs()); - } - } - - private static async Task WaitForCustomStatusAsync( - DaprWorkflowClient client, - string instanceId, - string expectedStatus, - CancellationToken cancellationToken) - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - var state = await client.GetWorkflowStateAsync( - instanceId, getInputsAndOutputs: true, cancellation: cancellationToken); - if (state is not null && - string.Equals(state.ReadCustomStatusAs(), expectedStatus, StringComparison.Ordinal)) - return; - - await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken); - } - } - - /// Waits indefinitely for an external event then returns its payload. - private sealed class WaitForEventWorkflow : Workflow - { - public override async Task RunAsync(WorkflowContext context, string input) - { - context.SetCustomStatus(WaitingStatus); - return await context.WaitForExternalEventAsync(EventName); - } - } - - private sealed class EchoActivity : WorkflowActivity - { - public override Task RunAsync(WorkflowActivityContext context, string input) => - Task.FromResult(input); - } - - private sealed class EchoWorkflow : Workflow - { - public override async Task RunAsync(WorkflowContext context, string input) => - await context.CallActivityAsync(nameof(EchoActivity), input); - } -} diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index c8c1655e9..8d06d238a 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -1,417 +1,417 @@ -// ------------------------------------------------------------------------ -// Copyright 2026 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Harnesses; -using Dapr.Testcontainers.Xunit.Attributes; -using Dapr.Workflow; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Dapr.IntegrationTest.Workflow; - -/// -/// Integration tests for workflow history propagation. -/// -/// These tests verify the SDK-side API works end-to-end: -/// - Scheduling child workflows with a history propagation scope does not cause errors -/// - The parent and child workflows complete successfully -/// - The child can call GetPropagatedHistory() without error -/// -/// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires -/// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the -/// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. -/// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK -/// sets in the CreateSubOrchestrationAction proto message. -/// -public sealed class HistoryPropagationWorkflowTests -{ - /// - /// Verifies that scheduling a child workflow with - /// (the default) completes successfully. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() - { - var instanceId = Guid.NewGuid().ToString(); - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrWhiteSpace(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - var output = result.ReadOutputAs(); - Assert.NotNull(output); - // No scope set → child should report no propagated history - Assert.False(output.ChildReceivedPropagatedHistory); - Assert.Equal(0, output.PropagatedEntryCount); - } - - /// - /// Verifies that scheduling a child workflow with - /// does not produce any errors and both workflows complete. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() - { - var instanceId = Guid.NewGuid().ToString(); - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrWhiteSpace(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - } - - /// - /// Verifies that scheduling a child workflow with - /// does not produce any errors and both workflows complete. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() - { - var instanceId = Guid.NewGuid().ToString(); - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrWhiteSpace(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - } - - /// - /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with - /// None scope returns null, not an exception. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() - { - var instanceId = Guid.NewGuid().ToString(); - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrWhiteSpace(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - var output = result.ReadOutputAs(); - Assert.NotNull(output); - Assert.False(output.ChildReceivedPropagatedHistory); - } - - /// - /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation - /// fluent builder and that the child workflow completes successfully. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() - { - var instanceId = Guid.NewGuid().ToString(); - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.RegisterWorkflow(); - opt.RegisterWorkflow(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrWhiteSpace(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - - await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); - var result = await client.WaitForWorkflowCompletionAsync(instanceId, - cancellation: TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - } - - private sealed record PropagationTestResult( - bool ChildReceivedPropagatedHistory, - int PropagatedEntryCount); - - /// - /// Parent workflow that schedules a child with no propagation scope (default). - /// - private sealed class NoPropagationParent : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - var childId = $"{context.InstanceId}-child"; - return await context.CallChildWorkflowAsync( - nameof(PropagatedHistoryReceiver), - input: null, - options: new ChildWorkflowTaskOptions(InstanceId: childId)); - } - } - - /// - /// Parent workflow that runs an activity first (to build some history), then schedules a child - /// with OwnHistory propagation. - /// - private sealed class OwnHistoryPropagationParent : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - // Run an activity to build some history - await context.CallActivityAsync( - nameof(EchoActivity), "ping"); - - var childId = $"{context.InstanceId}-child"; - var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) - .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); - - return await context.CallChildWorkflowAsync( - nameof(PropagatedHistoryReceiver), - input: null, - options: childOptions); - } - } - - /// - /// Grandparent → middle → child lineage, each using Lineage propagation. - /// - private sealed class LineagePropagationParent : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - var childId = $"{context.InstanceId}-middle"; - var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) - .WithHistoryPropagation(HistoryPropagationScope.Lineage); - - return await context.CallChildWorkflowAsync( - nameof(LineagePropagationMiddle), - input: null, - options: childOptions); - } - } - - private sealed class LineagePropagationMiddle : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - var childId = $"{context.InstanceId}-leaf"; - var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) - .WithHistoryPropagation(HistoryPropagationScope.Lineage); - - var result = await context.CallChildWorkflowAsync( - nameof(PropagatedHistoryReceiver), - input: null, - options: childOptions); - - return result is not null; - } - } - - /// - /// Parent workflow that uses the fluent WithHistoryPropagation builder style. - /// - private sealed class FluentBuilderParent : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - var childId = $"{context.InstanceId}-child"; - - // Use fluent builder chaining - var options = new ChildWorkflowTaskOptions(InstanceId: childId) - .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); - - var result = await context.CallChildWorkflowAsync( - nameof(PropagatedHistoryReceiver), - input: null, - options: options); - - return result is not null; - } - } - - /// - /// Child workflow that inspects its propagated history and reports back what it found. - /// - private sealed class PropagatedHistoryReceiver : Workflow - { - public override async Task RunAsync(WorkflowContext context, object? input) - { - var propagated = context.GetPropagatedHistory(); - // Yield via a zero-duration timer so this sub-orchestration completes through - // a proper async round-trip with the sidecar rather than synchronously on its - // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous - // first-turn completion causes the parent to hang waiting for the completion - // event that the sidecar never delivers for same-turn completions. - await context.CreateTimer(TimeSpan.Zero); - return new PropagationTestResult( - ChildReceivedPropagatedHistory: propagated is not null, - PropagatedEntryCount: propagated?.Entries.Count ?? 0); - } - } - - private sealed class EchoActivity : WorkflowActivity - { - public override Task RunAsync(WorkflowActivityContext context, string input) - => Task.FromResult(input); - } - - private sealed class SimpleActivityWorkflow : Workflow - { - public override async Task RunAsync(WorkflowContext context, string input) - { - return await context.CallActivityAsync(nameof(EchoActivity), input); - } - } -} +// // ------------------------------------------------------------------------ +// // Copyright 2026 The Dapr Authors +// // Licensed under the Apache License, Version 2.0 (the "License"); +// // you may not use this file except in compliance with the License. +// // You may obtain a copy of the License at +// // http://www.apache.org/licenses/LICENSE-2.0 +// // Unless required by applicable law or agreed to in writing, software +// // distributed under the License is distributed on an "AS IS" BASIS, +// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// // See the License for the specific language governing permissions and +// // limitations under the License. +// // ------------------------------------------------------------------------ +// +// using Dapr.Testcontainers.Common; +// using Dapr.Testcontainers.Harnesses; +// using Dapr.Testcontainers.Xunit.Attributes; +// using Dapr.Workflow; +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Dapr.IntegrationTest.Workflow; +// +// /// +// /// Integration tests for workflow history propagation. +// /// +// /// These tests verify the SDK-side API works end-to-end: +// /// - Scheduling child workflows with a history propagation scope does not cause errors +// /// - The parent and child workflows complete successfully +// /// - The child can call GetPropagatedHistory() without error +// /// +// /// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires +// /// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the +// /// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. +// /// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK +// /// sets in the CreateSubOrchestrationAction proto message. +// /// +// public sealed class HistoryPropagationWorkflowTests +// { +// /// +// /// Verifies that scheduling a child workflow with +// /// (the default) completes successfully. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// var output = result.ReadOutputAs(); +// Assert.NotNull(output); +// // No scope set → child should report no propagated history +// Assert.False(output.ChildReceivedPropagatedHistory); +// Assert.Equal(0, output.PropagatedEntryCount); +// } +// +// /// +// /// Verifies that scheduling a child workflow with +// /// does not produce any errors and both workflows complete. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// opt.RegisterActivity(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// } +// +// /// +// /// Verifies that scheduling a child workflow with +// /// does not produce any errors and both workflows complete. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// } +// +// /// +// /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with +// /// None scope returns null, not an exception. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// var output = result.ReadOutputAs(); +// Assert.NotNull(output); +// Assert.False(output.ChildReceivedPropagatedHistory); +// } +// +// /// +// /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation +// /// fluent builder and that the child workflow completes successfully. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// } +// +// private sealed record PropagationTestResult( +// bool ChildReceivedPropagatedHistory, +// int PropagatedEntryCount); +// +// /// +// /// Parent workflow that schedules a child with no propagation scope (default). +// /// +// private sealed class NoPropagationParent : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var childId = $"{context.InstanceId}-child"; +// return await context.CallChildWorkflowAsync( +// nameof(PropagatedHistoryReceiver), +// input: null, +// options: new ChildWorkflowTaskOptions(InstanceId: childId)); +// } +// } +// +// /// +// /// Parent workflow that runs an activity first (to build some history), then schedules a child +// /// with OwnHistory propagation. +// /// +// private sealed class OwnHistoryPropagationParent : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// // Run an activity to build some history +// await context.CallActivityAsync( +// nameof(EchoActivity), "ping"); +// +// var childId = $"{context.InstanceId}-child"; +// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// +// return await context.CallChildWorkflowAsync( +// nameof(PropagatedHistoryReceiver), +// input: null, +// options: childOptions); +// } +// } +// +// /// +// /// Grandparent → middle → child lineage, each using Lineage propagation. +// /// +// private sealed class LineagePropagationParent : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var childId = $"{context.InstanceId}-middle"; +// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// .WithHistoryPropagation(HistoryPropagationScope.Lineage); +// +// return await context.CallChildWorkflowAsync( +// nameof(LineagePropagationMiddle), +// input: null, +// options: childOptions); +// } +// } +// +// private sealed class LineagePropagationMiddle : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var childId = $"{context.InstanceId}-leaf"; +// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// .WithHistoryPropagation(HistoryPropagationScope.Lineage); +// +// var result = await context.CallChildWorkflowAsync( +// nameof(PropagatedHistoryReceiver), +// input: null, +// options: childOptions); +// +// return result is not null; +// } +// } +// +// /// +// /// Parent workflow that uses the fluent WithHistoryPropagation builder style. +// /// +// private sealed class FluentBuilderParent : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var childId = $"{context.InstanceId}-child"; +// +// // Use fluent builder chaining +// var options = new ChildWorkflowTaskOptions(InstanceId: childId) +// .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// +// var result = await context.CallChildWorkflowAsync( +// nameof(PropagatedHistoryReceiver), +// input: null, +// options: options); +// +// return result is not null; +// } +// } +// +// /// +// /// Child workflow that inspects its propagated history and reports back what it found. +// /// +// private sealed class PropagatedHistoryReceiver : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var propagated = context.GetPropagatedHistory(); +// // Yield via a zero-duration timer so this sub-orchestration completes through +// // a proper async round-trip with the sidecar rather than synchronously on its +// // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous +// // first-turn completion causes the parent to hang waiting for the completion +// // event that the sidecar never delivers for same-turn completions. +// await context.CreateTimer(TimeSpan.Zero); +// return new PropagationTestResult( +// ChildReceivedPropagatedHistory: propagated is not null, +// PropagatedEntryCount: propagated?.Entries.Count ?? 0); +// } +// } +// +// private sealed class EchoActivity : WorkflowActivity +// { +// public override Task RunAsync(WorkflowActivityContext context, string input) +// => Task.FromResult(input); +// } +// +// private sealed class SimpleActivityWorkflow : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, string input) +// { +// return await context.CallActivityAsync(nameof(EchoActivity), input); +// } +// } +// } diff --git a/test/Dapr.IntegrationTest.Workflow/MaxConcurrentActivitiesTests.cs b/test/Dapr.IntegrationTest.Workflow/MaxConcurrentActivitiesTests.cs deleted file mode 100644 index 897c61e2c..000000000 --- a/test/Dapr.IntegrationTest.Workflow/MaxConcurrentActivitiesTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Harnesses; -using Dapr.Testcontainers.Xunit.Attributes; -using Dapr.Workflow; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Dapr.IntegrationTest.Workflow; - -public sealed class MaxConcurrentActivitiesTests -{ - /// - /// Verifies that = 1 limits - /// activity execution to a single concurrent activity even when the workflow fans out more. - /// - [MinimumDaprRuntimeFact("1.17")] - public async Task ShouldRespectMaxConcurrentActivitiesLimitOfOne() - { - const int limit = 1; - const int activityCount = 5; - - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - var workflowInstanceId = Guid.NewGuid().ToString(); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.MaxConcurrentActivities = limit; - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrEmpty(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - ConcurrencyTrackingActivity.Reset(); - - using var scope = testApp.CreateScope(); - var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); - - await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(FanOutWorkflow), workflowInstanceId, activityCount); - var result = await daprWorkflowClient.WaitForWorkflowCompletionAsync( - workflowInstanceId, true, TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - Assert.True( - ConcurrencyTrackingActivity.MaxObservedConcurrency <= limit, - $"Expected max concurrent activities <= {limit}, but observed {ConcurrencyTrackingActivity.MaxObservedConcurrency}"); - } - - /// - /// Verifies that = 3 allows up to - /// 3 concurrent activities and that all activities complete successfully. - /// - [MinimumDaprRuntimeFact("1.17")] - public async Task ShouldRespectMaxConcurrentActivitiesLimitOfThree() - { - const int limit = 3; - const int activityCount = 10; - - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - var workflowInstanceId = Guid.NewGuid().ToString(); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.MaxConcurrentActivities = limit; - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrEmpty(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - ConcurrencyTrackingActivity.Reset(); - - using var scope = testApp.CreateScope(); - var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); - - await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(FanOutWorkflow), workflowInstanceId, activityCount); - var result = await daprWorkflowClient.WaitForWorkflowCompletionAsync( - workflowInstanceId, true, TestContext.Current.CancellationToken); - - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - Assert.True( - ConcurrencyTrackingActivity.MaxObservedConcurrency <= limit, - $"Expected max concurrent activities <= {limit}, but observed {ConcurrencyTrackingActivity.MaxObservedConcurrency}"); - } - - /// - /// Tracks the maximum number of concurrently executing activity instances using a shared - /// static counter. Each activity holds for a brief period so concurrent executions can - /// accumulate and be observed. - /// - private sealed class ConcurrencyTrackingActivity : WorkflowActivity - { - private static int _currentConcurrent; - private static int _maxObservedConcurrent; - private static readonly object Lock = new(); - - public static int MaxObservedConcurrency - { - get - { - lock (Lock) - { - return _maxObservedConcurrent; - } - } - } - - public static void Reset() - { - lock (Lock) - { - _currentConcurrent = 0; - _maxObservedConcurrent = 0; - } - } - - public override async Task RunAsync(WorkflowActivityContext context, int input) - { - lock (Lock) - { - _currentConcurrent++; - if (_currentConcurrent > _maxObservedConcurrent) - _maxObservedConcurrent = _currentConcurrent; - } - - // Hold briefly so any concurrent activities can be observed accumulating. - await Task.Delay(TimeSpan.FromMilliseconds(300)); - - lock (Lock) - { - _currentConcurrent--; - } - - return input; - } - } - - private sealed class FanOutWorkflow : Workflow - { - public override async Task RunAsync(WorkflowContext context, int input) - { - var tasks = new Task[input]; - for (var i = 0; i < input; i++) - { - tasks[i] = context.CallActivityAsync(nameof(ConcurrencyTrackingActivity), i); - } - - return await Task.WhenAll(tasks); - } - } -} diff --git a/test/Dapr.IntegrationTest.Workflow/MaxConcurrentWorkflowsTests.cs b/test/Dapr.IntegrationTest.Workflow/MaxConcurrentWorkflowsTests.cs deleted file mode 100644 index cfec310a2..000000000 --- a/test/Dapr.IntegrationTest.Workflow/MaxConcurrentWorkflowsTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Harnesses; -using Dapr.Testcontainers.Xunit.Attributes; -using Dapr.Workflow; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Dapr.IntegrationTest.Workflow; - -public sealed class MaxConcurrentWorkflowsTests -{ - /// - /// Verifies that setting to 1 - /// does not deadlock the runtime and that all scheduled workflows eventually complete. - /// - [MinimumDaprRuntimeFact("1.17")] - public async Task ShouldCompleteAllWorkflowsWhenLimitIsOne() - { - const int workflowCount = 3; - - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - var workflowInstanceIds = Enumerable.Range(0, workflowCount) - .Select(_ => Guid.NewGuid().ToString()) - .ToArray(); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.MaxConcurrentWorkflows = 1; - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrEmpty(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); - - // Schedule all workflows concurrently. - await Task.WhenAll(workflowInstanceIds.Select(id => - daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(EchoWorkflow), id, id))); - - // Wait for all to finish and assert each completed successfully. - var results = await Task.WhenAll(workflowInstanceIds.Select(id => - daprWorkflowClient.WaitForWorkflowCompletionAsync(id, true, TestContext.Current.CancellationToken))); - - foreach (var (result, id) in results.Zip(workflowInstanceIds)) - { - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - Assert.Equal(id, result.ReadOutputAs()); - } - } - - /// - /// Verifies that a custom value - /// greater than 1 does not deadlock the runtime and that all scheduled workflows complete. - /// - [MinimumDaprRuntimeFact("1.17")] - public async Task ShouldCompleteAllWorkflowsWithCustomConcurrencyLimit() - { - const int limit = 2; - const int workflowCount = 5; - - var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); - var workflowInstanceIds = Enumerable.Range(0, workflowCount) - .Select(_ => Guid.NewGuid().ToString()) - .ToArray(); - - await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( - needsActorState: true, - cancellationToken: TestContext.Current.CancellationToken); - await environment.StartAsync(TestContext.Current.CancellationToken); - - var harness = new DaprHarnessBuilder(componentsDir) - .WithEnvironment(environment) - .BuildWorkflow(); - - await using var testApp = await DaprHarnessBuilder.ForHarness(harness) - .ConfigureServices(builder => - { - builder.Services.AddDaprWorkflowBuilder( - configureRuntime: opt => - { - opt.MaxConcurrentWorkflows = limit; - opt.RegisterWorkflow(); - opt.RegisterActivity(); - }, - configureClient: (sp, clientBuilder) => - { - var config = sp.GetRequiredService(); - var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; - if (!string.IsNullOrEmpty(grpcEndpoint)) - clientBuilder.UseGrpcEndpoint(grpcEndpoint); - }); - }) - .BuildAndStartAsync(); - - using var scope = testApp.CreateScope(); - var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); - - await Task.WhenAll(workflowInstanceIds.Select(id => - daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(EchoWorkflow), id, id))); - - var results = await Task.WhenAll(workflowInstanceIds.Select(id => - daprWorkflowClient.WaitForWorkflowCompletionAsync(id, true, TestContext.Current.CancellationToken))); - - foreach (var (result, id) in results.Zip(workflowInstanceIds)) - { - Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); - Assert.Equal(id, result.ReadOutputAs()); - } - } - - private sealed class EchoActivity : WorkflowActivity - { - public override Task RunAsync(WorkflowActivityContext context, string input) => - Task.FromResult(input); - } - - private sealed class EchoWorkflow : Workflow - { - public override async Task RunAsync(WorkflowContext context, string input) => - await context.CallActivityAsync(nameof(EchoActivity), input); - } -} diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs index d823b39ba..d96480882 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs @@ -72,7 +72,6 @@ public override Task CallChildWorkflowAsync(string workflowNam public override Microsoft.Extensions.Logging.ILogger CreateReplaySafeLogger() => new NullLogger(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; - public override PropagatedHistory? GetPropagatedHistory() => null; private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs index 30b0b49b6..6d8d53b93 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs @@ -78,7 +78,6 @@ public override Task CallChildWorkflowAsync(string workflowNam public override Microsoft.Extensions.Logging.ILogger CreateReplaySafeLogger() => new NullLogger(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; - public override PropagatedHistory? GetPropagatedHistory() => null; private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs index a15db1e98..9d1243585 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs @@ -47,7 +47,6 @@ public override Task CallChildWorkflowAsync(string workflowNam public override Microsoft.Extensions.Logging.ILogger CreateReplaySafeLogger() => new NullLogger(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; - public override PropagatedHistory? GetPropagatedHistory() => null; private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { diff --git a/test/Dapr.Workflow.Test/Client/ProtoConvertersTests.cs b/test/Dapr.Workflow.Test/Client/ProtoConvertersTests.cs index 30050462f..4460319fb 100644 --- a/test/Dapr.Workflow.Test/Client/ProtoConvertersTests.cs +++ b/test/Dapr.Workflow.Test/Client/ProtoConvertersTests.cs @@ -15,7 +15,6 @@ using Dapr.DurableTask.Protobuf; using Dapr.Workflow.Client; using Dapr.Common.Serialization; -using Dapr.Workflow.Serialization; using Google.Protobuf.WellKnownTypes; namespace Dapr.Workflow.Test.Client; @@ -57,11 +56,11 @@ public void ToWorkflowMetadata_ShouldMapCoreFields_AndPreserveSerializerInstance var createdUtc = new DateTime(2025, 01, 02, 03, 04, 05, DateTimeKind.Utc); var updatedUtc = new DateTime(2025, 02, 03, 04, 05, 06, DateTimeKind.Utc); - var state = new OrchestrationState + var state = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "instance-123", Name = "MyWorkflow", - OrchestrationStatus = OrchestrationStatus.Running, + WorkflowStatus = OrchestrationStatus.Running, CreatedTimestamp = Timestamp.FromDateTime(createdUtc), LastUpdatedTimestamp = Timestamp.FromDateTime(updatedUtc), Input = "{\"hello\":\"in\"}", @@ -89,11 +88,11 @@ public void ToWorkflowMetadata_ShouldSetMinValueTimestamps_WhenProtoTimestampsAr { var serializer = new JsonDaprSerializer(); - var state = new OrchestrationState + var state = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Pending, + WorkflowStatus = OrchestrationStatus.Pending, CreatedTimestamp = null, LastUpdatedTimestamp = null }; @@ -109,11 +108,11 @@ public void ToWorkflowMetadata_ShouldSetSerializedFieldsToNull_WhenProtoStringsA { var serializer = new JsonDaprSerializer(); - var state = new OrchestrationState + var state = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Completed, + WorkflowStatus = OrchestrationStatus.Completed, Input = "", Output = null, CustomStatus = "" @@ -131,11 +130,11 @@ public void ToWorkflowMetadata_ShouldKeepSerializedFields_WhenProtoStringsContai { var serializer = new JsonDaprSerializer(); - var state = new OrchestrationState + var state = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Completed, + WorkflowStatus = OrchestrationStatus.Completed, Input = " ", Output = "\t", CustomStatus = "\r\n" @@ -153,11 +152,11 @@ public void ToWorkflowMetadata_ShouldMapRuntimeStatus_UsingToRuntimeStatus() { var serializer = new JsonDaprSerializer(); - var state = new OrchestrationState + var state = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.ContinuedAsNew + WorkflowStatus = OrchestrationStatus.ContinuedAsNew }; var metadata = ProtoConverters.ToWorkflowMetadata(state, serializer); diff --git a/test/Dapr.Workflow.Test/Client/WorkflowGrpcClientTests.cs b/test/Dapr.Workflow.Test/Client/WorkflowGrpcClientTests.cs index bfd42c4c2..efa8b516e 100644 --- a/test/Dapr.Workflow.Test/Client/WorkflowGrpcClientTests.cs +++ b/test/Dapr.Workflow.Test/Client/WorkflowGrpcClientTests.cs @@ -139,7 +139,7 @@ public async Task GetWorkflowMetadataAsync_ShouldPassThroughGetInputsAndOutputsF .Returns(CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState { InstanceId = "i", Name = "n", OrchestrationStatus = OrchestrationStatus.Running } + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", WorkflowStatus = OrchestrationStatus.Running } })); var client = new WorkflowGrpcClient(grpcClientMock.Object, NullLogger.Instance, serializer); @@ -162,11 +162,11 @@ public async Task WaitForWorkflowStartAsync_ShouldReturnImmediately_WhenStatusIs .Returns(CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Running + WorkflowStatus = OrchestrationStatus.Running } })); @@ -204,11 +204,11 @@ public async Task WaitForWorkflowCompletionAsync_ShouldReturnImmediately_WhenSta .Returns(CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Completed, + WorkflowStatus = OrchestrationStatus.Completed, Output = "{\"ok\":true}" } })); @@ -557,11 +557,11 @@ public async Task WaitForWorkflowStartAsync_ShouldPollUntilStatusIsNotPending() return CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = status + WorkflowStatus = status } }); }); @@ -601,11 +601,11 @@ public async Task WaitForWorkflowCompletionAsync_ShouldPollUntilTerminalStatus() return CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = status, + WorkflowStatus = status, Output = status == OrchestrationStatus.Completed ? "\"done\"" : string.Empty } }); @@ -640,11 +640,11 @@ public async Task WaitForWorkflowCompletionAsync_ShouldReturnImmediately_WhenSta .Returns(CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = protoStatus + WorkflowStatus = protoStatus } })); @@ -685,11 +685,11 @@ public async Task WaitForWorkflowCompletionAsync_ShouldRespectCancellationToken_ .Returns(CreateAsyncUnaryCall(new GetInstanceResponse { Exists = true, - OrchestrationState = new OrchestrationState + WorkflowState = new Dapr.DurableTask.Protobuf.WorkflowState { InstanceId = "i", Name = "n", - OrchestrationStatus = OrchestrationStatus.Running + WorkflowStatus = OrchestrationStatus.Running } })); diff --git a/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs index 899e25fa9..8dbc68265 100644 --- a/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs +++ b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs @@ -271,7 +271,6 @@ private sealed class FakeWorkflowContext : WorkflowContext public override ILogger CreateReplaySafeLogger(string categoryName) => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; public override ILogger CreateReplaySafeLogger(Type type) => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; public override ILogger CreateReplaySafeLogger() => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; - public override PropagatedHistory? GetPropagatedHistory() => null; } private sealed class SingleEnumerationEnumerable(IEnumerable inner) : IEnumerable diff --git a/test/Dapr.Workflow.Test/Versioning/WorkflowVersionTrackerTests.cs b/test/Dapr.Workflow.Test/Versioning/WorkflowVersionTrackerTests.cs index 08b37d07c..266d58c7f 100644 --- a/test/Dapr.Workflow.Test/Versioning/WorkflowVersionTrackerTests.cs +++ b/test/Dapr.Workflow.Test/Versioning/WorkflowVersionTrackerTests.cs @@ -25,8 +25,8 @@ public void Constructor_ExtractsPatchesFromHistory() // Arrange var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "patch1" } } } }, - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "patch2" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "patch1" } } } }, + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "patch2" } } } } }; // Act @@ -42,8 +42,8 @@ public void Constructor_ExtractsPatchesFromHistory_PreservingOrder() // Arrange var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "patch1" } } } }, - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "patch2" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "patch1" } } } }, + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "patch2" } } } } }; // Act @@ -59,7 +59,7 @@ public void Constructor_ExtractsPatchesFromHistory_AllowsDuplicates() // Arrange var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "p1", "p1", "p2" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "p1", "p1", "p2" } } } } }; // Act @@ -86,7 +86,7 @@ public void RequestPatch_Replay_ReturnsTrueOnlyIfInHistory() { var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "patch1" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "patch1" } } } } }; var tracker = new WorkflowVersionTracker(history); @@ -115,7 +115,7 @@ public void RequestPatch_Replay_UsesHistoryMembership() { var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "p1", "p2" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "p1", "p2" } } } } }; var tracker = new WorkflowVersionTracker(history); @@ -130,7 +130,7 @@ public void RequestPatch_Replay_OrderMismatch_DoesNotStall() { var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "p1", "p2" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "p1", "p2" } } } } }; var tracker = new WorkflowVersionTracker(history); @@ -146,7 +146,7 @@ public void RequestPatch_Replay_OrderMismatch_DoesNotStall() // { // var history = new List // { - // new() { OrchestratorStarted = new() { Version = new() { Patches = { "p1", "p2" } } } } + // new() { WorkflowStarted = new() { Version = new() { Patches = { "p1", "p2" } } } } // }; // var tracker = new WorkflowVersionTracker(history); // @@ -159,7 +159,7 @@ public void RequestPatch_Replay_OrderMismatch_DoesNotStall() // } [Fact] - public void OnOrchestratorStarted_WithNullVersion_DoesNothing() + public void OnWorkflowStarted_WithNullVersion_DoesNothing() { var tracker = new WorkflowVersionTracker([]); @@ -183,7 +183,7 @@ public void RequestPatch_WhenAlreadyDeterminedFalse_ReturnsFalse() { var history = new List { - new() { OrchestratorStarted = new OrchestratorStartedEvent { Version = new OrchestrationVersion { Patches = { "p1" } } } } + new() { WorkflowStarted = new WorkflowStartedEvent { Version = new WorkflowVersion { Patches = { "p1" } } } } }; var tracker = new WorkflowVersionTracker(history); diff --git a/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs b/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs index 131d850a1..3537c7a71 100644 --- a/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs @@ -27,7 +27,7 @@ public sealed class GrpcProtocolHandlerTests private static async Task RunHandlerUntilAsync( GrpcProtocolHandler handler, - Func> workflowHandler, + Func> workflowHandler, Func> activityHandler, Task until, TimeSpan timeout) @@ -48,7 +48,7 @@ private static async Task RunHandlerUntilAsync( private static async Task RunHandlerUntilAsync( GrpcProtocolHandler handler, - Func> workflowHandler, + Func> workflowHandler, Func> activityHandler, Func untilCondition, TimeSpan timeout, @@ -92,26 +92,6 @@ public void Constructor_ShouldThrowArgumentNullException_WhenLoggerFactoryIsNull Assert.Throws(() => new GrpcProtocolHandler(grpcClient, null!)); } - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenMaxConcurrentWorkItemsIsNotPositive(int value) - { - var grpcClient = CreateGrpcClientMock().Object; - - Assert.Throws(() => new GrpcProtocolHandler(grpcClient, NullLoggerFactory.Instance, value, 1)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenMaxConcurrentActivitiesIsNotPositive(int value) - { - var grpcClient = CreateGrpcClientMock().Object; - - Assert.Throws(() => new GrpcProtocolHandler(grpcClient, NullLoggerFactory.Instance, 1, value)); - } - [Fact] public async Task StartAsync_ShouldCompleteOrchestratorTask_ForOrchestratorWorkItem() { @@ -121,7 +101,7 @@ public async Task StartAsync_ShouldCompleteOrchestratorTask_ForOrchestratorWorkI { new WorkItem { - OrchestratorRequest = new OrchestratorRequest { InstanceId = "i-1" } + WorkflowRequest = new WorkflowRequest { InstanceId = "i-1" } } }; @@ -129,18 +109,18 @@ public async Task StartAsync_ShouldCompleteOrchestratorTask_ForOrchestratorWorkI .Setup(x => x.GetWorkItems(It.IsAny(), It.IsAny())) .Returns(CreateServerStreamingCall(workItems)); - var completedTcs = CreateTcs(); + var completedTcs = CreateTcs(); grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) - .Callback((r, _) => completedTcs.TrySetResult(r)) + .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Callback((r, _) => completedTcs.TrySetResult(r)) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); var handler = new GrpcProtocolHandler(grpcClientMock.Object, NullLoggerFactory.Instance); await RunHandlerUntilAsync( handler, - workflowHandler: (req, _) => Task.FromResult(new OrchestratorResponse { InstanceId = req.InstanceId }), + workflowHandler: (req, _) => Task.FromResult(new WorkflowResponse { InstanceId = req.InstanceId }), activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), until: completedTcs.Task, timeout: TimeSpan.FromSeconds(2)); @@ -163,7 +143,7 @@ public async Task StartAsync_ShouldCompleteActivityTask_ForActivityWorkItem() { Name = "act", TaskId = 42, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-2" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-2" } }, CompletionToken = completionToken } @@ -184,10 +164,10 @@ public async Task StartAsync_ShouldCompleteActivityTask_ForActivityWorkItem() await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (req, tok) => Task.FromResult(new ActivityResponse { - InstanceId = req.OrchestrationInstance.InstanceId, + InstanceId = req.WorkflowInstance.InstanceId, TaskId = req.TaskId, Result = "ok", CompletionToken = tok @@ -211,7 +191,7 @@ public async Task StartAsync_ShouldSendFailureResult_WhenOrchestratorHandlerThro { new WorkItem { - OrchestratorRequest = new OrchestratorRequest { InstanceId = "i-err" } + WorkflowRequest = new WorkflowRequest { InstanceId = "i-err" } } }; @@ -219,11 +199,11 @@ public async Task StartAsync_ShouldSendFailureResult_WhenOrchestratorHandlerThro .Setup(x => x.GetWorkItems(It.IsAny(), It.IsAny())) .Returns(CreateServerStreamingCall(workItems)); - var completedTcs = CreateTcs(); + var completedTcs = CreateTcs(); grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) - .Callback((r, _) => completedTcs.TrySetResult(r)) + .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Callback((r, _) => completedTcs.TrySetResult(r)) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); var handler = new GrpcProtocolHandler(grpcClientMock.Object, NullLoggerFactory.Instance); @@ -239,10 +219,10 @@ await RunHandlerUntilAsync( Assert.Equal("i-err", completed.InstanceId); Assert.Single(completed.Actions); - Assert.NotNull(completed.Actions[0].CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Failed, completed.Actions[0].CompleteOrchestration.OrchestrationStatus); - Assert.NotNull(completed.Actions[0].CompleteOrchestration.FailureDetails); - Assert.Contains("boom", completed.Actions[0].CompleteOrchestration.FailureDetails.ErrorMessage); + Assert.NotNull(completed.Actions[0].CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Failed, completed.Actions[0].CompleteWorkflow.WorkflowStatus); + Assert.NotNull(completed.Actions[0].CompleteWorkflow.FailureDetails); + Assert.Contains("boom", completed.Actions[0].CompleteWorkflow.FailureDetails.ErrorMessage); } [Fact] @@ -268,39 +248,6 @@ public async Task DisposeAsync_ShouldNotThrow_WhenCalledConcurrently() await Task.WhenAll(t1, t2); } - [Fact] - public async Task StartAsync_ShouldSendGetWorkItemsRequest_WithConfiguredConcurrencyLimits() - { - var grpcClientMock = CreateGrpcClientMock(); - - GetWorkItemsRequest? captured = null; - - grpcClientMock - .Setup(x => x.GetWorkItems(It.IsAny(), It.IsAny())) - .Returns((GetWorkItemsRequest r, CallOptions _) => - { - captured = r; - return CreateServerStreamingCall(Array.Empty()); - }); - - var handler = new GrpcProtocolHandler( - grpcClientMock.Object, - NullLoggerFactory.Instance, - maxConcurrentWorkItems: 7, - maxConcurrentActivities: 9); - - await RunHandlerUntilAsync( - handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), - activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), - untilCondition: () => captured is not null, - timeout: TimeSpan.FromSeconds(2)); - - Assert.NotNull(captured); - Assert.Equal(7, captured!.MaxConcurrentOrchestrationWorkItems); - Assert.Equal(9, captured.MaxConcurrentActivityWorkItems); - } - [Fact] public async Task StartAsync_ShouldReturnWithoutThrowing_WhenGrpcStreamIsCancelled() { @@ -318,7 +265,7 @@ public async Task StartAsync_ShouldReturnWithoutThrowing_WhenGrpcStreamIsCancell cts.Cancel(); await handler.StartAsync( - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), cancellationToken: cts.Token); @@ -341,7 +288,7 @@ public async Task StartAsync_ShouldSendActivityFailureResult_WhenActivityHandler { Name = "act", TaskId = 123, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-1" } } } }; @@ -361,7 +308,7 @@ public async Task StartAsync_ShouldSendActivityFailureResult_WhenActivityHandler await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => throw new InvalidOperationException("boom"), until: sentTcs.Task, timeout: TimeSpan.FromSeconds(2)); @@ -388,7 +335,7 @@ public async Task StartAsync_ShouldNotThrow_WhenSendingActivityFailureResultAlso { Name = "act", TaskId = 123, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-1" } } } }; @@ -408,7 +355,7 @@ public async Task StartAsync_ShouldNotThrow_WhenSendingActivityFailureResultAlso await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => throw new Exception("boom"), untilCondition: () => completeAttempted, timeout: TimeSpan.FromSeconds(2)); @@ -429,7 +376,7 @@ public async Task StartAsync_ShouldUseCreateActivityFailureResult_WithNullStackT { Name = "act", TaskId = 123, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-1" } } } }; @@ -449,7 +396,7 @@ public async Task StartAsync_ShouldUseCreateActivityFailureResult_WithNullStackT await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => throw new NullStackTraceException("boom"), until: sentTcs.Task, timeout: TimeSpan.FromSeconds(2)); @@ -474,7 +421,7 @@ public async Task StartAsync_ShouldNotCallCompleteActivityTask_WhenActivityHandl { Name = "act", TaskId = 1, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-1" } } } }; @@ -493,7 +440,7 @@ public async Task StartAsync_ShouldNotCallCompleteActivityTask_WhenActivityHandl using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); await handler.StartAsync( - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => { // Make StartAsync's linked token "IsCancellationRequested == true" @@ -516,7 +463,7 @@ public async Task StartAsync_ShouldNotCallCompleteOrchestratorTask_WhenWorkflowH { new WorkItem { - OrchestratorRequest = new OrchestratorRequest { InstanceId = "i-1" } + WorkflowRequest = new WorkflowRequest { InstanceId = "i-1" } } }; @@ -525,7 +472,7 @@ public async Task StartAsync_ShouldNotCallCompleteOrchestratorTask_WhenWorkflowH .Returns(CreateServerStreamingCallIgnoringCancellation(workItems)); grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); var handler = new GrpcProtocolHandler(grpcClientMock.Object, NullLoggerFactory.Instance); @@ -543,7 +490,7 @@ await handler.StartAsync( cancellationToken: cts.Token); grpcClientMock.Verify( - x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny()), + x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny()), Times.Never()); } @@ -556,9 +503,9 @@ public async Task StartAsync_ShouldCleanupCompletedActiveWorkItems_WhenActiveWor // We send 3 work items that complete quickly so RemoveAll(t => t.IsCompleted) is executed. var workItems = new[] { - new WorkItem { ActivityRequest = new ActivityRequest { Name = "a1", TaskId = 1, OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" } } }, - new WorkItem { ActivityRequest = new ActivityRequest { Name = "a2", TaskId = 2, OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" } } }, - new WorkItem { ActivityRequest = new ActivityRequest { Name = "a3", TaskId = 3, OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" } } }, + new WorkItem { ActivityRequest = new ActivityRequest { Name = "a1", TaskId = 1, WorkflowInstance = new WorkflowInstance { InstanceId = "i" } } }, + new WorkItem { ActivityRequest = new ActivityRequest { Name = "a2", TaskId = 2, WorkflowInstance = new WorkflowInstance { InstanceId = "i" } } }, + new WorkItem { ActivityRequest = new ActivityRequest { Name = "a3", TaskId = 3, WorkflowInstance = new WorkflowInstance { InstanceId = "i" } } }, }; grpcClientMock @@ -573,16 +520,14 @@ public async Task StartAsync_ShouldCleanupCompletedActiveWorkItems_WhenActiveWor var handler = new GrpcProtocolHandler( grpcClientMock.Object, - NullLoggerFactory.Instance, - maxConcurrentWorkItems: 1, - maxConcurrentActivities: 1); + NullLoggerFactory.Instance); await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (req, _) => Task.FromResult(new ActivityResponse { - InstanceId = req.OrchestrationInstance.InstanceId, + InstanceId = req.WorkflowInstance.InstanceId, TaskId = req.TaskId, Result = "ok" }), @@ -601,7 +546,7 @@ public async Task StartAsync_ShouldNotThrow_WhenWorkflowHandlerThrows_AndSending { new WorkItem { - OrchestratorRequest = new OrchestratorRequest { InstanceId = "i-1" } + WorkflowRequest = new WorkflowRequest { InstanceId = "i-1" } } }; @@ -612,7 +557,7 @@ public async Task StartAsync_ShouldNotThrow_WhenWorkflowHandlerThrows_AndSending var completeAttempted = false; grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) .Callback(() => completeAttempted = true) .Throws(new RpcException(new Status(StatusCode.Unavailable, "nope"))); @@ -649,7 +594,7 @@ public async Task StartAsync_ShouldHandleUnknownWorkItemType_AndWaitForActiveTas await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), untilCondition: () => getWorkItemsCalled, timeout: TimeSpan.FromSeconds(2)); @@ -674,7 +619,7 @@ public async Task StartAsync_ShouldRethrow_WhenReceiveLoopThrowsBeforeAnyItemsAr await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), untilCondition: () => Volatile.Read(ref getWorkItemsCalls) >= 1, timeout: TimeSpan.FromSeconds(2)); @@ -706,7 +651,7 @@ public async Task StartAsync_ShouldRethrow_WhenReceiveLoopThrowsAfterFirstItemIs // and can be canceled cleanly by the test harness. await RunHandlerUntilAsync( handler, - workflowHandler: (_,_) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_,_) => Task.FromResult(new WorkflowResponse()), activityHandler: (_,_) => Task.FromResult(new ActivityResponse()), untilCondition: () => Volatile.Read(ref getWorkItemsCalls) >= 1, timeout: TimeSpan.FromSeconds(2)); @@ -714,151 +659,6 @@ await RunHandlerUntilAsync( Assert.True(Volatile.Read(ref getWorkItemsCalls) >= 1); } - [Fact] - public async Task StartAsync_ShouldNotExceedMaxConcurrentOrchestrationWorkItems() - { - const int maxConcurrent = 2; - const int totalItems = 3; - - var grpcClientMock = CreateGrpcClientMock(); - - grpcClientMock - .Setup(x => x.GetWorkItems(It.IsAny(), It.IsAny())) - .Returns(CreateServerStreamingCall(Enumerable.Range(1, totalItems) - .Select(i => new WorkItem - { - OrchestratorRequest = new OrchestratorRequest { InstanceId = $"i-{i}" } - }))); - - grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) - .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); - - var activeCount = 0; - var completedCount = 0; - using var releaseGate = new SemaphoreSlim(0); - var maxConcurrentReachedTcs = CreateTcs(); - - var handler = new GrpcProtocolHandler( - grpcClientMock.Object, - NullLoggerFactory.Instance, - maxConcurrentWorkItems: maxConcurrent, - maxConcurrentActivities: 1); - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); - cts.CancelAfter(TimeSpan.FromSeconds(5)); - - var startTask = handler.StartAsync( - workflowHandler: async (req, _) => - { - var count = Interlocked.Increment(ref activeCount); - if (count == maxConcurrent) - maxConcurrentReachedTcs.TrySetResult(true); - - await releaseGate.WaitAsync(TestContext.Current.CancellationToken); - - Interlocked.Decrement(ref activeCount); - Interlocked.Increment(ref completedCount); - return new OrchestratorResponse { InstanceId = req.InstanceId }; - }, - activityHandler: (_, _) => Task.FromResult(new ActivityResponse()), - cancellationToken: cts.Token); - - // Wait for maxConcurrent handlers to be simultaneously active - await maxConcurrentReachedTcs.Task.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); - - // The orchestration semaphore prevents a 3rd handler from starting - Assert.Equal(maxConcurrent, Volatile.Read(ref activeCount)); - - // Release all handlers (including the one queued behind the semaphore) - releaseGate.Release(totalItems); - - // Wait for all to finish - var deadline = DateTime.UtcNow.AddSeconds(3); - while (Volatile.Read(ref completedCount) < totalItems && DateTime.UtcNow < deadline) - await Task.Delay(10, TestContext.Current.CancellationToken); - - cts.Cancel(); - await startTask; - - Assert.Equal(totalItems, Volatile.Read(ref completedCount)); - } - - [Fact] - public async Task StartAsync_ShouldNotExceedMaxConcurrentActivityWorkItems() - { - const int maxConcurrent = 2; - const int totalItems = 3; - - var grpcClientMock = CreateGrpcClientMock(); - - grpcClientMock - .Setup(x => x.GetWorkItems(It.IsAny(), It.IsAny())) - .Returns(CreateServerStreamingCall(Enumerable.Range(1, totalItems) - .Select(i => new WorkItem - { - ActivityRequest = new ActivityRequest - { - Name = $"act-{i}", - TaskId = i, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } - } - }))); - - grpcClientMock - .Setup(x => x.CompleteActivityTaskAsync(It.IsAny(), It.IsAny())) - .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); - - var activeCount = 0; - var completedCount = 0; - using var releaseGate = new SemaphoreSlim(0); - var maxConcurrentReachedTcs = CreateTcs(); - - var handler = new GrpcProtocolHandler( - grpcClientMock.Object, - NullLoggerFactory.Instance, - maxConcurrentWorkItems: 1, - maxConcurrentActivities: maxConcurrent); - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); - cts.CancelAfter(TimeSpan.FromSeconds(5)); - - var startTask = handler.StartAsync( - workflowHandler: (_, _) => Task.FromResult(new OrchestratorResponse()), - activityHandler: async (req, _) => - { - var count = Interlocked.Increment(ref activeCount); - if (count == maxConcurrent) - maxConcurrentReachedTcs.TrySetResult(true); - - await releaseGate.WaitAsync(TestContext.Current.CancellationToken); - - Interlocked.Decrement(ref activeCount); - Interlocked.Increment(ref completedCount); - return new ActivityResponse { InstanceId = req.OrchestrationInstance.InstanceId, TaskId = req.TaskId }; - }, - cancellationToken: cts.Token); - - // Wait for maxConcurrent handlers to be simultaneously active - await maxConcurrentReachedTcs.Task.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); - - // The activity semaphore prevents a 3rd handler from starting - Assert.Equal(maxConcurrent, Volatile.Read(ref activeCount)); - - // Release all handlers (including the one queued behind the semaphore) - releaseGate.Release(totalItems); - - // Wait for all to finish - var deadline = DateTime.UtcNow.AddSeconds(3); - while (Volatile.Read(ref completedCount) < totalItems && DateTime.UtcNow < deadline) - await Task.Delay(10, TestContext.Current.CancellationToken); - - cts.Cancel(); - await startTask; - - Assert.Equal(totalItems, Volatile.Read(ref completedCount)); - } - /// /// Regression test for: "Task failed: Status(StatusCode="Unknown", Detail="no such instance exists")" /// @@ -886,7 +686,7 @@ public async Task StartAsync_ShouldCallCompleteActivityTaskOnce_WhenActivitySucc { Name = "act", TaskId = 42, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i-1" } + WorkflowInstance = new WorkflowInstance { InstanceId = "i-1" } } } }; @@ -911,10 +711,10 @@ public async Task StartAsync_ShouldCallCompleteActivityTaskOnce_WhenActivitySucc await RunHandlerUntilAsync( handler, - workflowHandler: (_, _) => Task.FromResult(new OrchestratorResponse()), + workflowHandler: (_, _) => Task.FromResult(new WorkflowResponse()), activityHandler: (req, tok) => Task.FromResult(new ActivityResponse { - InstanceId = req.OrchestrationInstance.InstanceId, + InstanceId = req.WorkflowInstance.InstanceId, TaskId = req.TaskId, Result = "success", CompletionToken = tok @@ -945,7 +745,7 @@ public async Task StartAsync_ShouldCallCompleteOrchestratorTaskOnce_WhenWorkflow { new WorkItem { - OrchestratorRequest = new OrchestratorRequest { InstanceId = "i-1" } + WorkflowRequest = new WorkflowRequest { InstanceId = "i-1" } } }; @@ -954,11 +754,11 @@ public async Task StartAsync_ShouldCallCompleteOrchestratorTaskOnce_WhenWorkflow .Returns(CreateServerStreamingCall(workItems)); var completeCallCount = 0; - OrchestratorResponse? capturedResponse = null; + WorkflowResponse? capturedResponse = null; grpcClientMock - .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) - .Callback((r, _) => + .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Callback((r, _) => { Interlocked.Increment(ref completeCallCount); capturedResponse = r; @@ -969,7 +769,7 @@ public async Task StartAsync_ShouldCallCompleteOrchestratorTaskOnce_WhenWorkflow await RunHandlerUntilAsync( handler, - workflowHandler: (req, _) => Task.FromResult(new OrchestratorResponse { InstanceId = req.InstanceId }), + workflowHandler: (req, _) => Task.FromResult(new WorkflowResponse { InstanceId = req.InstanceId }), activityHandler: (_, _) => Task.FromResult(new ActivityResponse()), untilCondition: () => Volatile.Read(ref completeCallCount) >= 1, timeout: TimeSpan.FromSeconds(2)); @@ -979,7 +779,7 @@ await RunHandlerUntilAsync( Assert.Equal(1, Volatile.Read(ref completeCallCount)); Assert.NotNull(capturedResponse); Assert.DoesNotContain(capturedResponse!.Actions, - a => a.CompleteOrchestration?.OrchestrationStatus == OrchestrationStatus.Failed); + a => a.CompleteWorkflow?.WorkflowStatus == OrchestrationStatus.Failed); } [Fact] @@ -991,7 +791,7 @@ public async Task DelayOrStopAsync_ShouldSwallowCancellation_WhenTokenIsCanceled using var cts = new CancellationTokenSource(); cts.Cancel(); - var task = (Task)method.Invoke(null, new object?[] { TimeSpan.FromMilliseconds(1), cts.Token })!; + var task = (Task)method.Invoke(null, [TimeSpan.FromMilliseconds(1), cts.Token])!; await task; } diff --git a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs index f9f5ac3a2..50711bed9 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs @@ -15,7 +15,6 @@ using System.Text.Json; using Dapr.DurableTask.Protobuf; using Dapr.Common.Serialization; -using Dapr.Workflow.Serialization; using Dapr.Workflow.Versioning; using Dapr.Workflow.Worker; using Dapr.Workflow.Worker.Internal; @@ -50,7 +49,7 @@ public async Task CreateTimer_SetsTimerOriginCreateTimer() var action = Assert.Single(context.PendingActions); Assert.NotNull(action.CreateTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.OriginCreateTimer, action.CreateTimer.OriginCase); + Assert.Equal(CreateTimerAction.OriginOneofCase.CreateTimer, action.CreateTimer.OriginCase); } /// @@ -67,8 +66,8 @@ public async Task WaitForExternalEvent_FiniteTimeout_SetsTimerOriginExternalEven var timerAction = context.PendingActions .FirstOrDefault(a => a.CreateTimer != null); Assert.NotNull(timerAction); - Assert.Equal(CreateTimerAction.OriginOneofCase.OriginExternalEvent, timerAction!.CreateTimer!.OriginCase); - Assert.Equal("myEvent", timerAction.CreateTimer.OriginExternalEvent.Name); + Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); + Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); // Verify fireAt = startTime + timeout var expectedFireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(5)); @@ -98,7 +97,7 @@ public async Task ActivityRetry_SetsTimerOriginActivityRetry() inputType: typeof(object), run: (_, _) => throw new InvalidOperationException("boom"))); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -125,13 +124,13 @@ public async Task ActivityRetry_SetsTimerOriginActivityRetry() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); // Should have a retry timer with ActivityRetry origin var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); Assert.NotNull(retryTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.OriginActivityRetry, retryTimer!.CreateTimer!.OriginCase); - Assert.NotEmpty(retryTimer.CreateTimer.OriginActivityRetry.TaskExecutionId); + Assert.Equal(CreateTimerAction.OriginOneofCase.ActivityRetry, retryTimer!.CreateTimer!.OriginCase); + Assert.NotEmpty(retryTimer.CreateTimer.ActivityRetry.TaskExecutionId); } /// @@ -158,7 +157,7 @@ public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() run: (_, _) => throw new InvalidOperationException("boom"))); // History: first attempt fails, retry timer fires, second attempt fails - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -223,15 +222,15 @@ public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); // Should have a second retry timer with the same taskExecutionId as the first var retryTimers = response.Actions - .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.OriginActivityRetry) + .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ActivityRetry) .ToList(); Assert.Single(retryTimers); - Assert.NotEmpty(retryTimers[0].CreateTimer!.OriginActivityRetry.TaskExecutionId); + Assert.NotEmpty(retryTimers[0].CreateTimer!.ActivityRetry.TaskExecutionId); } /// @@ -255,7 +254,7 @@ public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() })); // We need the child to fail. The failure is indicated in history. - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -265,7 +264,7 @@ public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() new HistoryEvent { EventId = 0, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf", InstanceId = "child-0" @@ -274,7 +273,7 @@ public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() MakeOrchestratorStarted(StartTime.AddSeconds(1)), new HistoryEvent { - SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent { TaskScheduledId = 0, FailureDetails = new TaskFailureDetails @@ -287,13 +286,13 @@ public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); // Should have a retry timer with ChildWorkflowRetry origin var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); Assert.NotNull(retryTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.OriginChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); - Assert.NotEmpty(retryTimer.CreateTimer.OriginChildWorkflowRetry.InstanceId); + Assert.Equal(CreateTimerAction.OriginOneofCase.ChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); + Assert.NotEmpty(retryTimer.CreateTimer.ChildWorkflowRetry.InstanceId); } /// @@ -319,7 +318,7 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() // First attempt: child scheduled, created, and fails. // Then retry timer fires and second child scheduled, created, and fails. - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -329,7 +328,7 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() new HistoryEvent { EventId = 0, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf", InstanceId = "child-first" @@ -338,7 +337,7 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() MakeOrchestratorStarted(StartTime.AddSeconds(1)), new HistoryEvent { - SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent { TaskScheduledId = 0, FailureDetails = new TaskFailureDetails @@ -370,7 +369,7 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() new HistoryEvent { EventId = 2, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf", InstanceId = "child-second" @@ -379,7 +378,7 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() MakeOrchestratorStarted(StartTime.AddSeconds(3)), new HistoryEvent { - SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent { TaskScheduledId = 2, FailureDetails = new TaskFailureDetails @@ -392,17 +391,17 @@ public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); // Should have a second retry timer with the first child's instance ID var retryTimer = response.Actions.FirstOrDefault(a => - a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.OriginChildWorkflowRetry); + a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ChildWorkflowRetry); Assert.NotNull(retryTimer); // The instanceId should be stable — it should be the same value that was used // for the first retry timer (which we can't directly observe in this test since // the first timer was already consumed in history). But we can verify it's not empty. - Assert.NotEmpty(retryTimer!.CreateTimer!.OriginChildWorkflowRetry.InstanceId); + Assert.NotEmpty(retryTimer!.CreateTimer!.ChildWorkflowRetry.InstanceId); } // ===================================================================== @@ -421,8 +420,8 @@ public async Task WaitForExternalEvent_Indefinite_EmitsSentinelOptionalTimer() var timerAction = context.PendingActions .FirstOrDefault(a => a.CreateTimer != null); Assert.NotNull(timerAction); - Assert.Equal(CreateTimerAction.OriginOneofCase.OriginExternalEvent, timerAction!.CreateTimer!.OriginCase); - Assert.Equal("myEvent", timerAction.CreateTimer.OriginExternalEvent.Name); + Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); + Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); Assert.Equal(TimerOriginHelpers.ExternalEventIndefiniteFireAt, timerAction.CreateTimer.FireAt); } @@ -463,7 +462,7 @@ public async Task Replay_PostPatch_MatchesOptionalTimerNormally() return result; })); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -476,7 +475,7 @@ public async Task Replay_PostPatch_MatchesOptionalTimerNormally() TimerCreated = new TimerCreatedEvent { FireAt = TimerOriginHelpers.ExternalEventIndefiniteFireAt, - OriginExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } + ExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } } }, MakeOrchestratorStarted(StartTime.AddSeconds(1)), @@ -495,11 +494,11 @@ public async Task Replay_PostPatch_MatchesOptionalTimerNormally() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); } /// @@ -523,7 +522,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() run: (_, _) => Task.FromResult("result"))); // Pre-patch history: no optional timer, activity at EventId=0 - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -558,11 +557,11 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); // Verify no optional timer leaks into the result var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); @@ -587,7 +586,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() })); // Pre-patch history: no optional timer, child at EventId=0 - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -605,7 +604,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() new HistoryEvent { EventId = 0, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "Child", InstanceId = "child-1" @@ -617,7 +616,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() MakeOrchestratorStarted(StartTime.AddSeconds(2)), new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "\"childResult\"" @@ -626,11 +625,11 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); } /// @@ -652,7 +651,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() })); // Pre-patch history: no optional timer, user timer at EventId=0 - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -673,7 +672,7 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() TimerCreated = new TimerCreatedEvent { FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)), - OriginCreateTimer = new TimerOriginCreateTimer() + CreateTimer = new TimerOriginCreateTimer() } }, MakeOrchestratorStarted(StartTime.AddSeconds(6)), @@ -688,11 +687,11 @@ public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); } /// @@ -723,7 +722,7 @@ public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() // Pre-patch history: no optional timers. // ActA at EventId=0, ActB at EventId=1. - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -781,11 +780,11 @@ public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); // Verify no optional timers leak into the result var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); @@ -813,7 +812,6 @@ private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorke { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); var callInvoker = new Mock(MockBehavior.Loose); @@ -824,8 +822,7 @@ private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorke factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); return (worker, factory); } @@ -848,20 +845,20 @@ private static HistoryEvent MakeOrchestratorStarted(DateTime timestamp) return new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(timestamp), - OrchestratorStarted = new OrchestratorStartedEvent() + WorkflowStarted = new WorkflowStartedEvent() }; } private const string CompletionTokenValue = "abc123"; - private static async Task InvokeHandleOrchestratorResponseAsync( - WorkflowWorker worker, OrchestratorRequest request) + private static async Task InvokeHandleWorkflowResponseAsync( + WorkflowWorker worker, WorkflowRequest request) { - var method = typeof(WorkflowWorker).GetMethod("HandleOrchestratorResponseAsync", + var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(method); - var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; + var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; return await task; } diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs index eb9a21b32..9126be847 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs @@ -1,483 +1,483 @@ -// ------------------------------------------------------------------------ -// Copyright 2026 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using System.Text.Json; -using Dapr.Common.Serialization; -using Dapr.DurableTask.Protobuf; -using Dapr.Testcontainers.Xunit.Attributes; -using Dapr.Workflow.Versioning; -using Dapr.Workflow.Worker.Internal; -using Google.Protobuf.WellKnownTypes; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Dapr.Workflow.Test.Worker.Internal; - -/// -/// Tests for workflow history propagation via WorkflowOrchestrationContext. -/// -public class WorkflowHistoryPropagationTests -{ - private static WorkflowOrchestrationContext CreateContext( - string name = "TestWorkflow", - string instanceId = "instance-1", - string? appId = null, - IReadOnlyList? ownHistory = null, - IEnumerable? incomingPropagatedHistory = null) - { - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var tracker = new WorkflowVersionTracker([]); - return new WorkflowOrchestrationContext( - name: name, - instanceId: instanceId, - currentUtcDateTime: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), - workflowSerializer: serializer, - loggerFactory: NullLoggerFactory.Instance, - versionTracker: tracker, - appId: appId, - ownHistory: ownHistory, - incomingPropagatedHistory: incomingPropagatedHistory); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_ReturnsNull_WhenNoHistoryPropagated() - { - var context = CreateContext(); - Assert.Null(context.GetPropagatedHistory()); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_ReturnsNull_WhenEmptyPropagatedHistoryProvided() - { - var context = CreateContext(incomingPropagatedHistory: []); - Assert.Null(context.GetPropagatedHistory()); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_ReturnsSingleEntry_WhenOneSegmentPropagated() - { - var segment = new PropagatedHistorySegment - { - AppId = "parent-app", - InstanceId = "parent-instance", - WorkflowName = "ParentWorkflow" - }; - segment.Events.Add(MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWorkflow" })); - - var context = CreateContext(incomingPropagatedHistory: [segment]); - - var history = context.GetPropagatedHistory(); - - Assert.NotNull(history); - Assert.Single(history.Entries); - var entry = history.Entries[0]; - Assert.Equal("parent-app", entry.AppId); - Assert.Equal("parent-instance", entry.InstanceId); - Assert.Equal("ParentWorkflow", entry.WorkflowName); - Assert.Single(entry.Events); - Assert.Equal(HistoryEventKind.ExecutionStarted, entry.Events[0].Kind); - Assert.Equal(1, entry.Events[0].EventId); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_ReturnsMultipleEntries_ForLineagePropagation() - { - var parent = new PropagatedHistorySegment - { - AppId = "app-a", InstanceId = "inst-parent", WorkflowName = "ParentWf" - }; - var grandparent = new PropagatedHistorySegment - { - AppId = "app-b", InstanceId = "inst-grandparent", WorkflowName = "GrandparentWf" - }; - - var context = CreateContext(incomingPropagatedHistory: [parent, grandparent]); - - var history = context.GetPropagatedHistory(); - - Assert.NotNull(history); - Assert.Equal(2, history.Entries.Count); - Assert.Equal("inst-parent", history.Entries[0].InstanceId); - Assert.Equal("inst-grandparent", history.Entries[1].InstanceId); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_MapsAllKnownEventKinds() - { - var seg = new PropagatedHistorySegment { AppId = "app", InstanceId = "id", WorkflowName = "wf" }; - var kindMap = new Dictionary - { - { MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent()), HistoryEventKind.ExecutionStarted }, - { MakeEvent(2, e => e.ExecutionCompleted = new ExecutionCompletedEvent()), HistoryEventKind.ExecutionCompleted }, - { MakeEvent(3, e => e.ExecutionTerminated = new ExecutionTerminatedEvent()), HistoryEventKind.ExecutionTerminated }, - { MakeEvent(4, e => e.TaskScheduled = new TaskScheduledEvent { Name = "a" }), HistoryEventKind.TaskScheduled }, - { MakeEvent(5, e => e.TaskCompleted = new TaskCompletedEvent()), HistoryEventKind.TaskCompleted }, - { MakeEvent(6, e => e.TaskFailed = new TaskFailedEvent()), HistoryEventKind.TaskFailed }, - { MakeEvent(7, e => e.SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent()), HistoryEventKind.SubOrchestrationInstanceCreated }, - { MakeEvent(8, e => e.SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent()), HistoryEventKind.SubOrchestrationInstanceCompleted }, - { MakeEvent(9, e => e.SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent()), HistoryEventKind.SubOrchestrationInstanceFailed }, - { MakeEvent(10, e => e.TimerCreated = new TimerCreatedEvent()), HistoryEventKind.TimerCreated }, - { MakeEvent(11, e => e.TimerFired = new TimerFiredEvent()), HistoryEventKind.TimerFired }, - { MakeEvent(12, e => e.OrchestratorStarted = new OrchestratorStartedEvent()), HistoryEventKind.OrchestratorStarted }, - { MakeEvent(13, e => e.OrchestratorCompleted = new OrchestratorCompletedEvent()), HistoryEventKind.OrchestratorCompleted }, - { MakeEvent(14, e => e.EventSent = new EventSentEvent()), HistoryEventKind.EventSent }, - { MakeEvent(15, e => e.EventRaised = new EventRaisedEvent()), HistoryEventKind.EventRaised }, - { MakeEvent(16, e => e.ContinueAsNew = new ContinueAsNewEvent()), HistoryEventKind.ContinueAsNew }, - { MakeEvent(17, e => e.ExecutionSuspended = new ExecutionSuspendedEvent()), HistoryEventKind.ExecutionSuspended }, - { MakeEvent(18, e => e.ExecutionResumed = new ExecutionResumedEvent()), HistoryEventKind.ExecutionResumed }, - }; - - seg.Events.AddRange(kindMap.Keys); - var context = CreateContext(incomingPropagatedHistory: [seg]); - var history = context.GetPropagatedHistory()!; - var events = history.Entries[0].Events; - - foreach (var (protoEvent, expectedKind) in kindMap) - { - var mapped = events.FirstOrDefault(e => e.EventId == protoEvent.EventId); - Assert.NotNull(mapped); - Assert.Equal(expectedKind, mapped.Kind); - } - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_MapsTimestamp_Correctly() - { - var ts = new DateTimeOffset(2026, 3, 15, 10, 30, 0, TimeSpan.Zero); - var protoTs = Timestamp.FromDateTimeOffset(ts); - var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; - seg.Events.Add(new HistoryEvent - { - EventId = 1, - Timestamp = protoTs, - ExecutionStarted = new ExecutionStartedEvent() - }); - - var context = CreateContext(incomingPropagatedHistory: [seg]); - var entry = context.GetPropagatedHistory()!.Entries[0]; - - Assert.Equal(ts, entry.Events[0].Timestamp); - } - - [MinimumDaprRuntimeFact("1.18")] - public void GetPropagatedHistory_MapsUnknownEventType_ToUnknown() - { - var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; - // A HistoryEvent with no event type set → Unknown - seg.Events.Add(new HistoryEvent { EventId = 99 }); - - var context = CreateContext(incomingPropagatedHistory: [seg]); - var events = context.GetPropagatedHistory()!.Entries[0].Events; - - Assert.Equal(HistoryEventKind.Unknown, events[0].Kind); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterByAppId_ReturnsOnlyMatchingEntries() - { - var entries = new[] - { - new PropagatedHistoryEntry("app-a", "i1", "WfA", []), - new PropagatedHistoryEntry("app-b", "i2", "WfB", []), - new PropagatedHistoryEntry("APP-A", "i3", "WfA2", []), - }; - var history = new PropagatedHistory(entries); - - var filtered = history.FilterByAppId("app-a"); - - // Case-insensitive match - Assert.Equal(2, filtered.Entries.Count); - Assert.All(filtered.Entries, e => Assert.Equal("app-a", e.AppId, StringComparer.OrdinalIgnoreCase)); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterByInstanceId_ReturnsOnlyMatchingEntry() - { - PropagatedHistoryEntry[] entries = - [ - new("app", "instance-1", "Wf1", []), - new("app", "instance-2", "Wf2", []) - ]; - var history = new PropagatedHistory(entries); - - var filtered = history.FilterByInstanceId("instance-1"); - - Assert.Single(filtered.Entries); - Assert.Equal("instance-1", filtered.Entries[0].InstanceId); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterByInstanceId_IsCaseSensitive() - { - PropagatedHistoryEntry[] entries = [new("app", "Instance-1", "Wf", [])]; - var history = new PropagatedHistory(entries); - - // Exact case match - Assert.Single(history.FilterByInstanceId("Instance-1").Entries); - // Different case → no match - Assert.Empty(history.FilterByInstanceId("instance-1").Entries); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterByWorkflowName_ReturnsOnlyMatchingEntries() - { - PropagatedHistoryEntry[] entries = - [ - new("app", "i1", "PaymentWorkflow", []), - new("app", "i2", "OrderWorkflow", []), - new("app", "i3", "PaymentWorkflow", []) - ]; - var history = new PropagatedHistory(entries); - - var filtered = history.FilterByWorkflowName("PaymentWorkflow"); - - Assert.Equal(2, filtered.Entries.Count); - Assert.All(filtered.Entries, e => Assert.Equal("PaymentWorkflow", e.WorkflowName)); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterByWorkflowName_IsCaseSensitive() - { - PropagatedHistoryEntry[] entries = [new("app", "i", "PaymentWorkflow", [])]; - var history = new PropagatedHistory(entries); - - Assert.Single(history.FilterByWorkflowName("PaymentWorkflow").Entries); - Assert.Empty(history.FilterByWorkflowName("paymentworkflow").Entries); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterMethods_ReturnEmptyHistory_WhenNoMatches() - { - var history = new PropagatedHistory( - [ - new PropagatedHistoryEntry("app-a", "i1", "Wf1", []) - ]); - - Assert.Empty(history.FilterByAppId("app-z").Entries); - Assert.Empty(history.FilterByInstanceId("no-such-id").Entries); - Assert.Empty(history.FilterByWorkflowName("NoSuchWf").Entries); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterMethods_ThrowArgumentException_WhenNullOrWhitespace() - { - var history = new PropagatedHistory([]); - - // null throws ArgumentNullException (a subclass of ArgumentException) - Assert.ThrowsAny(() => history.FilterByAppId(null!)); - Assert.ThrowsAny(() => history.FilterByAppId("")); - Assert.ThrowsAny(() => history.FilterByAppId(" ")); - - Assert.ThrowsAny(() => history.FilterByInstanceId(null!)); - Assert.ThrowsAny(() => history.FilterByInstanceId("")); - - Assert.ThrowsAny(() => history.FilterByWorkflowName(null!)); - Assert.ThrowsAny(() => history.FilterByWorkflowName("")); - } - - [MinimumDaprRuntimeFact("1.18")] - public void FilterMethods_CanBeChained() - { - PropagatedHistoryEntry[] entries = - [ - new("app-a", "i1", "PaymentWorkflow", []), - new("app-a", "i2", "OrderWorkflow", []), - new("app-b", "i3", "PaymentWorkflow", []) - ]; - var history = new PropagatedHistory(entries); - - var filtered = history.FilterByAppId("app-a").FilterByWorkflowName("PaymentWorkflow"); - - Assert.Single(filtered.Entries); - Assert.Equal("i1", filtered.Entries[0].InstanceId); - } - - [MinimumDaprRuntimeFact("1.18")] - public void ChildWorkflowTaskOptions_DefaultPropagationScope_IsNone() - { - var options = new ChildWorkflowTaskOptions(); - Assert.Equal(HistoryPropagationScope.None, options.PropagationScope); - } - - [MinimumDaprRuntimeFact("1.18")] - public void WithHistoryPropagation_SetsPropagationScope_OwnHistory() - { - var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.OwnHistory); - Assert.Equal(HistoryPropagationScope.OwnHistory, options.PropagationScope); - } - - [MinimumDaprRuntimeFact("1.18")] - public void WithHistoryPropagation_SetsPropagationScope_Lineage() - { - var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.Lineage); - Assert.Equal(HistoryPropagationScope.Lineage, options.PropagationScope); - } - - [MinimumDaprRuntimeFact("1.18")] - public void WithHistoryPropagation_DoesNotMutateOriginalOptions() - { - var original = new ChildWorkflowTaskOptions(InstanceId: "id-1"); - var updated = original.WithHistoryPropagation(HistoryPropagationScope.OwnHistory); - - Assert.Equal(HistoryPropagationScope.None, original.PropagationScope); - Assert.Equal(HistoryPropagationScope.OwnHistory, updated.PropagationScope); - Assert.Equal("id-1", updated.InstanceId); - } - - [MinimumDaprRuntimeFact("1.18")] - public async Task CallChildWorkflowAsync_WithNone_DoesNotSetPropagationScope() - { - var context = CreateContext(instanceId: "parent", appId: "my-app"); - var childTask = context.CallChildWorkflowAsync( - "ChildWf", - options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.None)); - - // Complete the child synchronously via history - context.ProcessEvents([ - new HistoryEvent { EventId = 0, SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } }, - new HistoryEvent { SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent { TaskScheduledId = 0, Result = "99" } } - ], isReplaying: false); - - var action = context.PendingActions.OfType() - .Select(a => a.CreateSubOrchestration) - .FirstOrDefault(a => a is not null); - - // None: action either absent (cleared after history) or scope is None/unset - // The create action is removed from pending after history match - Assert.Empty(context.PendingActions); - Assert.Equal(99, await childTask); - } - - [MinimumDaprRuntimeFact("1.18")] - public void CallChildWorkflowAsync_WithOwnHistory_SetsPropagationScopeOnAction() - { - var ownHistory = new List - { - MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "TestWorkflow" }), - MakeEvent(2, e => e.TaskScheduled = new TaskScheduledEvent { Name = "SomeActivity" }), - }; - - var context = CreateContext(instanceId: "parent", appId: "my-app", ownHistory: ownHistory); - _ = context.CallChildWorkflowAsync( - "ChildWf", - options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); - - var action = context.PendingActions - .Select(a => a.CreateSubOrchestration) - .First(a => a is not null); - - Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); - Assert.Single(action.PropagatedHistory); - Assert.Equal("parent", action.PropagatedHistory[0].InstanceId); - Assert.Equal("my-app", action.PropagatedHistory[0].AppId); - Assert.Equal("TestWorkflow", action.PropagatedHistory[0].WorkflowName); - Assert.Equal(2, action.PropagatedHistory[0].Events.Count); - } - - [MinimumDaprRuntimeFact("1.18")] - public void CallChildWorkflowAsync_WithLineage_IncludesOwnAndAncestorHistory() - { - var grandparentSegment = new PropagatedHistorySegment - { - AppId = "grandparent-app", - InstanceId = "grandparent-inst", - WorkflowName = "GrandparentWf" - }; - - var ownHistory = new List - { - MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWf" }) - }; - - var context = CreateContext( - name: "ParentWf", - instanceId: "parent-inst", - appId: "parent-app", - ownHistory: ownHistory, - incomingPropagatedHistory: [grandparentSegment]); - - _ = context.CallChildWorkflowAsync( - "ChildWf", - options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); - - var action = context.PendingActions - .Select(a => a.CreateSubOrchestration) - .First(a => a is not null); - - Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.Lineage, action.HistoryPropagationScope); - // Own history segment + grandparent segment - Assert.Equal(2, action.PropagatedHistory.Count); - - var ownSeg = action.PropagatedHistory.First(s => s.InstanceId == "parent-inst"); - Assert.Equal("parent-app", ownSeg.AppId); - Assert.Equal("ParentWf", ownSeg.WorkflowName); - Assert.Single(ownSeg.Events); - - var ancestorSeg = action.PropagatedHistory.First(s => s.InstanceId == "grandparent-inst"); - Assert.Equal("grandparent-app", ancestorSeg.AppId); - Assert.Equal("GrandparentWf", ancestorSeg.WorkflowName); - } - - [MinimumDaprRuntimeFact("1.18")] - public void CallChildWorkflowAsync_WithOwnHistory_NoLineage_ExcludesAncestors() - { - var grandparentSegment = new PropagatedHistorySegment - { - AppId = "gp-app", InstanceId = "gp-inst", WorkflowName = "GpWf" - }; - - var context = CreateContext( - name: "ParentWf", - instanceId: "parent-inst", - appId: "parent-app", - incomingPropagatedHistory: [grandparentSegment]); - - _ = context.CallChildWorkflowAsync( - "ChildWf", - options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); - - var action = context.PendingActions - .Select(a => a.CreateSubOrchestration) - .First(a => a is not null); - - // Only own history, NOT grandparent - Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); - Assert.Single(action.PropagatedHistory); - Assert.Equal("parent-inst", action.PropagatedHistory[0].InstanceId); - } - - [MinimumDaprRuntimeFact("1.18")] - public void PropagatedHistory_Constructor_ThrowsOnNullEntries() - { - Assert.Throws(() => new PropagatedHistory(null!)); - } - - [MinimumDaprRuntimeFact("1.18")] - public void PropagatedHistory_Entries_ReflectsConstructorInput() - { - var entries = new[] { new PropagatedHistoryEntry("a", "b", "c", []) }; - var history = new PropagatedHistory(entries); - Assert.Single(history.Entries); - Assert.Same(entries[0], history.Entries[0]); - } - - private static HistoryEvent MakeEvent(int id, Action configure) - { - var e = new HistoryEvent - { - EventId = id, - Timestamp = Timestamp.FromDateTime(DateTime.UtcNow) - }; - configure(e); - return e; - } -} +// // ------------------------------------------------------------------------ +// // Copyright 2026 The Dapr Authors +// // Licensed under the Apache License, Version 2.0 (the "License"); +// // you may not use this file except in compliance with the License. +// // You may obtain a copy of the License at +// // http://www.apache.org/licenses/LICENSE-2.0 +// // Unless required by applicable law or agreed to in writing, software +// // distributed under the License is distributed on an "AS IS" BASIS, +// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// // See the License for the specific language governing permissions and +// // limitations under the License. +// // ------------------------------------------------------------------------ +// +// using System.Text.Json; +// using Dapr.Common.Serialization; +// using Dapr.DurableTask.Protobuf; +// using Dapr.Testcontainers.Xunit.Attributes; +// using Dapr.Workflow.Versioning; +// using Dapr.Workflow.Worker.Internal; +// using Google.Protobuf.WellKnownTypes; +// using Microsoft.Extensions.Logging.Abstractions; +// +// namespace Dapr.Workflow.Test.Worker.Internal; +// +// /// +// /// Tests for workflow history propagation via WorkflowOrchestrationContext. +// /// +// public class WorkflowHistoryPropagationTests +// { +// private static WorkflowOrchestrationContext CreateContext( +// string name = "TestWorkflow", +// string instanceId = "instance-1", +// string? appId = null, +// IReadOnlyList? ownHistory = null, +// IEnumerable? incomingPropagatedHistory = null) +// { +// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); +// var tracker = new WorkflowVersionTracker([]); +// return new WorkflowOrchestrationContext( +// name: name, +// instanceId: instanceId, +// currentUtcDateTime: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), +// workflowSerializer: serializer, +// loggerFactory: NullLoggerFactory.Instance, +// versionTracker: tracker, +// appId: appId, +// ownHistory: ownHistory, +// incomingPropagatedHistory: incomingPropagatedHistory); +// } +// +// // [MinimumDaprRuntimeFact("1.18")] +// // public void GetPropagatedHistory_ReturnsNull_WhenNoHistoryPropagated() +// // { +// // var context = CreateContext(); +// // Assert.Null(context.GetPropagatedHistory()); +// // } +// // +// // [MinimumDaprRuntimeFact("1.18")] +// // public void GetPropagatedHistory_ReturnsNull_WhenEmptyPropagatedHistoryProvided() +// // { +// // var context = CreateContext(incomingPropagatedHistory: []); +// // Assert.Null(context.GetPropagatedHistory()); +// // } +// // +// // [MinimumDaprRuntimeFact("1.18")] +// // public void GetPropagatedHistory_ReturnsSingleEntry_WhenOneSegmentPropagated() +// // { +// // var segment = new PropagatedHistorySegment +// // { +// // AppId = "parent-app", +// // InstanceId = "parent-instance", +// // WorkflowName = "ParentWorkflow" +// // }; +// // segment.Events.Add(MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWorkflow" })); +// // +// // var context = CreateContext(incomingPropagatedHistory: [segment]); +// // +// // var history = context.GetPropagatedHistory(); +// // +// // Assert.NotNull(history); +// // Assert.Single(history.Entries); +// // var entry = history.Entries[0]; +// // Assert.Equal("parent-app", entry.AppId); +// // Assert.Equal("parent-instance", entry.InstanceId); +// // Assert.Equal("ParentWorkflow", entry.WorkflowName); +// // Assert.Single(entry.Events); +// // Assert.Equal(HistoryEventKind.ExecutionStarted, entry.Events[0].Kind); +// // Assert.Equal(1, entry.Events[0].EventId); +// // } +// // +// // [MinimumDaprRuntimeFact("1.18")] +// // public void GetPropagatedHistory_ReturnsMultipleEntries_ForLineagePropagation() +// // { +// // var parent = new PropagatedHistorySegment +// // { +// // AppId = "app-a", InstanceId = "inst-parent", WorkflowName = "ParentWf" +// // }; +// // var grandparent = new PropagatedHistorySegment +// // { +// // AppId = "app-b", InstanceId = "inst-grandparent", WorkflowName = "GrandparentWf" +// // }; +// // +// // var context = CreateContext(incomingPropagatedHistory: [parent, grandparent]); +// // +// // var history = context.GetPropagatedHistory(); +// // +// // Assert.NotNull(history); +// // Assert.Equal(2, history.Entries.Count); +// // Assert.Equal("inst-parent", history.Entries[0].InstanceId); +// // Assert.Equal("inst-grandparent", history.Entries[1].InstanceId); +// // } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void GetPropagatedHistory_MapsAllKnownEventKinds() +// { +// var seg = new PropagatedHistorySegment { AppId = "app", InstanceId = "id", WorkflowName = "wf" }; +// var kindMap = new Dictionary +// { +// { MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent()), HistoryEventKind.ExecutionStarted }, +// { MakeEvent(2, e => e.ExecutionCompleted = new ExecutionCompletedEvent()), HistoryEventKind.ExecutionCompleted }, +// { MakeEvent(3, e => e.ExecutionTerminated = new ExecutionTerminatedEvent()), HistoryEventKind.ExecutionTerminated }, +// { MakeEvent(4, e => e.TaskScheduled = new TaskScheduledEvent { Name = "a" }), HistoryEventKind.TaskScheduled }, +// { MakeEvent(5, e => e.TaskCompleted = new TaskCompletedEvent()), HistoryEventKind.TaskCompleted }, +// { MakeEvent(6, e => e.TaskFailed = new TaskFailedEvent()), HistoryEventKind.TaskFailed }, +// { MakeEvent(7, e => e.SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent()), HistoryEventKind.SubOrchestrationInstanceCreated }, +// { MakeEvent(8, e => e.SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent()), HistoryEventKind.SubOrchestrationInstanceCompleted }, +// { MakeEvent(9, e => e.SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent()), HistoryEventKind.SubOrchestrationInstanceFailed }, +// { MakeEvent(10, e => e.TimerCreated = new TimerCreatedEvent()), HistoryEventKind.TimerCreated }, +// { MakeEvent(11, e => e.TimerFired = new TimerFiredEvent()), HistoryEventKind.TimerFired }, +// { MakeEvent(12, e => e.OrchestratorStarted = new OrchestratorStartedEvent()), HistoryEventKind.OrchestratorStarted }, +// { MakeEvent(13, e => e.OrchestratorCompleted = new OrchestratorCompletedEvent()), HistoryEventKind.OrchestratorCompleted }, +// { MakeEvent(14, e => e.EventSent = new EventSentEvent()), HistoryEventKind.EventSent }, +// { MakeEvent(15, e => e.EventRaised = new EventRaisedEvent()), HistoryEventKind.EventRaised }, +// { MakeEvent(16, e => e.ContinueAsNew = new ContinueAsNewEvent()), HistoryEventKind.ContinueAsNew }, +// { MakeEvent(17, e => e.ExecutionSuspended = new ExecutionSuspendedEvent()), HistoryEventKind.ExecutionSuspended }, +// { MakeEvent(18, e => e.ExecutionResumed = new ExecutionResumedEvent()), HistoryEventKind.ExecutionResumed }, +// }; +// +// seg.Events.AddRange(kindMap.Keys); +// var context = CreateContext(incomingPropagatedHistory: [seg]); +// var history = context.GetPropagatedHistory()!; +// var events = history.Entries[0].Events; +// +// foreach (var (protoEvent, expectedKind) in kindMap) +// { +// var mapped = events.FirstOrDefault(e => e.EventId == protoEvent.EventId); +// Assert.NotNull(mapped); +// Assert.Equal(expectedKind, mapped.Kind); +// } +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void GetPropagatedHistory_MapsTimestamp_Correctly() +// { +// var ts = new DateTimeOffset(2026, 3, 15, 10, 30, 0, TimeSpan.Zero); +// var protoTs = Timestamp.FromDateTimeOffset(ts); +// var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; +// seg.Events.Add(new HistoryEvent +// { +// EventId = 1, +// Timestamp = protoTs, +// ExecutionStarted = new ExecutionStartedEvent() +// }); +// +// var context = CreateContext(incomingPropagatedHistory: [seg]); +// var entry = context.GetPropagatedHistory()!.Entries[0]; +// +// Assert.Equal(ts, entry.Events[0].Timestamp); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void GetPropagatedHistory_MapsUnknownEventType_ToUnknown() +// { +// var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; +// // A HistoryEvent with no event type set → Unknown +// seg.Events.Add(new HistoryEvent { EventId = 99 }); +// +// var context = CreateContext(incomingPropagatedHistory: [seg]); +// var events = context.GetPropagatedHistory()!.Entries[0].Events; +// +// Assert.Equal(HistoryEventKind.Unknown, events[0].Kind); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterByAppId_ReturnsOnlyMatchingEntries() +// { +// var entries = new[] +// { +// new PropagatedHistoryEntry("app-a", "i1", "WfA", []), +// new PropagatedHistoryEntry("app-b", "i2", "WfB", []), +// new PropagatedHistoryEntry("APP-A", "i3", "WfA2", []), +// }; +// var history = new PropagatedHistory(entries); +// +// var filtered = history.FilterByAppId("app-a"); +// +// // Case-insensitive match +// Assert.Equal(2, filtered.Entries.Count); +// Assert.All(filtered.Entries, e => Assert.Equal("app-a", e.AppId, StringComparer.OrdinalIgnoreCase)); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterByInstanceId_ReturnsOnlyMatchingEntry() +// { +// PropagatedHistoryEntry[] entries = +// [ +// new("app", "instance-1", "Wf1", []), +// new("app", "instance-2", "Wf2", []) +// ]; +// var history = new PropagatedHistory(entries); +// +// var filtered = history.FilterByInstanceId("instance-1"); +// +// Assert.Single(filtered.Entries); +// Assert.Equal("instance-1", filtered.Entries[0].InstanceId); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterByInstanceId_IsCaseSensitive() +// { +// PropagatedHistoryEntry[] entries = [new("app", "Instance-1", "Wf", [])]; +// var history = new PropagatedHistory(entries); +// +// // Exact case match +// Assert.Single(history.FilterByInstanceId("Instance-1").Entries); +// // Different case → no match +// Assert.Empty(history.FilterByInstanceId("instance-1").Entries); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterByWorkflowName_ReturnsOnlyMatchingEntries() +// { +// PropagatedHistoryEntry[] entries = +// [ +// new("app", "i1", "PaymentWorkflow", []), +// new("app", "i2", "OrderWorkflow", []), +// new("app", "i3", "PaymentWorkflow", []) +// ]; +// var history = new PropagatedHistory(entries); +// +// var filtered = history.FilterByWorkflowName("PaymentWorkflow"); +// +// Assert.Equal(2, filtered.Entries.Count); +// Assert.All(filtered.Entries, e => Assert.Equal("PaymentWorkflow", e.WorkflowName)); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterByWorkflowName_IsCaseSensitive() +// { +// PropagatedHistoryEntry[] entries = [new("app", "i", "PaymentWorkflow", [])]; +// var history = new PropagatedHistory(entries); +// +// Assert.Single(history.FilterByWorkflowName("PaymentWorkflow").Entries); +// Assert.Empty(history.FilterByWorkflowName("paymentworkflow").Entries); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterMethods_ReturnEmptyHistory_WhenNoMatches() +// { +// var history = new PropagatedHistory( +// [ +// new PropagatedHistoryEntry("app-a", "i1", "Wf1", []) +// ]); +// +// Assert.Empty(history.FilterByAppId("app-z").Entries); +// Assert.Empty(history.FilterByInstanceId("no-such-id").Entries); +// Assert.Empty(history.FilterByWorkflowName("NoSuchWf").Entries); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterMethods_ThrowArgumentException_WhenNullOrWhitespace() +// { +// var history = new PropagatedHistory([]); +// +// // null throws ArgumentNullException (a subclass of ArgumentException) +// Assert.ThrowsAny(() => history.FilterByAppId(null!)); +// Assert.ThrowsAny(() => history.FilterByAppId("")); +// Assert.ThrowsAny(() => history.FilterByAppId(" ")); +// +// Assert.ThrowsAny(() => history.FilterByInstanceId(null!)); +// Assert.ThrowsAny(() => history.FilterByInstanceId("")); +// +// Assert.ThrowsAny(() => history.FilterByWorkflowName(null!)); +// Assert.ThrowsAny(() => history.FilterByWorkflowName("")); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void FilterMethods_CanBeChained() +// { +// PropagatedHistoryEntry[] entries = +// [ +// new("app-a", "i1", "PaymentWorkflow", []), +// new("app-a", "i2", "OrderWorkflow", []), +// new("app-b", "i3", "PaymentWorkflow", []) +// ]; +// var history = new PropagatedHistory(entries); +// +// var filtered = history.FilterByAppId("app-a").FilterByWorkflowName("PaymentWorkflow"); +// +// Assert.Single(filtered.Entries); +// Assert.Equal("i1", filtered.Entries[0].InstanceId); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void ChildWorkflowTaskOptions_DefaultPropagationScope_IsNone() +// { +// var options = new ChildWorkflowTaskOptions(); +// Assert.Equal(HistoryPropagationScope.None, options.PropagationScope); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void WithHistoryPropagation_SetsPropagationScope_OwnHistory() +// { +// var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// Assert.Equal(HistoryPropagationScope.OwnHistory, options.PropagationScope); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void WithHistoryPropagation_SetsPropagationScope_Lineage() +// { +// var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.Lineage); +// Assert.Equal(HistoryPropagationScope.Lineage, options.PropagationScope); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void WithHistoryPropagation_DoesNotMutateOriginalOptions() +// { +// var original = new ChildWorkflowTaskOptions(InstanceId: "id-1"); +// var updated = original.WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// +// Assert.Equal(HistoryPropagationScope.None, original.PropagationScope); +// Assert.Equal(HistoryPropagationScope.OwnHistory, updated.PropagationScope); +// Assert.Equal("id-1", updated.InstanceId); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task CallChildWorkflowAsync_WithNone_DoesNotSetPropagationScope() +// { +// var context = CreateContext(instanceId: "parent", appId: "my-app"); +// var childTask = context.CallChildWorkflowAsync( +// "ChildWf", +// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.None)); +// +// // Complete the child synchronously via history +// context.ProcessEvents([ +// new HistoryEvent { EventId = 0, SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } }, +// new HistoryEvent { SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent { TaskScheduledId = 0, Result = "99" } } +// ], isReplaying: false); +// +// var action = context.PendingActions.OfType() +// .Select(a => a.CreateSubOrchestration) +// .FirstOrDefault(a => a is not null); +// +// // None: action either absent (cleared after history) or scope is None/unset +// // The create action is removed from pending after history match +// Assert.Empty(context.PendingActions); +// Assert.Equal(99, await childTask); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void CallChildWorkflowAsync_WithOwnHistory_SetsPropagationScopeOnAction() +// { +// var ownHistory = new List +// { +// MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "TestWorkflow" }), +// MakeEvent(2, e => e.TaskScheduled = new TaskScheduledEvent { Name = "SomeActivity" }), +// }; +// +// var context = CreateContext(instanceId: "parent", appId: "my-app", ownHistory: ownHistory); +// _ = context.CallChildWorkflowAsync( +// "ChildWf", +// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); +// +// var action = context.PendingActions +// .Select(a => a.CreateSubOrchestration) +// .First(a => a is not null); +// +// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); +// Assert.Single(action.PropagatedHistory); +// Assert.Equal("parent", action.PropagatedHistory[0].InstanceId); +// Assert.Equal("my-app", action.PropagatedHistory[0].AppId); +// Assert.Equal("TestWorkflow", action.PropagatedHistory[0].WorkflowName); +// Assert.Equal(2, action.PropagatedHistory[0].Events.Count); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void CallChildWorkflowAsync_WithLineage_IncludesOwnAndAncestorHistory() +// { +// var grandparentSegment = new PropagatedHistorySegment +// { +// AppId = "grandparent-app", +// InstanceId = "grandparent-inst", +// WorkflowName = "GrandparentWf" +// }; +// +// var ownHistory = new List +// { +// MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWf" }) +// }; +// +// var context = CreateContext( +// name: "ParentWf", +// instanceId: "parent-inst", +// appId: "parent-app", +// ownHistory: ownHistory, +// incomingPropagatedHistory: [grandparentSegment]); +// +// _ = context.CallChildWorkflowAsync( +// "ChildWf", +// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); +// +// var action = context.PendingActions +// .Select(a => a.CreateSubOrchestration) +// .First(a => a is not null); +// +// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.Lineage, action.HistoryPropagationScope); +// // Own history segment + grandparent segment +// Assert.Equal(2, action.PropagatedHistory.Count); +// +// var ownSeg = action.PropagatedHistory.First(s => s.InstanceId == "parent-inst"); +// Assert.Equal("parent-app", ownSeg.AppId); +// Assert.Equal("ParentWf", ownSeg.WorkflowName); +// Assert.Single(ownSeg.Events); +// +// var ancestorSeg = action.PropagatedHistory.First(s => s.InstanceId == "grandparent-inst"); +// Assert.Equal("grandparent-app", ancestorSeg.AppId); +// Assert.Equal("GrandparentWf", ancestorSeg.WorkflowName); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void CallChildWorkflowAsync_WithOwnHistory_NoLineage_ExcludesAncestors() +// { +// var grandparentSegment = new PropagatedHistorySegment +// { +// AppId = "gp-app", InstanceId = "gp-inst", WorkflowName = "GpWf" +// }; +// +// var context = CreateContext( +// name: "ParentWf", +// instanceId: "parent-inst", +// appId: "parent-app", +// incomingPropagatedHistory: [grandparentSegment]); +// +// _ = context.CallChildWorkflowAsync( +// "ChildWf", +// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); +// +// var action = context.PendingActions +// .Select(a => a.CreateSubOrchestration) +// .First(a => a is not null); +// +// // Only own history, NOT grandparent +// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); +// Assert.Single(action.PropagatedHistory); +// Assert.Equal("parent-inst", action.PropagatedHistory[0].InstanceId); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void PropagatedHistory_Constructor_ThrowsOnNullEntries() +// { +// Assert.Throws(() => new PropagatedHistory(null!)); +// } +// +// [MinimumDaprRuntimeFact("1.18")] +// public void PropagatedHistory_Entries_ReflectsConstructorInput() +// { +// var entries = new[] { new PropagatedHistoryEntry("a", "b", "c", []) }; +// var history = new PropagatedHistory(entries); +// Assert.Single(history.Entries); +// Assert.Same(entries[0], history.Entries[0]); +// } +// +// private static HistoryEvent MakeEvent(int id, Action configure) +// { +// var e = new HistoryEvent +// { +// EventId = id, +// Timestamp = Timestamp.FromDateTime(DateTime.UtcNow) +// }; +// configure(e); +// return e; +// } +// } diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index 04690e2dd..9e762cad9 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -46,11 +46,11 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenCompletionCorrelatio new HistoryEvent { EventId = 0, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "42" @@ -91,7 +91,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenRuntimeUsesCreatedEv new HistoryEvent { EventId = 100, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf", InstanceId = childInstanceId @@ -99,7 +99,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenRuntimeUsesCreatedEv }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 100, Result = "21" @@ -115,7 +115,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenRuntimeUsesCreatedEv } [Fact] - public void CallChildWorkflowAsync_ShouldPutRouterOnCreateSubOrchestrationAction_WhenAppIdProvided() + public void CallChildWorkflowAsync_ShouldPutRouterOnCreateChildWorkflowAction_WhenAppIdProvided() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -138,11 +138,11 @@ public void CallChildWorkflowAsync_ShouldPutRouterOnCreateSubOrchestrationAction options: new ChildWorkflowTaskOptions(InstanceId: "child-1", TargetAppId: targetAppId)); var action = Assert.Single(context.PendingActions); - Assert.NotNull(action.CreateSubOrchestration); + Assert.NotNull(action.CreateChildWorkflow); - Assert.NotNull(action.CreateSubOrchestration.Router); - Assert.Equal(targetAppId, action.CreateSubOrchestration.Router.TargetAppID); - Assert.Equal(sourceAppId, action.CreateSubOrchestration.Router.SourceAppID); + Assert.NotNull(action.CreateChildWorkflow.Router); + Assert.Equal(targetAppId, action.CreateChildWorkflow.Router.TargetAppID); + Assert.Equal(sourceAppId, action.CreateChildWorkflow.Router.SourceAppID); // wrapper router may exist too (compat), but inner is the important one Assert.NotNull(action.Router); @@ -618,18 +618,18 @@ public async Task CallChildWorkflowAsync_ShouldRetry_WhenRetryPolicyProvided() var task = context.CallChildWorkflowAsync("ChildWf", options: options); var firstAction = Assert.Single(context.PendingActions); - Assert.NotNull(firstAction.CreateSubOrchestration); + Assert.NotNull(firstAction.CreateChildWorkflow); context.ProcessEvents( [ new HistoryEvent { EventId = firstAction.Id, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } }, new HistoryEvent { - SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent { TaskScheduledId = firstAction.Id, FailureDetails = new TaskFailureDetails @@ -660,18 +660,18 @@ public async Task CallChildWorkflowAsync_ShouldRetry_WhenRetryPolicyProvided() isReplaying: true); var retryAction = Assert.Single(context.PendingActions); - Assert.NotNull(retryAction.CreateSubOrchestration); + Assert.NotNull(retryAction.CreateChildWorkflow); context.ProcessEvents( [ new HistoryEvent { EventId = retryAction.Id, - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = retryAction.Id, Result = "\"ok\"" @@ -1161,7 +1161,7 @@ public void CreateReplaySafeLogger_GenericOverload_ShouldReturnReplaySafeLogger( } [Fact] - public void ContinueAsNew_ShouldAddCompleteOrchestrationAction_WithCarryoverEvents_WhenPreserveUnprocessedEventsIsTrue() + public void ContinueAsNew_ShouldAddCompleteWorkflowAction_WithCarryoverEvents_WhenPreserveUnprocessedEventsIsTrue() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -1190,10 +1190,10 @@ public void ContinueAsNew_ShouldAddCompleteOrchestrationAction_WithCarryoverEven Assert.Single(context.PendingActions); var action = context.PendingActions.First(); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.ContinuedAsNew, action.CompleteOrchestration.OrchestrationStatus); - Assert.Contains("\"v\":9", action.CompleteOrchestration.Result); - Assert.Equal(2, action.CompleteOrchestration.CarryoverEvents.Count); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.ContinuedAsNew, action.CompleteWorkflow.WorkflowStatus); + Assert.Contains("\"v\":9", action.CompleteWorkflow.Result); + Assert.Equal(2, action.CompleteWorkflow.CarryoverEvents.Count); } [Fact] @@ -1222,9 +1222,9 @@ public void ContinueAsNew_ShouldNotCarryOverEvents_WhenPreserveUnprocessedEvents Assert.Single(context.PendingActions); var action = context.PendingActions.First(); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.ContinuedAsNew, action.CompleteOrchestration.OrchestrationStatus); - Assert.Empty(action.CompleteOrchestration.CarryoverEvents); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.ContinuedAsNew, action.CompleteWorkflow.WorkflowStatus); + Assert.Empty(action.CompleteWorkflow.CarryoverEvents); } [Fact] @@ -1390,7 +1390,7 @@ public async Task WaitForExternalEventAsync_WithTimeoutOverload_ShouldReturnResu } [Fact] - public void CallChildWorkflowAsync_ShouldScheduleSubOrchestration_WhenNotReplaying_AndUseProvidedInstanceId() + public void CallChildWorkflowAsync_ShouldScheduleChildWorkflow_WhenNotReplaying_AndUseProvidedInstanceId() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -1413,14 +1413,14 @@ public void CallChildWorkflowAsync_ShouldScheduleSubOrchestration_WhenNotReplayi Assert.Single(context.PendingActions); var action = context.PendingActions.First(); - Assert.NotNull(action.CreateSubOrchestration); - Assert.Equal("ChildWf", action.CreateSubOrchestration.Name); - Assert.Equal("child-123", action.CreateSubOrchestration.InstanceId); - Assert.Contains("\"v\":1", action.CreateSubOrchestration.Input); + Assert.NotNull(action.CreateChildWorkflow); + Assert.Equal("ChildWf", action.CreateChildWorkflow.Name); + Assert.Equal("child-123", action.CreateChildWorkflow.InstanceId); + Assert.Contains("\"v\":1", action.CreateChildWorkflow.Input); } [Fact] - public async Task CallChildWorkflowAsync_ShouldReturnCompletedResult_FromHistorySubOrchestrationCompleted() + public async Task CallChildWorkflowAsync_ShouldReturnCompletedResult_FromHistoryChildWorkflowCompleted() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -1429,11 +1429,11 @@ public async Task CallChildWorkflowAsync_ShouldReturnCompletedResult_FromHistory { new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent { Result = "42" } + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { Result = "42" } } }; @@ -1454,7 +1454,7 @@ public async Task CallChildWorkflowAsync_ShouldReturnCompletedResult_FromHistory } [Fact] - public async Task CallChildWorkflowAsync_ShouldThrowWorkflowTaskFailedException_FromHistorySubOrchestrationFailed() + public async Task CallChildWorkflowAsync_ShouldThrowWorkflowTaskFailedException_FromHistoryChildWorkflowFailed() { var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); var tracker = new WorkflowVersionTracker([]); @@ -1463,11 +1463,11 @@ public async Task CallChildWorkflowAsync_ShouldThrowWorkflowTaskFailedException_ { new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } }, new HistoryEvent { - SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent { FailureDetails = new TaskFailureDetails { diff --git a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs index 24e02fd6b..c374babf8 100644 --- a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs +++ b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs @@ -47,7 +47,6 @@ public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_From var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); // W3C traceparent: version-traceId-spanId-flags const string expectedTraceId = "0af7651916cd43dd8448eb211c80319c"; @@ -79,15 +78,14 @@ public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_From factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 1, Input = string.Empty, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-1" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-1" }, ParentTraceContext = new TraceContext { TraceParent = traceParent, @@ -125,7 +123,6 @@ public async Task HandleActivityResponseAsync_ShouldPropagateTraceId_ToDownstrea var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); const string expectedTraceId = "4bf92f3577b34da6a3ce929d0e0e4736"; const string parentSpanId = "00f067aa0ba902b7"; @@ -148,15 +145,14 @@ public async Task HandleActivityResponseAsync_ShouldPropagateTraceId_ToDownstrea factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 2, Input = string.Empty, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-2" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-2" }, ParentTraceContext = new TraceContext { TraceParent = traceParent } }; @@ -179,7 +175,6 @@ public async Task HandleActivityResponseAsync_ShouldLeaveActivityCurrentNull_Whe var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); Activity? observedCurrent = null; @@ -197,8 +192,7 @@ public async Task HandleActivityResponseAsync_ShouldLeaveActivityCurrentNull_Whe factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Ensure the surrounding test runner hasn't left an ambient activity on this // async context that could mask a regression. @@ -209,7 +203,7 @@ public async Task HandleActivityResponseAsync_ShouldLeaveActivityCurrentNull_Whe Name = "act", TaskId = 3, Input = string.Empty, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-3" } + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-3" } }; var response = await InvokeHandleActivityResponseAsync(worker, request); @@ -231,7 +225,6 @@ public async Task HandleActivityResponseAsync_ShouldFallBackToSetParentId_WhenTr var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); const string malformedParentId = "not-a-valid-w3c-traceparent"; @@ -251,15 +244,14 @@ public async Task HandleActivityResponseAsync_ShouldFallBackToSetParentId_WhenTr factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 4, Input = string.Empty, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-4" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-4" }, ParentTraceContext = new TraceContext { TraceParent = malformedParentId } }; @@ -279,7 +271,6 @@ public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_With // Explicitly do NOT register any ActivityListener here to reproduce the real-world scenario. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); const string expectedTraceId = "0af7651916cd43dd8448eb211c80319c"; const string parentSpanId = "b7ad6b7169203331"; @@ -308,8 +299,7 @@ public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_With factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); Activity.Current = null; @@ -318,7 +308,7 @@ public async Task HandleActivityResponseAsync_ShouldPopulateActivityCurrent_With Name = "act", TaskId = 5, Input = string.Empty, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-5" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-5" }, ParentTraceContext = new TraceContext { TraceParent = traceParent, @@ -340,7 +330,7 @@ public void Constructor_ShouldThrowArgumentNullException_WhenGrpcClientIsNull() { Assert.Throws(() => new WorkflowWorker(null!, Mock.Of(), Mock.Of(), Mock.Of(), - new ServiceCollection().BuildServiceProvider(), new WorkflowRuntimeOptions())); + new ServiceCollection().BuildServiceProvider())); } [Fact] @@ -350,7 +340,7 @@ public void Constructor_ShouldThrowArgumentNullException_WhenWorkflowsFactoryIsN Assert.Throws(() => new WorkflowWorker(grpcClient, null!, Mock.Of(), Mock.Of(), - new ServiceCollection().BuildServiceProvider(), new WorkflowRuntimeOptions())); + new ServiceCollection().BuildServiceProvider())); } [Fact] @@ -360,7 +350,7 @@ public void Constructor_ShouldThrowArgumentNullException_WhenLoggerFactoryIsNull Assert.Throws(() => new WorkflowWorker(grpcClient, Mock.Of(), null!, Mock.Of(), - new ServiceCollection().BuildServiceProvider(), new WorkflowRuntimeOptions())); + new ServiceCollection().BuildServiceProvider())); } [Fact] @@ -370,7 +360,7 @@ public void Constructor_ShouldThrowArgumentNullException_WhenSerializerIsNull() Assert.Throws(() => new WorkflowWorker(grpcClient, Mock.Of(), Mock.Of(), null!, - new ServiceCollection().BuildServiceProvider(), new WorkflowRuntimeOptions())); + new ServiceCollection().BuildServiceProvider())); } [Fact] @@ -380,7 +370,7 @@ public void Constructor_ShouldThrowArgumentNullException_WhenServiceProviderIsNu Assert.Throws(() => new WorkflowWorker(grpcClient, Mock.Of(), Mock.Of(), Mock.Of(), - null!, new WorkflowRuntimeOptions())); + null!)); } [Fact] @@ -402,8 +392,7 @@ public async Task StopAsync_ShouldNotThrow_WhenProtocolHandlerWasNeverCreated() Mock.Of(), NullLoggerFactory.Instance, Mock.Of(), - new ServiceCollection().BuildServiceProvider(), - new WorkflowRuntimeOptions()); + new ServiceCollection().BuildServiceProvider()); await worker.StopAsync(CancellationToken.None); } @@ -417,10 +406,9 @@ public async Task StopAsync_ShouldDisposeProtocolHandler_WhenPresent() Mock.Of(), NullLoggerFactory.Instance, Mock.Of(), - new ServiceCollection().BuildServiceProvider(), - new WorkflowRuntimeOptions()); + new ServiceCollection().BuildServiceProvider()); - var protocolHandler = new GrpcProtocolHandler(CreateGrpcClientMock().Object, NullLoggerFactory.Instance, 1, 1); + var protocolHandler = new GrpcProtocolHandler(CreateGrpcClientMock().Object, NullLoggerFactory.Instance); var field = typeof(WorkflowWorker).GetField("_protocolHandler", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(field); @@ -434,7 +422,6 @@ public async Task ExecuteAsync_ShouldComplete_WhenGrpcStreamCompletesImmediately { var services = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); @@ -451,8 +438,7 @@ public async Task ExecuteAsync_ShouldComplete_WhenGrpcStreamCompletesImmediately factory, NullLoggerFactory.Instance, serializer, - services, - options); + services); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); @@ -460,17 +446,16 @@ public async Task ExecuteAsync_ShouldComplete_WhenGrpcStreamCompletesImmediately // Wait until the worker actually tries to connect, then stop it cleanly. await startedTcs.Task.WaitAsync(cts.Token); - cts.Cancel(); + await cts.CancelAsync(); await executeTask; } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnTerminatedCompletion_WhenReplayLatestEventIsExecutionTerminated() + public async Task HandleWorkflowResponseAsync_ShouldReturnTerminatedCompletion_WhenReplayLatestEventIsExecutionTerminated() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); // Intentionally no workflow registrations: this verifies the termination path // is acknowledged before workflow lookup/instantiation. @@ -479,10 +464,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnTerminatedCompleti new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -501,20 +485,19 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnTerminatedCompleti } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var action = Assert.Single(response.Actions); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Terminated, action.CompleteOrchestration!.OrchestrationStatus); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Terminated, action.CompleteWorkflow!.WorkflowStatus); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldNotReturnTerminatedCompletion_WhenReplayLatestEventIsNotExecutionTerminated() + public async Task HandleWorkflowResponseAsync_ShouldNotReturnTerminatedCompletion_WhenReplayLatestEventIsNotExecutionTerminated() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); // Intentionally no workflow registrations. If the termination short-circuit does NOT trigger, // normal path should fail with WorkflowNotFound-style completion. @@ -523,10 +506,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldNotReturnTerminatedCompl new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -544,36 +526,34 @@ public async Task HandleOrchestratorResponseAsync_ShouldNotReturnTerminatedCompl }, new HistoryEvent { - OrchestratorStarted = new OrchestratorStartedEvent() + WorkflowStarted = new WorkflowStartedEvent() } } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var action = Assert.Single(response.Actions); - Assert.NotNull(action.CompleteOrchestration); - Assert.NotEqual(OrchestrationStatus.Terminated, action.CompleteOrchestration!.OrchestrationStatus); - Assert.Equal(OrchestrationStatus.Failed, action.CompleteOrchestration.OrchestrationStatus); + Assert.NotNull(action.CompleteWorkflow); + Assert.NotEqual(OrchestrationStatus.Terminated, action.CompleteWorkflow!.WorkflowStatus); + Assert.Equal(OrchestrationStatus.Failed, action.CompleteWorkflow.WorkflowStatus); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnEmptyResponse_WhenLatestEventIsExecutionSuspended() + public async Task HandleWorkflowResponseAsync_ShouldReturnEmptyResponse_WhenLatestEventIsExecutionSuspended() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -592,28 +572,26 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnEmptyResponse_When } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); Assert.Empty(response.Actions); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldNotShortCircuit_WhenLatestEventIsExecutionResumed() + public async Task HandleWorkflowResponseAsync_ShouldNotShortCircuit_WhenLatestEventIsExecutionResumed() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -632,12 +610,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldNotShortCircuit_WhenLate } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var action = Assert.Single(response.Actions); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Failed, action.CompleteOrchestration!.OrchestrationStatus); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Failed, action.CompleteWorkflow!.WorkflowStatus); } [Fact] @@ -645,7 +623,6 @@ public async Task ExecuteAsync_ShouldSwallowOperationCanceledException_WhenStopp { var services = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); @@ -663,8 +640,7 @@ public async Task ExecuteAsync_ShouldSwallowOperationCanceledException_WhenStopp factory, NullLoggerFactory.Instance, serializer, - services, - options); + services); using var cts = new CancellationTokenSource(); await cts.CancelAsync(); @@ -689,8 +665,7 @@ public async Task ExecuteAsync_ShouldRethrow_WhenOptionsHaveInvalidConcurrency() new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - services, - options); + services); await Assert.ThrowsAsync(() => InvokeExecuteAsync(worker, CancellationToken.None)); } @@ -700,7 +675,6 @@ public void CreateCallOptions_ShouldIncludeUserAgentAndApiToken_WhenConfigured() { var services = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -715,7 +689,6 @@ public void CreateCallOptions_ShouldIncludeUserAgentAndApiToken_WhenConfigured() NullLoggerFactory.Instance, serializer, services, - options, configuration); using var cts = new CancellationTokenSource(); @@ -733,7 +706,6 @@ public void CreateCallOptions_ShouldNotIncludeApiTokenHeader_WhenTokenIsEmpty() { var services = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -748,7 +720,6 @@ public void CreateCallOptions_ShouldNotIncludeApiTokenHeader_WhenTokenIsEmpty() NullLoggerFactory.Instance, serializer, services, - options, configuration); var callOptions = InvokeCreateCallOptions(worker, CancellationToken.None); @@ -775,7 +746,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenCompletionEventArriv { new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } @@ -790,7 +761,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenCompletionEventArriv { new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "99" @@ -819,7 +790,7 @@ public void CallChildWorkflowAsync_ShouldPreserveRouterTargetAppId_OnScheduledAc _ = context.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions { TargetAppId = appId2 }); var action = Assert.Single(context.PendingActions); - Assert.NotNull(action.CreateSubOrchestration); + Assert.NotNull(action.CreateChildWorkflow); Assert.NotNull(action.Router); Assert.Equal(appId1, action.Router.SourceAppID); Assert.Equal(appId2, action.Router.TargetAppID); @@ -837,7 +808,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenCompletionArrivedBef { new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "13" @@ -853,7 +824,7 @@ public async Task CallChildWorkflowAsync_ShouldComplete_WhenCompletionArrivedBef context.ProcessEvents([ new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } } ], false); @@ -878,14 +849,14 @@ public async Task CallChildWorkflowAsync_ShouldIgnoreDuplicateCompletionEvents() context.ProcessEvents([ new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } } ], true); context.ProcessEvents([ new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 999, Result = "100" @@ -893,7 +864,7 @@ public async Task CallChildWorkflowAsync_ShouldIgnoreDuplicateCompletionEvents() }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "200" @@ -906,11 +877,10 @@ public async Task CallChildWorkflowAsync_ShouldIgnoreDuplicateCompletionEvents() } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldAllowWorkflowToComplete_OnSecondPass_WhenChildCompletionInHistory() + public async Task HandleWorkflowResponseAsync_ShouldAllowWorkflowToComplete_OnSecondPass_WhenChildCompletionInHistory() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); @@ -929,11 +899,10 @@ await ctx.CallChildWorkflowAsync("TargetWorkflow", input: 7, factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Pass 1: only ExecutionStarted, so it should schedule CreateSubOrchestration and yield (not completed) - var pass1 = new OrchestratorRequest + var pass1 = new WorkflowRequest { InstanceId = "initial-workflow-instance", PastEvents = @@ -945,12 +914,12 @@ await ctx.CallChildWorkflowAsync("TargetWorkflow", input: 7, } }; - var resp1 = await InvokeHandleOrchestratorResponseAsync(worker, pass1); - Assert.Contains(resp1.Actions, a => a.CreateSubOrchestration != null); - Assert.DoesNotContain(resp1.Actions, a => a.CompleteOrchestration != null); + var resp1 = await InvokeHandleWorkflowResponseAsync(worker, pass1); + Assert.Contains(resp1.Actions, a => a.CreateChildWorkflow != null); + Assert.DoesNotContain(resp1.Actions, a => a.CompleteWorkflow != null); // Pass 2: include sub-orchestration completed with taskScheduledId=0 - var pass2 = new OrchestratorRequest + var pass2 = new WorkflowRequest { InstanceId = "initial-workflow-instance", PastEvents = @@ -961,7 +930,7 @@ await ctx.CallChildWorkflowAsync("TargetWorkflow", input: 7, }, new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { InstanceId = "remote-workflow-instance", Name = "TargetWorkflow", @@ -970,7 +939,7 @@ await ctx.CallChildWorkflowAsync("TargetWorkflow", input: 7, }, new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "21" @@ -979,9 +948,9 @@ await ctx.CallChildWorkflowAsync("TargetWorkflow", input: 7, } }; - var resp2 = await InvokeHandleOrchestratorResponseAsync(worker, pass2); - Assert.Contains(resp2.Actions, a => a.CompleteOrchestration != null); - Assert.Equal(OrchestrationStatus.Completed, resp2.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!.OrchestrationStatus); + var resp2 = await InvokeHandleWorkflowResponseAsync(worker, pass2); + Assert.Contains(resp2.Actions, a => a.CompleteWorkflow != null); + Assert.Equal(OrchestrationStatus.Completed, resp2.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!.WorkflowStatus); } [Fact] @@ -996,7 +965,7 @@ public async Task CallChildWorkflowAsync_ShouldOnlyCompleteAfterCreation_WhenCom { new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "21" @@ -1010,7 +979,7 @@ public async Task CallChildWorkflowAsync_ShouldOnlyCompleteAfterCreation_WhenCom context.ProcessEvents([ new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } } ], false); @@ -1032,7 +1001,7 @@ public async Task CallChildWorkflowAsync_ShouldCompleteOnlyForMatchingTaskSchedu { new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } } }; @@ -1044,7 +1013,7 @@ public async Task CallChildWorkflowAsync_ShouldCompleteOnlyForMatchingTaskSchedu { new HistoryEvent { - SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent { Name = "ChildWf" } } }; @@ -1054,7 +1023,7 @@ public async Task CallChildWorkflowAsync_ShouldCompleteOnlyForMatchingTaskSchedu { new HistoryEvent { - SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent { TaskScheduledId = 0, Result = "7" @@ -1070,50 +1039,46 @@ public async Task CallChildWorkflowAsync_ShouldCompleteOnlyForMatchingTaskSchedu } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnEmptyActions_WhenWorkflowNameMissingInHistory() + public async Task HandleWorkflowResponseAsync_ShouldReturnEmptyActions_WhenWorkflowNameMissingInHistory() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = { new HistoryEvent { TimerFired = new TimerFiredEvent() } } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var action = Assert.Single(response.Actions); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Failed, action.CompleteOrchestration.OrchestrationStatus); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Failed, action.CompleteWorkflow.WorkflowStatus); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnEmptyActions_WhenWorkflowNotInRegistry() + public async Task HandleWorkflowResponseAsync_ShouldReturnEmptyActions_WhenWorkflowNotInRegistry() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), // no registrations NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1125,21 +1090,20 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnEmptyActions_WhenW } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var action = Assert.Single(response.Actions); - Assert.NotNull(action.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Failed, action.CompleteOrchestration.OrchestrationStatus); - Assert.Equal("WorkflowNotFound", action.CompleteOrchestration.FailureDetails.ErrorType); + Assert.NotNull(action.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Failed, action.CompleteWorkflow.WorkflowStatus); + Assert.Equal("WorkflowNotFound", action.CompleteWorkflow.FailureDetails.ErrorType); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnActivationFailure_WhenWorkflowActivationFails() + public async Task HandleWorkflowResponseAsync_ShouldReturnActivationFailure_WhenWorkflowActivationFails() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflowActivationError("wf", new InvalidOperationException("No service for type 'IMyService' has been registered.")); @@ -1149,10 +1113,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnActivationFailure_ factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1164,23 +1127,22 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnActivationFailure_ } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); var activationAction = Assert.Single(response.Actions); - Assert.NotNull(activationAction.CompleteOrchestration); - Assert.Equal(OrchestrationStatus.Failed, activationAction.CompleteOrchestration.OrchestrationStatus); - Assert.NotEqual("WorkflowNotFound", activationAction.CompleteOrchestration.FailureDetails.ErrorType); - Assert.Contains("failed to activate", activationAction.CompleteOrchestration.FailureDetails.ErrorMessage); - Assert.Contains("IMyService", activationAction.CompleteOrchestration.FailureDetails.ErrorMessage); + Assert.NotNull(activationAction.CompleteWorkflow); + Assert.Equal(OrchestrationStatus.Failed, activationAction.CompleteWorkflow.WorkflowStatus); + Assert.NotEqual("WorkflowNotFound", activationAction.CompleteWorkflow.FailureDetails.ErrorType); + Assert.Contains("failed to activate", activationAction.CompleteWorkflow.FailureDetails.ErrorMessage); + Assert.Contains("IMyService", activationAction.CompleteWorkflow.FailureDetails.ErrorMessage); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldCompleteWorkflow_AndIncludeOutputAndCustomStatus() + public async Task HandleWorkflowResponseAsync_ShouldCompleteWorkflow_AndIncludeOutputAndCustomStatus() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -1196,10 +1158,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldCompleteWorkflow_AndIncl factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1211,25 +1172,24 @@ public async Task HandleOrchestratorResponseAsync_ShouldCompleteWorkflow_AndIncl } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); Assert.Contains("\"step\":7", response.CustomStatus); var completion = response.Actions - .FirstOrDefault(a => a.CompleteOrchestration != null)?.CompleteOrchestration; + .FirstOrDefault(a => a.CompleteWorkflow != null)?.CompleteWorkflow; Assert.NotNull(completion); - Assert.Equal(OrchestrationStatus.Completed, completion.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Completed, completion.WorkflowStatus); Assert.Equal("42", completion.Result); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldNotAddCompletedAction_WhenWorkflowContinuesAsNew() + public async Task HandleWorkflowResponseAsync_ShouldNotAddCompletedAction_WhenWorkflowContinuesAsNew() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -1245,10 +1205,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldNotAddCompletedAction_Wh factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1260,24 +1219,23 @@ public async Task HandleOrchestratorResponseAsync_ShouldNotAddCompletedAction_Wh } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var completeActions = response.Actions.Where(a => a.CompleteOrchestration != null).ToList(); + var completeActions = response.Actions.Where(a => a.CompleteWorkflow != null).ToList(); Assert.Single(completeActions); - Assert.Equal(OrchestrationStatus.ContinuedAsNew, completeActions[0].CompleteOrchestration!.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.ContinuedAsNew, completeActions[0].CompleteWorkflow!.WorkflowStatus); Assert.DoesNotContain(response.Actions, - a => a.CompleteOrchestration?.OrchestrationStatus == OrchestrationStatus.Completed); + a => a.CompleteWorkflow?.WorkflowStatus == OrchestrationStatus.Completed); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_WhenWorkflowThrows() + public async Task HandleWorkflowResponseAsync_ShouldReturnFailedCompletion_WhenWorkflowThrows() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -1289,10 +1247,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_W factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1304,13 +1261,13 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_W } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.NotNull(complete.FailureDetails); Assert.Contains("boom", complete.FailureDetails.ErrorMessage); } @@ -1320,21 +1277,19 @@ public async Task HandleActivityResponseAsync_ShouldReturnNotFoundFailure_WhenAc { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 7, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "1" }; @@ -1352,7 +1307,6 @@ public async Task HandleActivityResponseAsync_ShouldReturnActivationFailure_When { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivityActivationError("act", new InvalidOperationException("No service for type 'IEmailSender' has been registered.")); @@ -1362,14 +1316,13 @@ public async Task HandleActivityResponseAsync_ShouldReturnActivationFailure_When factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 7, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "1" }; @@ -1388,7 +1341,6 @@ public async Task HandleActivityResponseAsync_ShouldExecuteActivity_AndSerialize { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivity("act", new InlineActivity( @@ -1404,14 +1356,13 @@ public async Task HandleActivityResponseAsync_ShouldExecuteActivity_AndSerialize factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 7, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "21" }; @@ -1428,7 +1379,6 @@ public async Task HandleActivityResponseAsync_ShouldReturnFailureDetails_WhenAct { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivity("act", new InlineActivity( @@ -1440,14 +1390,13 @@ public async Task HandleActivityResponseAsync_ShouldReturnFailureDetails_WhenAct factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 7, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "1" }; @@ -1464,7 +1413,6 @@ public async Task ExecuteAsync_ShouldRetry_WhenGrpcProtocolHandlerStartFailsWith { var services = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); @@ -1481,8 +1429,7 @@ public async Task ExecuteAsync_ShouldRetry_WhenGrpcProtocolHandlerStartFailsWith factory, NullLoggerFactory.Instance, serializer, - services, - options); + services); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); @@ -1500,11 +1447,10 @@ public async Task ExecuteAsync_ShouldRetry_WhenGrpcProtocolHandlerStartFailsWith } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldUseFirstEventTimestamp_WhenPresent_AndSerializeEmptyResult_WhenOutputIsNull() + public async Task HandleWorkflowResponseAsync_ShouldUseFirstEventTimestamp_WhenPresent_AndSerializeEmptyResult_WhenOutputIsNull() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -1524,10 +1470,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldUseFirstEventTimestamp_W factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1545,22 +1490,21 @@ public async Task HandleOrchestratorResponseAsync_ShouldUseFirstEventTimestamp_W } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); Assert.Null(response.CustomStatus); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); Assert.Equal(string.Empty, complete.Result); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldAdvanceCurrentUtcDateTime_WhenTimerFires() + public async Task HandleWorkflowResponseAsync_ShouldAdvanceCurrentUtcDateTime_WhenTimerFires() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var beginDateTime = new DateTime(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); var factory = new StubWorkflowsFactory(); @@ -1580,10 +1524,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldAdvanceCurrentUtcDateTim factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1608,7 +1551,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldAdvanceCurrentUtcDateTim new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)), - OrchestratorStarted = new OrchestratorStartedEvent() + WorkflowStarted = new WorkflowStartedEvent() }, new HistoryEvent { @@ -1621,11 +1564,11 @@ public async Task HandleOrchestratorResponseAsync_ShouldAdvanceCurrentUtcDateTim } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); Assert.Equal(string.Empty, complete.Result); } @@ -1634,15 +1577,14 @@ public async Task HandleOrchestratorResponseAsync_ShouldAdvanceCurrentUtcDateTim /// await on every replay, not the current turn's timestamp. /// /// The bug: WorkflowWorker initialised _currentUtcDateTime with the *current turn's* - /// OrchestratorStarted timestamp (T3) instead of the *first* history event's timestamp (T1). + /// WorkflowStarted timestamp (T3) instead of the *first* history event's timestamp (T1). /// The workflow code ran before ProcessEvents and read the wrong time. /// [Fact] - public async Task HandleOrchestratorResponseAsync_CurrentUtcDateTime_IsConsistentBeforeFirstAwait_OnReplay() + public async Task HandleWorkflowResponseAsync_CurrentUtcDateTime_IsConsistentBeforeFirstAwait_OnReplay() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var t1 = new DateTime(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); // workflow started var t2 = t1.AddSeconds(5); // activity completed @@ -1670,13 +1612,12 @@ public async Task HandleOrchestratorResponseAsync_CurrentUtcDateTime_IsConsisten factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Simulate a replay turn: PastEvents contain the first turn's history (activity scheduled - // and completed), NewEvents hold the current turn's OrchestratorStarted at the later time T3. + // and completed), NewEvents hold the current turn's WorkflowStarted at the later time T3. // Before the fix, CurrentUtcDateTime before the first await would be T3, not T1. - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1694,7 +1635,7 @@ public async Task HandleOrchestratorResponseAsync_CurrentUtcDateTime_IsConsisten new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(t2), - OrchestratorStarted = new OrchestratorStartedEvent() + WorkflowStarted = new WorkflowStartedEvent() }, new HistoryEvent { @@ -1712,32 +1653,31 @@ public async Task HandleOrchestratorResponseAsync_CurrentUtcDateTime_IsConsisten new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(t3), - OrchestratorStarted = new OrchestratorStartedEvent() + WorkflowStarted = new WorkflowStartedEvent() } } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); // Before the fix this was T3 (the current turn's timestamp). It must be T1 so that // the value the workflow observes before its first await is consistent across replays. Assert.Equal(t1, capturedBeforeAwait); // After the activity completes the clock should have advanced to T2, as recorded - // by the OrchestratorStarted event that preceded the TaskCompleted event. + // by the WorkflowStarted event that preceded the TaskCompleted event. Assert.Equal(t2, capturedAfterActivityAwait); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldCompleted_WhenEventReceived() + public async Task HandleWorkflowResponseAsync_ShouldCompleted_WhenEventReceived() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var beginDateTime = new DateTime(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); var factory = new StubWorkflowsFactory(); @@ -1754,10 +1694,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldCompleted_WhenEventRecei factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1765,62 +1704,57 @@ public async Task HandleOrchestratorResponseAsync_ShouldCompleted_WhenEventRecei new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime), - ExecutionStarted = new ExecutionStartedEvent - { - Name = "wf", - Input = "123" - } + ExecutionStarted = new ExecutionStartedEvent { Name = "wf", Input = "123" } }, new HistoryEvent { EventId = 0, - TimerCreated = new TimerCreatedEvent - { - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)) - } + TimerCreated = + new TimerCreatedEvent + { + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( + beginDateTime.AddSeconds(5)) + } }, new HistoryEvent { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(2)), - OrchestratorStarted = new OrchestratorStartedEvent() + Timestamp = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(2)), + WorkflowStarted = new WorkflowStartedEvent() }, + new HistoryEvent { EventRaised = new EventRaisedEvent { Name = "myevent" } }, new HistoryEvent { - EventRaised = new EventRaisedEvent - { - Name = "myevent" - } - }, - new HistoryEvent - { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)), - OrchestratorStarted = new OrchestratorStartedEvent() + Timestamp = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)), + WorkflowStarted = new WorkflowStartedEvent() }, new HistoryEvent { TimerFired = new TimerFiredEvent { TimerId = 0, - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)) + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( + beginDateTime.AddSeconds(5)) } } } }; + - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); Assert.Equal(string.Empty, complete.Result); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnFailureDetails_WhenTimerFires() + public async Task HandleWorkflowResponseAsync_ShouldReturnFailureDetails_WhenTimerFires() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var beginDateTime = new DateTime(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); var factory = new StubWorkflowsFactory(); @@ -1837,10 +1771,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailureDetails_Whe factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -1848,62 +1781,58 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailureDetails_Whe new HistoryEvent { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime), - ExecutionStarted = new ExecutionStartedEvent - { - Name = "wf", - Input = "123" - } + ExecutionStarted = new ExecutionStartedEvent { Name = "wf", Input = "123" } }, new HistoryEvent { EventId = 0, - TimerCreated = new TimerCreatedEvent - { - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)) - } + TimerCreated = + new TimerCreatedEvent + { + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( + beginDateTime.AddSeconds(5)) + } }, new HistoryEvent { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)), - OrchestratorStarted = new OrchestratorStartedEvent() + Timestamp = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)), + WorkflowStarted = new WorkflowStartedEvent() }, new HistoryEvent { TimerFired = new TimerFiredEvent { TimerId = 0, - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(5)) + FireAt = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( + beginDateTime.AddSeconds(5)) } }, new HistoryEvent { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(10)), - OrchestratorStarted = new OrchestratorStartedEvent() + Timestamp = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(beginDateTime.AddSeconds(10)), + WorkflowStarted = new WorkflowStartedEvent() }, - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "myevent" - } - } + new HistoryEvent { EventRaised = new EventRaisedEvent { Name = "myevent" } } } }; + - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.NotNull(complete.FailureDetails); } [Fact] - public async Task HandleActivityResponseAsync_ShouldUseEmptyInstanceId_WhenOrchestrationInstanceIsNull_AndReturnEmptyResult_WhenOutputIsNull() + public async Task HandleActivityResponseAsync_ShouldUseEmptyInstanceId_WhenWorkflowInstanceIsNull_AndReturnEmptyResult_WhenOutputIsNull() { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivity("act", new InlineActivity( @@ -1915,14 +1844,13 @@ public async Task HandleActivityResponseAsync_ShouldUseEmptyInstanceId_WhenOrche factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); var request = new ActivityRequest { Name = "act", TaskId = 9, - OrchestrationInstance = null, + WorkflowInstance = null, Input = "" // empty input -> no deserialization branch }; @@ -1938,118 +1866,113 @@ public async Task HandleActivityResponseAsync_ShouldUseEmptyInstanceId_WhenOrche // RequiresHistoryStreaming // ------------------------------------------------------------------------- - [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldStreamHistory_WhenRequiresHistoryStreamingIsTrue() - { - // When RequiresHistoryStreaming is set, the worker must fetch past history - // via StreamInstanceHistory and merge it with the inline PastEvents before - // running the workflow. Here we put the ExecutionStarted event inside the - // stream (not in PastEvents) so the workflow can only complete if streaming works. - var sp = new ServiceCollection().BuildServiceProvider(); - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); - - var factory = new StubWorkflowsFactory(); - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(int), - run: (_, input) => Task.FromResult((int)input! + 1))); - - // The streamed chunk carries the ExecutionStarted event. - var streamedChunk = new HistoryChunk(); - streamedChunk.Events.Add(new HistoryEvent - { - ExecutionStarted = new ExecutionStartedEvent { Name = "wf", Input = "10" } - }); - - var grpcClientMock = CreateGrpcClientMock(); - grpcClientMock - .Setup(x => x.StreamInstanceHistory(It.IsAny(), It.IsAny())) - .Returns(CreateHistoryStreamingCall(SingleItemAsync(streamedChunk))); - - var worker = new WorkflowWorker( - grpcClientMock.Object, - factory, - NullLoggerFactory.Instance, - serializer, - sp, - options); - - var request = new OrchestratorRequest - { - InstanceId = "stream-i", - RequiresHistoryStreaming = true - }; - - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); - - Assert.Equal("stream-i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteOrchestration != null).CompleteOrchestration!; - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); - Assert.Equal("11", complete.Result); - - grpcClientMock.Verify( - x => x.StreamInstanceHistory(It.IsAny(), It.IsAny()), - Times.Once()); - } + // [Fact] + // public async Task HandleWorkflowResponseAsync_ShouldStreamHistory_WhenRequiresHistoryStreamingIsTrue() + // { + // // When RequiresHistoryStreaming is set, the worker must fetch past history + // // via StreamInstanceHistory and merge it with the inline PastEvents before + // // running the workflow. Here we put the ExecutionStarted event inside the + // // stream (not in PastEvents) so the workflow can only complete if streaming works. + // var sp = new ServiceCollection().BuildServiceProvider(); + // var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + // + // var factory = new StubWorkflowsFactory(); + // factory.AddWorkflow("wf", new InlineWorkflow( + // inputType: typeof(int), + // run: (_, input) => Task.FromResult((int)input! + 1))); + // + // // The streamed chunk carries the ExecutionStarted event. + // var streamedChunk = new HistoryChunk(); + // streamedChunk.Events.Add(new HistoryEvent + // { + // ExecutionStarted = new ExecutionStartedEvent { Name = "wf", Input = "10" } + // }); + // + // var grpcClientMock = CreateGrpcClientMock(); + // grpcClientMock + // .Setup(x => x.GetInstanceHistoryAsync(It.IsAny(), It.IsAny())) + // .Returns(CreateHistoryStreamingCall(SingleItemAsync(streamedChunk))); + // + // var worker = new WorkflowWorker( + // grpcClientMock.Object, + // factory, + // NullLoggerFactory.Instance, + // serializer, + // sp); + // + // var request = new WorkflowRequest + // { + // InstanceId = "stream-i", + // RequiresHistoryStreaming = true + // }; + // + // var response = await InvokeHandleWorkflowResponseAsync(worker, request); + // + // Assert.Equal("stream-i", response.InstanceId); + // var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + // Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + // Assert.Equal("11", complete.Result); + // + // grpcClientMock.Verify( + // x => x.StreamInstanceHistory(It.IsAny(), It.IsAny()), + // Times.Once()); + // } // ------------------------------------------------------------------------- // Workflow-name extraction fallbacks // ------------------------------------------------------------------------- - [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldExtractWorkflowName_FromHistoryState() - { - // When no ExecutionStarted event is present, the worker falls back to - // HistoryState.OrchestrationState.Name to identify the workflow. - var sp = new ServiceCollection().BuildServiceProvider(); - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); - - var factory = new StubWorkflowsFactory(); - factory.AddWorkflow("fallback-wf", new InlineWorkflow( - inputType: typeof(object), - run: (_, _) => Task.FromResult(null))); - - var worker = new WorkflowWorker( - CreateGrpcClientMock().Object, - factory, - NullLoggerFactory.Instance, - serializer, - sp, - options); - - var request = new OrchestratorRequest - { - InstanceId = "i", - PastEvents = - { - // Only a HistoryState event, no ExecutionStarted. - new HistoryEvent - { - HistoryState = new HistoryStateEvent - { - OrchestrationState = new OrchestrationState { Name = "fallback-wf" } - } - } - } - }; - - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; - Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); - } + // [Fact] + // public async Task HandleWorkflowResponseAsync_ShouldExtractWorkflowName_FromHistoryState() + // { + // // When no ExecutionStarted event is present, the worker falls back to + // // HistoryState.WorkflowState.Name to identify the workflow. + // var sp = new ServiceCollection().BuildServiceProvider(); + // var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + // + // var factory = new StubWorkflowsFactory(); + // factory.AddWorkflow("fallback-wf", new InlineWorkflow( + // inputType: typeof(object), + // run: (_, _) => Task.FromResult(null))); + // + // var worker = new WorkflowWorker( + // CreateGrpcClientMock().Object, + // factory, + // NullLoggerFactory.Instance, + // serializer, + // sp); + // + // var request = new WorkflowRequest + // { + // InstanceId = "i", + // PastEvents = + // { + // // Only a HistoryState event, no ExecutionStarted. + // new HistoryEvent + // { + // HistoryState = new HistoryStateEvent + // { + // WorkflowState = new WorkflowState { Name = "fallback-wf" } + // } + // } + // } + // }; + // + // var response = await InvokeHandleWorkflowResponseAsync(worker, request); + // + // Assert.Equal("i", response.InstanceId); + // var complete = Assert.Single(response.Actions).CompleteWorkflow; + // Assert.NotNull(complete); + // Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + // } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldExtractWorkflowName_FromOrchestratorStartedVersion() + public async Task HandleWorkflowResponseAsync_ShouldExtractWorkflowName_FromWorkflowStartedVersion() { - // Third fallback: OrchestratorStarted.Version.Name when both ExecutionStarted + // Third fallback: WorkflowStarted.Version.Name when both ExecutionStarted // and HistoryState are absent or have no name. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("version-wf", new InlineWorkflow( @@ -2061,10 +1984,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldExtractWorkflowName_From factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2073,20 +1995,20 @@ public async Task HandleOrchestratorResponseAsync_ShouldExtractWorkflowName_From { Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)), - OrchestratorStarted = new OrchestratorStartedEvent + WorkflowStarted = new WorkflowStartedEvent { - Version = new OrchestrationVersion { Name = "version-wf" } + Version = new WorkflowVersion { Name = "version-wf" } } } } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); } // ------------------------------------------------------------------------- @@ -2094,7 +2016,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldExtractWorkflowName_From // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldResolveFromRouter_WhenHistoryHasNoWorkflowName() + public async Task HandleWorkflowResponseAsync_ShouldResolveFromRouter_WhenHistoryHasNoWorkflowName() { // When the history carries no workflow name, the worker consults // IWorkflowRouterRegistry.TryResolveLatest and stamps the version in the response. @@ -2117,18 +2039,16 @@ public async Task HandleOrchestratorResponseAsync_ShouldResolveFromRouter_WhenHi .BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - // No ExecutionStarted / HistoryState / OrchestratorStarted.Version — name must come from router. - var request = new OrchestratorRequest + // No ExecutionStarted / HistoryState / WorkflowStarted.Version — name must come from router. + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2137,12 +2057,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldResolveFromRouter_WhenHi } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Completed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); // resolvedFromRouter=true must stamp the version into the response. Assert.NotNull(response.Version); @@ -2150,7 +2070,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldResolveFromRouter_WhenHi } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNameMissingError_WhenRouterCannotResolveLatest() + public async Task HandleWorkflowResponseAsync_ShouldReturnWorkflowNameMissingError_WhenRouterCannotResolveLatest() { // If IWorkflowRouterRegistry.TryResolveLatest returns false and no name is // in the history, the worker must fail the orchestration with WorkflowNameMissing. @@ -2167,32 +2087,30 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNameMissin .BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = { new HistoryEvent { TimerFired = new TimerFiredEvent() } } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.Equal("WorkflowNameMissing", complete.FailureDetails.ErrorType); } [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNotFoundError_WhenRouterDoesNotContainWorkflow() + public async Task HandleWorkflowResponseAsync_ShouldReturnWorkflowNotFoundError_WhenRouterDoesNotContainWorkflow() { // If IWorkflowRouterRegistry is present but Contains("wf") returns false, // the worker must fail the orchestration with WorkflowNotFound. @@ -2209,17 +2127,15 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNotFoundEr .BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2231,11 +2147,11 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNotFoundEr } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.Equal("WorkflowNotFound", complete.FailureDetails.ErrorType); Assert.Contains("wf", complete.FailureDetails.ErrorMessage); } @@ -2245,14 +2161,13 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnWorkflowNotFoundEr // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldIncludeVersionInResponse_WhenWorkflowUsesIsPatched() + public async Task HandleWorkflowResponseAsync_ShouldIncludeVersionInResponse_WhenWorkflowUsesIsPatched() { // When the workflow calls ctx.IsPatched(), the version tracker sets // IncludeVersionInNextResponse=true and the worker stamps the version into - // the OrchestratorResponse. + // the WorkflowResponse. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); const string patchName = "my-feature-patch"; @@ -2270,10 +2185,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldIncludeVersionInResponse factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2285,7 +2199,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldIncludeVersionInResponse } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); Assert.NotNull(response.Version); @@ -2298,13 +2212,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldIncludeVersionInResponse // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldYield_WhenWorkflowAwaitsActivityNotYetComplete() + public async Task HandleWorkflowResponseAsync_ShouldYield_WhenWorkflowAwaitsActivityNotYetComplete() { // First turn: workflow awaits an activity but no completion event is present. - // The response must contain a ScheduleTask action and no CompleteOrchestration. + // The response must contain a ScheduleTask action and no CompleteWorkflow. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -2323,11 +2236,10 @@ public async Task HandleOrchestratorResponseAsync_ShouldYield_WhenWorkflowAwaits factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Only ExecutionStarted — activity has not been scheduled or completed yet. - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2339,10 +2251,10 @@ public async Task HandleOrchestratorResponseAsync_ShouldYield_WhenWorkflowAwaits } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - Assert.DoesNotContain(response.Actions, a => a.CompleteOrchestration != null); + Assert.DoesNotContain(response.Actions, a => a.CompleteWorkflow != null); Assert.Contains(response.Actions, a => a.ScheduleTask != null); } @@ -2351,14 +2263,13 @@ public async Task HandleOrchestratorResponseAsync_ShouldYield_WhenWorkflowAwaits // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_WhenWorkflowReturnsFaultedTask() + public async Task HandleWorkflowResponseAsync_ShouldReturnFailedCompletion_WhenWorkflowReturnsFaultedTask() { // Unlike a synchronous throw (which hits the outer catch), a workflow that // returns an already-faulted Task exercises the inner try/catch around // `await runTask`, where IsNonRetriable is explicitly set to true. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddWorkflow("wf", new InlineWorkflow( @@ -2370,10 +2281,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_W factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2385,12 +2295,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_W } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.NotNull(complete.FailureDetails); Assert.Contains("task-fault", complete.FailureDetails.ErrorMessage); // The inner catch always marks workflow failures as non-retriable. @@ -2402,13 +2312,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailedCompletion_W // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldReturnFailed_WhenUnexpectedExceptionEscapes() + public async Task HandleWorkflowResponseAsync_ShouldReturnFailed_WhenUnexpectedExceptionEscapes() { // An exception thrown by the serializer during input deserialization is // outside the inner try/catch, so it is caught by the outer handler which - // returns an OrchestratorResponse with OrchestrationStatus.Failed. + // returns an WorkflowResponse with OrchestrationStatus.Failed. var sp = new ServiceCollection().BuildServiceProvider(); - var options = new WorkflowRuntimeOptions(); var faultySerializer = new Mock(); faultySerializer @@ -2425,10 +2334,9 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailed_WhenUnexpec factory, NullLoggerFactory.Instance, faultySerializer.Object, - sp, - options); + sp); - var request = new OrchestratorRequest + var request = new WorkflowRequest { InstanceId = "i", PastEvents = @@ -2441,12 +2349,12 @@ public async Task HandleOrchestratorResponseAsync_ShouldReturnFailed_WhenUnexpec } }; - var response = await InvokeHandleOrchestratorResponseAsync(worker, request); + var response = await InvokeHandleWorkflowResponseAsync(worker, request); Assert.Equal("i", response.InstanceId); - var complete = Assert.Single(response.Actions).CompleteOrchestration; + var complete = Assert.Single(response.Actions).CompleteWorkflow; Assert.NotNull(complete); - Assert.Equal(OrchestrationStatus.Failed, complete.OrchestrationStatus); + Assert.Equal(OrchestrationStatus.Failed, complete.WorkflowStatus); Assert.NotNull(complete.FailureDetails); Assert.Contains("serializer-exploded", complete.FailureDetails.ErrorMessage); } @@ -2463,7 +2371,6 @@ public async Task HandleActivityResponseAsync_ShouldUseTaskExecutionId_WhenProvi // the activity still executes successfully and the correct task id is echoed. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivity("act", new InlineActivity( @@ -2475,8 +2382,7 @@ public async Task HandleActivityResponseAsync_ShouldUseTaskExecutionId_WhenProvi factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); const string execId = "exec-abc-123"; @@ -2485,7 +2391,7 @@ public async Task HandleActivityResponseAsync_ShouldUseTaskExecutionId_WhenProvi Name = "act", TaskId = 77, TaskExecutionId = execId, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "wf-1" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "wf-1" }, Input = "" }; @@ -2501,22 +2407,20 @@ public async Task HandleActivityResponseAsync_ShouldUseTaskExecutionId_WhenProvi // ------------------------------------------------------------------------- [Fact] - public async Task HandleOrchestratorResponseAsync_ShouldEchoCompletionToken_InAllResponsePaths() + public async Task HandleWorkflowResponseAsync_ShouldEchoCompletionToken_InAllResponsePaths() { // CompletionToken must be round-tripped back in every response path. var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var worker = new WorkflowWorker( CreateGrpcClientMock().Object, new StubWorkflowsFactory(), NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Suspended path - var suspendedResponse = await InvokeHandleOrchestratorResponseAsync(worker, new OrchestratorRequest + var suspendedResponse = await InvokeHandleWorkflowResponseAsync(worker, new WorkflowRequest { InstanceId = "i", PastEvents = { new HistoryEvent { ExecutionStarted = new ExecutionStartedEvent { Name = "wf" } } }, @@ -2525,7 +2429,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldEchoCompletionToken_InAl Assert.Equal(CompletionTokenValue, suspendedResponse.CompletionToken); // Terminated path - var terminatedResponse = await InvokeHandleOrchestratorResponseAsync(worker, new OrchestratorRequest + var terminatedResponse = await InvokeHandleWorkflowResponseAsync(worker, new WorkflowRequest { InstanceId = "i", NewEvents = { new HistoryEvent { ExecutionTerminated = new ExecutionTerminatedEvent() } } @@ -2533,7 +2437,7 @@ public async Task HandleOrchestratorResponseAsync_ShouldEchoCompletionToken_InAl Assert.Equal(CompletionTokenValue, terminatedResponse.CompletionToken); // WorkflowNotFound path - var notFoundResponse = await InvokeHandleOrchestratorResponseAsync(worker, new OrchestratorRequest + var notFoundResponse = await InvokeHandleWorkflowResponseAsync(worker, new WorkflowRequest { InstanceId = "i", PastEvents = { new HistoryEvent { ExecutionStarted = new ExecutionStartedEvent { Name = "not-registered" } } } @@ -2546,7 +2450,6 @@ public async Task HandleActivityResponseAsync_ShouldEchoCompletionToken_InAllRes { var sp = new ServiceCollection().BuildServiceProvider(); var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); var factory = new StubWorkflowsFactory(); factory.AddActivity("ok-act", new InlineActivity( @@ -2558,15 +2461,14 @@ public async Task HandleActivityResponseAsync_ShouldEchoCompletionToken_InAllRes factory, NullLoggerFactory.Instance, serializer, - sp, - options); + sp); // Success path var successResponse = await InvokeHandleActivityResponseAsync(worker, new ActivityRequest { Name = "ok-act", TaskId = 1, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "" }); Assert.Equal(CompletionTokenValue, successResponse.CompletionToken); @@ -2576,7 +2478,7 @@ public async Task HandleActivityResponseAsync_ShouldEchoCompletionToken_InAllRes { Name = "missing-act", TaskId = 2, - OrchestrationInstance = new OrchestrationInstance { InstanceId = "i" }, + WorkflowInstance = new WorkflowInstance { InstanceId = "i" }, Input = "" }); Assert.Equal(CompletionTokenValue, notFoundResponse.CompletionToken); @@ -2607,12 +2509,12 @@ private static bool HasHeader(CallOptions options, string key, out string? value return entry is not null; } - private static async Task InvokeHandleOrchestratorResponseAsync(WorkflowWorker worker, OrchestratorRequest request) + private static async Task InvokeHandleWorkflowResponseAsync(WorkflowWorker worker, WorkflowRequest request) { - var method = typeof(WorkflowWorker).GetMethod("HandleOrchestratorResponseAsync", BindingFlags.Instance | BindingFlags.NonPublic); + var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(method); - var task = (Task)method.Invoke(worker, [request, CompletionTokenValue])!; + var task = (Task)method.Invoke(worker, [request, CompletionTokenValue])!; return await task; } @@ -2643,17 +2545,17 @@ private static AsyncServerStreamingCall CreateServerStreamingCall(IAsy () => { }); } - private static AsyncServerStreamingCall CreateHistoryStreamingCall(IAsyncEnumerable chunks) - { - var stream = new TestAsyncStreamReader(chunks); - - return new AsyncServerStreamingCall( - stream, - Task.FromResult(new Metadata()), - () => Status.DefaultSuccess, - () => [], - () => { }); - } + // private static AsyncServerStreamingCall CreateHistoryStreamingCall(IAsyncEnumerable chunks) + // { + // var stream = new TestAsyncStreamReader(chunks); + // + // return new AsyncServerStreamingCall( + // stream, + // Task.FromResult(new Metadata()), + // () => Status.DefaultSuccess, + // () => [], + // () => { }); + // } private static async IAsyncEnumerable SingleItemAsync(T item) { diff --git a/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs b/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs index 0b6f26992..9a504d13d 100644 --- a/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs +++ b/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs @@ -418,7 +418,6 @@ private sealed class FakeWorkflowContext : WorkflowContext public override ILogger CreateReplaySafeLogger(string categoryName) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger(Type type) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger() => throw new NotSupportedException(); - public override PropagatedHistory? GetPropagatedHistory() => null; } private sealed class FakeActivityContext : WorkflowActivityContext diff --git a/test/Dapr.Workflow.Test/WorkflowRuntimeOptionsTests.cs b/test/Dapr.Workflow.Test/WorkflowRuntimeOptionsTests.cs index 9f8e2d5cc..3fc97b158 100644 --- a/test/Dapr.Workflow.Test/WorkflowRuntimeOptionsTests.cs +++ b/test/Dapr.Workflow.Test/WorkflowRuntimeOptionsTests.cs @@ -21,22 +21,6 @@ namespace Dapr.Workflow.Test; public class WorkflowRuntimeOptionsTests { - [Fact] - public void MaxConcurrentWorkflows_ShouldThrow_WhenSetLessThan1() - { - var options = new WorkflowRuntimeOptions(); - - Assert.Throws(() => options.MaxConcurrentWorkflows = 0); - } - - [Fact] - public void MaxConcurrentActivities_ShouldThrow_WhenSetLessThan1() - { - var options = new WorkflowRuntimeOptions(); - - Assert.Throws(() => options.MaxConcurrentActivities = 0); - } - [Fact] public void UseGrpcChannelOptions_ShouldThrowArgumentNullException_WhenNull() { @@ -75,7 +59,7 @@ public void ApplyRegistrations_ShouldApplyWorkflowAndActivityRegistrations_ToFac var factory = new WorkflowsFactory(NullLogger.Instance); options.ApplyRegistrations(factory); - var sp = new Microsoft.Extensions.DependencyInjection.ServiceCollection().BuildServiceProvider(); + var sp = new ServiceCollection().BuildServiceProvider(); Assert.True(factory.TryCreateWorkflow(new("wf-fn"), sp, out var workflow, out _)); Assert.NotNull(workflow); @@ -83,34 +67,6 @@ public void ApplyRegistrations_ShouldApplyWorkflowAndActivityRegistrations_ToFac Assert.True(factory.TryCreateActivity(new("act-fn"), sp, out var activity, out _)); Assert.NotNull(activity); } - - [Fact] - public void MaxConcurrentWorkflows_ShouldDefaultTo100_AndAllowSettingValidValues() - { - var options = new WorkflowRuntimeOptions(); - - Assert.Equal(100, options.MaxConcurrentWorkflows); - - options.MaxConcurrentWorkflows = 1; - Assert.Equal(1, options.MaxConcurrentWorkflows); - - options.MaxConcurrentWorkflows = 250; - Assert.Equal(250, options.MaxConcurrentWorkflows); - } - - [Fact] - public void MaxConcurrentActivities_ShouldDefaultTo100_AndAllowSettingValidValues() - { - var options = new WorkflowRuntimeOptions(); - - Assert.Equal(100, options.MaxConcurrentActivities); - - options.MaxConcurrentActivities = 1; - Assert.Equal(1, options.MaxConcurrentActivities); - - options.MaxConcurrentActivities = 250; - Assert.Equal(250, options.MaxConcurrentActivities); - } [Fact] public void RegisterWorkflow_GenericWithName_ShouldRegisterUsingProvidedName_WhenApplied() diff --git a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs index 956935911..c8445270f 100644 --- a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs @@ -560,7 +560,6 @@ private sealed class FakeWorkflowContext : WorkflowContext public override ILogger CreateReplaySafeLogger(string categoryName) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger(Type type) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger() => throw new NotSupportedException(); - public override PropagatedHistory? GetPropagatedHistory() => null; } private sealed class FakeActivityContext : WorkflowActivityContext From 4ea452552c51a1c52d8ad42e3e8f8a89687598fb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 18 May 2026 01:36:13 -0500 Subject: [PATCH 11/17] Disabling new feature tests for now Signed-off-by: Whit Waldo --- .../HistoryPropagationWorkflowTests.cs | 512 +++++ .../Worker/Internal/TimerOriginTests.cs | 1820 ++++++++--------- 2 files changed, 1422 insertions(+), 910 deletions(-) diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index 8d06d238a..1b129c9ca 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -1,3 +1,422 @@ +// <<<<<<< HEAD +// // // ------------------------------------------------------------------------ +// // // Copyright 2026 The Dapr Authors +// // // Licensed under the Apache License, Version 2.0 (the "License"); +// // // you may not use this file except in compliance with the License. +// // // You may obtain a copy of the License at +// // // http://www.apache.org/licenses/LICENSE-2.0 +// // // Unless required by applicable law or agreed to in writing, software +// // // distributed under the License is distributed on an "AS IS" BASIS, +// // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// // // See the License for the specific language governing permissions and +// // // limitations under the License. +// // // ------------------------------------------------------------------------ +// // +// // using Dapr.Testcontainers.Common; +// // using Dapr.Testcontainers.Harnesses; +// // using Dapr.Testcontainers.Xunit.Attributes; +// // using Dapr.Workflow; +// // using Microsoft.Extensions.Configuration; +// // using Microsoft.Extensions.DependencyInjection; +// // +// // namespace Dapr.IntegrationTest.Workflow; +// // +// // /// +// // /// Integration tests for workflow history propagation. +// // /// +// // /// These tests verify the SDK-side API works end-to-end: +// // /// - Scheduling child workflows with a history propagation scope does not cause errors +// // /// - The parent and child workflows complete successfully +// // /// - The child can call GetPropagatedHistory() without error +// // /// +// // /// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires +// // /// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the +// // /// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. +// // /// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK +// // /// sets in the CreateSubOrchestrationAction proto message. +// // /// +// // public sealed class HistoryPropagationWorkflowTests +// // { +// // /// +// // /// Verifies that scheduling a child workflow with +// // /// (the default) completes successfully. +// // /// +// // [MinimumDaprRuntimeFact("1.18")] +// // public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() +// // { +// // var instanceId = Guid.NewGuid().ToString(); +// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// // +// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// // needsActorState: true, +// // cancellationToken: TestContext.Current.CancellationToken); +// // await environment.StartAsync(TestContext.Current.CancellationToken); +// // +// // var harness = new DaprHarnessBuilder(componentsDir) +// // .WithEnvironment(environment) +// // .BuildWorkflow(); +// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// // .ConfigureServices(builder => +// // { +// // builder.Services.AddDaprWorkflowBuilder( +// // configureRuntime: opt => +// // { +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // }, +// // configureClient: (sp, clientBuilder) => +// // { +// // var config = sp.GetRequiredService(); +// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// // }); +// // }) +// // .BuildAndStartAsync(); +// // +// // using var scope = testApp.CreateScope(); +// // var client = scope.ServiceProvider.GetRequiredService(); +// // +// // await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); +// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// // cancellation: TestContext.Current.CancellationToken); +// // +// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// // var output = result.ReadOutputAs(); +// // Assert.NotNull(output); +// // // No scope set → child should report no propagated history +// // Assert.False(output.ChildReceivedPropagatedHistory); +// // Assert.Equal(0, output.PropagatedEntryCount); +// // } +// // +// // /// +// // /// Verifies that scheduling a child workflow with +// // /// does not produce any errors and both workflows complete. +// // /// +// // [MinimumDaprRuntimeFact("1.18")] +// // public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() +// // { +// // var instanceId = Guid.NewGuid().ToString(); +// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// // +// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// // needsActorState: true, +// // cancellationToken: TestContext.Current.CancellationToken); +// // await environment.StartAsync(TestContext.Current.CancellationToken); +// // +// // var harness = new DaprHarnessBuilder(componentsDir) +// // .WithEnvironment(environment) +// // .BuildWorkflow(); +// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// // .ConfigureServices(builder => +// // { +// // builder.Services.AddDaprWorkflowBuilder( +// // configureRuntime: opt => +// // { +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // opt.RegisterActivity(); +// // }, +// // configureClient: (sp, clientBuilder) => +// // { +// // var config = sp.GetRequiredService(); +// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// // }); +// // }) +// // .BuildAndStartAsync(); +// // +// // using var scope = testApp.CreateScope(); +// // var client = scope.ServiceProvider.GetRequiredService(); +// // +// // await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); +// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// // cancellation: TestContext.Current.CancellationToken); +// // +// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// // } +// // +// // /// +// // /// Verifies that scheduling a child workflow with +// // /// does not produce any errors and both workflows complete. +// // /// +// // [MinimumDaprRuntimeFact("1.18")] +// // public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() +// // { +// // var instanceId = Guid.NewGuid().ToString(); +// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// // +// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// // needsActorState: true, +// // cancellationToken: TestContext.Current.CancellationToken); +// // await environment.StartAsync(TestContext.Current.CancellationToken); +// // +// // var harness = new DaprHarnessBuilder(componentsDir) +// // .WithEnvironment(environment) +// // .BuildWorkflow(); +// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// // .ConfigureServices(builder => +// // { +// // builder.Services.AddDaprWorkflowBuilder( +// // configureRuntime: opt => +// // { +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // }, +// // configureClient: (sp, clientBuilder) => +// // { +// // var config = sp.GetRequiredService(); +// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// // }); +// // }) +// // .BuildAndStartAsync(); +// // +// // using var scope = testApp.CreateScope(); +// // var client = scope.ServiceProvider.GetRequiredService(); +// // +// // await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); +// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// // cancellation: TestContext.Current.CancellationToken); +// // +// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// // } +// // +// // /// +// // /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with +// // /// None scope returns null, not an exception. +// // /// +// // [MinimumDaprRuntimeFact("1.18")] +// // public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() +// // { +// // var instanceId = Guid.NewGuid().ToString(); +// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// // +// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// // needsActorState: true, +// // cancellationToken: TestContext.Current.CancellationToken); +// // await environment.StartAsync(TestContext.Current.CancellationToken); +// // +// // var harness = new DaprHarnessBuilder(componentsDir) +// // .WithEnvironment(environment) +// // .BuildWorkflow(); +// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// // .ConfigureServices(builder => +// // { +// // builder.Services.AddDaprWorkflowBuilder( +// // configureRuntime: opt => +// // { +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // }, +// // configureClient: (sp, clientBuilder) => +// // { +// // var config = sp.GetRequiredService(); +// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// // }); +// // }) +// // .BuildAndStartAsync(); +// // +// // using var scope = testApp.CreateScope(); +// // var client = scope.ServiceProvider.GetRequiredService(); +// // +// // await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); +// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// // cancellation: TestContext.Current.CancellationToken); +// // +// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// // var output = result.ReadOutputAs(); +// // Assert.NotNull(output); +// // Assert.False(output.ChildReceivedPropagatedHistory); +// // } +// // +// // /// +// // /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation +// // /// fluent builder and that the child workflow completes successfully. +// // /// +// // [MinimumDaprRuntimeFact("1.18")] +// // public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() +// // { +// // var instanceId = Guid.NewGuid().ToString(); +// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// // +// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// // needsActorState: true, +// // cancellationToken: TestContext.Current.CancellationToken); +// // await environment.StartAsync(TestContext.Current.CancellationToken); +// // +// // var harness = new DaprHarnessBuilder(componentsDir) +// // .WithEnvironment(environment) +// // .BuildWorkflow(); +// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// // .ConfigureServices(builder => +// // { +// // builder.Services.AddDaprWorkflowBuilder( +// // configureRuntime: opt => +// // { +// // opt.RegisterWorkflow(); +// // opt.RegisterWorkflow(); +// // }, +// // configureClient: (sp, clientBuilder) => +// // { +// // var config = sp.GetRequiredService(); +// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// // }); +// // }) +// // .BuildAndStartAsync(); +// // +// // using var scope = testApp.CreateScope(); +// // var client = scope.ServiceProvider.GetRequiredService(); +// // +// // await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); +// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// // cancellation: TestContext.Current.CancellationToken); +// // +// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// // } +// // +// // private sealed record PropagationTestResult( +// // bool ChildReceivedPropagatedHistory, +// // int PropagatedEntryCount); +// // +// // /// +// // /// Parent workflow that schedules a child with no propagation scope (default). +// // /// +// // private sealed class NoPropagationParent : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // var childId = $"{context.InstanceId}-child"; +// // return await context.CallChildWorkflowAsync( +// // nameof(PropagatedHistoryReceiver), +// // input: null, +// // options: new ChildWorkflowTaskOptions(InstanceId: childId)); +// // } +// // } +// // +// // /// +// // /// Parent workflow that runs an activity first (to build some history), then schedules a child +// // /// with OwnHistory propagation. +// // /// +// // private sealed class OwnHistoryPropagationParent : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // // Run an activity to build some history +// // await context.CallActivityAsync( +// // nameof(EchoActivity), "ping"); +// // +// // var childId = $"{context.InstanceId}-child"; +// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// // .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// // +// // return await context.CallChildWorkflowAsync( +// // nameof(PropagatedHistoryReceiver), +// // input: null, +// // options: childOptions); +// // } +// // } +// // +// // /// +// // /// Grandparent → middle → child lineage, each using Lineage propagation. +// // /// +// // private sealed class LineagePropagationParent : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // var childId = $"{context.InstanceId}-middle"; +// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// // .WithHistoryPropagation(HistoryPropagationScope.Lineage); +// // +// // return await context.CallChildWorkflowAsync( +// // nameof(LineagePropagationMiddle), +// // input: null, +// // options: childOptions); +// // } +// // } +// // +// // private sealed class LineagePropagationMiddle : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // var childId = $"{context.InstanceId}-leaf"; +// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) +// // .WithHistoryPropagation(HistoryPropagationScope.Lineage); +// // +// // var result = await context.CallChildWorkflowAsync( +// // nameof(PropagatedHistoryReceiver), +// // input: null, +// // options: childOptions); +// // +// // return result is not null; +// // } +// // } +// // +// // /// +// // /// Parent workflow that uses the fluent WithHistoryPropagation builder style. +// // /// +// // private sealed class FluentBuilderParent : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // var childId = $"{context.InstanceId}-child"; +// // +// // // Use fluent builder chaining +// // var options = new ChildWorkflowTaskOptions(InstanceId: childId) +// // .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); +// // +// // var result = await context.CallChildWorkflowAsync( +// // nameof(PropagatedHistoryReceiver), +// // input: null, +// // options: options); +// // +// // return result is not null; +// // } +// // } +// // +// // /// +// // /// Child workflow that inspects its propagated history and reports back what it found. +// // /// +// // private sealed class PropagatedHistoryReceiver : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, object? input) +// // { +// // var propagated = context.GetPropagatedHistory(); +// // // Yield via a zero-duration timer so this sub-orchestration completes through +// // // a proper async round-trip with the sidecar rather than synchronously on its +// // // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous +// // // first-turn completion causes the parent to hang waiting for the completion +// // // event that the sidecar never delivers for same-turn completions. +// // await context.CreateTimer(TimeSpan.Zero); +// // return new PropagationTestResult( +// // ChildReceivedPropagatedHistory: propagated is not null, +// // PropagatedEntryCount: propagated?.Entries.Count ?? 0); +// // } +// // } +// // +// // private sealed class EchoActivity : WorkflowActivity +// // { +// // public override Task RunAsync(WorkflowActivityContext context, string input) +// // => Task.FromResult(input); +// // } +// // +// // private sealed class SimpleActivityWorkflow : Workflow +// // { +// // public override async Task RunAsync(WorkflowContext context, string input) +// // { +// // return await context.CallActivityAsync(nameof(EchoActivity), input); +// // } +// // } +// // } +// // // ------------------------------------------------------------------------ // // Copyright 2026 The Dapr Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -89,6 +508,61 @@ // } // // /// +// /// Variant of where the child workflow +// /// yields via an activity round-trip instead of a zero-duration timer. This exercises the same +// /// "child sub-orchestration must perform at least one async operation" requirement but through +// /// a different awaitable so both yield mechanisms are covered. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ShouldCompleteSuccessfully_WithNoPropagationScope_ChildYieldsViaActivity() +// { +// var instanceId = Guid.NewGuid().ToString(); +// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); +// +// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( +// needsActorState: true, +// cancellationToken: TestContext.Current.CancellationToken); +// await environment.StartAsync(TestContext.Current.CancellationToken); +// +// var harness = new DaprHarnessBuilder(componentsDir) +// .WithEnvironment(environment) +// .BuildWorkflow(); +// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) +// .ConfigureServices(builder => +// { +// builder.Services.AddDaprWorkflowBuilder( +// configureRuntime: opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterWorkflow(); +// opt.RegisterActivity(); +// }, +// configureClient: (sp, clientBuilder) => +// { +// var config = sp.GetRequiredService(); +// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; +// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) +// clientBuilder.UseGrpcEndpoint(grpcEndpoint); +// }); +// }) +// .BuildAndStartAsync(); +// +// using var scope = testApp.CreateScope(); +// var client = scope.ServiceProvider.GetRequiredService(); +// +// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParentWithActivityChild), instanceId); +// var result = await client.WaitForWorkflowCompletionAsync(instanceId, +// cancellation: TestContext.Current.CancellationToken); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// var output = result.ReadOutputAs(); +// Assert.NotNull(output); +// // No scope set → child should report no propagated history +// Assert.False(output.ChildReceivedPropagatedHistory); +// Assert.Equal(0, output.PropagatedEntryCount); +// } +// +// /// // /// Verifies that scheduling a child workflow with // /// does not produce any errors and both workflows complete. // /// @@ -401,6 +875,43 @@ // } // } // +// /// +// /// Variant of that yields via an activity round-trip +// /// instead of a zero-duration timer. Used to verify that the activity-based async yield works +// /// equivalently for satisfying the "sub-orchestration must perform at least one async operation" +// /// requirement. +// /// +// private sealed class PropagatedHistoryReceiverWithActivity : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var propagated = context.GetPropagatedHistory(); +// // Yield via an activity round-trip so this sub-orchestration completes through +// // a proper async round-trip with the sidecar rather than synchronously on its +// // first turn. +// await context.CallActivityAsync(nameof(EchoActivity), "yield"); +// return new PropagationTestResult( +// ChildReceivedPropagatedHistory: propagated is not null, +// PropagatedEntryCount: propagated?.Entries.Count ?? 0); +// } +// } +// +// /// +// /// Parent workflow paired with . Schedules +// /// the activity-yielding child with no propagation scope (default). +// /// +// private sealed class NoPropagationParentWithActivityChild : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, object? input) +// { +// var childId = $"{context.InstanceId}-child"; +// return await context.CallChildWorkflowAsync( +// nameof(PropagatedHistoryReceiverWithActivity), +// input: null, +// options: new ChildWorkflowTaskOptions(InstanceId: childId)); +// } +// } +// // private sealed class EchoActivity : WorkflowActivity // { // public override Task RunAsync(WorkflowActivityContext context, string input) @@ -415,3 +926,4 @@ // } // } // } +// >>>>>>> origin/workflow-propagation-fix diff --git a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs index 50711bed9..b15ba6bc8 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs @@ -1,910 +1,910 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using System.Reflection; -using System.Text.Json; -using Dapr.DurableTask.Protobuf; -using Dapr.Common.Serialization; -using Dapr.Workflow.Versioning; -using Dapr.Workflow.Worker; -using Dapr.Workflow.Worker.Internal; -using Dapr.Workflow.Abstractions; -using Dapr.Testcontainers.Xunit.Attributes; -using Grpc.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; - -namespace Dapr.Workflow.Test.Worker.Internal; - -/// -/// Tests for timer origin assignment and optional timer replay compatibility. -/// -public class TimerOriginTests -{ - private static readonly DateTime StartTime = new(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); - - // ===================================================================== - // Origin assignment tests (Tests 1–6) - // ===================================================================== - - /// - /// Test 1 — CreateTimer(delay) sets TimerOriginCreateTimer. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task CreateTimer_SetsTimerOriginCreateTimer() - { - var context = CreateContext(); - _ = context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); - - var action = Assert.Single(context.PendingActions); - Assert.NotNull(action.CreateTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.CreateTimer, action.CreateTimer.OriginCase); - } - - /// - /// Test 2 — finite-timeout WaitForExternalEvent sets TimerOriginExternalEvent. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task WaitForExternalEvent_FiniteTimeout_SetsTimerOriginExternalEvent() - { - var context = CreateContext(); - var timeout = TimeSpan.FromSeconds(5); - _ = context.WaitForExternalEventAsync("myEvent", timeout); - - // There should be a pending CreateTimer action with ExternalEvent origin - var timerAction = context.PendingActions - .FirstOrDefault(a => a.CreateTimer != null); - Assert.NotNull(timerAction); - Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); - Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); - - // Verify fireAt = startTime + timeout - var expectedFireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(5)); - Assert.Equal(expectedFireAt, timerAction.CreateTimer.FireAt); - } - - /// - /// Test 3 — activity retry timer sets TimerOriginActivityRetry. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ActivityRetry_SetsTimerOriginActivityRetry() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions - { - RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, - firstRetryInterval: TimeSpan.FromSeconds(1)) - }); - return null; - })); - factory.AddActivity("failAct", new InlineActivity( - inputType: typeof(object), - run: (_, _) => throw new InvalidOperationException("boom"))); - - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - new HistoryEvent - { - EventId = 0, - TaskScheduled = new TaskScheduledEvent { Name = "failAct" } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - TaskFailed = new TaskFailedEvent - { - TaskScheduledId = 0, - FailureDetails = new TaskFailureDetails - { - ErrorType = "InvalidOperationException", - ErrorMessage = "boom" - } - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - // Should have a retry timer with ActivityRetry origin - var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); - Assert.NotNull(retryTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.ActivityRetry, retryTimer!.CreateTimer!.OriginCase); - Assert.NotEmpty(retryTimer.CreateTimer.ActivityRetry.TaskExecutionId); - } - - /// - /// Test 4 — activity retry taskExecutionId is stable across attempts. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions - { - RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, - firstRetryInterval: TimeSpan.FromSeconds(1)) - }); - return null; - })); - factory.AddActivity("failAct", new InlineActivity( - inputType: typeof(object), - run: (_, _) => throw new InvalidOperationException("boom"))); - - // History: first attempt fails, retry timer fires, second attempt fails - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - // First attempt scheduled and fails - new HistoryEvent - { - EventId = 0, - TaskScheduled = new TaskScheduledEvent { Name = "failAct" } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - TaskFailed = new TaskFailedEvent - { - TaskScheduledId = 0, - FailureDetails = new TaskFailureDetails - { - ErrorType = "InvalidOperationException", - ErrorMessage = "boom" - } - } - }, - // Retry timer created and fires - new HistoryEvent - { - EventId = 1, - TimerCreated = new TimerCreatedEvent - { - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - TimerFired = new TimerFiredEvent - { - TimerId = 1, - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) - } - }, - // Second attempt scheduled and fails - new HistoryEvent - { - EventId = 2, - TaskScheduled = new TaskScheduledEvent { Name = "failAct" } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(3)), - new HistoryEvent - { - TaskFailed = new TaskFailedEvent - { - TaskScheduledId = 2, - FailureDetails = new TaskFailureDetails - { - ErrorType = "InvalidOperationException", - ErrorMessage = "boom" - } - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - // Should have a second retry timer with the same taskExecutionId as the first - var retryTimers = response.Actions - .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ActivityRetry) - .ToList(); - - Assert.Single(retryTimers); - Assert.NotEmpty(retryTimers[0].CreateTimer!.ActivityRetry.TaskExecutionId); - } - - /// - /// Test 5 — child workflow retry timer sets TimerOriginChildWorkflowRetry. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions - { - RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, - firstRetryInterval: TimeSpan.FromSeconds(1)) - }); - return null; - })); - - // We need the child to fail. The failure is indicated in history. - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - // First child scheduled - new HistoryEvent - { - EventId = 0, - ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent - { - Name = "ChildWf", - InstanceId = "child-0" - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent - { - TaskScheduledId = 0, - FailureDetails = new TaskFailureDetails - { - ErrorType = "Exception", - ErrorMessage = "child failed" - } - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - // Should have a retry timer with ChildWorkflowRetry origin - var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); - Assert.NotNull(retryTimer); - Assert.Equal(CreateTimerAction.OriginOneofCase.ChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); - Assert.NotEmpty(retryTimer.CreateTimer.ChildWorkflowRetry.InstanceId); - } - - /// - /// Test 6 — child workflow retry instanceId always points to first child. - /// Verifies the first-child rule across multiple retries. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions - { - RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, - firstRetryInterval: TimeSpan.FromSeconds(1)) - }); - return null; - })); - - // First attempt: child scheduled, created, and fails. - // Then retry timer fires and second child scheduled, created, and fails. - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - // First child - we don't know the exact generated instanceId, so match by EventId - new HistoryEvent - { - EventId = 0, - ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent - { - Name = "ChildWf", - InstanceId = "child-first" - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent - { - TaskScheduledId = 0, - FailureDetails = new TaskFailureDetails - { - ErrorType = "Exception", - ErrorMessage = "child failed" - } - } - }, - // Retry timer - new HistoryEvent - { - EventId = 1, - TimerCreated = new TimerCreatedEvent - { - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - TimerFired = new TimerFiredEvent - { - TimerId = 1, - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) - } - }, - // Second child scheduled and fails - new HistoryEvent - { - EventId = 2, - ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent - { - Name = "ChildWf", - InstanceId = "child-second" - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(3)), - new HistoryEvent - { - ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent - { - TaskScheduledId = 2, - FailureDetails = new TaskFailureDetails - { - ErrorType = "Exception", - ErrorMessage = "child failed again" - } - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - // Should have a second retry timer with the first child's instance ID - var retryTimer = response.Actions.FirstOrDefault(a => - a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ChildWorkflowRetry); - Assert.NotNull(retryTimer); - - // The instanceId should be stable — it should be the same value that was used - // for the first retry timer (which we can't directly observe in this test since - // the first timer was already consumed in history). But we can verify it's not empty. - Assert.NotEmpty(retryTimer!.CreateTimer!.ChildWorkflowRetry.InstanceId); - } - - // ===================================================================== - // Optional timer — happy path (Tests 7–8) - // ===================================================================== - - /// - /// Test 7 — indefinite WaitForExternalEvent emits the sentinel optional timer. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task WaitForExternalEvent_Indefinite_EmitsSentinelOptionalTimer() - { - var context = CreateContext(); - _ = context.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); - - var timerAction = context.PendingActions - .FirstOrDefault(a => a.CreateTimer != null); - Assert.NotNull(timerAction); - Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); - Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); - Assert.Equal(TimerOriginHelpers.ExternalEventIndefiniteFireAt, timerAction.CreateTimer.FireAt); - } - - /// - /// Test 8 — zero-timeout WaitForExternalEvent emits no timer. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task WaitForExternalEvent_ZeroTimeout_EmitsNoTimer() - { - var context = CreateContext(); - - // Zero timeout should throw TaskCanceledException and emit no timer - await Assert.ThrowsAsync(() => - context.WaitForExternalEventAsync("myEvent", TimeSpan.Zero)); - - var timerAction = context.PendingActions - .FirstOrDefault(a => a.CreateTimer != null); - Assert.Null(timerAction); - } - - // ===================================================================== - // Optional timer — replay compatibility (Tests 9–13) - // ===================================================================== - - /// - /// Test 9 — post-patch replay matches the optional timer normally. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task Replay_PostPatch_MatchesOptionalTimerNormally() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - var result = await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); - return result; - })); - - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - // Post-patch history includes the optional timer - new HistoryEvent - { - EventId = 0, - TimerCreated = new TimerCreatedEvent - { - FireAt = TimerOriginHelpers.ExternalEventIndefiniteFireAt, - ExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - }, - NewEvents = - { - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "myEvent", - Input = "\"hello\"" - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; - Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); - } - - /// - /// Test 10 — pre-patch replay, indefinite wait followed by CallActivity. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); - var result = await ctx.CallActivityAsync("A"); - return result; - })); - factory.AddActivity("A", new InlineActivity( - inputType: typeof(object), - run: (_, _) => Task.FromResult("result"))); - - // Pre-patch history: no optional timer, activity at EventId=0 - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "myEvent", - Input = "\"eventPayload\"" - } - }, - new HistoryEvent - { - EventId = 0, - TaskScheduled = new TaskScheduledEvent { Name = "A" } - }, - }, - NewEvents = - { - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - TaskCompleted = new TaskCompletedEvent - { - TaskScheduledId = 0, - Result = "\"result\"" - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; - Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); - - // Verify no optional timer leaks into the result - var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); - Assert.Empty(timerActions); - } - - /// - /// Test 11 — pre-patch replay, indefinite wait followed by CallChildWorkflow. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); - var result = await ctx.CallChildWorkflowAsync("Child"); - return result; - })); - - // Pre-patch history: no optional timer, child at EventId=0 - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "myEvent", - Input = "\"eventPayload\"" - } - }, - new HistoryEvent - { - EventId = 0, - ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent - { - Name = "Child", - InstanceId = "child-1" - } - } - }, - NewEvents = - { - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent - { - TaskScheduledId = 0, - Result = "\"childResult\"" - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; - Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); - } - - /// - /// Test 12 — pre-patch replay, indefinite wait followed by a user CreateTimer. - /// Asymmetric TimerCreated-specific branch: pending is optional timer, incoming is CreateTimer origin. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); - await ctx.CreateTimer(TimeSpan.FromSeconds(5)); - return null; - })); - - // Pre-patch history: no optional timer, user timer at EventId=0 - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "myEvent", - Input = "\"payload\"" - } - }, - new HistoryEvent - { - EventId = 0, - TimerCreated = new TimerCreatedEvent - { - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)), - CreateTimer = new TimerOriginCreateTimer() - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(6)), - new HistoryEvent - { - TimerFired = new TimerFiredEvent - { - TimerId = 0, - FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)) - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; - Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); - } - - /// - /// Test 13 — pre-patch replay, two indefinite waits in sequence. - /// Validates drop-and-shift composes correctly across multiple optional timers. - /// - [MinimumDaprRuntimeFact("1.18")] - public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() - { - var (worker, factory) = CreateWorkerAndFactory(); - - factory.AddWorkflow("wf", new InlineWorkflow( - inputType: typeof(object), - run: async (ctx, _) => - { - await ctx.WaitForExternalEventAsync("A", TimeSpan.FromSeconds(-1)); - await ctx.CallActivityAsync("ActA"); - await ctx.WaitForExternalEventAsync("B", TimeSpan.FromSeconds(-1)); - await ctx.CallActivityAsync("ActB"); - return null; - })); - factory.AddActivity("ActA", new InlineActivity( - inputType: typeof(object), - run: (_, _) => Task.FromResult("resultA"))); - factory.AddActivity("ActB", new InlineActivity( - inputType: typeof(object), - run: (_, _) => Task.FromResult("resultB"))); - - // Pre-patch history: no optional timers. - // ActA at EventId=0, ActB at EventId=1. - var request = new WorkflowRequest - { - InstanceId = "i", - PastEvents = - { - MakeExecutionStarted("wf"), - MakeOrchestratorStarted(StartTime.AddSeconds(1)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "A", - Input = "\"payloadA\"" - } - }, - new HistoryEvent - { - EventId = 0, - TaskScheduled = new TaskScheduledEvent { Name = "ActA" } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(2)), - new HistoryEvent - { - TaskCompleted = new TaskCompletedEvent - { - TaskScheduledId = 0, - Result = "\"resultA\"" - } - }, - MakeOrchestratorStarted(StartTime.AddSeconds(3)), - new HistoryEvent - { - EventRaised = new EventRaisedEvent - { - Name = "B", - Input = "\"payloadB\"" - } - }, - new HistoryEvent - { - EventId = 1, - TaskScheduled = new TaskScheduledEvent { Name = "ActB" } - }, - }, - NewEvents = - { - MakeOrchestratorStarted(StartTime.AddSeconds(4)), - new HistoryEvent - { - TaskCompleted = new TaskCompletedEvent - { - TaskScheduledId = 1, - Result = "\"resultB\"" - } - } - } - }; - - var response = await InvokeHandleWorkflowResponseAsync(worker, request); - - Assert.Equal("i", response.InstanceId); - var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; - Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); - - // Verify no optional timers leak into the result - var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); - Assert.Empty(timerActions); - } - - // ===================================================================== - // Helper methods - // ===================================================================== - - private static WorkflowOrchestrationContext CreateContext() - { - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var tracker = new WorkflowVersionTracker([]); - return new WorkflowOrchestrationContext( - name: "wf", - instanceId: "instance-1", - currentUtcDateTime: StartTime, - workflowSerializer: serializer, - loggerFactory: NullLoggerFactory.Instance, - versionTracker: tracker); - } - - private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorkerAndFactory() - { - var sp = new ServiceCollection().BuildServiceProvider(); - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var factory = new StubWorkflowsFactory(); - - var callInvoker = new Mock(MockBehavior.Loose); - var grpcClient = new Mock(callInvoker.Object); - - var worker = new WorkflowWorker( - grpcClient.Object, - factory, - NullLoggerFactory.Instance, - serializer, - sp); - - return (worker, factory); - } - - private static HistoryEvent MakeExecutionStarted(string name, string? input = null) - { - return new HistoryEvent - { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime), - ExecutionStarted = new ExecutionStartedEvent - { - Name = name, - Input = input - } - }; - } - - private static HistoryEvent MakeOrchestratorStarted(DateTime timestamp) - { - return new HistoryEvent - { - Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(timestamp), - WorkflowStarted = new WorkflowStartedEvent() - }; - } - - private const string CompletionTokenValue = "abc123"; - - private static async Task InvokeHandleWorkflowResponseAsync( - WorkflowWorker worker, WorkflowRequest request) - { - var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(method); - - var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; - return await task; - } - - private sealed class StubWorkflowsFactory : IWorkflowsFactory - { - private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _activities = new(StringComparer.OrdinalIgnoreCase); - - public void AddWorkflow(string name, IWorkflow wf) => _workflows[name] = wf; - public void AddActivity(string name, IWorkflowActivity act) => _activities[name] = act; - - public void RegisterWorkflow(string? name = null) where TWorkflow : class, IWorkflow - => throw new NotSupportedException(); - public void RegisterWorkflow(string name, - Func> implementation) => throw new NotSupportedException(); - public void RegisterActivity(string? name = null) where TActivity : class, IWorkflowActivity - => throw new NotSupportedException(); - public void RegisterActivity(string name, - Func> implementation) => throw new NotSupportedException(); - - public bool TryCreateWorkflow(TaskIdentifier identifier, IServiceProvider serviceProvider, - out IWorkflow? workflow, out Exception? activationException) - { - activationException = null; - return _workflows.TryGetValue(identifier.Name, out workflow); - } - - public bool TryCreateActivity(TaskIdentifier identifier, IServiceProvider serviceProvider, - out IWorkflowActivity? activity, out Exception? activationException) - { - activationException = null; - return _activities.TryGetValue(identifier.Name, out activity); - } - } - - private sealed class InlineWorkflow(Type inputType, Func> run) : IWorkflow - { - public Type InputType { get; } = inputType; - public Type OutputType => typeof(object); - public Task RunAsync(WorkflowContext context, object? input) => run(context, input); - } - - private sealed class InlineActivity(Type inputType, Func> run) : IWorkflowActivity - { - public Type InputType { get; } = inputType; - public Type OutputType => typeof(object); - public Task RunAsync(WorkflowActivityContext context, object? input) => run(context, input); - } -} +// // ------------------------------------------------------------------------ +// // Copyright 2025 The Dapr Authors +// // Licensed under the Apache License, Version 2.0 (the "License"); +// // you may not use this file except in compliance with the License. +// // You may obtain a copy of the License at +// // http://www.apache.org/licenses/LICENSE-2.0 +// // Unless required by applicable law or agreed to in writing, software +// // distributed under the License is distributed on an "AS IS" BASIS, +// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// // See the License for the specific language governing permissions and +// // limitations under the License. +// // ------------------------------------------------------------------------ +// +// using System.Reflection; +// using System.Text.Json; +// using Dapr.DurableTask.Protobuf; +// using Dapr.Common.Serialization; +// using Dapr.Workflow.Versioning; +// using Dapr.Workflow.Worker; +// using Dapr.Workflow.Worker.Internal; +// using Dapr.Workflow.Abstractions; +// using Dapr.Testcontainers.Xunit.Attributes; +// using Grpc.Core; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Logging.Abstractions; +// using Moq; +// +// namespace Dapr.Workflow.Test.Worker.Internal; +// +// /// +// /// Tests for timer origin assignment and optional timer replay compatibility. +// /// +// public class TimerOriginTests +// { +// private static readonly DateTime StartTime = new(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); +// +// // ===================================================================== +// // Origin assignment tests (Tests 1–6) +// // ===================================================================== +// +// /// +// /// Test 1 — CreateTimer(delay) sets TimerOriginCreateTimer. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task CreateTimer_SetsTimerOriginCreateTimer() +// { +// var context = CreateContext(); +// _ = context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); +// +// var action = Assert.Single(context.PendingActions); +// Assert.NotNull(action.CreateTimer); +// Assert.Equal(CreateTimerAction.OriginOneofCase.CreateTimer, action.CreateTimer.OriginCase); +// } +// +// /// +// /// Test 2 — finite-timeout WaitForExternalEvent sets TimerOriginExternalEvent. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task WaitForExternalEvent_FiniteTimeout_SetsTimerOriginExternalEvent() +// { +// var context = CreateContext(); +// var timeout = TimeSpan.FromSeconds(5); +// _ = context.WaitForExternalEventAsync("myEvent", timeout); +// +// // There should be a pending CreateTimer action with ExternalEvent origin +// var timerAction = context.PendingActions +// .FirstOrDefault(a => a.CreateTimer != null); +// Assert.NotNull(timerAction); +// Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); +// Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); +// +// // Verify fireAt = startTime + timeout +// var expectedFireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(5)); +// Assert.Equal(expectedFireAt, timerAction.CreateTimer.FireAt); +// } +// +// /// +// /// Test 3 — activity retry timer sets TimerOriginActivityRetry. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ActivityRetry_SetsTimerOriginActivityRetry() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions +// { +// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, +// firstRetryInterval: TimeSpan.FromSeconds(1)) +// }); +// return null; +// })); +// factory.AddActivity("failAct", new InlineActivity( +// inputType: typeof(object), +// run: (_, _) => throw new InvalidOperationException("boom"))); +// +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// new HistoryEvent +// { +// EventId = 0, +// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// TaskFailed = new TaskFailedEvent +// { +// TaskScheduledId = 0, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "InvalidOperationException", +// ErrorMessage = "boom" +// } +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// // Should have a retry timer with ActivityRetry origin +// var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); +// Assert.NotNull(retryTimer); +// Assert.Equal(CreateTimerAction.OriginOneofCase.ActivityRetry, retryTimer!.CreateTimer!.OriginCase); +// Assert.NotEmpty(retryTimer.CreateTimer.ActivityRetry.TaskExecutionId); +// } +// +// /// +// /// Test 4 — activity retry taskExecutionId is stable across attempts. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions +// { +// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, +// firstRetryInterval: TimeSpan.FromSeconds(1)) +// }); +// return null; +// })); +// factory.AddActivity("failAct", new InlineActivity( +// inputType: typeof(object), +// run: (_, _) => throw new InvalidOperationException("boom"))); +// +// // History: first attempt fails, retry timer fires, second attempt fails +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// // First attempt scheduled and fails +// new HistoryEvent +// { +// EventId = 0, +// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// TaskFailed = new TaskFailedEvent +// { +// TaskScheduledId = 0, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "InvalidOperationException", +// ErrorMessage = "boom" +// } +// } +// }, +// // Retry timer created and fires +// new HistoryEvent +// { +// EventId = 1, +// TimerCreated = new TimerCreatedEvent +// { +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// TimerFired = new TimerFiredEvent +// { +// TimerId = 1, +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) +// } +// }, +// // Second attempt scheduled and fails +// new HistoryEvent +// { +// EventId = 2, +// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(3)), +// new HistoryEvent +// { +// TaskFailed = new TaskFailedEvent +// { +// TaskScheduledId = 2, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "InvalidOperationException", +// ErrorMessage = "boom" +// } +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// // Should have a second retry timer with the same taskExecutionId as the first +// var retryTimers = response.Actions +// .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ActivityRetry) +// .ToList(); +// +// Assert.Single(retryTimers); +// Assert.NotEmpty(retryTimers[0].CreateTimer!.ActivityRetry.TaskExecutionId); +// } +// +// /// +// /// Test 5 — child workflow retry timer sets TimerOriginChildWorkflowRetry. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions +// { +// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, +// firstRetryInterval: TimeSpan.FromSeconds(1)) +// }); +// return null; +// })); +// +// // We need the child to fail. The failure is indicated in history. +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// // First child scheduled +// new HistoryEvent +// { +// EventId = 0, +// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent +// { +// Name = "ChildWf", +// InstanceId = "child-0" +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent +// { +// TaskScheduledId = 0, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "Exception", +// ErrorMessage = "child failed" +// } +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// // Should have a retry timer with ChildWorkflowRetry origin +// var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); +// Assert.NotNull(retryTimer); +// Assert.Equal(CreateTimerAction.OriginOneofCase.ChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); +// Assert.NotEmpty(retryTimer.CreateTimer.ChildWorkflowRetry.InstanceId); +// } +// +// /// +// /// Test 6 — child workflow retry instanceId always points to first child. +// /// Verifies the first-child rule across multiple retries. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions +// { +// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, +// firstRetryInterval: TimeSpan.FromSeconds(1)) +// }); +// return null; +// })); +// +// // First attempt: child scheduled, created, and fails. +// // Then retry timer fires and second child scheduled, created, and fails. +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// // First child - we don't know the exact generated instanceId, so match by EventId +// new HistoryEvent +// { +// EventId = 0, +// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent +// { +// Name = "ChildWf", +// InstanceId = "child-first" +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent +// { +// TaskScheduledId = 0, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "Exception", +// ErrorMessage = "child failed" +// } +// } +// }, +// // Retry timer +// new HistoryEvent +// { +// EventId = 1, +// TimerCreated = new TimerCreatedEvent +// { +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// TimerFired = new TimerFiredEvent +// { +// TimerId = 1, +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) +// } +// }, +// // Second child scheduled and fails +// new HistoryEvent +// { +// EventId = 2, +// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent +// { +// Name = "ChildWf", +// InstanceId = "child-second" +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(3)), +// new HistoryEvent +// { +// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent +// { +// TaskScheduledId = 2, +// FailureDetails = new TaskFailureDetails +// { +// ErrorType = "Exception", +// ErrorMessage = "child failed again" +// } +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// // Should have a second retry timer with the first child's instance ID +// var retryTimer = response.Actions.FirstOrDefault(a => +// a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ChildWorkflowRetry); +// Assert.NotNull(retryTimer); +// +// // The instanceId should be stable — it should be the same value that was used +// // for the first retry timer (which we can't directly observe in this test since +// // the first timer was already consumed in history). But we can verify it's not empty. +// Assert.NotEmpty(retryTimer!.CreateTimer!.ChildWorkflowRetry.InstanceId); +// } +// +// // ===================================================================== +// // Optional timer — happy path (Tests 7–8) +// // ===================================================================== +// +// /// +// /// Test 7 — indefinite WaitForExternalEvent emits the sentinel optional timer. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task WaitForExternalEvent_Indefinite_EmitsSentinelOptionalTimer() +// { +// var context = CreateContext(); +// _ = context.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); +// +// var timerAction = context.PendingActions +// .FirstOrDefault(a => a.CreateTimer != null); +// Assert.NotNull(timerAction); +// Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); +// Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); +// Assert.Equal(TimerOriginHelpers.ExternalEventIndefiniteFireAt, timerAction.CreateTimer.FireAt); +// } +// +// /// +// /// Test 8 — zero-timeout WaitForExternalEvent emits no timer. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task WaitForExternalEvent_ZeroTimeout_EmitsNoTimer() +// { +// var context = CreateContext(); +// +// // Zero timeout should throw TaskCanceledException and emit no timer +// await Assert.ThrowsAsync(() => +// context.WaitForExternalEventAsync("myEvent", TimeSpan.Zero)); +// +// var timerAction = context.PendingActions +// .FirstOrDefault(a => a.CreateTimer != null); +// Assert.Null(timerAction); +// } +// +// // ===================================================================== +// // Optional timer — replay compatibility (Tests 9–13) +// // ===================================================================== +// +// /// +// /// Test 9 — post-patch replay matches the optional timer normally. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task Replay_PostPatch_MatchesOptionalTimerNormally() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// var result = await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); +// return result; +// })); +// +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// // Post-patch history includes the optional timer +// new HistoryEvent +// { +// EventId = 0, +// TimerCreated = new TimerCreatedEvent +// { +// FireAt = TimerOriginHelpers.ExternalEventIndefiniteFireAt, +// ExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// }, +// NewEvents = +// { +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "myEvent", +// Input = "\"hello\"" +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// Assert.Equal("i", response.InstanceId); +// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; +// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); +// } +// +// /// +// /// Test 10 — pre-patch replay, indefinite wait followed by CallActivity. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); +// var result = await ctx.CallActivityAsync("A"); +// return result; +// })); +// factory.AddActivity("A", new InlineActivity( +// inputType: typeof(object), +// run: (_, _) => Task.FromResult("result"))); +// +// // Pre-patch history: no optional timer, activity at EventId=0 +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "myEvent", +// Input = "\"eventPayload\"" +// } +// }, +// new HistoryEvent +// { +// EventId = 0, +// TaskScheduled = new TaskScheduledEvent { Name = "A" } +// }, +// }, +// NewEvents = +// { +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// TaskCompleted = new TaskCompletedEvent +// { +// TaskScheduledId = 0, +// Result = "\"result\"" +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// Assert.Equal("i", response.InstanceId); +// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; +// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); +// +// // Verify no optional timer leaks into the result +// var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); +// Assert.Empty(timerActions); +// } +// +// /// +// /// Test 11 — pre-patch replay, indefinite wait followed by CallChildWorkflow. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); +// var result = await ctx.CallChildWorkflowAsync("Child"); +// return result; +// })); +// +// // Pre-patch history: no optional timer, child at EventId=0 +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "myEvent", +// Input = "\"eventPayload\"" +// } +// }, +// new HistoryEvent +// { +// EventId = 0, +// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent +// { +// Name = "Child", +// InstanceId = "child-1" +// } +// } +// }, +// NewEvents = +// { +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent +// { +// TaskScheduledId = 0, +// Result = "\"childResult\"" +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// Assert.Equal("i", response.InstanceId); +// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; +// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); +// } +// +// /// +// /// Test 12 — pre-patch replay, indefinite wait followed by a user CreateTimer. +// /// Asymmetric TimerCreated-specific branch: pending is optional timer, incoming is CreateTimer origin. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); +// await ctx.CreateTimer(TimeSpan.FromSeconds(5)); +// return null; +// })); +// +// // Pre-patch history: no optional timer, user timer at EventId=0 +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "myEvent", +// Input = "\"payload\"" +// } +// }, +// new HistoryEvent +// { +// EventId = 0, +// TimerCreated = new TimerCreatedEvent +// { +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)), +// CreateTimer = new TimerOriginCreateTimer() +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(6)), +// new HistoryEvent +// { +// TimerFired = new TimerFiredEvent +// { +// TimerId = 0, +// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)) +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// Assert.Equal("i", response.InstanceId); +// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; +// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); +// } +// +// /// +// /// Test 13 — pre-patch replay, two indefinite waits in sequence. +// /// Validates drop-and-shift composes correctly across multiple optional timers. +// /// +// [MinimumDaprRuntimeFact("1.18")] +// public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() +// { +// var (worker, factory) = CreateWorkerAndFactory(); +// +// factory.AddWorkflow("wf", new InlineWorkflow( +// inputType: typeof(object), +// run: async (ctx, _) => +// { +// await ctx.WaitForExternalEventAsync("A", TimeSpan.FromSeconds(-1)); +// await ctx.CallActivityAsync("ActA"); +// await ctx.WaitForExternalEventAsync("B", TimeSpan.FromSeconds(-1)); +// await ctx.CallActivityAsync("ActB"); +// return null; +// })); +// factory.AddActivity("ActA", new InlineActivity( +// inputType: typeof(object), +// run: (_, _) => Task.FromResult("resultA"))); +// factory.AddActivity("ActB", new InlineActivity( +// inputType: typeof(object), +// run: (_, _) => Task.FromResult("resultB"))); +// +// // Pre-patch history: no optional timers. +// // ActA at EventId=0, ActB at EventId=1. +// var request = new WorkflowRequest +// { +// InstanceId = "i", +// PastEvents = +// { +// MakeExecutionStarted("wf"), +// MakeOrchestratorStarted(StartTime.AddSeconds(1)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "A", +// Input = "\"payloadA\"" +// } +// }, +// new HistoryEvent +// { +// EventId = 0, +// TaskScheduled = new TaskScheduledEvent { Name = "ActA" } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(2)), +// new HistoryEvent +// { +// TaskCompleted = new TaskCompletedEvent +// { +// TaskScheduledId = 0, +// Result = "\"resultA\"" +// } +// }, +// MakeOrchestratorStarted(StartTime.AddSeconds(3)), +// new HistoryEvent +// { +// EventRaised = new EventRaisedEvent +// { +// Name = "B", +// Input = "\"payloadB\"" +// } +// }, +// new HistoryEvent +// { +// EventId = 1, +// TaskScheduled = new TaskScheduledEvent { Name = "ActB" } +// }, +// }, +// NewEvents = +// { +// MakeOrchestratorStarted(StartTime.AddSeconds(4)), +// new HistoryEvent +// { +// TaskCompleted = new TaskCompletedEvent +// { +// TaskScheduledId = 1, +// Result = "\"resultB\"" +// } +// } +// } +// }; +// +// var response = await InvokeHandleWorkflowResponseAsync(worker, request); +// +// Assert.Equal("i", response.InstanceId); +// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; +// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); +// +// // Verify no optional timers leak into the result +// var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); +// Assert.Empty(timerActions); +// } +// +// // ===================================================================== +// // Helper methods +// // ===================================================================== +// +// private static WorkflowOrchestrationContext CreateContext() +// { +// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); +// var tracker = new WorkflowVersionTracker([]); +// return new WorkflowOrchestrationContext( +// name: "wf", +// instanceId: "instance-1", +// currentUtcDateTime: StartTime, +// workflowSerializer: serializer, +// loggerFactory: NullLoggerFactory.Instance, +// versionTracker: tracker); +// } +// +// private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorkerAndFactory() +// { +// var sp = new ServiceCollection().BuildServiceProvider(); +// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); +// var factory = new StubWorkflowsFactory(); +// +// var callInvoker = new Mock(MockBehavior.Loose); +// var grpcClient = new Mock(callInvoker.Object); +// +// var worker = new WorkflowWorker( +// grpcClient.Object, +// factory, +// NullLoggerFactory.Instance, +// serializer, +// sp); +// +// return (worker, factory); +// } +// +// private static HistoryEvent MakeExecutionStarted(string name, string? input = null) +// { +// return new HistoryEvent +// { +// Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime), +// ExecutionStarted = new ExecutionStartedEvent +// { +// Name = name, +// Input = input +// } +// }; +// } +// +// private static HistoryEvent MakeOrchestratorStarted(DateTime timestamp) +// { +// return new HistoryEvent +// { +// Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(timestamp), +// WorkflowStarted = new WorkflowStartedEvent() +// }; +// } +// +// private const string CompletionTokenValue = "abc123"; +// +// private static async Task InvokeHandleWorkflowResponseAsync( +// WorkflowWorker worker, WorkflowRequest request) +// { +// var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", +// BindingFlags.Instance | BindingFlags.NonPublic); +// Assert.NotNull(method); +// +// var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; +// return await task; +// } +// +// private sealed class StubWorkflowsFactory : IWorkflowsFactory +// { +// private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase); +// private readonly Dictionary _activities = new(StringComparer.OrdinalIgnoreCase); +// +// public void AddWorkflow(string name, IWorkflow wf) => _workflows[name] = wf; +// public void AddActivity(string name, IWorkflowActivity act) => _activities[name] = act; +// +// public void RegisterWorkflow(string? name = null) where TWorkflow : class, IWorkflow +// => throw new NotSupportedException(); +// public void RegisterWorkflow(string name, +// Func> implementation) => throw new NotSupportedException(); +// public void RegisterActivity(string? name = null) where TActivity : class, IWorkflowActivity +// => throw new NotSupportedException(); +// public void RegisterActivity(string name, +// Func> implementation) => throw new NotSupportedException(); +// +// public bool TryCreateWorkflow(TaskIdentifier identifier, IServiceProvider serviceProvider, +// out IWorkflow? workflow, out Exception? activationException) +// { +// activationException = null; +// return _workflows.TryGetValue(identifier.Name, out workflow); +// } +// +// public bool TryCreateActivity(TaskIdentifier identifier, IServiceProvider serviceProvider, +// out IWorkflowActivity? activity, out Exception? activationException) +// { +// activationException = null; +// return _activities.TryGetValue(identifier.Name, out activity); +// } +// } +// +// private sealed class InlineWorkflow(Type inputType, Func> run) : IWorkflow +// { +// public Type InputType { get; } = inputType; +// public Type OutputType => typeof(object); +// public Task RunAsync(WorkflowContext context, object? input) => run(context, input); +// } +// +// private sealed class InlineActivity(Type inputType, Func> run) : IWorkflowActivity +// { +// public Type InputType { get; } = inputType; +// public Type OutputType => typeof(object); +// public Task RunAsync(WorkflowActivityContext context, object? input) => run(context, input); +// } +// } From a47ca17c47d27bed9c05b8dd189008c25c6c38d5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 18 May 2026 01:36:25 -0500 Subject: [PATCH 12/17] Removed deprecated tests Signed-off-by: Whit Waldo --- .../Worker/WorkflowWorkerTests.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs index c374babf8..df6b2bc9b 100644 --- a/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs +++ b/test/Dapr.Workflow.Test/Worker/WorkflowWorkerTests.cs @@ -373,16 +373,6 @@ public void Constructor_ShouldThrowArgumentNullException_WhenServiceProviderIsNu null!)); } - [Fact] - public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - var grpcClient = CreateGrpcClientMock().Object; - - Assert.Throws(() => - new WorkflowWorker(grpcClient, Mock.Of(), Mock.Of(), Mock.Of(), - new ServiceCollection().BuildServiceProvider(), null!)); - } - [Fact] public async Task StopAsync_ShouldNotThrow_WhenProtocolHandlerWasNeverCreated() { @@ -648,28 +638,6 @@ public async Task ExecuteAsync_ShouldSwallowOperationCanceledException_WhenStopp await InvokeExecuteAsync(worker, cts.Token); } - [Fact] - public async Task ExecuteAsync_ShouldRethrow_WhenOptionsHaveInvalidConcurrency() - { - var services = new ServiceCollection().BuildServiceProvider(); - var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var options = new WorkflowRuntimeOptions(); - - // Bypass property validation to simulate corrupted configuration. - typeof(WorkflowRuntimeOptions) - .GetField("_maxConcurrentWorkflows", BindingFlags.Instance | BindingFlags.NonPublic)! - .SetValue(options, 0); - - var worker = new WorkflowWorker( - CreateGrpcClientMock().Object, - new StubWorkflowsFactory(), - NullLoggerFactory.Instance, - serializer, - services); - - await Assert.ThrowsAsync(() => InvokeExecuteAsync(worker, CancellationToken.None)); - } - [Fact] public void CreateCallOptions_ShouldIncludeUserAgentAndApiToken_WhenConfigured() { From b96bcb8f9355a3b2c8d5a930a6c788c27f1adee6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 07:47:46 +0000 Subject: [PATCH 13/17] Fix NullReferenceException in WorkflowWorker when PropagatedHistory is unset Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/3d331cd1-0e46-4c9b-bb01-5f676dd59f0e Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- src/Dapr.Workflow/Worker/WorkflowWorker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/Worker/WorkflowWorker.cs b/src/Dapr.Workflow/Worker/WorkflowWorker.cs index 8e9048c07..e13c1b4ac 100644 --- a/src/Dapr.Workflow/Worker/WorkflowWorker.cs +++ b/src/Dapr.Workflow/Worker/WorkflowWorker.cs @@ -318,7 +318,7 @@ private async Task HandleWorkflowResponseAsync(WorkflowRequest ?? currentUtcDateTime; // Initialize the context with the FULL history - var incomingPropagatedHistory = request.PropagatedHistory.Chunks.Count > 0 + var incomingPropagatedHistory = request.PropagatedHistory?.Chunks.Count > 0 ? request.PropagatedHistory.Chunks : null; var context = new WorkflowOrchestrationContext(workflowName, request.InstanceId, currentUtcDateTime, From 56b934ccf3c3cd4ce33e5f150c711f6d66c1bbe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 08:32:13 +0000 Subject: [PATCH 14/17] Use CompleteOrchestratorTask for compatibility with Dapr runtimes < 1.18 Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/847ff050-0e63-4b35-8340-dfae20990da3 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../Worker/Grpc/GrpcProtocolHandler.cs | 4 +++- .../Worker/Grpc/GrpcProtocolHandlerTests.cs | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs b/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs index 097fc8641..05f8a76fb 100644 --- a/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs +++ b/src/Dapr.Workflow/Worker/Grpc/GrpcProtocolHandler.cs @@ -247,7 +247,9 @@ private async Task ProcessWorkflowAsync(WorkflowRequest request, string completi try { var grpcCallOptions = CreateCallOptions(cancellationToken); - await _grpcClient.CompleteWorkflowTaskAsync(result, grpcCallOptions); +#pragma warning disable CS0612 // Deprecated: kept for compatibility with Dapr runtimes < 1.18 where CompleteWorkflowTask is not available. + await _grpcClient.CompleteOrchestratorTaskAsync(result, grpcCallOptions); +#pragma warning restore CS0612 } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { diff --git a/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs b/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs index 3537c7a71..02fafcb17 100644 --- a/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Grpc/GrpcProtocolHandlerTests.cs @@ -19,6 +19,8 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; +#pragma warning disable CS0612 // Tests reference deprecated CompleteOrchestratorTaskAsync intentionally for compatibility with Dapr runtimes < 1.18. + namespace Dapr.Workflow.Test.Worker.Grpc; public sealed class GrpcProtocolHandlerTests @@ -112,7 +114,7 @@ public async Task StartAsync_ShouldCompleteOrchestratorTask_ForOrchestratorWorkI var completedTcs = CreateTcs(); grpcClientMock - .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) .Callback((r, _) => completedTcs.TrySetResult(r)) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); @@ -202,7 +204,7 @@ public async Task StartAsync_ShouldSendFailureResult_WhenOrchestratorHandlerThro var completedTcs = CreateTcs(); grpcClientMock - .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) .Callback((r, _) => completedTcs.TrySetResult(r)) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); @@ -472,7 +474,7 @@ public async Task StartAsync_ShouldNotCallCompleteOrchestratorTask_WhenWorkflowH .Returns(CreateServerStreamingCallIgnoringCancellation(workItems)); grpcClientMock - .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) .Returns(CreateAsyncUnaryCall(new CompleteTaskResponse())); var handler = new GrpcProtocolHandler(grpcClientMock.Object, NullLoggerFactory.Instance); @@ -490,7 +492,7 @@ await handler.StartAsync( cancellationToken: cts.Token); grpcClientMock.Verify( - x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny()), + x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny()), Times.Never()); } @@ -557,7 +559,7 @@ public async Task StartAsync_ShouldNotThrow_WhenWorkflowHandlerThrows_AndSending var completeAttempted = false; grpcClientMock - .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) .Callback(() => completeAttempted = true) .Throws(new RpcException(new Status(StatusCode.Unavailable, "nope"))); @@ -757,7 +759,7 @@ public async Task StartAsync_ShouldCallCompleteOrchestratorTaskOnce_WhenWorkflow WorkflowResponse? capturedResponse = null; grpcClientMock - .Setup(x => x.CompleteWorkflowTaskAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.CompleteOrchestratorTaskAsync(It.IsAny(), It.IsAny())) .Callback((r, _) => { Interlocked.Increment(ref completeCallCount); From b5e45c29702a03ea807208d06594d008fae2b8b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 10:16:08 +0000 Subject: [PATCH 15/17] Re-enable TimerOriginTests unit tests Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/1fddc697-c7d7-417f-a3b3-bbe5c0d8eaff Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../Worker/Internal/TimerOriginTests.cs | 1820 ++++++++--------- 1 file changed, 910 insertions(+), 910 deletions(-) diff --git a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs index b15ba6bc8..50711bed9 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/TimerOriginTests.cs @@ -1,910 +1,910 @@ -// // ------------------------------------------------------------------------ -// // Copyright 2025 The Dapr Authors -// // Licensed under the Apache License, Version 2.0 (the "License"); -// // you may not use this file except in compliance with the License. -// // You may obtain a copy of the License at -// // http://www.apache.org/licenses/LICENSE-2.0 -// // Unless required by applicable law or agreed to in writing, software -// // distributed under the License is distributed on an "AS IS" BASIS, -// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // See the License for the specific language governing permissions and -// // limitations under the License. -// // ------------------------------------------------------------------------ -// -// using System.Reflection; -// using System.Text.Json; -// using Dapr.DurableTask.Protobuf; -// using Dapr.Common.Serialization; -// using Dapr.Workflow.Versioning; -// using Dapr.Workflow.Worker; -// using Dapr.Workflow.Worker.Internal; -// using Dapr.Workflow.Abstractions; -// using Dapr.Testcontainers.Xunit.Attributes; -// using Grpc.Core; -// using Microsoft.Extensions.DependencyInjection; -// using Microsoft.Extensions.Logging.Abstractions; -// using Moq; -// -// namespace Dapr.Workflow.Test.Worker.Internal; -// -// /// -// /// Tests for timer origin assignment and optional timer replay compatibility. -// /// -// public class TimerOriginTests -// { -// private static readonly DateTime StartTime = new(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); -// -// // ===================================================================== -// // Origin assignment tests (Tests 1–6) -// // ===================================================================== -// -// /// -// /// Test 1 — CreateTimer(delay) sets TimerOriginCreateTimer. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task CreateTimer_SetsTimerOriginCreateTimer() -// { -// var context = CreateContext(); -// _ = context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); -// -// var action = Assert.Single(context.PendingActions); -// Assert.NotNull(action.CreateTimer); -// Assert.Equal(CreateTimerAction.OriginOneofCase.CreateTimer, action.CreateTimer.OriginCase); -// } -// -// /// -// /// Test 2 — finite-timeout WaitForExternalEvent sets TimerOriginExternalEvent. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task WaitForExternalEvent_FiniteTimeout_SetsTimerOriginExternalEvent() -// { -// var context = CreateContext(); -// var timeout = TimeSpan.FromSeconds(5); -// _ = context.WaitForExternalEventAsync("myEvent", timeout); -// -// // There should be a pending CreateTimer action with ExternalEvent origin -// var timerAction = context.PendingActions -// .FirstOrDefault(a => a.CreateTimer != null); -// Assert.NotNull(timerAction); -// Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); -// Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); -// -// // Verify fireAt = startTime + timeout -// var expectedFireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(5)); -// Assert.Equal(expectedFireAt, timerAction.CreateTimer.FireAt); -// } -// -// /// -// /// Test 3 — activity retry timer sets TimerOriginActivityRetry. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ActivityRetry_SetsTimerOriginActivityRetry() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions -// { -// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, -// firstRetryInterval: TimeSpan.FromSeconds(1)) -// }); -// return null; -// })); -// factory.AddActivity("failAct", new InlineActivity( -// inputType: typeof(object), -// run: (_, _) => throw new InvalidOperationException("boom"))); -// -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// new HistoryEvent -// { -// EventId = 0, -// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// TaskFailed = new TaskFailedEvent -// { -// TaskScheduledId = 0, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "InvalidOperationException", -// ErrorMessage = "boom" -// } -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// // Should have a retry timer with ActivityRetry origin -// var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); -// Assert.NotNull(retryTimer); -// Assert.Equal(CreateTimerAction.OriginOneofCase.ActivityRetry, retryTimer!.CreateTimer!.OriginCase); -// Assert.NotEmpty(retryTimer.CreateTimer.ActivityRetry.TaskExecutionId); -// } -// -// /// -// /// Test 4 — activity retry taskExecutionId is stable across attempts. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions -// { -// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, -// firstRetryInterval: TimeSpan.FromSeconds(1)) -// }); -// return null; -// })); -// factory.AddActivity("failAct", new InlineActivity( -// inputType: typeof(object), -// run: (_, _) => throw new InvalidOperationException("boom"))); -// -// // History: first attempt fails, retry timer fires, second attempt fails -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// // First attempt scheduled and fails -// new HistoryEvent -// { -// EventId = 0, -// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// TaskFailed = new TaskFailedEvent -// { -// TaskScheduledId = 0, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "InvalidOperationException", -// ErrorMessage = "boom" -// } -// } -// }, -// // Retry timer created and fires -// new HistoryEvent -// { -// EventId = 1, -// TimerCreated = new TimerCreatedEvent -// { -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// TimerFired = new TimerFiredEvent -// { -// TimerId = 1, -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) -// } -// }, -// // Second attempt scheduled and fails -// new HistoryEvent -// { -// EventId = 2, -// TaskScheduled = new TaskScheduledEvent { Name = "failAct" } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(3)), -// new HistoryEvent -// { -// TaskFailed = new TaskFailedEvent -// { -// TaskScheduledId = 2, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "InvalidOperationException", -// ErrorMessage = "boom" -// } -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// // Should have a second retry timer with the same taskExecutionId as the first -// var retryTimers = response.Actions -// .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ActivityRetry) -// .ToList(); -// -// Assert.Single(retryTimers); -// Assert.NotEmpty(retryTimers[0].CreateTimer!.ActivityRetry.TaskExecutionId); -// } -// -// /// -// /// Test 5 — child workflow retry timer sets TimerOriginChildWorkflowRetry. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions -// { -// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, -// firstRetryInterval: TimeSpan.FromSeconds(1)) -// }); -// return null; -// })); -// -// // We need the child to fail. The failure is indicated in history. -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// // First child scheduled -// new HistoryEvent -// { -// EventId = 0, -// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent -// { -// Name = "ChildWf", -// InstanceId = "child-0" -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent -// { -// TaskScheduledId = 0, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "Exception", -// ErrorMessage = "child failed" -// } -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// // Should have a retry timer with ChildWorkflowRetry origin -// var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); -// Assert.NotNull(retryTimer); -// Assert.Equal(CreateTimerAction.OriginOneofCase.ChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); -// Assert.NotEmpty(retryTimer.CreateTimer.ChildWorkflowRetry.InstanceId); -// } -// -// /// -// /// Test 6 — child workflow retry instanceId always points to first child. -// /// Verifies the first-child rule across multiple retries. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions -// { -// RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, -// firstRetryInterval: TimeSpan.FromSeconds(1)) -// }); -// return null; -// })); -// -// // First attempt: child scheduled, created, and fails. -// // Then retry timer fires and second child scheduled, created, and fails. -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// // First child - we don't know the exact generated instanceId, so match by EventId -// new HistoryEvent -// { -// EventId = 0, -// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent -// { -// Name = "ChildWf", -// InstanceId = "child-first" -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent -// { -// TaskScheduledId = 0, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "Exception", -// ErrorMessage = "child failed" -// } -// } -// }, -// // Retry timer -// new HistoryEvent -// { -// EventId = 1, -// TimerCreated = new TimerCreatedEvent -// { -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// TimerFired = new TimerFiredEvent -// { -// TimerId = 1, -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) -// } -// }, -// // Second child scheduled and fails -// new HistoryEvent -// { -// EventId = 2, -// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent -// { -// Name = "ChildWf", -// InstanceId = "child-second" -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(3)), -// new HistoryEvent -// { -// ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent -// { -// TaskScheduledId = 2, -// FailureDetails = new TaskFailureDetails -// { -// ErrorType = "Exception", -// ErrorMessage = "child failed again" -// } -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// // Should have a second retry timer with the first child's instance ID -// var retryTimer = response.Actions.FirstOrDefault(a => -// a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ChildWorkflowRetry); -// Assert.NotNull(retryTimer); -// -// // The instanceId should be stable — it should be the same value that was used -// // for the first retry timer (which we can't directly observe in this test since -// // the first timer was already consumed in history). But we can verify it's not empty. -// Assert.NotEmpty(retryTimer!.CreateTimer!.ChildWorkflowRetry.InstanceId); -// } -// -// // ===================================================================== -// // Optional timer — happy path (Tests 7–8) -// // ===================================================================== -// -// /// -// /// Test 7 — indefinite WaitForExternalEvent emits the sentinel optional timer. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task WaitForExternalEvent_Indefinite_EmitsSentinelOptionalTimer() -// { -// var context = CreateContext(); -// _ = context.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); -// -// var timerAction = context.PendingActions -// .FirstOrDefault(a => a.CreateTimer != null); -// Assert.NotNull(timerAction); -// Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); -// Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); -// Assert.Equal(TimerOriginHelpers.ExternalEventIndefiniteFireAt, timerAction.CreateTimer.FireAt); -// } -// -// /// -// /// Test 8 — zero-timeout WaitForExternalEvent emits no timer. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task WaitForExternalEvent_ZeroTimeout_EmitsNoTimer() -// { -// var context = CreateContext(); -// -// // Zero timeout should throw TaskCanceledException and emit no timer -// await Assert.ThrowsAsync(() => -// context.WaitForExternalEventAsync("myEvent", TimeSpan.Zero)); -// -// var timerAction = context.PendingActions -// .FirstOrDefault(a => a.CreateTimer != null); -// Assert.Null(timerAction); -// } -// -// // ===================================================================== -// // Optional timer — replay compatibility (Tests 9–13) -// // ===================================================================== -// -// /// -// /// Test 9 — post-patch replay matches the optional timer normally. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task Replay_PostPatch_MatchesOptionalTimerNormally() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// var result = await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); -// return result; -// })); -// -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// // Post-patch history includes the optional timer -// new HistoryEvent -// { -// EventId = 0, -// TimerCreated = new TimerCreatedEvent -// { -// FireAt = TimerOriginHelpers.ExternalEventIndefiniteFireAt, -// ExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// }, -// NewEvents = -// { -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "myEvent", -// Input = "\"hello\"" -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// Assert.Equal("i", response.InstanceId); -// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; -// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); -// } -// -// /// -// /// Test 10 — pre-patch replay, indefinite wait followed by CallActivity. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); -// var result = await ctx.CallActivityAsync("A"); -// return result; -// })); -// factory.AddActivity("A", new InlineActivity( -// inputType: typeof(object), -// run: (_, _) => Task.FromResult("result"))); -// -// // Pre-patch history: no optional timer, activity at EventId=0 -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "myEvent", -// Input = "\"eventPayload\"" -// } -// }, -// new HistoryEvent -// { -// EventId = 0, -// TaskScheduled = new TaskScheduledEvent { Name = "A" } -// }, -// }, -// NewEvents = -// { -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// TaskCompleted = new TaskCompletedEvent -// { -// TaskScheduledId = 0, -// Result = "\"result\"" -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// Assert.Equal("i", response.InstanceId); -// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; -// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); -// -// // Verify no optional timer leaks into the result -// var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); -// Assert.Empty(timerActions); -// } -// -// /// -// /// Test 11 — pre-patch replay, indefinite wait followed by CallChildWorkflow. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); -// var result = await ctx.CallChildWorkflowAsync("Child"); -// return result; -// })); -// -// // Pre-patch history: no optional timer, child at EventId=0 -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "myEvent", -// Input = "\"eventPayload\"" -// } -// }, -// new HistoryEvent -// { -// EventId = 0, -// ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent -// { -// Name = "Child", -// InstanceId = "child-1" -// } -// } -// }, -// NewEvents = -// { -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent -// { -// TaskScheduledId = 0, -// Result = "\"childResult\"" -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// Assert.Equal("i", response.InstanceId); -// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; -// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); -// } -// -// /// -// /// Test 12 — pre-patch replay, indefinite wait followed by a user CreateTimer. -// /// Asymmetric TimerCreated-specific branch: pending is optional timer, incoming is CreateTimer origin. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); -// await ctx.CreateTimer(TimeSpan.FromSeconds(5)); -// return null; -// })); -// -// // Pre-patch history: no optional timer, user timer at EventId=0 -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "myEvent", -// Input = "\"payload\"" -// } -// }, -// new HistoryEvent -// { -// EventId = 0, -// TimerCreated = new TimerCreatedEvent -// { -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)), -// CreateTimer = new TimerOriginCreateTimer() -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(6)), -// new HistoryEvent -// { -// TimerFired = new TimerFiredEvent -// { -// TimerId = 0, -// FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)) -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// Assert.Equal("i", response.InstanceId); -// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; -// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); -// } -// -// /// -// /// Test 13 — pre-patch replay, two indefinite waits in sequence. -// /// Validates drop-and-shift composes correctly across multiple optional timers. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() -// { -// var (worker, factory) = CreateWorkerAndFactory(); -// -// factory.AddWorkflow("wf", new InlineWorkflow( -// inputType: typeof(object), -// run: async (ctx, _) => -// { -// await ctx.WaitForExternalEventAsync("A", TimeSpan.FromSeconds(-1)); -// await ctx.CallActivityAsync("ActA"); -// await ctx.WaitForExternalEventAsync("B", TimeSpan.FromSeconds(-1)); -// await ctx.CallActivityAsync("ActB"); -// return null; -// })); -// factory.AddActivity("ActA", new InlineActivity( -// inputType: typeof(object), -// run: (_, _) => Task.FromResult("resultA"))); -// factory.AddActivity("ActB", new InlineActivity( -// inputType: typeof(object), -// run: (_, _) => Task.FromResult("resultB"))); -// -// // Pre-patch history: no optional timers. -// // ActA at EventId=0, ActB at EventId=1. -// var request = new WorkflowRequest -// { -// InstanceId = "i", -// PastEvents = -// { -// MakeExecutionStarted("wf"), -// MakeOrchestratorStarted(StartTime.AddSeconds(1)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "A", -// Input = "\"payloadA\"" -// } -// }, -// new HistoryEvent -// { -// EventId = 0, -// TaskScheduled = new TaskScheduledEvent { Name = "ActA" } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(2)), -// new HistoryEvent -// { -// TaskCompleted = new TaskCompletedEvent -// { -// TaskScheduledId = 0, -// Result = "\"resultA\"" -// } -// }, -// MakeOrchestratorStarted(StartTime.AddSeconds(3)), -// new HistoryEvent -// { -// EventRaised = new EventRaisedEvent -// { -// Name = "B", -// Input = "\"payloadB\"" -// } -// }, -// new HistoryEvent -// { -// EventId = 1, -// TaskScheduled = new TaskScheduledEvent { Name = "ActB" } -// }, -// }, -// NewEvents = -// { -// MakeOrchestratorStarted(StartTime.AddSeconds(4)), -// new HistoryEvent -// { -// TaskCompleted = new TaskCompletedEvent -// { -// TaskScheduledId = 1, -// Result = "\"resultB\"" -// } -// } -// } -// }; -// -// var response = await InvokeHandleWorkflowResponseAsync(worker, request); -// -// Assert.Equal("i", response.InstanceId); -// var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; -// Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); -// -// // Verify no optional timers leak into the result -// var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); -// Assert.Empty(timerActions); -// } -// -// // ===================================================================== -// // Helper methods -// // ===================================================================== -// -// private static WorkflowOrchestrationContext CreateContext() -// { -// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); -// var tracker = new WorkflowVersionTracker([]); -// return new WorkflowOrchestrationContext( -// name: "wf", -// instanceId: "instance-1", -// currentUtcDateTime: StartTime, -// workflowSerializer: serializer, -// loggerFactory: NullLoggerFactory.Instance, -// versionTracker: tracker); -// } -// -// private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorkerAndFactory() -// { -// var sp = new ServiceCollection().BuildServiceProvider(); -// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); -// var factory = new StubWorkflowsFactory(); -// -// var callInvoker = new Mock(MockBehavior.Loose); -// var grpcClient = new Mock(callInvoker.Object); -// -// var worker = new WorkflowWorker( -// grpcClient.Object, -// factory, -// NullLoggerFactory.Instance, -// serializer, -// sp); -// -// return (worker, factory); -// } -// -// private static HistoryEvent MakeExecutionStarted(string name, string? input = null) -// { -// return new HistoryEvent -// { -// Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime), -// ExecutionStarted = new ExecutionStartedEvent -// { -// Name = name, -// Input = input -// } -// }; -// } -// -// private static HistoryEvent MakeOrchestratorStarted(DateTime timestamp) -// { -// return new HistoryEvent -// { -// Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(timestamp), -// WorkflowStarted = new WorkflowStartedEvent() -// }; -// } -// -// private const string CompletionTokenValue = "abc123"; -// -// private static async Task InvokeHandleWorkflowResponseAsync( -// WorkflowWorker worker, WorkflowRequest request) -// { -// var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", -// BindingFlags.Instance | BindingFlags.NonPublic); -// Assert.NotNull(method); -// -// var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; -// return await task; -// } -// -// private sealed class StubWorkflowsFactory : IWorkflowsFactory -// { -// private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase); -// private readonly Dictionary _activities = new(StringComparer.OrdinalIgnoreCase); -// -// public void AddWorkflow(string name, IWorkflow wf) => _workflows[name] = wf; -// public void AddActivity(string name, IWorkflowActivity act) => _activities[name] = act; -// -// public void RegisterWorkflow(string? name = null) where TWorkflow : class, IWorkflow -// => throw new NotSupportedException(); -// public void RegisterWorkflow(string name, -// Func> implementation) => throw new NotSupportedException(); -// public void RegisterActivity(string? name = null) where TActivity : class, IWorkflowActivity -// => throw new NotSupportedException(); -// public void RegisterActivity(string name, -// Func> implementation) => throw new NotSupportedException(); -// -// public bool TryCreateWorkflow(TaskIdentifier identifier, IServiceProvider serviceProvider, -// out IWorkflow? workflow, out Exception? activationException) -// { -// activationException = null; -// return _workflows.TryGetValue(identifier.Name, out workflow); -// } -// -// public bool TryCreateActivity(TaskIdentifier identifier, IServiceProvider serviceProvider, -// out IWorkflowActivity? activity, out Exception? activationException) -// { -// activationException = null; -// return _activities.TryGetValue(identifier.Name, out activity); -// } -// } -// -// private sealed class InlineWorkflow(Type inputType, Func> run) : IWorkflow -// { -// public Type InputType { get; } = inputType; -// public Type OutputType => typeof(object); -// public Task RunAsync(WorkflowContext context, object? input) => run(context, input); -// } -// -// private sealed class InlineActivity(Type inputType, Func> run) : IWorkflowActivity -// { -// public Type InputType { get; } = inputType; -// public Type OutputType => typeof(object); -// public Task RunAsync(WorkflowActivityContext context, object? input) => run(context, input); -// } -// } +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.Json; +using Dapr.DurableTask.Protobuf; +using Dapr.Common.Serialization; +using Dapr.Workflow.Versioning; +using Dapr.Workflow.Worker; +using Dapr.Workflow.Worker.Internal; +using Dapr.Workflow.Abstractions; +using Dapr.Testcontainers.Xunit.Attributes; +using Grpc.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace Dapr.Workflow.Test.Worker.Internal; + +/// +/// Tests for timer origin assignment and optional timer replay compatibility. +/// +public class TimerOriginTests +{ + private static readonly DateTime StartTime = new(2025, 01, 01, 12, 0, 0, DateTimeKind.Utc); + + // ===================================================================== + // Origin assignment tests (Tests 1–6) + // ===================================================================== + + /// + /// Test 1 — CreateTimer(delay) sets TimerOriginCreateTimer. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task CreateTimer_SetsTimerOriginCreateTimer() + { + var context = CreateContext(); + _ = context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); + + var action = Assert.Single(context.PendingActions); + Assert.NotNull(action.CreateTimer); + Assert.Equal(CreateTimerAction.OriginOneofCase.CreateTimer, action.CreateTimer.OriginCase); + } + + /// + /// Test 2 — finite-timeout WaitForExternalEvent sets TimerOriginExternalEvent. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task WaitForExternalEvent_FiniteTimeout_SetsTimerOriginExternalEvent() + { + var context = CreateContext(); + var timeout = TimeSpan.FromSeconds(5); + _ = context.WaitForExternalEventAsync("myEvent", timeout); + + // There should be a pending CreateTimer action with ExternalEvent origin + var timerAction = context.PendingActions + .FirstOrDefault(a => a.CreateTimer != null); + Assert.NotNull(timerAction); + Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); + Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); + + // Verify fireAt = startTime + timeout + var expectedFireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(5)); + Assert.Equal(expectedFireAt, timerAction.CreateTimer.FireAt); + } + + /// + /// Test 3 — activity retry timer sets TimerOriginActivityRetry. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ActivityRetry_SetsTimerOriginActivityRetry() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions + { + RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, + firstRetryInterval: TimeSpan.FromSeconds(1)) + }); + return null; + })); + factory.AddActivity("failAct", new InlineActivity( + inputType: typeof(object), + run: (_, _) => throw new InvalidOperationException("boom"))); + + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + new HistoryEvent + { + EventId = 0, + TaskScheduled = new TaskScheduledEvent { Name = "failAct" } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + TaskFailed = new TaskFailedEvent + { + TaskScheduledId = 0, + FailureDetails = new TaskFailureDetails + { + ErrorType = "InvalidOperationException", + ErrorMessage = "boom" + } + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + // Should have a retry timer with ActivityRetry origin + var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); + Assert.NotNull(retryTimer); + Assert.Equal(CreateTimerAction.OriginOneofCase.ActivityRetry, retryTimer!.CreateTimer!.OriginCase); + Assert.NotEmpty(retryTimer.CreateTimer.ActivityRetry.TaskExecutionId); + } + + /// + /// Test 4 — activity retry taskExecutionId is stable across attempts. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ActivityRetry_TaskExecutionId_IsStableAcrossAttempts() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.CallActivityAsync("failAct", options: new WorkflowTaskOptions + { + RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(1)) + }); + return null; + })); + factory.AddActivity("failAct", new InlineActivity( + inputType: typeof(object), + run: (_, _) => throw new InvalidOperationException("boom"))); + + // History: first attempt fails, retry timer fires, second attempt fails + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + // First attempt scheduled and fails + new HistoryEvent + { + EventId = 0, + TaskScheduled = new TaskScheduledEvent { Name = "failAct" } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + TaskFailed = new TaskFailedEvent + { + TaskScheduledId = 0, + FailureDetails = new TaskFailureDetails + { + ErrorType = "InvalidOperationException", + ErrorMessage = "boom" + } + } + }, + // Retry timer created and fires + new HistoryEvent + { + EventId = 1, + TimerCreated = new TimerCreatedEvent + { + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + TimerFired = new TimerFiredEvent + { + TimerId = 1, + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) + } + }, + // Second attempt scheduled and fails + new HistoryEvent + { + EventId = 2, + TaskScheduled = new TaskScheduledEvent { Name = "failAct" } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(3)), + new HistoryEvent + { + TaskFailed = new TaskFailedEvent + { + TaskScheduledId = 2, + FailureDetails = new TaskFailureDetails + { + ErrorType = "InvalidOperationException", + ErrorMessage = "boom" + } + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + // Should have a second retry timer with the same taskExecutionId as the first + var retryTimers = response.Actions + .Where(a => a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ActivityRetry) + .ToList(); + + Assert.Single(retryTimers); + Assert.NotEmpty(retryTimers[0].CreateTimer!.ActivityRetry.TaskExecutionId); + } + + /// + /// Test 5 — child workflow retry timer sets TimerOriginChildWorkflowRetry. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ChildWorkflowRetry_SetsTimerOriginChildWorkflowRetry() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions + { + RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 2, + firstRetryInterval: TimeSpan.FromSeconds(1)) + }); + return null; + })); + + // We need the child to fail. The failure is indicated in history. + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + // First child scheduled + new HistoryEvent + { + EventId = 0, + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent + { + Name = "ChildWf", + InstanceId = "child-0" + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent + { + TaskScheduledId = 0, + FailureDetails = new TaskFailureDetails + { + ErrorType = "Exception", + ErrorMessage = "child failed" + } + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + // Should have a retry timer with ChildWorkflowRetry origin + var retryTimer = response.Actions.FirstOrDefault(a => a.CreateTimer != null); + Assert.NotNull(retryTimer); + Assert.Equal(CreateTimerAction.OriginOneofCase.ChildWorkflowRetry, retryTimer!.CreateTimer!.OriginCase); + Assert.NotEmpty(retryTimer.CreateTimer.ChildWorkflowRetry.InstanceId); + } + + /// + /// Test 6 — child workflow retry instanceId always points to first child. + /// Verifies the first-child rule across multiple retries. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ChildWorkflowRetry_InstanceId_AlwaysPointsToFirstChild() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.CallChildWorkflowAsync("ChildWf", options: new ChildWorkflowTaskOptions + { + RetryPolicy = new WorkflowRetryPolicy(maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(1)) + }); + return null; + })); + + // First attempt: child scheduled, created, and fails. + // Then retry timer fires and second child scheduled, created, and fails. + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + // First child - we don't know the exact generated instanceId, so match by EventId + new HistoryEvent + { + EventId = 0, + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent + { + Name = "ChildWf", + InstanceId = "child-first" + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent + { + TaskScheduledId = 0, + FailureDetails = new TaskFailureDetails + { + ErrorType = "Exception", + ErrorMessage = "child failed" + } + } + }, + // Retry timer + new HistoryEvent + { + EventId = 1, + TimerCreated = new TimerCreatedEvent + { + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + TimerFired = new TimerFiredEvent + { + TimerId = 1, + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(2)) + } + }, + // Second child scheduled and fails + new HistoryEvent + { + EventId = 2, + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent + { + Name = "ChildWf", + InstanceId = "child-second" + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(3)), + new HistoryEvent + { + ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent + { + TaskScheduledId = 2, + FailureDetails = new TaskFailureDetails + { + ErrorType = "Exception", + ErrorMessage = "child failed again" + } + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + // Should have a second retry timer with the first child's instance ID + var retryTimer = response.Actions.FirstOrDefault(a => + a.CreateTimer?.OriginCase == CreateTimerAction.OriginOneofCase.ChildWorkflowRetry); + Assert.NotNull(retryTimer); + + // The instanceId should be stable — it should be the same value that was used + // for the first retry timer (which we can't directly observe in this test since + // the first timer was already consumed in history). But we can verify it's not empty. + Assert.NotEmpty(retryTimer!.CreateTimer!.ChildWorkflowRetry.InstanceId); + } + + // ===================================================================== + // Optional timer — happy path (Tests 7–8) + // ===================================================================== + + /// + /// Test 7 — indefinite WaitForExternalEvent emits the sentinel optional timer. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task WaitForExternalEvent_Indefinite_EmitsSentinelOptionalTimer() + { + var context = CreateContext(); + _ = context.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); + + var timerAction = context.PendingActions + .FirstOrDefault(a => a.CreateTimer != null); + Assert.NotNull(timerAction); + Assert.Equal(CreateTimerAction.OriginOneofCase.ExternalEvent, timerAction!.CreateTimer!.OriginCase); + Assert.Equal("myEvent", timerAction.CreateTimer.ExternalEvent.Name); + Assert.Equal(TimerOriginHelpers.ExternalEventIndefiniteFireAt, timerAction.CreateTimer.FireAt); + } + + /// + /// Test 8 — zero-timeout WaitForExternalEvent emits no timer. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task WaitForExternalEvent_ZeroTimeout_EmitsNoTimer() + { + var context = CreateContext(); + + // Zero timeout should throw TaskCanceledException and emit no timer + await Assert.ThrowsAsync(() => + context.WaitForExternalEventAsync("myEvent", TimeSpan.Zero)); + + var timerAction = context.PendingActions + .FirstOrDefault(a => a.CreateTimer != null); + Assert.Null(timerAction); + } + + // ===================================================================== + // Optional timer — replay compatibility (Tests 9–13) + // ===================================================================== + + /// + /// Test 9 — post-patch replay matches the optional timer normally. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task Replay_PostPatch_MatchesOptionalTimerNormally() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + var result = await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); + return result; + })); + + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + // Post-patch history includes the optional timer + new HistoryEvent + { + EventId = 0, + TimerCreated = new TimerCreatedEvent + { + FireAt = TimerOriginHelpers.ExternalEventIndefiniteFireAt, + ExternalEvent = new TimerOriginExternalEvent { Name = "myEvent" } + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + }, + NewEvents = + { + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "myEvent", + Input = "\"hello\"" + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + Assert.Equal("i", response.InstanceId); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + } + + /// + /// Test 10 — pre-patch replay, indefinite wait followed by CallActivity. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallActivity() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); + var result = await ctx.CallActivityAsync("A"); + return result; + })); + factory.AddActivity("A", new InlineActivity( + inputType: typeof(object), + run: (_, _) => Task.FromResult("result"))); + + // Pre-patch history: no optional timer, activity at EventId=0 + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "myEvent", + Input = "\"eventPayload\"" + } + }, + new HistoryEvent + { + EventId = 0, + TaskScheduled = new TaskScheduledEvent { Name = "A" } + }, + }, + NewEvents = + { + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + TaskCompleted = new TaskCompletedEvent + { + TaskScheduledId = 0, + Result = "\"result\"" + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + Assert.Equal("i", response.InstanceId); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + + // Verify no optional timer leaks into the result + var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); + Assert.Empty(timerActions); + } + + /// + /// Test 11 — pre-patch replay, indefinite wait followed by CallChildWorkflow. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task Replay_PrePatch_IndefiniteWait_FollowedByCallChildWorkflow() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); + var result = await ctx.CallChildWorkflowAsync("Child"); + return result; + })); + + // Pre-patch history: no optional timer, child at EventId=0 + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "myEvent", + Input = "\"eventPayload\"" + } + }, + new HistoryEvent + { + EventId = 0, + ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent + { + Name = "Child", + InstanceId = "child-1" + } + } + }, + NewEvents = + { + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent + { + TaskScheduledId = 0, + Result = "\"childResult\"" + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + Assert.Equal("i", response.InstanceId); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + } + + /// + /// Test 12 — pre-patch replay, indefinite wait followed by a user CreateTimer. + /// Asymmetric TimerCreated-specific branch: pending is optional timer, incoming is CreateTimer origin. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task Replay_PrePatch_IndefiniteWait_FollowedByUserCreateTimer() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.WaitForExternalEventAsync("myEvent", TimeSpan.FromSeconds(-1)); + await ctx.CreateTimer(TimeSpan.FromSeconds(5)); + return null; + })); + + // Pre-patch history: no optional timer, user timer at EventId=0 + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "myEvent", + Input = "\"payload\"" + } + }, + new HistoryEvent + { + EventId = 0, + TimerCreated = new TimerCreatedEvent + { + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)), + CreateTimer = new TimerOriginCreateTimer() + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(6)), + new HistoryEvent + { + TimerFired = new TimerFiredEvent + { + TimerId = 0, + FireAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime.AddSeconds(6)) + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + Assert.Equal("i", response.InstanceId); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + } + + /// + /// Test 13 — pre-patch replay, two indefinite waits in sequence. + /// Validates drop-and-shift composes correctly across multiple optional timers. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task Replay_PrePatch_TwoIndefiniteWaitsInSequence() + { + var (worker, factory) = CreateWorkerAndFactory(); + + factory.AddWorkflow("wf", new InlineWorkflow( + inputType: typeof(object), + run: async (ctx, _) => + { + await ctx.WaitForExternalEventAsync("A", TimeSpan.FromSeconds(-1)); + await ctx.CallActivityAsync("ActA"); + await ctx.WaitForExternalEventAsync("B", TimeSpan.FromSeconds(-1)); + await ctx.CallActivityAsync("ActB"); + return null; + })); + factory.AddActivity("ActA", new InlineActivity( + inputType: typeof(object), + run: (_, _) => Task.FromResult("resultA"))); + factory.AddActivity("ActB", new InlineActivity( + inputType: typeof(object), + run: (_, _) => Task.FromResult("resultB"))); + + // Pre-patch history: no optional timers. + // ActA at EventId=0, ActB at EventId=1. + var request = new WorkflowRequest + { + InstanceId = "i", + PastEvents = + { + MakeExecutionStarted("wf"), + MakeOrchestratorStarted(StartTime.AddSeconds(1)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "A", + Input = "\"payloadA\"" + } + }, + new HistoryEvent + { + EventId = 0, + TaskScheduled = new TaskScheduledEvent { Name = "ActA" } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(2)), + new HistoryEvent + { + TaskCompleted = new TaskCompletedEvent + { + TaskScheduledId = 0, + Result = "\"resultA\"" + } + }, + MakeOrchestratorStarted(StartTime.AddSeconds(3)), + new HistoryEvent + { + EventRaised = new EventRaisedEvent + { + Name = "B", + Input = "\"payloadB\"" + } + }, + new HistoryEvent + { + EventId = 1, + TaskScheduled = new TaskScheduledEvent { Name = "ActB" } + }, + }, + NewEvents = + { + MakeOrchestratorStarted(StartTime.AddSeconds(4)), + new HistoryEvent + { + TaskCompleted = new TaskCompletedEvent + { + TaskScheduledId = 1, + Result = "\"resultB\"" + } + } + } + }; + + var response = await InvokeHandleWorkflowResponseAsync(worker, request); + + Assert.Equal("i", response.InstanceId); + var complete = response.Actions.Single(a => a.CompleteWorkflow != null).CompleteWorkflow!; + Assert.Equal(OrchestrationStatus.Completed, complete.WorkflowStatus); + + // Verify no optional timers leak into the result + var timerActions = response.Actions.Where(a => a.CreateTimer != null).ToList(); + Assert.Empty(timerActions); + } + + // ===================================================================== + // Helper methods + // ===================================================================== + + private static WorkflowOrchestrationContext CreateContext() + { + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + return new WorkflowOrchestrationContext( + name: "wf", + instanceId: "instance-1", + currentUtcDateTime: StartTime, + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + versionTracker: tracker); + } + + private static (WorkflowWorker worker, StubWorkflowsFactory factory) CreateWorkerAndFactory() + { + var sp = new ServiceCollection().BuildServiceProvider(); + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var factory = new StubWorkflowsFactory(); + + var callInvoker = new Mock(MockBehavior.Loose); + var grpcClient = new Mock(callInvoker.Object); + + var worker = new WorkflowWorker( + grpcClient.Object, + factory, + NullLoggerFactory.Instance, + serializer, + sp); + + return (worker, factory); + } + + private static HistoryEvent MakeExecutionStarted(string name, string? input = null) + { + return new HistoryEvent + { + Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(StartTime), + ExecutionStarted = new ExecutionStartedEvent + { + Name = name, + Input = input + } + }; + } + + private static HistoryEvent MakeOrchestratorStarted(DateTime timestamp) + { + return new HistoryEvent + { + Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(timestamp), + WorkflowStarted = new WorkflowStartedEvent() + }; + } + + private const string CompletionTokenValue = "abc123"; + + private static async Task InvokeHandleWorkflowResponseAsync( + WorkflowWorker worker, WorkflowRequest request) + { + var method = typeof(WorkflowWorker).GetMethod("HandleWorkflowResponseAsync", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); + + var task = (Task)method!.Invoke(worker, [request, CompletionTokenValue])!; + return await task; + } + + private sealed class StubWorkflowsFactory : IWorkflowsFactory + { + private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _activities = new(StringComparer.OrdinalIgnoreCase); + + public void AddWorkflow(string name, IWorkflow wf) => _workflows[name] = wf; + public void AddActivity(string name, IWorkflowActivity act) => _activities[name] = act; + + public void RegisterWorkflow(string? name = null) where TWorkflow : class, IWorkflow + => throw new NotSupportedException(); + public void RegisterWorkflow(string name, + Func> implementation) => throw new NotSupportedException(); + public void RegisterActivity(string? name = null) where TActivity : class, IWorkflowActivity + => throw new NotSupportedException(); + public void RegisterActivity(string name, + Func> implementation) => throw new NotSupportedException(); + + public bool TryCreateWorkflow(TaskIdentifier identifier, IServiceProvider serviceProvider, + out IWorkflow? workflow, out Exception? activationException) + { + activationException = null; + return _workflows.TryGetValue(identifier.Name, out workflow); + } + + public bool TryCreateActivity(TaskIdentifier identifier, IServiceProvider serviceProvider, + out IWorkflowActivity? activity, out Exception? activationException) + { + activationException = null; + return _activities.TryGetValue(identifier.Name, out activity); + } + } + + private sealed class InlineWorkflow(Type inputType, Func> run) : IWorkflow + { + public Type InputType { get; } = inputType; + public Type OutputType => typeof(object); + public Task RunAsync(WorkflowContext context, object? input) => run(context, input); + } + + private sealed class InlineActivity(Type inputType, Func> run) : IWorkflowActivity + { + public Type InputType { get; } = inputType; + public Type OutputType => typeof(object); + public Task RunAsync(WorkflowActivityContext context, object? input) => run(context, input); + } +} From 9829e913ed89b6475c87e554af0294cfebb80213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 11:07:22 +0000 Subject: [PATCH 16/17] Re-implement workflow history propagation Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/ac031e69-2bb9-4785-80aa-f9de3c971af3 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../PropagatedHistory.cs | 170 +-- .../WorkflowContext.cs | 23 + .../Internal/WorkflowOrchestrationContext.cs | 79 + .../HistoryPropagationWorkflowTests.cs | 1346 +++++------------ .../ParallelExtensionsTest.cs | 2 + .../WorkflowHistoryPropagationTests.cs | 840 +++++----- .../Worker/WorkflowsFactoryTests.cs | 2 + ...orkflowServiceCollectionExtensionsTests.cs | 2 + 8 files changed, 967 insertions(+), 1497 deletions(-) diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs index 711563e8f..eda643b7d 100644 --- a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs +++ b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs @@ -1,85 +1,85 @@ -// // ------------------------------------------------------------------------ -// // Copyright 2026 The Dapr Authors -// // Licensed under the Apache License, Version 2.0 (the "License"); -// // you may not use this file except in compliance with the License. -// // You may obtain a copy of the License at -// // http://www.apache.org/licenses/LICENSE-2.0 -// // Unless required by applicable law or agreed to in writing, software -// // distributed under the License is distributed on an "AS IS" BASIS, -// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // See the License for the specific language governing permissions and -// // limitations under the License. -// // ------------------------------------------------------------------------ -// -// namespace Dapr.Workflow; -// -// using System; -// using System.Collections.Generic; -// using System.Linq; -// -// /// -// /// Contains the workflow history that was propagated from ancestor workflow instances. -// /// Each entry corresponds to a single ancestor's history. -// /// -// /// -// /// A workflow receives propagated history when it is scheduled with a -// /// other than . -// /// Use to retrieve the propagated history -// /// inside a workflow implementation. -// /// -// public sealed class PropagatedHistory -// { -// private readonly IReadOnlyList _entries; -// -// /// -// /// Initializes a new instance of with the given entries. -// /// -// /// The propagated history entries from ancestor workflows. -// public PropagatedHistory(IReadOnlyList entries) -// { -// _entries = entries ?? throw new ArgumentNullException(nameof(entries)); -// } -// -// /// -// /// Gets the ordered list of propagated history entries. -// /// The first entry corresponds to the immediate parent workflow; subsequent entries -// /// correspond to progressively older ancestors when is used. -// /// -// public IReadOnlyList Entries => _entries; -// -// /// -// /// Returns a new containing only entries from the specified App ID. -// /// -// /// The Dapr App ID to filter by. -// /// A filtered instance. -// public PropagatedHistory FilterByAppId(string appId) -// { -// ArgumentException.ThrowIfNullOrWhiteSpace(appId); -// return new PropagatedHistory( -// _entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList()); -// } -// -// /// -// /// Returns a new containing only the entry with the specified instance ID. -// /// -// /// The workflow instance ID to filter by. -// /// A filtered instance. -// public PropagatedHistory FilterByInstanceId(string instanceId) -// { -// ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); -// return new PropagatedHistory( -// _entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList()); -// } -// -// /// -// /// Returns a new containing only entries for the specified workflow name. -// /// -// /// The workflow name to filter by. -// /// A filtered instance. -// public PropagatedHistory FilterByWorkflowName(string workflowName) -// { -// ArgumentException.ThrowIfNullOrWhiteSpace(workflowName); -// return new PropagatedHistory( -// _entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList()); -// } -// } +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow; + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// Contains the workflow history that was propagated from ancestor workflow instances. +/// Each entry corresponds to a single ancestor's history. +/// +/// +/// A workflow receives propagated history when it is scheduled with a +/// other than . +/// Use to retrieve the propagated history +/// inside a workflow implementation. +/// +public sealed class PropagatedHistory +{ + private readonly IReadOnlyList _entries; + + /// + /// Initializes a new instance of with the given entries. + /// + /// The propagated history entries from ancestor workflows. + public PropagatedHistory(IReadOnlyList entries) + { + _entries = entries ?? throw new ArgumentNullException(nameof(entries)); + } + + /// + /// Gets the ordered list of propagated history entries. + /// The first entry corresponds to the immediate parent workflow; subsequent entries + /// correspond to progressively older ancestors when is used. + /// + public IReadOnlyList Entries => _entries; + + /// + /// Returns a new containing only entries from the specified App ID. + /// + /// The Dapr App ID to filter by. + /// A filtered instance. + public PropagatedHistory FilterByAppId(string appId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(appId); + return new PropagatedHistory( + _entries.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase)).ToList()); + } + + /// + /// Returns a new containing only the entry with the specified instance ID. + /// + /// The workflow instance ID to filter by. + /// A filtered instance. + public PropagatedHistory FilterByInstanceId(string instanceId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(instanceId); + return new PropagatedHistory( + _entries.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal)).ToList()); + } + + /// + /// Returns a new containing only entries for the specified workflow name. + /// + /// The workflow name to filter by. + /// A filtered instance. + public PropagatedHistory FilterByWorkflowName(string workflowName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(workflowName); + return new PropagatedHistory( + _entries.Where(e => string.Equals(e.WorkflowName, workflowName, StringComparison.Ordinal)).ToList()); + } +} diff --git a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs index c2071f3b3..de355eaf5 100644 --- a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs +++ b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs @@ -330,4 +330,27 @@ public virtual Task CallChildWorkflowAsync( /// /// The new value. public abstract Guid NewGuid(); + + /// + /// Gets the workflow history that was propagated from ancestor workflow instances, or null + /// if no history was propagated to this workflow. + /// + /// + /// + /// A workflow receives propagated history when it is scheduled as a child workflow and the parent + /// specified a other than . + /// + /// + /// Use , , + /// or to narrow down the returned entries. + /// + /// + /// This method always returns the same value regardless of whether the workflow is replaying. + /// + /// + /// + /// A containing entries from ancestor workflows, + /// or null if no history was propagated to this workflow instance. + /// + public abstract PropagatedHistory? GetPropagatedHistory(); } diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index e84c4c6b4..ca3e58068 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -71,6 +71,7 @@ internal sealed class WorkflowOrchestrationContext : WorkflowContext private static readonly Guid InstanceIdNamespace = new("6f927a2e-9c7e-4a1d-9b8d-7a86f2e7f62f"); private readonly string? _appId; + private readonly PropagatedHistory? _propagatedHistory; private int _sequenceNumber; private int _guidCounter; private object? _customStatus; @@ -99,6 +100,11 @@ public WorkflowOrchestrationContext(string name, string instanceId, DateTime cur _appId = appId; // Necessary for setting the source app ID value on the task router _versionTracker = versionTracker; + var propagatedChunks = incomingPropagatedHistory?.ToList(); + _propagatedHistory = propagatedChunks is { Count: > 0 } + ? new PropagatedHistory(propagatedChunks.Select(ConvertChunk).ToList()) + : null; + _logger.LogWorkflowContextConstructorSetup(name, instanceId); } @@ -508,6 +514,9 @@ public override Guid NewGuid() return CreateGuidFromName(_instanceGuid, Encoding.UTF8.GetBytes(name)); } + /// + public override PropagatedHistory? GetPropagatedHistory() => _propagatedHistory; + /// public override ILogger CreateReplaySafeLogger(string categoryName) => new ReplaySafeLogger(_loggerFactory.CreateLogger(categoryName), () => IsReplaying); @@ -982,4 +991,74 @@ private static WorkflowTaskFailedException CreateTaskFailedException(TaskFailedE return new WorkflowTaskFailedException($"Task failed: {failureDetails.ErrorMessage}", failureDetails); } + + /// + /// Converts a proto message to a domain . + /// + /// + /// Each entry is the deterministic byte representation + /// of a single ; we parse the bytes here on demand rather than re-marshalling. + /// Malformed event bytes are skipped (mapped to nothing) so a single bad event cannot crash the workflow. + /// + private static PropagatedHistoryEntry ConvertChunk(PropagatedHistoryChunk chunk) + { + var events = new List(chunk.RawEvents.Count); + foreach (var rawEvent in chunk.RawEvents) + { + HistoryEvent? historyEvent; + try + { + historyEvent = HistoryEvent.Parser.ParseFrom(rawEvent); + } + catch (InvalidProtocolBufferException) + { + continue; + } + + events.Add(new PropagatedHistoryEvent( + historyEvent.EventId, + MapEventKind(historyEvent), + MapTimestamp(historyEvent.Timestamp))); + } + + return new PropagatedHistoryEntry( + chunk.AppId, + chunk.InstanceId, + chunk.WorkflowName, + events); + } + + /// + /// Maps a proto to a . + /// + private static HistoryEventKind MapEventKind(HistoryEvent e) => e.EventTypeCase switch + { + HistoryEvent.EventTypeOneofCase.ExecutionStarted => HistoryEventKind.ExecutionStarted, + HistoryEvent.EventTypeOneofCase.ExecutionCompleted => HistoryEventKind.ExecutionCompleted, + HistoryEvent.EventTypeOneofCase.ExecutionTerminated => HistoryEventKind.ExecutionTerminated, + HistoryEvent.EventTypeOneofCase.TaskScheduled => HistoryEventKind.TaskScheduled, + HistoryEvent.EventTypeOneofCase.TaskCompleted => HistoryEventKind.TaskCompleted, + HistoryEvent.EventTypeOneofCase.TaskFailed => HistoryEventKind.TaskFailed, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceCreated => HistoryEventKind.SubOrchestrationInstanceCreated, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceCompleted => HistoryEventKind.SubOrchestrationInstanceCompleted, + HistoryEvent.EventTypeOneofCase.ChildWorkflowInstanceFailed => HistoryEventKind.SubOrchestrationInstanceFailed, + HistoryEvent.EventTypeOneofCase.TimerCreated => HistoryEventKind.TimerCreated, + HistoryEvent.EventTypeOneofCase.TimerFired => HistoryEventKind.TimerFired, + HistoryEvent.EventTypeOneofCase.WorkflowStarted => HistoryEventKind.OrchestratorStarted, + HistoryEvent.EventTypeOneofCase.WorkflowCompleted => HistoryEventKind.OrchestratorCompleted, + HistoryEvent.EventTypeOneofCase.EventSent => HistoryEventKind.EventSent, + HistoryEvent.EventTypeOneofCase.EventRaised => HistoryEventKind.EventRaised, + HistoryEvent.EventTypeOneofCase.ContinueAsNew => HistoryEventKind.ContinueAsNew, + HistoryEvent.EventTypeOneofCase.ExecutionSuspended => HistoryEventKind.ExecutionSuspended, + HistoryEvent.EventTypeOneofCase.ExecutionResumed => HistoryEventKind.ExecutionResumed, + _ => HistoryEventKind.Unknown + }; + + /// + /// Converts a proto Timestamp to a . + /// + private static DateTimeOffset MapTimestamp(Timestamp? timestamp) => + timestamp is not null + ? new DateTimeOffset(timestamp.ToDateTime(), TimeSpan.Zero) + : DateTimeOffset.MinValue; } diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs index 1b129c9ca..c8c1655e9 100644 --- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs +++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs @@ -1,929 +1,417 @@ -// <<<<<<< HEAD -// // // ------------------------------------------------------------------------ -// // // Copyright 2026 The Dapr Authors -// // // Licensed under the Apache License, Version 2.0 (the "License"); -// // // you may not use this file except in compliance with the License. -// // // You may obtain a copy of the License at -// // // http://www.apache.org/licenses/LICENSE-2.0 -// // // Unless required by applicable law or agreed to in writing, software -// // // distributed under the License is distributed on an "AS IS" BASIS, -// // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // // See the License for the specific language governing permissions and -// // // limitations under the License. -// // // ------------------------------------------------------------------------ -// // -// // using Dapr.Testcontainers.Common; -// // using Dapr.Testcontainers.Harnesses; -// // using Dapr.Testcontainers.Xunit.Attributes; -// // using Dapr.Workflow; -// // using Microsoft.Extensions.Configuration; -// // using Microsoft.Extensions.DependencyInjection; -// // -// // namespace Dapr.IntegrationTest.Workflow; -// // -// // /// -// // /// Integration tests for workflow history propagation. -// // /// -// // /// These tests verify the SDK-side API works end-to-end: -// // /// - Scheduling child workflows with a history propagation scope does not cause errors -// // /// - The parent and child workflows complete successfully -// // /// - The child can call GetPropagatedHistory() without error -// // /// -// // /// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires -// // /// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the -// // /// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. -// // /// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK -// // /// sets in the CreateSubOrchestrationAction proto message. -// // /// -// // public sealed class HistoryPropagationWorkflowTests -// // { -// // /// -// // /// Verifies that scheduling a child workflow with -// // /// (the default) completes successfully. -// // /// -// // [MinimumDaprRuntimeFact("1.18")] -// // public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() -// // { -// // var instanceId = Guid.NewGuid().ToString(); -// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// // -// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// // needsActorState: true, -// // cancellationToken: TestContext.Current.CancellationToken); -// // await environment.StartAsync(TestContext.Current.CancellationToken); -// // -// // var harness = new DaprHarnessBuilder(componentsDir) -// // .WithEnvironment(environment) -// // .BuildWorkflow(); -// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// // .ConfigureServices(builder => -// // { -// // builder.Services.AddDaprWorkflowBuilder( -// // configureRuntime: opt => -// // { -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // }, -// // configureClient: (sp, clientBuilder) => -// // { -// // var config = sp.GetRequiredService(); -// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// // }); -// // }) -// // .BuildAndStartAsync(); -// // -// // using var scope = testApp.CreateScope(); -// // var client = scope.ServiceProvider.GetRequiredService(); -// // -// // await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); -// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// // cancellation: TestContext.Current.CancellationToken); -// // -// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// // var output = result.ReadOutputAs(); -// // Assert.NotNull(output); -// // // No scope set → child should report no propagated history -// // Assert.False(output.ChildReceivedPropagatedHistory); -// // Assert.Equal(0, output.PropagatedEntryCount); -// // } -// // -// // /// -// // /// Verifies that scheduling a child workflow with -// // /// does not produce any errors and both workflows complete. -// // /// -// // [MinimumDaprRuntimeFact("1.18")] -// // public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() -// // { -// // var instanceId = Guid.NewGuid().ToString(); -// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// // -// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// // needsActorState: true, -// // cancellationToken: TestContext.Current.CancellationToken); -// // await environment.StartAsync(TestContext.Current.CancellationToken); -// // -// // var harness = new DaprHarnessBuilder(componentsDir) -// // .WithEnvironment(environment) -// // .BuildWorkflow(); -// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// // .ConfigureServices(builder => -// // { -// // builder.Services.AddDaprWorkflowBuilder( -// // configureRuntime: opt => -// // { -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // opt.RegisterActivity(); -// // }, -// // configureClient: (sp, clientBuilder) => -// // { -// // var config = sp.GetRequiredService(); -// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// // }); -// // }) -// // .BuildAndStartAsync(); -// // -// // using var scope = testApp.CreateScope(); -// // var client = scope.ServiceProvider.GetRequiredService(); -// // -// // await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); -// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// // cancellation: TestContext.Current.CancellationToken); -// // -// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// // } -// // -// // /// -// // /// Verifies that scheduling a child workflow with -// // /// does not produce any errors and both workflows complete. -// // /// -// // [MinimumDaprRuntimeFact("1.18")] -// // public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() -// // { -// // var instanceId = Guid.NewGuid().ToString(); -// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// // -// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// // needsActorState: true, -// // cancellationToken: TestContext.Current.CancellationToken); -// // await environment.StartAsync(TestContext.Current.CancellationToken); -// // -// // var harness = new DaprHarnessBuilder(componentsDir) -// // .WithEnvironment(environment) -// // .BuildWorkflow(); -// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// // .ConfigureServices(builder => -// // { -// // builder.Services.AddDaprWorkflowBuilder( -// // configureRuntime: opt => -// // { -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // }, -// // configureClient: (sp, clientBuilder) => -// // { -// // var config = sp.GetRequiredService(); -// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// // }); -// // }) -// // .BuildAndStartAsync(); -// // -// // using var scope = testApp.CreateScope(); -// // var client = scope.ServiceProvider.GetRequiredService(); -// // -// // await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); -// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// // cancellation: TestContext.Current.CancellationToken); -// // -// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// // } -// // -// // /// -// // /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with -// // /// None scope returns null, not an exception. -// // /// -// // [MinimumDaprRuntimeFact("1.18")] -// // public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() -// // { -// // var instanceId = Guid.NewGuid().ToString(); -// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// // -// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// // needsActorState: true, -// // cancellationToken: TestContext.Current.CancellationToken); -// // await environment.StartAsync(TestContext.Current.CancellationToken); -// // -// // var harness = new DaprHarnessBuilder(componentsDir) -// // .WithEnvironment(environment) -// // .BuildWorkflow(); -// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// // .ConfigureServices(builder => -// // { -// // builder.Services.AddDaprWorkflowBuilder( -// // configureRuntime: opt => -// // { -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // }, -// // configureClient: (sp, clientBuilder) => -// // { -// // var config = sp.GetRequiredService(); -// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// // }); -// // }) -// // .BuildAndStartAsync(); -// // -// // using var scope = testApp.CreateScope(); -// // var client = scope.ServiceProvider.GetRequiredService(); -// // -// // await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); -// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// // cancellation: TestContext.Current.CancellationToken); -// // -// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// // var output = result.ReadOutputAs(); -// // Assert.NotNull(output); -// // Assert.False(output.ChildReceivedPropagatedHistory); -// // } -// // -// // /// -// // /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation -// // /// fluent builder and that the child workflow completes successfully. -// // /// -// // [MinimumDaprRuntimeFact("1.18")] -// // public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() -// // { -// // var instanceId = Guid.NewGuid().ToString(); -// // var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// // -// // await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// // needsActorState: true, -// // cancellationToken: TestContext.Current.CancellationToken); -// // await environment.StartAsync(TestContext.Current.CancellationToken); -// // -// // var harness = new DaprHarnessBuilder(componentsDir) -// // .WithEnvironment(environment) -// // .BuildWorkflow(); -// // await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// // .ConfigureServices(builder => -// // { -// // builder.Services.AddDaprWorkflowBuilder( -// // configureRuntime: opt => -// // { -// // opt.RegisterWorkflow(); -// // opt.RegisterWorkflow(); -// // }, -// // configureClient: (sp, clientBuilder) => -// // { -// // var config = sp.GetRequiredService(); -// // var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// // if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// // clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// // }); -// // }) -// // .BuildAndStartAsync(); -// // -// // using var scope = testApp.CreateScope(); -// // var client = scope.ServiceProvider.GetRequiredService(); -// // -// // await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); -// // var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// // cancellation: TestContext.Current.CancellationToken); -// // -// // Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// // } -// // -// // private sealed record PropagationTestResult( -// // bool ChildReceivedPropagatedHistory, -// // int PropagatedEntryCount); -// // -// // /// -// // /// Parent workflow that schedules a child with no propagation scope (default). -// // /// -// // private sealed class NoPropagationParent : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // var childId = $"{context.InstanceId}-child"; -// // return await context.CallChildWorkflowAsync( -// // nameof(PropagatedHistoryReceiver), -// // input: null, -// // options: new ChildWorkflowTaskOptions(InstanceId: childId)); -// // } -// // } -// // -// // /// -// // /// Parent workflow that runs an activity first (to build some history), then schedules a child -// // /// with OwnHistory propagation. -// // /// -// // private sealed class OwnHistoryPropagationParent : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // // Run an activity to build some history -// // await context.CallActivityAsync( -// // nameof(EchoActivity), "ping"); -// // -// // var childId = $"{context.InstanceId}-child"; -// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// // .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// // -// // return await context.CallChildWorkflowAsync( -// // nameof(PropagatedHistoryReceiver), -// // input: null, -// // options: childOptions); -// // } -// // } -// // -// // /// -// // /// Grandparent → middle → child lineage, each using Lineage propagation. -// // /// -// // private sealed class LineagePropagationParent : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // var childId = $"{context.InstanceId}-middle"; -// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// // .WithHistoryPropagation(HistoryPropagationScope.Lineage); -// // -// // return await context.CallChildWorkflowAsync( -// // nameof(LineagePropagationMiddle), -// // input: null, -// // options: childOptions); -// // } -// // } -// // -// // private sealed class LineagePropagationMiddle : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // var childId = $"{context.InstanceId}-leaf"; -// // var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// // .WithHistoryPropagation(HistoryPropagationScope.Lineage); -// // -// // var result = await context.CallChildWorkflowAsync( -// // nameof(PropagatedHistoryReceiver), -// // input: null, -// // options: childOptions); -// // -// // return result is not null; -// // } -// // } -// // -// // /// -// // /// Parent workflow that uses the fluent WithHistoryPropagation builder style. -// // /// -// // private sealed class FluentBuilderParent : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // var childId = $"{context.InstanceId}-child"; -// // -// // // Use fluent builder chaining -// // var options = new ChildWorkflowTaskOptions(InstanceId: childId) -// // .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// // -// // var result = await context.CallChildWorkflowAsync( -// // nameof(PropagatedHistoryReceiver), -// // input: null, -// // options: options); -// // -// // return result is not null; -// // } -// // } -// // -// // /// -// // /// Child workflow that inspects its propagated history and reports back what it found. -// // /// -// // private sealed class PropagatedHistoryReceiver : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, object? input) -// // { -// // var propagated = context.GetPropagatedHistory(); -// // // Yield via a zero-duration timer so this sub-orchestration completes through -// // // a proper async round-trip with the sidecar rather than synchronously on its -// // // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous -// // // first-turn completion causes the parent to hang waiting for the completion -// // // event that the sidecar never delivers for same-turn completions. -// // await context.CreateTimer(TimeSpan.Zero); -// // return new PropagationTestResult( -// // ChildReceivedPropagatedHistory: propagated is not null, -// // PropagatedEntryCount: propagated?.Entries.Count ?? 0); -// // } -// // } -// // -// // private sealed class EchoActivity : WorkflowActivity -// // { -// // public override Task RunAsync(WorkflowActivityContext context, string input) -// // => Task.FromResult(input); -// // } -// // -// // private sealed class SimpleActivityWorkflow : Workflow -// // { -// // public override async Task RunAsync(WorkflowContext context, string input) -// // { -// // return await context.CallActivityAsync(nameof(EchoActivity), input); -// // } -// // } -// // } -// -// // ------------------------------------------------------------------------ -// // Copyright 2026 The Dapr Authors -// // Licensed under the Apache License, Version 2.0 (the "License"); -// // you may not use this file except in compliance with the License. -// // You may obtain a copy of the License at -// // http://www.apache.org/licenses/LICENSE-2.0 -// // Unless required by applicable law or agreed to in writing, software -// // distributed under the License is distributed on an "AS IS" BASIS, -// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // See the License for the specific language governing permissions and -// // limitations under the License. -// // ------------------------------------------------------------------------ -// -// using Dapr.Testcontainers.Common; -// using Dapr.Testcontainers.Harnesses; -// using Dapr.Testcontainers.Xunit.Attributes; -// using Dapr.Workflow; -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.DependencyInjection; -// -// namespace Dapr.IntegrationTest.Workflow; -// -// /// -// /// Integration tests for workflow history propagation. -// /// -// /// These tests verify the SDK-side API works end-to-end: -// /// - Scheduling child workflows with a history propagation scope does not cause errors -// /// - The parent and child workflows complete successfully -// /// - The child can call GetPropagatedHistory() without error -// /// -// /// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires -// /// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the -// /// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. -// /// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK -// /// sets in the CreateSubOrchestrationAction proto message. -// /// -// public sealed class HistoryPropagationWorkflowTests -// { -// /// -// /// Verifies that scheduling a child workflow with -// /// (the default) completes successfully. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// var output = result.ReadOutputAs(); -// Assert.NotNull(output); -// // No scope set → child should report no propagated history -// Assert.False(output.ChildReceivedPropagatedHistory); -// Assert.Equal(0, output.PropagatedEntryCount); -// } -// -// /// -// /// Variant of where the child workflow -// /// yields via an activity round-trip instead of a zero-duration timer. This exercises the same -// /// "child sub-orchestration must perform at least one async operation" requirement but through -// /// a different awaitable so both yield mechanisms are covered. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ShouldCompleteSuccessfully_WithNoPropagationScope_ChildYieldsViaActivity() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// opt.RegisterActivity(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParentWithActivityChild), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// var output = result.ReadOutputAs(); -// Assert.NotNull(output); -// // No scope set → child should report no propagated history -// Assert.False(output.ChildReceivedPropagatedHistory); -// Assert.Equal(0, output.PropagatedEntryCount); -// } -// -// /// -// /// Verifies that scheduling a child workflow with -// /// does not produce any errors and both workflows complete. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// opt.RegisterActivity(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// } -// -// /// -// /// Verifies that scheduling a child workflow with -// /// does not produce any errors and both workflows complete. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// } -// -// /// -// /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with -// /// None scope returns null, not an exception. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// var output = result.ReadOutputAs(); -// Assert.NotNull(output); -// Assert.False(output.ChildReceivedPropagatedHistory); -// } -// -// /// -// /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation -// /// fluent builder and that the child workflow completes successfully. -// /// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() -// { -// var instanceId = Guid.NewGuid().ToString(); -// var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); -// -// await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( -// needsActorState: true, -// cancellationToken: TestContext.Current.CancellationToken); -// await environment.StartAsync(TestContext.Current.CancellationToken); -// -// var harness = new DaprHarnessBuilder(componentsDir) -// .WithEnvironment(environment) -// .BuildWorkflow(); -// await using var testApp = await DaprHarnessBuilder.ForHarness(harness) -// .ConfigureServices(builder => -// { -// builder.Services.AddDaprWorkflowBuilder( -// configureRuntime: opt => -// { -// opt.RegisterWorkflow(); -// opt.RegisterWorkflow(); -// }, -// configureClient: (sp, clientBuilder) => -// { -// var config = sp.GetRequiredService(); -// var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; -// if (!string.IsNullOrWhiteSpace(grpcEndpoint)) -// clientBuilder.UseGrpcEndpoint(grpcEndpoint); -// }); -// }) -// .BuildAndStartAsync(); -// -// using var scope = testApp.CreateScope(); -// var client = scope.ServiceProvider.GetRequiredService(); -// -// await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); -// var result = await client.WaitForWorkflowCompletionAsync(instanceId, -// cancellation: TestContext.Current.CancellationToken); -// -// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); -// } -// -// private sealed record PropagationTestResult( -// bool ChildReceivedPropagatedHistory, -// int PropagatedEntryCount); -// -// /// -// /// Parent workflow that schedules a child with no propagation scope (default). -// /// -// private sealed class NoPropagationParent : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var childId = $"{context.InstanceId}-child"; -// return await context.CallChildWorkflowAsync( -// nameof(PropagatedHistoryReceiver), -// input: null, -// options: new ChildWorkflowTaskOptions(InstanceId: childId)); -// } -// } -// -// /// -// /// Parent workflow that runs an activity first (to build some history), then schedules a child -// /// with OwnHistory propagation. -// /// -// private sealed class OwnHistoryPropagationParent : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// // Run an activity to build some history -// await context.CallActivityAsync( -// nameof(EchoActivity), "ping"); -// -// var childId = $"{context.InstanceId}-child"; -// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// -// return await context.CallChildWorkflowAsync( -// nameof(PropagatedHistoryReceiver), -// input: null, -// options: childOptions); -// } -// } -// -// /// -// /// Grandparent → middle → child lineage, each using Lineage propagation. -// /// -// private sealed class LineagePropagationParent : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var childId = $"{context.InstanceId}-middle"; -// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// .WithHistoryPropagation(HistoryPropagationScope.Lineage); -// -// return await context.CallChildWorkflowAsync( -// nameof(LineagePropagationMiddle), -// input: null, -// options: childOptions); -// } -// } -// -// private sealed class LineagePropagationMiddle : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var childId = $"{context.InstanceId}-leaf"; -// var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) -// .WithHistoryPropagation(HistoryPropagationScope.Lineage); -// -// var result = await context.CallChildWorkflowAsync( -// nameof(PropagatedHistoryReceiver), -// input: null, -// options: childOptions); -// -// return result is not null; -// } -// } -// -// /// -// /// Parent workflow that uses the fluent WithHistoryPropagation builder style. -// /// -// private sealed class FluentBuilderParent : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var childId = $"{context.InstanceId}-child"; -// -// // Use fluent builder chaining -// var options = new ChildWorkflowTaskOptions(InstanceId: childId) -// .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// -// var result = await context.CallChildWorkflowAsync( -// nameof(PropagatedHistoryReceiver), -// input: null, -// options: options); -// -// return result is not null; -// } -// } -// -// /// -// /// Child workflow that inspects its propagated history and reports back what it found. -// /// -// private sealed class PropagatedHistoryReceiver : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var propagated = context.GetPropagatedHistory(); -// // Yield via a zero-duration timer so this sub-orchestration completes through -// // a proper async round-trip with the sidecar rather than synchronously on its -// // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous -// // first-turn completion causes the parent to hang waiting for the completion -// // event that the sidecar never delivers for same-turn completions. -// await context.CreateTimer(TimeSpan.Zero); -// return new PropagationTestResult( -// ChildReceivedPropagatedHistory: propagated is not null, -// PropagatedEntryCount: propagated?.Entries.Count ?? 0); -// } -// } -// -// /// -// /// Variant of that yields via an activity round-trip -// /// instead of a zero-duration timer. Used to verify that the activity-based async yield works -// /// equivalently for satisfying the "sub-orchestration must perform at least one async operation" -// /// requirement. -// /// -// private sealed class PropagatedHistoryReceiverWithActivity : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var propagated = context.GetPropagatedHistory(); -// // Yield via an activity round-trip so this sub-orchestration completes through -// // a proper async round-trip with the sidecar rather than synchronously on its -// // first turn. -// await context.CallActivityAsync(nameof(EchoActivity), "yield"); -// return new PropagationTestResult( -// ChildReceivedPropagatedHistory: propagated is not null, -// PropagatedEntryCount: propagated?.Entries.Count ?? 0); -// } -// } -// -// /// -// /// Parent workflow paired with . Schedules -// /// the activity-yielding child with no propagation scope (default). -// /// -// private sealed class NoPropagationParentWithActivityChild : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, object? input) -// { -// var childId = $"{context.InstanceId}-child"; -// return await context.CallChildWorkflowAsync( -// nameof(PropagatedHistoryReceiverWithActivity), -// input: null, -// options: new ChildWorkflowTaskOptions(InstanceId: childId)); -// } -// } -// -// private sealed class EchoActivity : WorkflowActivity -// { -// public override Task RunAsync(WorkflowActivityContext context, string input) -// => Task.FromResult(input); -// } -// -// private sealed class SimpleActivityWorkflow : Workflow -// { -// public override async Task RunAsync(WorkflowContext context, string input) -// { -// return await context.CallActivityAsync(nameof(EchoActivity), input); -// } -// } -// } -// >>>>>>> origin/workflow-propagation-fix +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Testcontainers.Common; +using Dapr.Testcontainers.Harnesses; +using Dapr.Testcontainers.Xunit.Attributes; +using Dapr.Workflow; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.IntegrationTest.Workflow; + +/// +/// Integration tests for workflow history propagation. +/// +/// These tests verify the SDK-side API works end-to-end: +/// - Scheduling child workflows with a history propagation scope does not cause errors +/// - The parent and child workflows complete successfully +/// - The child can call GetPropagatedHistory() without error +/// +/// NOTE: Full history content propagation (non-null PropagatedHistory in the child) requires +/// Dapr sidecar support for the propagated_history field in OrchestratorRequest. When the +/// sidecar supports this, GetPropagatedHistory() will return a non-null PropagatedHistory. +/// The sidecar reads the history_propagation_scope and propagated_history fields that the SDK +/// sets in the CreateSubOrchestrationAction proto message. +/// +public sealed class HistoryPropagationWorkflowTests +{ + /// + /// Verifies that scheduling a child workflow with + /// (the default) completes successfully. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ShouldCompleteSuccessfully_WithNoPropagationScope() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + var output = result.ReadOutputAs(); + Assert.NotNull(output); + // No scope set → child should report no propagated history + Assert.False(output.ChildReceivedPropagatedHistory); + Assert.Equal(0, output.PropagatedEntryCount); + } + + /// + /// Verifies that scheduling a child workflow with + /// does not produce any errors and both workflows complete. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ShouldCompleteSuccessfully_WithOwnHistoryPropagationScope() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterActivity(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(OwnHistoryPropagationParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + } + + /// + /// Verifies that scheduling a child workflow with + /// does not produce any errors and both workflows complete. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task ShouldCompleteSuccessfully_WithLineagePropagationScope() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(LineagePropagationParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + } + + /// + /// Verifies that calling GetPropagatedHistory() inside a child workflow scheduled with + /// None scope returns null, not an exception. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task GetPropagatedHistory_ReturnsNull_WhenScheduledWithNoneScope() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(NoPropagationParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + var output = result.ReadOutputAs(); + Assert.NotNull(output); + Assert.False(output.ChildReceivedPropagatedHistory); + } + + /// + /// Verifies propagation scope option is preserved through the WorkflowTaskOptions.WithHistoryPropagation + /// fluent builder and that the child workflow completes successfully. + /// + [MinimumDaprRuntimeFact("1.18")] + public async Task WithHistoryPropagation_FluentBuilder_WorksCorrectly() + { + var instanceId = Guid.NewGuid().ToString(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync( + needsActorState: true, + cancellationToken: TestContext.Current.CancellationToken); + await environment.StartAsync(TestContext.Current.CancellationToken); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildWorkflow(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprWorkflowBuilder( + configureRuntime: opt => + { + opt.RegisterWorkflow(); + opt.RegisterWorkflow(); + }, + configureClient: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrWhiteSpace(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + await client.ScheduleNewWorkflowAsync(nameof(FluentBuilderParent), instanceId); + var result = await client.WaitForWorkflowCompletionAsync(instanceId, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); + } + + private sealed record PropagationTestResult( + bool ChildReceivedPropagatedHistory, + int PropagatedEntryCount); + + /// + /// Parent workflow that schedules a child with no propagation scope (default). + /// + private sealed class NoPropagationParent : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var childId = $"{context.InstanceId}-child"; + return await context.CallChildWorkflowAsync( + nameof(PropagatedHistoryReceiver), + input: null, + options: new ChildWorkflowTaskOptions(InstanceId: childId)); + } + } + + /// + /// Parent workflow that runs an activity first (to build some history), then schedules a child + /// with OwnHistory propagation. + /// + private sealed class OwnHistoryPropagationParent : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + // Run an activity to build some history + await context.CallActivityAsync( + nameof(EchoActivity), "ping"); + + var childId = $"{context.InstanceId}-child"; + var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) + .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); + + return await context.CallChildWorkflowAsync( + nameof(PropagatedHistoryReceiver), + input: null, + options: childOptions); + } + } + + /// + /// Grandparent → middle → child lineage, each using Lineage propagation. + /// + private sealed class LineagePropagationParent : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var childId = $"{context.InstanceId}-middle"; + var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) + .WithHistoryPropagation(HistoryPropagationScope.Lineage); + + return await context.CallChildWorkflowAsync( + nameof(LineagePropagationMiddle), + input: null, + options: childOptions); + } + } + + private sealed class LineagePropagationMiddle : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var childId = $"{context.InstanceId}-leaf"; + var childOptions = new ChildWorkflowTaskOptions(InstanceId: childId) + .WithHistoryPropagation(HistoryPropagationScope.Lineage); + + var result = await context.CallChildWorkflowAsync( + nameof(PropagatedHistoryReceiver), + input: null, + options: childOptions); + + return result is not null; + } + } + + /// + /// Parent workflow that uses the fluent WithHistoryPropagation builder style. + /// + private sealed class FluentBuilderParent : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var childId = $"{context.InstanceId}-child"; + + // Use fluent builder chaining + var options = new ChildWorkflowTaskOptions(InstanceId: childId) + .WithHistoryPropagation(HistoryPropagationScope.OwnHistory); + + var result = await context.CallChildWorkflowAsync( + nameof(PropagatedHistoryReceiver), + input: null, + options: options); + + return result is not null; + } + } + + /// + /// Child workflow that inspects its propagated history and reports back what it found. + /// + private sealed class PropagatedHistoryReceiver : Workflow + { + public override async Task RunAsync(WorkflowContext context, object? input) + { + var propagated = context.GetPropagatedHistory(); + // Yield via a zero-duration timer so this sub-orchestration completes through + // a proper async round-trip with the sidecar rather than synchronously on its + // first turn. All Dapr sub-orchestrations must be asynchronous — a synchronous + // first-turn completion causes the parent to hang waiting for the completion + // event that the sidecar never delivers for same-turn completions. + await context.CreateTimer(TimeSpan.Zero); + return new PropagationTestResult( + ChildReceivedPropagatedHistory: propagated is not null, + PropagatedEntryCount: propagated?.Entries.Count ?? 0); + } + } + + private sealed class EchoActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, string input) + => Task.FromResult(input); + } + + private sealed class SimpleActivityWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + return await context.CallActivityAsync(nameof(EchoActivity), input); + } + } +} diff --git a/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs index 8dbc68265..08092464f 100644 --- a/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs +++ b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs @@ -268,6 +268,8 @@ private sealed class FakeWorkflowContext : WorkflowContext public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) => throw new NotSupportedException(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) => throw new NotSupportedException(); public override Guid NewGuid() => Guid.NewGuid(); + + public override PropagatedHistory? GetPropagatedHistory() => null; public override ILogger CreateReplaySafeLogger(string categoryName) => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; public override ILogger CreateReplaySafeLogger(Type type) => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; public override ILogger CreateReplaySafeLogger() => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs index 9126be847..f7b55f809 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs @@ -1,483 +1,357 @@ -// // ------------------------------------------------------------------------ -// // Copyright 2026 The Dapr Authors -// // Licensed under the Apache License, Version 2.0 (the "License"); -// // you may not use this file except in compliance with the License. -// // You may obtain a copy of the License at -// // http://www.apache.org/licenses/LICENSE-2.0 -// // Unless required by applicable law or agreed to in writing, software -// // distributed under the License is distributed on an "AS IS" BASIS, -// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // See the License for the specific language governing permissions and -// // limitations under the License. -// // ------------------------------------------------------------------------ -// -// using System.Text.Json; -// using Dapr.Common.Serialization; -// using Dapr.DurableTask.Protobuf; -// using Dapr.Testcontainers.Xunit.Attributes; -// using Dapr.Workflow.Versioning; -// using Dapr.Workflow.Worker.Internal; -// using Google.Protobuf.WellKnownTypes; -// using Microsoft.Extensions.Logging.Abstractions; -// -// namespace Dapr.Workflow.Test.Worker.Internal; -// -// /// -// /// Tests for workflow history propagation via WorkflowOrchestrationContext. -// /// -// public class WorkflowHistoryPropagationTests -// { -// private static WorkflowOrchestrationContext CreateContext( -// string name = "TestWorkflow", -// string instanceId = "instance-1", -// string? appId = null, -// IReadOnlyList? ownHistory = null, -// IEnumerable? incomingPropagatedHistory = null) -// { -// var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); -// var tracker = new WorkflowVersionTracker([]); -// return new WorkflowOrchestrationContext( -// name: name, -// instanceId: instanceId, -// currentUtcDateTime: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), -// workflowSerializer: serializer, -// loggerFactory: NullLoggerFactory.Instance, -// versionTracker: tracker, -// appId: appId, -// ownHistory: ownHistory, -// incomingPropagatedHistory: incomingPropagatedHistory); -// } -// -// // [MinimumDaprRuntimeFact("1.18")] -// // public void GetPropagatedHistory_ReturnsNull_WhenNoHistoryPropagated() -// // { -// // var context = CreateContext(); -// // Assert.Null(context.GetPropagatedHistory()); -// // } -// // -// // [MinimumDaprRuntimeFact("1.18")] -// // public void GetPropagatedHistory_ReturnsNull_WhenEmptyPropagatedHistoryProvided() -// // { -// // var context = CreateContext(incomingPropagatedHistory: []); -// // Assert.Null(context.GetPropagatedHistory()); -// // } -// // -// // [MinimumDaprRuntimeFact("1.18")] -// // public void GetPropagatedHistory_ReturnsSingleEntry_WhenOneSegmentPropagated() -// // { -// // var segment = new PropagatedHistorySegment -// // { -// // AppId = "parent-app", -// // InstanceId = "parent-instance", -// // WorkflowName = "ParentWorkflow" -// // }; -// // segment.Events.Add(MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWorkflow" })); -// // -// // var context = CreateContext(incomingPropagatedHistory: [segment]); -// // -// // var history = context.GetPropagatedHistory(); -// // -// // Assert.NotNull(history); -// // Assert.Single(history.Entries); -// // var entry = history.Entries[0]; -// // Assert.Equal("parent-app", entry.AppId); -// // Assert.Equal("parent-instance", entry.InstanceId); -// // Assert.Equal("ParentWorkflow", entry.WorkflowName); -// // Assert.Single(entry.Events); -// // Assert.Equal(HistoryEventKind.ExecutionStarted, entry.Events[0].Kind); -// // Assert.Equal(1, entry.Events[0].EventId); -// // } -// // -// // [MinimumDaprRuntimeFact("1.18")] -// // public void GetPropagatedHistory_ReturnsMultipleEntries_ForLineagePropagation() -// // { -// // var parent = new PropagatedHistorySegment -// // { -// // AppId = "app-a", InstanceId = "inst-parent", WorkflowName = "ParentWf" -// // }; -// // var grandparent = new PropagatedHistorySegment -// // { -// // AppId = "app-b", InstanceId = "inst-grandparent", WorkflowName = "GrandparentWf" -// // }; -// // -// // var context = CreateContext(incomingPropagatedHistory: [parent, grandparent]); -// // -// // var history = context.GetPropagatedHistory(); -// // -// // Assert.NotNull(history); -// // Assert.Equal(2, history.Entries.Count); -// // Assert.Equal("inst-parent", history.Entries[0].InstanceId); -// // Assert.Equal("inst-grandparent", history.Entries[1].InstanceId); -// // } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void GetPropagatedHistory_MapsAllKnownEventKinds() -// { -// var seg = new PropagatedHistorySegment { AppId = "app", InstanceId = "id", WorkflowName = "wf" }; -// var kindMap = new Dictionary -// { -// { MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent()), HistoryEventKind.ExecutionStarted }, -// { MakeEvent(2, e => e.ExecutionCompleted = new ExecutionCompletedEvent()), HistoryEventKind.ExecutionCompleted }, -// { MakeEvent(3, e => e.ExecutionTerminated = new ExecutionTerminatedEvent()), HistoryEventKind.ExecutionTerminated }, -// { MakeEvent(4, e => e.TaskScheduled = new TaskScheduledEvent { Name = "a" }), HistoryEventKind.TaskScheduled }, -// { MakeEvent(5, e => e.TaskCompleted = new TaskCompletedEvent()), HistoryEventKind.TaskCompleted }, -// { MakeEvent(6, e => e.TaskFailed = new TaskFailedEvent()), HistoryEventKind.TaskFailed }, -// { MakeEvent(7, e => e.SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent()), HistoryEventKind.SubOrchestrationInstanceCreated }, -// { MakeEvent(8, e => e.SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent()), HistoryEventKind.SubOrchestrationInstanceCompleted }, -// { MakeEvent(9, e => e.SubOrchestrationInstanceFailed = new SubOrchestrationInstanceFailedEvent()), HistoryEventKind.SubOrchestrationInstanceFailed }, -// { MakeEvent(10, e => e.TimerCreated = new TimerCreatedEvent()), HistoryEventKind.TimerCreated }, -// { MakeEvent(11, e => e.TimerFired = new TimerFiredEvent()), HistoryEventKind.TimerFired }, -// { MakeEvent(12, e => e.OrchestratorStarted = new OrchestratorStartedEvent()), HistoryEventKind.OrchestratorStarted }, -// { MakeEvent(13, e => e.OrchestratorCompleted = new OrchestratorCompletedEvent()), HistoryEventKind.OrchestratorCompleted }, -// { MakeEvent(14, e => e.EventSent = new EventSentEvent()), HistoryEventKind.EventSent }, -// { MakeEvent(15, e => e.EventRaised = new EventRaisedEvent()), HistoryEventKind.EventRaised }, -// { MakeEvent(16, e => e.ContinueAsNew = new ContinueAsNewEvent()), HistoryEventKind.ContinueAsNew }, -// { MakeEvent(17, e => e.ExecutionSuspended = new ExecutionSuspendedEvent()), HistoryEventKind.ExecutionSuspended }, -// { MakeEvent(18, e => e.ExecutionResumed = new ExecutionResumedEvent()), HistoryEventKind.ExecutionResumed }, -// }; -// -// seg.Events.AddRange(kindMap.Keys); -// var context = CreateContext(incomingPropagatedHistory: [seg]); -// var history = context.GetPropagatedHistory()!; -// var events = history.Entries[0].Events; -// -// foreach (var (protoEvent, expectedKind) in kindMap) -// { -// var mapped = events.FirstOrDefault(e => e.EventId == protoEvent.EventId); -// Assert.NotNull(mapped); -// Assert.Equal(expectedKind, mapped.Kind); -// } -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void GetPropagatedHistory_MapsTimestamp_Correctly() -// { -// var ts = new DateTimeOffset(2026, 3, 15, 10, 30, 0, TimeSpan.Zero); -// var protoTs = Timestamp.FromDateTimeOffset(ts); -// var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; -// seg.Events.Add(new HistoryEvent -// { -// EventId = 1, -// Timestamp = protoTs, -// ExecutionStarted = new ExecutionStartedEvent() -// }); -// -// var context = CreateContext(incomingPropagatedHistory: [seg]); -// var entry = context.GetPropagatedHistory()!.Entries[0]; -// -// Assert.Equal(ts, entry.Events[0].Timestamp); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void GetPropagatedHistory_MapsUnknownEventType_ToUnknown() -// { -// var seg = new PropagatedHistorySegment { AppId = "a", InstanceId = "i", WorkflowName = "w" }; -// // A HistoryEvent with no event type set → Unknown -// seg.Events.Add(new HistoryEvent { EventId = 99 }); -// -// var context = CreateContext(incomingPropagatedHistory: [seg]); -// var events = context.GetPropagatedHistory()!.Entries[0].Events; -// -// Assert.Equal(HistoryEventKind.Unknown, events[0].Kind); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterByAppId_ReturnsOnlyMatchingEntries() -// { -// var entries = new[] -// { -// new PropagatedHistoryEntry("app-a", "i1", "WfA", []), -// new PropagatedHistoryEntry("app-b", "i2", "WfB", []), -// new PropagatedHistoryEntry("APP-A", "i3", "WfA2", []), -// }; -// var history = new PropagatedHistory(entries); -// -// var filtered = history.FilterByAppId("app-a"); -// -// // Case-insensitive match -// Assert.Equal(2, filtered.Entries.Count); -// Assert.All(filtered.Entries, e => Assert.Equal("app-a", e.AppId, StringComparer.OrdinalIgnoreCase)); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterByInstanceId_ReturnsOnlyMatchingEntry() -// { -// PropagatedHistoryEntry[] entries = -// [ -// new("app", "instance-1", "Wf1", []), -// new("app", "instance-2", "Wf2", []) -// ]; -// var history = new PropagatedHistory(entries); -// -// var filtered = history.FilterByInstanceId("instance-1"); -// -// Assert.Single(filtered.Entries); -// Assert.Equal("instance-1", filtered.Entries[0].InstanceId); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterByInstanceId_IsCaseSensitive() -// { -// PropagatedHistoryEntry[] entries = [new("app", "Instance-1", "Wf", [])]; -// var history = new PropagatedHistory(entries); -// -// // Exact case match -// Assert.Single(history.FilterByInstanceId("Instance-1").Entries); -// // Different case → no match -// Assert.Empty(history.FilterByInstanceId("instance-1").Entries); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterByWorkflowName_ReturnsOnlyMatchingEntries() -// { -// PropagatedHistoryEntry[] entries = -// [ -// new("app", "i1", "PaymentWorkflow", []), -// new("app", "i2", "OrderWorkflow", []), -// new("app", "i3", "PaymentWorkflow", []) -// ]; -// var history = new PropagatedHistory(entries); -// -// var filtered = history.FilterByWorkflowName("PaymentWorkflow"); -// -// Assert.Equal(2, filtered.Entries.Count); -// Assert.All(filtered.Entries, e => Assert.Equal("PaymentWorkflow", e.WorkflowName)); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterByWorkflowName_IsCaseSensitive() -// { -// PropagatedHistoryEntry[] entries = [new("app", "i", "PaymentWorkflow", [])]; -// var history = new PropagatedHistory(entries); -// -// Assert.Single(history.FilterByWorkflowName("PaymentWorkflow").Entries); -// Assert.Empty(history.FilterByWorkflowName("paymentworkflow").Entries); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterMethods_ReturnEmptyHistory_WhenNoMatches() -// { -// var history = new PropagatedHistory( -// [ -// new PropagatedHistoryEntry("app-a", "i1", "Wf1", []) -// ]); -// -// Assert.Empty(history.FilterByAppId("app-z").Entries); -// Assert.Empty(history.FilterByInstanceId("no-such-id").Entries); -// Assert.Empty(history.FilterByWorkflowName("NoSuchWf").Entries); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterMethods_ThrowArgumentException_WhenNullOrWhitespace() -// { -// var history = new PropagatedHistory([]); -// -// // null throws ArgumentNullException (a subclass of ArgumentException) -// Assert.ThrowsAny(() => history.FilterByAppId(null!)); -// Assert.ThrowsAny(() => history.FilterByAppId("")); -// Assert.ThrowsAny(() => history.FilterByAppId(" ")); -// -// Assert.ThrowsAny(() => history.FilterByInstanceId(null!)); -// Assert.ThrowsAny(() => history.FilterByInstanceId("")); -// -// Assert.ThrowsAny(() => history.FilterByWorkflowName(null!)); -// Assert.ThrowsAny(() => history.FilterByWorkflowName("")); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void FilterMethods_CanBeChained() -// { -// PropagatedHistoryEntry[] entries = -// [ -// new("app-a", "i1", "PaymentWorkflow", []), -// new("app-a", "i2", "OrderWorkflow", []), -// new("app-b", "i3", "PaymentWorkflow", []) -// ]; -// var history = new PropagatedHistory(entries); -// -// var filtered = history.FilterByAppId("app-a").FilterByWorkflowName("PaymentWorkflow"); -// -// Assert.Single(filtered.Entries); -// Assert.Equal("i1", filtered.Entries[0].InstanceId); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void ChildWorkflowTaskOptions_DefaultPropagationScope_IsNone() -// { -// var options = new ChildWorkflowTaskOptions(); -// Assert.Equal(HistoryPropagationScope.None, options.PropagationScope); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void WithHistoryPropagation_SetsPropagationScope_OwnHistory() -// { -// var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// Assert.Equal(HistoryPropagationScope.OwnHistory, options.PropagationScope); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void WithHistoryPropagation_SetsPropagationScope_Lineage() -// { -// var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.Lineage); -// Assert.Equal(HistoryPropagationScope.Lineage, options.PropagationScope); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void WithHistoryPropagation_DoesNotMutateOriginalOptions() -// { -// var original = new ChildWorkflowTaskOptions(InstanceId: "id-1"); -// var updated = original.WithHistoryPropagation(HistoryPropagationScope.OwnHistory); -// -// Assert.Equal(HistoryPropagationScope.None, original.PropagationScope); -// Assert.Equal(HistoryPropagationScope.OwnHistory, updated.PropagationScope); -// Assert.Equal("id-1", updated.InstanceId); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public async Task CallChildWorkflowAsync_WithNone_DoesNotSetPropagationScope() -// { -// var context = CreateContext(instanceId: "parent", appId: "my-app"); -// var childTask = context.CallChildWorkflowAsync( -// "ChildWf", -// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.None)); -// -// // Complete the child synchronously via history -// context.ProcessEvents([ -// new HistoryEvent { EventId = 0, SubOrchestrationInstanceCreated = new SubOrchestrationInstanceCreatedEvent { Name = "ChildWf" } }, -// new HistoryEvent { SubOrchestrationInstanceCompleted = new SubOrchestrationInstanceCompletedEvent { TaskScheduledId = 0, Result = "99" } } -// ], isReplaying: false); -// -// var action = context.PendingActions.OfType() -// .Select(a => a.CreateSubOrchestration) -// .FirstOrDefault(a => a is not null); -// -// // None: action either absent (cleared after history) or scope is None/unset -// // The create action is removed from pending after history match -// Assert.Empty(context.PendingActions); -// Assert.Equal(99, await childTask); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void CallChildWorkflowAsync_WithOwnHistory_SetsPropagationScopeOnAction() -// { -// var ownHistory = new List -// { -// MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "TestWorkflow" }), -// MakeEvent(2, e => e.TaskScheduled = new TaskScheduledEvent { Name = "SomeActivity" }), -// }; -// -// var context = CreateContext(instanceId: "parent", appId: "my-app", ownHistory: ownHistory); -// _ = context.CallChildWorkflowAsync( -// "ChildWf", -// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); -// -// var action = context.PendingActions -// .Select(a => a.CreateSubOrchestration) -// .First(a => a is not null); -// -// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); -// Assert.Single(action.PropagatedHistory); -// Assert.Equal("parent", action.PropagatedHistory[0].InstanceId); -// Assert.Equal("my-app", action.PropagatedHistory[0].AppId); -// Assert.Equal("TestWorkflow", action.PropagatedHistory[0].WorkflowName); -// Assert.Equal(2, action.PropagatedHistory[0].Events.Count); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void CallChildWorkflowAsync_WithLineage_IncludesOwnAndAncestorHistory() -// { -// var grandparentSegment = new PropagatedHistorySegment -// { -// AppId = "grandparent-app", -// InstanceId = "grandparent-inst", -// WorkflowName = "GrandparentWf" -// }; -// -// var ownHistory = new List -// { -// MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWf" }) -// }; -// -// var context = CreateContext( -// name: "ParentWf", -// instanceId: "parent-inst", -// appId: "parent-app", -// ownHistory: ownHistory, -// incomingPropagatedHistory: [grandparentSegment]); -// -// _ = context.CallChildWorkflowAsync( -// "ChildWf", -// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); -// -// var action = context.PendingActions -// .Select(a => a.CreateSubOrchestration) -// .First(a => a is not null); -// -// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.Lineage, action.HistoryPropagationScope); -// // Own history segment + grandparent segment -// Assert.Equal(2, action.PropagatedHistory.Count); -// -// var ownSeg = action.PropagatedHistory.First(s => s.InstanceId == "parent-inst"); -// Assert.Equal("parent-app", ownSeg.AppId); -// Assert.Equal("ParentWf", ownSeg.WorkflowName); -// Assert.Single(ownSeg.Events); -// -// var ancestorSeg = action.PropagatedHistory.First(s => s.InstanceId == "grandparent-inst"); -// Assert.Equal("grandparent-app", ancestorSeg.AppId); -// Assert.Equal("GrandparentWf", ancestorSeg.WorkflowName); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void CallChildWorkflowAsync_WithOwnHistory_NoLineage_ExcludesAncestors() -// { -// var grandparentSegment = new PropagatedHistorySegment -// { -// AppId = "gp-app", InstanceId = "gp-inst", WorkflowName = "GpWf" -// }; -// -// var context = CreateContext( -// name: "ParentWf", -// instanceId: "parent-inst", -// appId: "parent-app", -// incomingPropagatedHistory: [grandparentSegment]); -// -// _ = context.CallChildWorkflowAsync( -// "ChildWf", -// options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); -// -// var action = context.PendingActions -// .Select(a => a.CreateSubOrchestration) -// .First(a => a is not null); -// -// // Only own history, NOT grandparent -// Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); -// Assert.Single(action.PropagatedHistory); -// Assert.Equal("parent-inst", action.PropagatedHistory[0].InstanceId); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void PropagatedHistory_Constructor_ThrowsOnNullEntries() -// { -// Assert.Throws(() => new PropagatedHistory(null!)); -// } -// -// [MinimumDaprRuntimeFact("1.18")] -// public void PropagatedHistory_Entries_ReflectsConstructorInput() -// { -// var entries = new[] { new PropagatedHistoryEntry("a", "b", "c", []) }; -// var history = new PropagatedHistory(entries); -// Assert.Single(history.Entries); -// Assert.Same(entries[0], history.Entries[0]); -// } -// -// private static HistoryEvent MakeEvent(int id, Action configure) -// { -// var e = new HistoryEvent -// { -// EventId = id, -// Timestamp = Timestamp.FromDateTime(DateTime.UtcNow) -// }; -// configure(e); -// return e; -// } -// } +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using Dapr.Common.Serialization; +using Dapr.DurableTask.Protobuf; +using Dapr.Workflow.Versioning; +using Dapr.Workflow.Worker.Internal; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Dapr.Workflow.Test.Worker.Internal; + +/// +/// Tests for workflow history propagation: the SDK API surface for declaring +/// a propagation scope on , propagating +/// the scope to the outgoing , and +/// exposing inbound propagated history through . +/// +public class WorkflowHistoryPropagationTests +{ + private static readonly DateTime StartTime = new(2026, 01, 01, 0, 0, 0, DateTimeKind.Utc); + + private static WorkflowOrchestrationContext CreateContext( + string name = "TestWorkflow", + string instanceId = "instance-1", + string? appId = null, + IReadOnlyList? ownHistory = null, + IEnumerable? incomingPropagatedHistory = null) + { + var serializer = new JsonDaprSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + return new WorkflowOrchestrationContext( + name: name, + instanceId: instanceId, + currentUtcDateTime: StartTime, + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + versionTracker: tracker, + appId: appId, + ownHistory: ownHistory, + incomingPropagatedHistory: incomingPropagatedHistory); + } + + private static HistoryEvent MakeEvent(int eventId, Action configure, DateTime? timestamp = null) + { + var ev = new HistoryEvent + { + EventId = eventId, + Timestamp = Timestamp.FromDateTime(timestamp ?? StartTime) + }; + configure(ev); + return ev; + } + + private static PropagatedHistoryChunk MakeChunk(string appId, string instanceId, string workflowName, + params HistoryEvent[] events) + { + var chunk = new PropagatedHistoryChunk + { + AppId = appId, + InstanceId = instanceId, + WorkflowName = workflowName, + }; + foreach (var ev in events) + { + chunk.RawEvents.Add(ev.ToByteString()); + } + return chunk; + } + + // ------------------------------------------------------------------ + // GetPropagatedHistory + // ------------------------------------------------------------------ + + [Fact] + public void GetPropagatedHistory_ReturnsNull_WhenNoHistoryPropagated() + { + var context = CreateContext(); + Assert.Null(context.GetPropagatedHistory()); + } + + [Fact] + public void GetPropagatedHistory_ReturnsNull_WhenEmptyChunksProvided() + { + var context = CreateContext(incomingPropagatedHistory: []); + Assert.Null(context.GetPropagatedHistory()); + } + + [Fact] + public void GetPropagatedHistory_ReturnsSingleEntry_WhenOneChunkPropagated() + { + var chunk = MakeChunk("parent-app", "parent-instance", "ParentWorkflow", + MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWorkflow" })); + + var context = CreateContext(incomingPropagatedHistory: [chunk]); + + var history = context.GetPropagatedHistory(); + + Assert.NotNull(history); + Assert.Single(history.Entries); + Assert.Equal("parent-app", history.Entries[0].AppId); + Assert.Equal("parent-instance", history.Entries[0].InstanceId); + Assert.Equal("ParentWorkflow", history.Entries[0].WorkflowName); + Assert.Single(history.Entries[0].Events); + Assert.Equal(HistoryEventKind.ExecutionStarted, history.Entries[0].Events[0].Kind); + } + + [Fact] + public void GetPropagatedHistory_PreservesChunkOrder() + { + var parent = MakeChunk("p-app", "p-inst", "ParentWf", + MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "ParentWf" })); + var grandparent = MakeChunk("gp-app", "gp-inst", "GrandparentWf", + MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent { Name = "GrandparentWf" })); + + var context = CreateContext(incomingPropagatedHistory: [parent, grandparent]); + var history = context.GetPropagatedHistory(); + + Assert.NotNull(history); + Assert.Equal(2, history.Entries.Count); + Assert.Equal("p-inst", history.Entries[0].InstanceId); + Assert.Equal("gp-inst", history.Entries[1].InstanceId); + } + + [Fact] + public void GetPropagatedHistory_MapsAllHistoryEventKinds() + { + var mappings = new Dictionary + { + { MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent()), HistoryEventKind.ExecutionStarted }, + { MakeEvent(2, e => e.ExecutionCompleted = new ExecutionCompletedEvent()), HistoryEventKind.ExecutionCompleted }, + { MakeEvent(3, e => e.ExecutionTerminated = new ExecutionTerminatedEvent()), HistoryEventKind.ExecutionTerminated }, + { MakeEvent(4, e => e.TaskScheduled = new TaskScheduledEvent { Name = "a" }), HistoryEventKind.TaskScheduled }, + { MakeEvent(5, e => e.TaskCompleted = new TaskCompletedEvent()), HistoryEventKind.TaskCompleted }, + { MakeEvent(6, e => e.TaskFailed = new TaskFailedEvent()), HistoryEventKind.TaskFailed }, + { MakeEvent(7, e => e.ChildWorkflowInstanceCreated = new ChildWorkflowInstanceCreatedEvent()), HistoryEventKind.SubOrchestrationInstanceCreated }, + { MakeEvent(8, e => e.ChildWorkflowInstanceCompleted = new ChildWorkflowInstanceCompletedEvent()), HistoryEventKind.SubOrchestrationInstanceCompleted }, + { MakeEvent(9, e => e.ChildWorkflowInstanceFailed = new ChildWorkflowInstanceFailedEvent()), HistoryEventKind.SubOrchestrationInstanceFailed }, + { MakeEvent(10, e => e.TimerCreated = new TimerCreatedEvent()), HistoryEventKind.TimerCreated }, + { MakeEvent(11, e => e.TimerFired = new TimerFiredEvent()), HistoryEventKind.TimerFired }, + { MakeEvent(12, e => e.WorkflowStarted = new WorkflowStartedEvent()), HistoryEventKind.OrchestratorStarted }, + { MakeEvent(13, e => e.WorkflowCompleted = new WorkflowCompletedEvent()), HistoryEventKind.OrchestratorCompleted }, + { MakeEvent(14, e => e.EventSent = new EventSentEvent()), HistoryEventKind.EventSent }, + { MakeEvent(15, e => e.EventRaised = new EventRaisedEvent()), HistoryEventKind.EventRaised }, + { MakeEvent(16, e => e.ContinueAsNew = new ContinueAsNewEvent()), HistoryEventKind.ContinueAsNew }, + { MakeEvent(17, e => e.ExecutionSuspended = new ExecutionSuspendedEvent()), HistoryEventKind.ExecutionSuspended }, + { MakeEvent(18, e => e.ExecutionResumed = new ExecutionResumedEvent()), HistoryEventKind.ExecutionResumed }, + }; + + var chunk = MakeChunk("app", "inst", "Wf", mappings.Keys.ToArray()); + var context = CreateContext(incomingPropagatedHistory: [chunk]); + var entry = context.GetPropagatedHistory()!.Entries.Single(); + + Assert.Equal(mappings.Count, entry.Events.Count); + var expectedKinds = mappings.Values.ToList(); + for (var i = 0; i < entry.Events.Count; i++) + { + Assert.Equal(expectedKinds[i], entry.Events[i].Kind); + } + } + + [Fact] + public void GetPropagatedHistory_MapsUnsetEventTypeToUnknown() + { + // An event with no oneof case set should be mapped to Unknown rather than crashing. + var bareEvent = new HistoryEvent { EventId = 42, Timestamp = Timestamp.FromDateTime(StartTime) }; + var chunk = MakeChunk("app", "inst", "Wf", bareEvent); + + var context = CreateContext(incomingPropagatedHistory: [chunk]); + var entry = context.GetPropagatedHistory()!.Entries.Single(); + + Assert.Single(entry.Events); + Assert.Equal(HistoryEventKind.Unknown, entry.Events[0].Kind); + Assert.Equal(42, entry.Events[0].EventId); + } + + [Fact] + public void GetPropagatedHistory_SkipsMalformedRawEvents() + { + var chunk = new PropagatedHistoryChunk + { + AppId = "app", + InstanceId = "inst", + WorkflowName = "Wf", + }; + // Add a malformed event (not a valid serialized HistoryEvent). + chunk.RawEvents.Add(ByteString.CopyFrom(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff })); + // Followed by a well-formed event. + chunk.RawEvents.Add(MakeEvent(7, e => e.TaskCompleted = new TaskCompletedEvent()).ToByteString()); + + var context = CreateContext(incomingPropagatedHistory: [chunk]); + var entry = context.GetPropagatedHistory()!.Entries.Single(); + + // Only the well-formed event survives. + Assert.Single(entry.Events); + Assert.Equal(7, entry.Events[0].EventId); + Assert.Equal(HistoryEventKind.TaskCompleted, entry.Events[0].Kind); + } + + [Fact] + public void GetPropagatedHistory_PreservesEventTimestamp() + { + var when = new DateTime(2026, 06, 15, 12, 30, 45, DateTimeKind.Utc); + var chunk = MakeChunk("app", "inst", "Wf", + MakeEvent(1, e => e.ExecutionStarted = new ExecutionStartedEvent(), timestamp: when)); + + var context = CreateContext(incomingPropagatedHistory: [chunk]); + var entry = context.GetPropagatedHistory()!.Entries.Single(); + + Assert.Equal(when, entry.Events[0].Timestamp.UtcDateTime); + } + + // ------------------------------------------------------------------ + // PropagatedHistory filters + // ------------------------------------------------------------------ + + [Fact] + public void FilterByAppId_ReturnsOnlyMatchingEntries_CaseInsensitive() + { + var history = new PropagatedHistory(new[] + { + new PropagatedHistoryEntry("app-a", "i1", "WfA", []), + new PropagatedHistoryEntry("app-b", "i2", "WfB", []), + new PropagatedHistoryEntry("APP-A", "i3", "WfA2", []), + }); + + var filtered = history.FilterByAppId("app-a"); + + Assert.Equal(2, filtered.Entries.Count); + Assert.All(filtered.Entries, e => Assert.Equal("app-a", e.AppId, StringComparer.OrdinalIgnoreCase)); + } + + [Fact] + public void FilterByInstanceId_ReturnsOnlyMatchingEntry_CaseSensitive() + { + var history = new PropagatedHistory(new[] + { + new PropagatedHistoryEntry("app", "Instance-1", "Wf", []), + new PropagatedHistoryEntry("app", "instance-2", "Wf", []), + }); + + Assert.Single(history.FilterByInstanceId("Instance-1").Entries); + Assert.Empty(history.FilterByInstanceId("instance-1").Entries); + } + + [Fact] + public void FilterByWorkflowName_ReturnsOnlyMatchingEntries_CaseSensitive() + { + var history = new PropagatedHistory(new[] + { + new PropagatedHistoryEntry("app", "i1", "PaymentWorkflow", []), + new PropagatedHistoryEntry("app", "i2", "OrderWorkflow", []), + new PropagatedHistoryEntry("app", "i3", "PaymentWorkflow", []), + new PropagatedHistoryEntry("app", "i4", "paymentworkflow", []), + }); + + var filtered = history.FilterByWorkflowName("PaymentWorkflow"); + + Assert.Equal(2, filtered.Entries.Count); + Assert.All(filtered.Entries, e => Assert.Equal("PaymentWorkflow", e.WorkflowName)); + } + + [Fact] + public void Filters_ThrowOnEmptyOrWhitespace() + { + var history = new PropagatedHistory([]); + Assert.Throws(() => history.FilterByAppId(string.Empty)); + Assert.Throws(() => history.FilterByInstanceId(" ")); + Assert.Throws(() => history.FilterByWorkflowName(string.Empty)); + } + + [Fact] + public void PropagatedHistory_Ctor_ThrowsOnNullEntries() + { + Assert.Throws(() => new PropagatedHistory(null!)); + } + + // ------------------------------------------------------------------ + // ChildWorkflowTaskOptions.WithHistoryPropagation + // ------------------------------------------------------------------ + + [Fact] + public void WithHistoryPropagation_SetsScope() + { + var options = new ChildWorkflowTaskOptions().WithHistoryPropagation(HistoryPropagationScope.Lineage); + Assert.Equal(HistoryPropagationScope.Lineage, options.PropagationScope); + } + + [Fact] + public void WithHistoryPropagation_DoesNotMutateOriginal() + { + var original = new ChildWorkflowTaskOptions(InstanceId: "id-1"); + var updated = original.WithHistoryPropagation(HistoryPropagationScope.OwnHistory); + + Assert.Equal(HistoryPropagationScope.None, original.PropagationScope); + Assert.Equal(HistoryPropagationScope.OwnHistory, updated.PropagationScope); + Assert.Equal("id-1", updated.InstanceId); + } + + // ------------------------------------------------------------------ + // CallChildWorkflowAsync — outbound HistoryPropagationScope on action + // ------------------------------------------------------------------ + + [Fact] + public void CallChildWorkflowAsync_DefaultScope_LeavesActionScopeUnset() + { + var context = CreateContext(instanceId: "parent", appId: "my-app"); + _ = context.CallChildWorkflowAsync("ChildWf"); + + var action = context.PendingActions + .Select(a => a.CreateChildWorkflow) + .First(a => a is not null); + + // Default = None => HistoryPropagationScope field is left at its proto default (None / unset). + Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.None, action.HistoryPropagationScope); + } + + [Fact] + public void CallChildWorkflowAsync_WithOwnHistory_SetsScopeOnAction() + { + var context = CreateContext(instanceId: "parent", appId: "my-app"); + _ = context.CallChildWorkflowAsync("ChildWf", + options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); + + var action = context.PendingActions + .Select(a => a.CreateChildWorkflow) + .First(a => a is not null); + + Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.OwnHistory, action.HistoryPropagationScope); + } + + [Fact] + public void CallChildWorkflowAsync_WithLineage_SetsScopeOnAction() + { + var context = CreateContext(instanceId: "parent", appId: "my-app"); + _ = context.CallChildWorkflowAsync("ChildWf", + options: new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); + + var action = context.PendingActions + .Select(a => a.CreateChildWorkflow) + .First(a => a is not null); + + Assert.Equal(Dapr.DurableTask.Protobuf.HistoryPropagationScope.Lineage, action.HistoryPropagationScope); + } +} diff --git a/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs b/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs index 9a504d13d..cf7531f74 100644 --- a/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs +++ b/test/Dapr.Workflow.Test/Worker/WorkflowsFactoryTests.cs @@ -415,6 +415,8 @@ private sealed class FakeWorkflowContext : WorkflowContext public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) => throw new NotSupportedException(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) => throw new NotSupportedException(); public override Guid NewGuid() => Guid.NewGuid(); + + public override PropagatedHistory? GetPropagatedHistory() => null; public override ILogger CreateReplaySafeLogger(string categoryName) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger(Type type) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger() => throw new NotSupportedException(); diff --git a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs index c8445270f..e00a9fcc1 100644 --- a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs @@ -557,6 +557,8 @@ private sealed class FakeWorkflowContext : WorkflowContext public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) => throw new NotSupportedException(); public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) => throw new NotSupportedException(); public override Guid NewGuid() => Guid.NewGuid(); + + public override PropagatedHistory? GetPropagatedHistory() => null; public override ILogger CreateReplaySafeLogger(string categoryName) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger(Type type) => throw new NotSupportedException(); public override ILogger CreateReplaySafeLogger() => throw new NotSupportedException(); From 9fd34ca9c7353ac8cacca4ea6fb2114cd2cbe841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 12:41:25 +0000 Subject: [PATCH 17/17] Add GetPropagatedHistory override to abstractions-test fakes Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/8dab905c-86f8-49de-92fd-509540d785e4 Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com> --- .../WorkflowContextDelegationTests.cs | 2 ++ .../WorkflowContextWaitForExternalEventTests.cs | 2 ++ test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs index d96480882..fc8f5b7c8 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextDelegationTests.cs @@ -73,6 +73,8 @@ public override Task CallChildWorkflowAsync(string workflowNam public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; + public override PropagatedHistory? GetPropagatedHistory() => null; + private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { IDisposable? Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) => null; diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs index 6d8d53b93..8ad4e5d35 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowContextWaitForExternalEventTests.cs @@ -79,6 +79,8 @@ public override Task CallChildWorkflowAsync(string workflowNam public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; + public override PropagatedHistory? GetPropagatedHistory() => null; + private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { IDisposable? Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) => null; diff --git a/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs b/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs index 9d1243585..4602bc009 100644 --- a/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs +++ b/test/Dapr.Workflow.Abstractions.Test/WorkflowTests.cs @@ -48,6 +48,8 @@ public override Task CallChildWorkflowAsync(string workflowNam public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) { } public override Guid NewGuid() => Guid.Empty; + public override PropagatedHistory? GetPropagatedHistory() => null; + private sealed class NullLogger : Microsoft.Extensions.Logging.ILogger { IDisposable? Microsoft.Extensions.Logging.ILogger.BeginScope(TState state) => null;