diff --git a/.autover/autover.json b/.autover/autover.json
index 02f2ad0db..b0a7dbc47 100644
--- a/.autover/autover.json
+++ b/.autover/autover.json
@@ -52,6 +52,11 @@
"Path": "Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj",
"PrereleaseLabel": "preview"
},
+ {
+ "Name": "Amazon.Lambda.DurableExecution.Testing",
+ "Path": "Libraries/src/Amazon.Lambda.DurableExecution.Testing/Amazon.Lambda.DurableExecution.Testing.csproj",
+ "PrereleaseLabel": "preview"
+ },
{
"Name": "Amazon.Lambda.DynamoDBEvents",
"Path": "Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj"
diff --git a/.autover/changes/durable-execution-testing-cloud-runner-history.json b/.autover/changes/durable-execution-testing-cloud-runner-history.json
new file mode 100644
index 000000000..a8fdea162
--- /dev/null
+++ b/.autover/changes/durable-execution-testing-cloud-runner-history.json
@@ -0,0 +1,11 @@
+{
+ "Projects": [
+ {
+ "Name": "Amazon.Lambda.DurableExecution.Testing",
+ "Type": "Patch",
+ "ChangelogMessages": [
+ "Fix CloudDurableTestRunner against deployed functions: reconstruct recorded operations from GetDurableExecutionHistory (the token-free, externally-pollable history API) instead of GetDurableExecutionState, which requires a runtime-only CheckpointToken an external poller cannot obtain; and make StartAsync fire-and-forget (Event invocation with a generated DurableExecutionName, ARN resolved via ListDurableExecutionsByFunction) so callback workflows that suspend no longer deadlock against a synchronous invoke. Preview."
+ ]
+ }
+ ]
+}
diff --git a/Libraries/Amazon.Lambda.DurableExecution.slnf b/Libraries/Amazon.Lambda.DurableExecution.slnf
new file mode 100644
index 000000000..645c64888
--- /dev/null
+++ b/Libraries/Amazon.Lambda.DurableExecution.slnf
@@ -0,0 +1,17 @@
+{
+ "solution": {
+ "path": "Libraries.sln",
+ "projects": [
+ "src\\Amazon.Lambda.Core\\Amazon.Lambda.Core.csproj",
+ "src\\Amazon.Lambda.RuntimeSupport\\Amazon.Lambda.RuntimeSupport.csproj",
+ "src\\Amazon.Lambda.Serialization.SystemTextJson\\Amazon.Lambda.Serialization.SystemTextJson.csproj",
+ "src\\Amazon.Lambda.TestUtilities\\Amazon.Lambda.TestUtilities.csproj",
+ "src\\Amazon.Lambda.DurableExecution\\Amazon.Lambda.DurableExecution.csproj",
+ "src\\Amazon.Lambda.DurableExecution.Testing\\Amazon.Lambda.DurableExecution.Testing.csproj",
+ "test\\Amazon.Lambda.DurableExecution.Tests\\Amazon.Lambda.DurableExecution.Tests.csproj",
+ "test\\Amazon.Lambda.DurableExecution.Testing.Tests\\Amazon.Lambda.DurableExecution.Testing.Tests.csproj",
+ "test\\Amazon.Lambda.DurableExecution.IntegrationTests\\Amazon.Lambda.DurableExecution.IntegrationTests.csproj",
+ "test\\Amazon.Lambda.DurableExecution.AotPublishTest\\Amazon.Lambda.DurableExecution.AotPublishTest.csproj"
+ ]
+ }
+}
diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln
index 1b2bedcd5..cb34a6776 100644
--- a/Libraries/Libraries.sln
+++ b/Libraries/Libraries.sln
@@ -163,7 +163,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecut
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecution.AotPublishTest", "test\Amazon.Lambda.DurableExecution.AotPublishTest\Amazon.Lambda.DurableExecution.AotPublishTest.csproj", "{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2}"
EndProject
+<<<<<<< Updated upstream
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnnotationsClassLibraryFunction", "test\Amazon.Lambda.DurableExecution.IntegrationTests\TestFunctions\AnnotationsClassLibraryFunction\AnnotationsClassLibraryFunction.csproj", "{D55E2D57-8374-4573-999B-6E64E109C25F}"
+=======
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecution.Testing", "src\Amazon.Lambda.DurableExecution.Testing\Amazon.Lambda.DurableExecution.Testing.csproj", "{AD9A8A6E-5690-4959-A797-0B7D10FA40F3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecution.Testing.Tests", "test\Amazon.Lambda.DurableExecution.Testing.Tests\Amazon.Lambda.DurableExecution.Testing.Tests.csproj", "{0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}"
+>>>>>>> Stashed changes
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -1027,6 +1033,7 @@ Global
{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2}.Release|x64.Build.0 = Release|Any CPU
{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2}.Release|x86.ActiveCfg = Release|Any CPU
{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2}.Release|x86.Build.0 = Release|Any CPU
+<<<<<<< Updated upstream
{D55E2D57-8374-4573-999B-6E64E109C25F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1039,6 +1046,32 @@ Global
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x64.Build.0 = Release|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x86.ActiveCfg = Release|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x86.Build.0 = Release|Any CPU
+=======
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|x64.Build.0 = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Debug|x86.Build.0 = Debug|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|x64.ActiveCfg = Release|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|x64.Build.0 = Release|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|x86.ActiveCfg = Release|Any CPU
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3}.Release|x86.Build.0 = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|x64.Build.0 = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Debug|x86.Build.0 = Debug|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|x64.ActiveCfg = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|x64.Build.0 = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|x86.ActiveCfg = Release|Any CPU
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B}.Release|x86.Build.0 = Release|Any CPU
+>>>>>>> Stashed changes
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1119,7 +1152,12 @@ Global
{57150BA6-3826-431F-8F58-B1D11FAFC5D4} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{CA132CAB-FF4F-4312-B3A3-66DE9D360F27} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
+<<<<<<< Updated upstream
{D55E2D57-8374-4573-999B-6E64E109C25F} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
+=======
+ {AD9A8A6E-5690-4959-A797-0B7D10FA40F3} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
+ {0460CF9D-B5CC-47C5-9B30-CEA84695AB3B} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
+>>>>>>> Stashed changes
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/Amazon.Lambda.DurableExecution.Testing.csproj b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/Amazon.Lambda.DurableExecution.Testing.csproj
new file mode 100644
index 000000000..90aff9aec
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/Amazon.Lambda.DurableExecution.Testing.csproj
@@ -0,0 +1,42 @@
+
+
+
+
+
+ $(DefaultPackageTargets)
+ Testing utilities for Amazon Lambda Durable Execution - test durable workflows locally without deploying to AWS.
+ Amazon.Lambda.DurableExecution.Testing
+
+ 0.0.1-preview
+ Amazon.Lambda.DurableExecution.Testing
+ Amazon.Lambda.DurableExecution.Testing
+ AWS;Amazon;Lambda;Durable;Workflow;Testing
+ enable
+ enable
+ true
+ $(NoWarn);AWSLAMBDA001
+
+
+
+
+ <_Parameter1>Amazon.Lambda.DurableExecution.Testing.Tests, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4"
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CheckpointProcessor.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CheckpointProcessor.cs
new file mode 100644
index 000000000..8d709d776
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CheckpointProcessor.cs
@@ -0,0 +1,311 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Model;
+using SdkOperationUpdate = Amazon.Lambda.Model.OperationUpdate;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Processes checkpoint updates against the in-memory operation store.
+/// Handles action-to-status mapping, callback ID minting, time skipping,
+/// and producing the "new operations" response the runtime expects.
+///
+internal sealed class CheckpointProcessor
+{
+ private readonly InMemoryOperationStore _store;
+ private readonly bool _skipTime;
+ private readonly object _pendingGate = new();
+ private readonly List _pendingInvokes = new();
+
+ public CheckpointProcessor(InMemoryOperationStore store, bool skipTime)
+ {
+ _store = store;
+ _skipTime = skipTime;
+ }
+
+ ///
+ /// A chained-invoke (ctx.InvokeAsync) that has been started by the
+ /// workflow but not yet resolved. The runtime suspends after emitting the
+ /// START and expects an external system to run the target function; in the
+ /// local harness the
+ /// drains these between invocations and resolves them via the
+ /// . The target function name lives only on the
+ /// wire-format OperationUpdate.ChainedInvokeOptions, so it is captured
+ /// here rather than on the persisted .
+ ///
+ internal readonly record struct PendingInvoke(string OperationId, string FunctionName, string? Payload);
+
+ ///
+ /// Returns and clears the chained-invokes started since the last drain.
+ ///
+ public IReadOnlyList DrainPendingInvokes()
+ {
+ lock (_pendingGate)
+ {
+ if (_pendingInvokes.Count == 0)
+ return Array.Empty();
+ var drained = _pendingInvokes.ToArray();
+ _pendingInvokes.Clear();
+ return drained;
+ }
+ }
+
+ ///
+ /// Processes a batch of updates and returns the new checkpoint token
+ /// and any operations that were created or modified (to feed back to
+ /// the runtime's onNewOperations callback).
+ ///
+ public (string NewToken, IReadOnlyList NewOperations) Process(
+ string arn,
+ string? currentToken,
+ IReadOnlyList updates)
+ {
+ var newOperations = new List();
+
+ foreach (var update in updates)
+ {
+ var operation = ApplyUpdate(arn, update);
+ newOperations.Add(operation);
+ }
+
+ var newToken = _store.IncrementToken(arn);
+ return (newToken, newOperations);
+ }
+
+ private Operation ApplyUpdate(string arn, SdkOperationUpdate update)
+ {
+ var existing = _store.GetOperation(arn, update.Id);
+ var operation = existing ?? new Operation { Id = update.Id };
+
+ operation.Type = update.Type?.Value ?? operation.Type;
+ operation.Name = update.Name ?? operation.Name;
+ operation.ParentId = update.ParentId ?? operation.ParentId;
+ operation.SubType = update.SubType ?? operation.SubType;
+
+ var action = update.Action?.Value;
+ ApplyAction(operation, action, update);
+
+ if (_skipTime)
+ ApplyTimeSkipping(operation, action);
+
+ // A chained-invoke START suspends the workflow until an external system
+ // resolves it. Record it so the orchestrator can run the registered
+ // sibling and stamp the result/error before the next replay. The function
+ // name is only carried on the wire-format update, not on the Operation.
+ if (action == "START"
+ && operation.Type == OperationTypes.ChainedInvoke
+ && update.ChainedInvokeOptions?.FunctionName is { } functionName)
+ {
+ lock (_pendingGate)
+ {
+ _pendingInvokes.Add(new PendingInvoke(operation.Id!, functionName, update.Payload));
+ }
+ }
+
+ _store.Upsert(arn, operation);
+ return operation;
+ }
+
+ private static void ApplyAction(Operation operation, string? action, SdkOperationUpdate update)
+ {
+ switch (action)
+ {
+ case "START":
+ operation.Status = OperationStatuses.Started;
+ operation.StartTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ ApplyStartDetails(operation, update);
+ break;
+
+ case "SUCCEED":
+ operation.Status = OperationStatuses.Succeeded;
+ operation.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ ApplySucceedDetails(operation, update);
+ break;
+
+ case "FAIL":
+ operation.Status = OperationStatuses.Failed;
+ operation.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ ApplyFailDetails(operation, update);
+ break;
+
+ case "RETRY":
+ operation.Status = OperationStatuses.Pending;
+ ApplyRetryDetails(operation, update);
+ break;
+
+ case "CANCEL":
+ operation.Status = OperationStatuses.Cancelled;
+ operation.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ break;
+ }
+ }
+
+ private static void ApplyStartDetails(Operation operation, SdkOperationUpdate update)
+ {
+ switch (operation.Type)
+ {
+ case OperationTypes.Step:
+ operation.StepDetails ??= new StepDetails();
+ // A plain step re-emits START before every attempt, so START owns
+ // the attempt count. WaitForCondition (Type=STEP, SubType=WaitForCondition)
+ // emits START only once and advances the count on each RETRY instead,
+ // so it must NOT increment here.
+ if (operation.SubType != OperationSubTypes.WaitForCondition)
+ operation.StepDetails.Attempt = (operation.StepDetails.Attempt ?? 0) + 1;
+ break;
+
+ case OperationTypes.Wait:
+ operation.WaitDetails ??= new WaitDetails();
+ if (update.WaitOptions?.WaitSeconds is { } seconds)
+ {
+ operation.WaitDetails.ScheduledEndTimestamp =
+ DateTimeOffset.UtcNow.AddSeconds(seconds).ToUnixTimeMilliseconds();
+ }
+ break;
+
+ case OperationTypes.Callback:
+ operation.CallbackDetails ??= new CallbackDetails();
+ operation.CallbackDetails.CallbackId = $"cb-{operation.Id}";
+ break;
+
+ case OperationTypes.ChainedInvoke:
+ operation.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ break;
+
+ case OperationTypes.Context:
+ operation.ContextDetails ??= new ContextDetails();
+ break;
+
+ case OperationTypes.Execution:
+ operation.ExecutionDetails ??= new ExecutionDetails();
+ break;
+ }
+ }
+
+ private static void ApplySucceedDetails(Operation operation, SdkOperationUpdate update)
+ {
+ var payload = update.Payload;
+ switch (operation.Type)
+ {
+ case OperationTypes.Step:
+ operation.StepDetails ??= new StepDetails();
+ operation.StepDetails.Result = payload;
+ operation.StepDetails.Error = null;
+ break;
+
+ case OperationTypes.ChainedInvoke:
+ operation.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ operation.ChainedInvokeDetails.Result = payload;
+ operation.ChainedInvokeDetails.Error = null;
+ break;
+
+ case OperationTypes.Context:
+ operation.ContextDetails ??= new ContextDetails();
+ operation.ContextDetails.Result = payload;
+ operation.ContextDetails.Error = null;
+ break;
+
+ case OperationTypes.Callback:
+ operation.CallbackDetails ??= new CallbackDetails();
+ operation.CallbackDetails.Result = payload;
+ operation.CallbackDetails.Error = null;
+ break;
+ }
+ }
+
+ private static void ApplyFailDetails(Operation operation, SdkOperationUpdate update)
+ {
+ var error = MapSdkError(update.Error);
+ switch (operation.Type)
+ {
+ case OperationTypes.Step:
+ operation.StepDetails ??= new StepDetails();
+ operation.StepDetails.Error = error;
+ break;
+
+ case OperationTypes.ChainedInvoke:
+ operation.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ operation.ChainedInvokeDetails.Error = error;
+ break;
+
+ case OperationTypes.Context:
+ operation.ContextDetails ??= new ContextDetails();
+ operation.ContextDetails.Error = error;
+ break;
+
+ case OperationTypes.Callback:
+ operation.CallbackDetails ??= new CallbackDetails();
+ operation.CallbackDetails.Error = error;
+ break;
+ }
+ }
+
+ private static void ApplyRetryDetails(Operation operation, SdkOperationUpdate update)
+ {
+ // Both retried steps and WaitForCondition polls are wire-encoded as
+ // Type=STEP (WaitForCondition uses SubType=WaitForCondition); the runtime
+ // never emits a WAIT-typed RETRY, so this single STEP branch covers both.
+ if (operation.Type == OperationTypes.Step)
+ {
+ operation.StepDetails ??= new StepDetails();
+ if (update.StepOptions?.NextAttemptDelaySeconds is { } delaySeconds)
+ {
+ operation.StepDetails.NextAttemptTimestamp =
+ DateTimeOffset.UtcNow.AddSeconds(delaySeconds).ToUnixTimeMilliseconds();
+ }
+ operation.StepDetails.Error = MapSdkError(update.Error);
+
+ // WaitForCondition emits START once and advances per RETRY: it carries
+ // the next poll state in Payload and relies on the persistence layer to
+ // own the attempt count. Persist both so the next replay resumes from
+ // the latest state with an advanced attempt number (a plain step RETRY
+ // carries no Payload and owns its count via START, so leave it alone).
+ if (operation.SubType == OperationSubTypes.WaitForCondition)
+ {
+ if (update.Payload is not null)
+ operation.StepDetails.Result = update.Payload;
+ operation.StepDetails.Attempt = (operation.StepDetails.Attempt ?? 0) + 1;
+ }
+ }
+ }
+
+ private void ApplyTimeSkipping(Operation operation, string? action)
+ {
+ if (action == "START" && operation.Type == OperationTypes.Wait)
+ {
+ operation.Status = OperationStatuses.Succeeded;
+ operation.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ if (operation.WaitDetails != null)
+ {
+ operation.WaitDetails.ScheduledEndTimestamp =
+ DateTimeOffset.UtcNow.AddMilliseconds(-1).ToUnixTimeMilliseconds();
+ }
+ }
+
+ // A retried step (or WaitForCondition poll, also Type=STEP) becomes
+ // immediately READY under time-skipping so the next replay runs the next
+ // attempt without waiting for the backoff/poll delay.
+ if (action == "RETRY" && operation.Type == OperationTypes.Step)
+ {
+ operation.Status = OperationStatuses.Ready;
+ if (operation.StepDetails != null)
+ {
+ operation.StepDetails.NextAttemptTimestamp =
+ DateTimeOffset.UtcNow.AddMilliseconds(-1).ToUnixTimeMilliseconds();
+ }
+ }
+ }
+
+ private static ErrorObject? MapSdkError(Amazon.Lambda.Model.ErrorObject? sdkError)
+ {
+ if (sdkError == null) return null;
+ return new ErrorObject
+ {
+ ErrorType = sdkError.ErrorType,
+ ErrorMessage = sdkError.ErrorMessage,
+ ErrorData = sdkError.ErrorData,
+ StackTrace = sdkError.StackTrace
+ };
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudDurableTestRunner.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudDurableTestRunner.cs
new file mode 100644
index 000000000..2df43f51d
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudDurableTestRunner.cs
@@ -0,0 +1,543 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+using Amazon.Lambda;
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution.Services;
+using Amazon.Lambda.Model;
+using Amazon.Lambda.Serialization.SystemTextJson;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Cloud test runner that invokes a real deployed durable Lambda function
+/// and polls for results. Provides the same
+/// interface as the local runner for portable test code.
+///
+public sealed class CloudDurableTestRunner : IDurableTestRunner, IAsyncDisposable
+{
+ private readonly string _functionArn;
+ private readonly IAmazonLambda _lambdaClient;
+ private readonly ILambdaSerializer _serializer;
+ private readonly CloudTestRunnerOptions _options;
+
+ ///
+ /// Creates a cloud test runner targeting a deployed durable function.
+ ///
+ /// Qualified function ARN (with alias, version, or $LATEST).
+ /// AWS Lambda client. If null, creates a default client.
+ /// Cloud runner options. If null, uses defaults.
+ public CloudDurableTestRunner(
+ string functionArn,
+ IAmazonLambda? lambdaClient = null,
+ CloudTestRunnerOptions? options = null)
+ {
+ _functionArn = functionArn ?? throw new ArgumentNullException(nameof(functionArn));
+ _lambdaClient = lambdaClient ?? new AmazonLambdaClient();
+ _options = options ?? new CloudTestRunnerOptions();
+ _serializer = _options.Serializer ?? new DefaultLambdaJsonSerializer();
+ }
+
+ ///
+ public async Task> RunAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ var arn = await StartAsync(input, timeout, cancellationToken);
+ return await WaitForResultAsync(arn, timeout, cancellationToken);
+ }
+
+ ///
+ public async Task StartAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(timeout ?? _options.DefaultTimeout);
+
+ var payload = SerializeToString(input);
+
+ // Fire-and-forget (Event) invocation. A synchronous (RequestResponse)
+ // durable invoke blocks until the execution reaches a terminal state, so
+ // a workflow that suspends on a callback would deadlock against the
+ // caller — the test can only deliver the callback after StartAsync
+ // returns. An Event invoke starts the execution and returns immediately;
+ // we then resolve the ARN by listing executions by the name we minted.
+ var executionName = $"cloud-test-{Guid.NewGuid():N}";
+
+ await _lambdaClient.InvokeAsync(new InvokeRequest
+ {
+ FunctionName = _functionArn,
+ InvocationType = InvocationType.Event,
+ Payload = payload,
+ DurableExecutionName = executionName,
+ }, timeoutCts.Token);
+
+ return await ResolveExecutionArnAsync(executionName, timeoutCts.Token);
+ }
+
+ ///
+ /// Polls ListDurableExecutionsByFunction until the execution started
+ /// with appears, returning its ARN. The
+ /// listing API is eventually consistent, so the execution may not be visible
+ /// on the first call after an Event invoke.
+ ///
+ private async Task ResolveExecutionArnAsync(
+ string executionName, CancellationToken cancellationToken)
+ {
+ // Filter by name only. The listing API rejects a request that supplies a
+ // Qualifier alongside DurableExecutionName, so we pass the unqualified
+ // function ARN and match the name client-side.
+ var functionName = StripQualifier(_functionArn);
+
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var response = await _lambdaClient.ListDurableExecutionsByFunctionAsync(
+ new ListDurableExecutionsByFunctionRequest
+ {
+ FunctionName = functionName,
+ DurableExecutionName = executionName,
+ }, cancellationToken);
+
+ var match = response.DurableExecutions?
+ .FirstOrDefault(e => e.DurableExecutionName == executionName);
+ if (match?.DurableExecutionArn is { } arn)
+ return arn;
+
+ await Task.Delay(_options.PollInterval, cancellationToken);
+ }
+ }
+
+ // Returns the unqualified function ARN: "arn:...:function:Name[:Qualifier]"
+ // → "arn:...:function:Name". Non-ARN identifiers and already-unqualified
+ // ARNs are returned unchanged.
+ private static string StripQualifier(string functionArn)
+ {
+ const string arnPrefix = "arn:aws:lambda:";
+ if (!functionArn.StartsWith(arnPrefix, StringComparison.Ordinal))
+ return functionArn;
+
+ // arn:aws:lambda:region:acct:function:name[:qualifier] — the qualifier,
+ // if present, is the 8th colon-delimited field (index 7).
+ var parts = functionArn.Split(':');
+ return parts.Length >= 8 ? string.Join(':', parts[..7]) : functionArn;
+ }
+
+ ///
+ public async Task> WaitForResultAsync(
+ string durableExecutionArn,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(timeout ?? _options.DefaultTimeout);
+
+ // Terminal status and the workflow result/error are execution-level concepts
+ // surfaced by GetDurableExecution — they are NOT on the EXECUTION operation in
+ // the GetDurableExecutionState stream (that op only carries the input payload).
+ while (true)
+ {
+ timeoutCts.Token.ThrowIfCancellationRequested();
+
+ var execution = await _lambdaClient.GetDurableExecutionAsync(
+ new GetDurableExecutionRequest { DurableExecutionArn = durableExecutionArn },
+ timeoutCts.Token);
+
+ if (IsTerminal(execution.Status))
+ {
+ var operations = await FetchAllOperationsAsync(durableExecutionArn, timeoutCts.Token);
+ return BuildTestResult(durableExecutionArn, execution, operations);
+ }
+
+ await Task.Delay(_options.PollInterval, timeoutCts.Token);
+ }
+ }
+
+ private static bool IsTerminal(ExecutionStatus? status) =>
+ status == ExecutionStatus.SUCCEEDED
+ || status == ExecutionStatus.FAILED
+ || status == ExecutionStatus.TIMED_OUT
+ || status == ExecutionStatus.STOPPED;
+
+ ///
+ /// Reconstructs the operation list from the execution history event stream.
+ ///
+ ///
+ /// We deliberately use GetDurableExecutionHistory rather than
+ /// GetDurableExecutionState: the state API requires a
+ /// CheckpointToken that only the Lambda runtime receives on each
+ /// invocation, so an external poller (this runner) cannot call it. The
+ /// history API is token-free and intended for out-of-band observers. We fold
+ /// the per-operation lifecycle events (Started → Succeeded/Failed/…) back into
+ /// the same shape the rest of this class expects, so
+ /// inspection behaves identically to the local runner.
+ ///
+ private async Task> FetchAllOperationsAsync(
+ string durableExecutionArn, CancellationToken cancellationToken)
+ {
+ // Preserve first-seen order so step lists read top-to-bottom like the
+ // workflow body, then fold each event into its operation by Id.
+ var operations = new Dictionary(StringComparer.Ordinal);
+ var order = new List();
+ string? marker = null;
+
+ do
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var response = await _lambdaClient.GetDurableExecutionHistoryAsync(
+ new GetDurableExecutionHistoryRequest
+ {
+ DurableExecutionArn = durableExecutionArn,
+ IncludeExecutionData = true,
+ Marker = marker,
+ }, cancellationToken);
+
+ foreach (var evt in response.Events ?? new List())
+ {
+ if (evt.Id is not { } id) continue;
+
+ if (!operations.TryGetValue(id, out var op))
+ {
+ op = new Operation { Id = id };
+ operations[id] = op;
+ order.Add(id);
+ }
+
+ ApplyEvent(op, evt);
+ }
+
+ marker = string.IsNullOrEmpty(response.NextMarker) ? null : response.NextMarker;
+ }
+ while (marker is not null);
+
+ return order.Select(id => operations[id]).ToList();
+ }
+
+ ///
+ /// Folds a single history into the running
+ /// it belongs to. Started events seed the type,
+ /// name, and start time; terminal events set the status, end time, and the
+ /// type-specific result/error payload.
+ ///
+ private static void ApplyEvent(Operation op, Event evt)
+ {
+ // Name / ParentId / SubType ride on every event for the operation; the
+ // later events repeat them, so last-write-wins is fine.
+ if (!string.IsNullOrEmpty(evt.Name)) op.Name = evt.Name;
+ if (!string.IsNullOrEmpty(evt.ParentId)) op.ParentId = evt.ParentId;
+ if (!string.IsNullOrEmpty(evt.SubType)) op.SubType = evt.SubType;
+
+ var ts = evt.EventTimestamp.HasValue
+ ? new DateTimeOffset(evt.EventTimestamp.Value, TimeSpan.Zero).ToUnixTimeMilliseconds()
+ : (long?)null;
+
+ var type = evt.EventType;
+
+ // Step
+ if (type == EventType.StepStarted)
+ {
+ op.Type = OperationTypes.Step;
+ op.Status = OperationStatuses.Started;
+ op.StartTimestamp ??= ts;
+ }
+ else if (type == EventType.StepSucceeded)
+ {
+ op.Type = OperationTypes.Step;
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = ts;
+ op.StepDetails ??= new StepDetails();
+ op.StepDetails.Result = evt.StepSucceededDetails?.Result?.Payload;
+ if (evt.StepSucceededDetails?.RetryDetails?.CurrentAttempt is { } attempt)
+ op.StepDetails.Attempt = attempt;
+ }
+ else if (type == EventType.StepFailed)
+ {
+ op.Type = OperationTypes.Step;
+ op.Status = OperationStatuses.Failed;
+ op.EndTimestamp = ts;
+ op.StepDetails ??= new StepDetails();
+ op.StepDetails.Error = MapEventError(evt.StepFailedDetails?.Error);
+ if (evt.StepFailedDetails?.RetryDetails?.CurrentAttempt is { } attempt)
+ op.StepDetails.Attempt = attempt;
+ }
+ // Wait
+ else if (type == EventType.WaitStarted)
+ {
+ op.Type = OperationTypes.Wait;
+ op.Status = OperationStatuses.Started;
+ op.StartTimestamp ??= ts;
+ if (evt.WaitStartedDetails?.ScheduledEndTimestamp is { } end)
+ op.WaitDetails = new WaitDetails
+ {
+ ScheduledEndTimestamp = new DateTimeOffset(end, TimeSpan.Zero).ToUnixTimeMilliseconds(),
+ };
+ }
+ else if (type == EventType.WaitSucceeded)
+ {
+ op.Type = OperationTypes.Wait;
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = ts;
+ }
+ else if (type == EventType.WaitCancelled)
+ {
+ op.Type = OperationTypes.Wait;
+ op.Status = OperationStatuses.Cancelled;
+ op.EndTimestamp = ts;
+ }
+ // Callback
+ else if (type == EventType.CallbackStarted)
+ {
+ op.Type = OperationTypes.Callback;
+ op.Status = OperationStatuses.Started;
+ op.StartTimestamp ??= ts;
+ op.CallbackDetails ??= new CallbackDetails();
+ op.CallbackDetails.CallbackId = evt.CallbackStartedDetails?.CallbackId;
+ }
+ else if (type == EventType.CallbackSucceeded)
+ {
+ op.Type = OperationTypes.Callback;
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = ts;
+ op.CallbackDetails ??= new CallbackDetails();
+ op.CallbackDetails.Result = evt.CallbackSucceededDetails?.Result?.Payload;
+ }
+ else if (type == EventType.CallbackFailed)
+ {
+ op.Type = OperationTypes.Callback;
+ op.Status = OperationStatuses.Failed;
+ op.EndTimestamp = ts;
+ op.CallbackDetails ??= new CallbackDetails();
+ op.CallbackDetails.Error = MapEventError(evt.CallbackFailedDetails?.Error);
+ }
+ else if (type == EventType.CallbackTimedOut)
+ {
+ op.Type = OperationTypes.Callback;
+ op.Status = OperationStatuses.TimedOut;
+ op.EndTimestamp = ts;
+ }
+ // Chained invoke
+ else if (type == EventType.ChainedInvokeStarted)
+ {
+ op.Type = OperationTypes.ChainedInvoke;
+ op.Status = OperationStatuses.Started;
+ op.StartTimestamp ??= ts;
+ }
+ else if (type == EventType.ChainedInvokeSucceeded)
+ {
+ op.Type = OperationTypes.ChainedInvoke;
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = ts;
+ op.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ op.ChainedInvokeDetails.Result = evt.ChainedInvokeSucceededDetails?.Result?.Payload;
+ }
+ else if (type == EventType.ChainedInvokeFailed)
+ {
+ op.Type = OperationTypes.ChainedInvoke;
+ op.Status = OperationStatuses.Failed;
+ op.EndTimestamp = ts;
+ op.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ op.ChainedInvokeDetails.Error = MapEventError(evt.ChainedInvokeFailedDetails?.Error);
+ }
+ // Child context
+ else if (type == EventType.ContextStarted)
+ {
+ op.Type = OperationTypes.Context;
+ op.Status = OperationStatuses.Started;
+ op.StartTimestamp ??= ts;
+ }
+ else if (type == EventType.ContextSucceeded)
+ {
+ op.Type = OperationTypes.Context;
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = ts;
+ op.ContextDetails ??= new ContextDetails();
+ op.ContextDetails.Result = evt.ContextSucceededDetails?.Result?.Payload;
+ }
+ else if (type == EventType.ContextFailed)
+ {
+ op.Type = OperationTypes.Context;
+ op.Status = OperationStatuses.Failed;
+ op.EndTimestamp = ts;
+ op.ContextDetails ??= new ContextDetails();
+ op.ContextDetails.Error = MapEventError(evt.ContextFailedDetails?.Error);
+ }
+ // Execution-level events: keep the type so they're filtered out of the
+ // step list by BuildTestResult; the workflow result/error come from
+ // GetDurableExecution, not from here.
+ else if (type == EventType.ExecutionStarted
+ || type == EventType.ExecutionSucceeded
+ || type == EventType.ExecutionFailed
+ || type == EventType.ExecutionTimedOut
+ || type == EventType.ExecutionStopped)
+ {
+ op.Type = OperationTypes.Execution;
+ }
+ }
+
+ private static ErrorObject? MapEventError(EventError? eventError)
+ {
+ var sdkError = eventError?.Payload;
+ if (sdkError is null) return null;
+ return new ErrorObject
+ {
+ ErrorType = sdkError.ErrorType,
+ ErrorMessage = sdkError.ErrorMessage,
+ ErrorData = sdkError.ErrorData,
+ StackTrace = sdkError.StackTrace,
+ };
+ }
+
+ ///
+ public async Task WaitForCallbackAsync(
+ string durableExecutionArn,
+ string? name = null,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(timeout ?? _options.DefaultTimeout);
+
+ while (true)
+ {
+ timeoutCts.Token.ThrowIfCancellationRequested();
+
+ // Reconstruct from history (token-free) — see FetchAllOperationsAsync.
+ var operations = await FetchAllOperationsAsync(durableExecutionArn, timeoutCts.Token);
+
+ foreach (var op in operations)
+ {
+ if (op.Type == OperationTypes.Callback
+ && op.Status == OperationStatuses.Started
+ && op.CallbackDetails?.CallbackId is { } cbId)
+ {
+ if (name is null || MatchesCallbackName(op.Name, name))
+ return cbId;
+ }
+ }
+
+ await Task.Delay(_options.PollInterval, timeoutCts.Token);
+ }
+ }
+
+ ///
+ public async Task SendCallbackSuccessAsync(
+ string callbackId,
+ TResult result,
+ CancellationToken cancellationToken = default)
+ {
+ var serialized = SerializeToString(result);
+ await _lambdaClient.SendDurableExecutionCallbackSuccessAsync(
+ new SendDurableExecutionCallbackSuccessRequest
+ {
+ CallbackId = callbackId,
+ Result = new MemoryStream(Encoding.UTF8.GetBytes(serialized)),
+ }, cancellationToken);
+ }
+
+ ///
+ public async Task SendCallbackFailureAsync(
+ string callbackId,
+ ErrorObject? error = null,
+ CancellationToken cancellationToken = default)
+ {
+ await _lambdaClient.SendDurableExecutionCallbackFailureAsync(
+ new SendDurableExecutionCallbackFailureRequest
+ {
+ CallbackId = callbackId,
+ Error = error is not null ? new Amazon.Lambda.Model.ErrorObject
+ {
+ ErrorType = error.ErrorType,
+ ErrorMessage = error.ErrorMessage,
+ ErrorData = error.ErrorData,
+ StackTrace = error.StackTrace?.ToList(),
+ } : null,
+ }, cancellationToken);
+ }
+
+ ///
+ public async Task SendCallbackHeartbeatAsync(
+ string callbackId,
+ CancellationToken cancellationToken = default)
+ {
+ await _lambdaClient.SendDurableExecutionCallbackHeartbeatAsync(
+ new SendDurableExecutionCallbackHeartbeatRequest
+ {
+ CallbackId = callbackId,
+ }, cancellationToken);
+ }
+
+ ///
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ private TestResult BuildTestResult(
+ string arn, GetDurableExecutionResponse execution, IReadOnlyList allOps)
+ {
+ var status = MapExecutionStatus(execution.Status);
+
+ TOutput? result = default;
+ if (status == InvocationStatus.Succeeded && !string.IsNullOrEmpty(execution.Result))
+ {
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(execution.Result));
+ result = _serializer.Deserialize(stream);
+ }
+
+ var steps = allOps
+ .Where(o => o.Type != OperationTypes.Execution)
+ .Select(o => new TestStep(o, _serializer))
+ .ToList();
+
+ return new TestResult(
+ status: status,
+ result: result,
+ error: MapSdkError(execution.Error),
+ durableExecutionArn: arn,
+ invocationCount: -1,
+ steps: steps);
+ }
+
+ // The runtime's terminal states beyond Succeeded (FAILED/TIMED_OUT/STOPPED)
+ // all map to Failed since InvocationStatus has no finer terminal distinction.
+ private static InvocationStatus MapExecutionStatus(ExecutionStatus? status)
+ {
+ if (status == ExecutionStatus.SUCCEEDED) return InvocationStatus.Succeeded;
+ if (status == ExecutionStatus.FAILED
+ || status == ExecutionStatus.TIMED_OUT
+ || status == ExecutionStatus.STOPPED) return InvocationStatus.Failed;
+ return InvocationStatus.Pending;
+ }
+
+ private static ErrorObject? MapSdkError(Amazon.Lambda.Model.ErrorObject? sdkError)
+ {
+ if (sdkError is null) return null;
+ return new ErrorObject
+ {
+ ErrorType = sdkError.ErrorType,
+ ErrorMessage = sdkError.ErrorMessage,
+ ErrorData = sdkError.ErrorData,
+ StackTrace = sdkError.StackTrace,
+ };
+ }
+
+ private static bool MatchesCallbackName(string? opName, string name)
+ {
+ if (opName is null) return false;
+ if (string.Equals(opName, name, StringComparison.Ordinal)) return true;
+ if (string.Equals(opName, $"{name}-callback", StringComparison.Ordinal)) return true;
+ return false;
+ }
+
+ private string SerializeToString(T value)
+ {
+ using var stream = new MemoryStream();
+ _serializer.Serialize(value, stream);
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestException.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestException.cs
new file mode 100644
index 000000000..083c5ee84
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestException.cs
@@ -0,0 +1,21 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Thrown by the cloud test runner when the cloud invocation fails in a way
+/// specific to the test harness (e.g., missing DurableExecutionArn in the response).
+///
+public sealed class CloudTestException : Exception
+{
+ ///
+ /// Creates a new cloud test exception.
+ ///
+ public CloudTestException(string message) : base(message) { }
+
+ ///
+ /// Creates a new cloud test exception with an inner exception.
+ ///
+ public CloudTestException(string message, Exception innerException) : base(message, innerException) { }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestRunnerOptions.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestRunnerOptions.cs
new file mode 100644
index 000000000..af5bd8b65
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/CloudTestRunnerOptions.cs
@@ -0,0 +1,28 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Configuration for the cloud durable test runner.
+///
+public sealed record CloudTestRunnerOptions
+{
+ ///
+ /// Interval between state-polling calls. Default: 2 seconds.
+ ///
+ public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(2);
+
+ ///
+ /// Wall-clock timeout for polling operations. Default: 5 minutes.
+ ///
+ public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Serializer used for payload and result deserialization. When null, uses
+ /// DefaultLambdaJsonSerializer from Amazon.Lambda.Serialization.SystemTextJson.
+ ///
+ public ILambdaSerializer? Serializer { get; init; }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/DurableTestRunner.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/DurableTestRunner.cs
new file mode 100644
index 000000000..745007404
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/DurableTestRunner.cs
@@ -0,0 +1,266 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Lambda.Serialization.SystemTextJson;
+using Amazon.Lambda.TestUtilities;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Local test runner for durable workflows. Drives the workflow to completion
+/// in-process using the real runtime engine with an in-memory service backend.
+///
+public sealed class DurableTestRunner : IDurableTestRunner, IAsyncDisposable
+{
+ private readonly Func> _handler;
+ private readonly TestRunnerOptions _options;
+ private readonly ILambdaSerializer _serializer;
+ private readonly ILambdaContext _lambdaContext;
+ private readonly InMemoryOperationStore _store;
+ private readonly CheckpointProcessor _processor;
+ private readonly InMemoryDurableServiceClient _serviceClient;
+ private readonly FunctionRegistry _registry;
+ private readonly Dictionary> _completedResults = new();
+ private readonly HashSet _consumedCallbackIds = new();
+ private ExecutionOrchestrator? _lastOrchestrator;
+ private TInput? _lastStartInput;
+
+ ///
+ /// Creates a new local test runner for the given workflow handler.
+ ///
+ public DurableTestRunner(
+ Func> handler,
+ TestRunnerOptions? options = null)
+ : this(handler, options, registry: null)
+ {
+ }
+
+ ///
+ /// Creates a local test runner that shares an existing .
+ /// Used when a durable sibling is invoked: the nested runner inherits the parent's
+ /// registered functions so chains of durable-to-durable invokes resolve.
+ ///
+ internal DurableTestRunner(
+ Func> handler,
+ TestRunnerOptions? options,
+ FunctionRegistry? registry)
+ {
+ _handler = handler ?? throw new ArgumentNullException(nameof(handler));
+ _options = options ?? new TestRunnerOptions();
+ _serializer = _options.Serializer ?? new DefaultLambdaJsonSerializer();
+ _lambdaContext = CreateLambdaContext();
+ _store = new InMemoryOperationStore();
+ _processor = new CheckpointProcessor(_store, _options.SkipTime);
+ _serviceClient = new InMemoryDurableServiceClient(_store, _processor);
+ _registry = registry ?? new FunctionRegistry(_options);
+ }
+
+ ///
+ /// Registers a plain (non-durable) Lambda handler as a sibling function.
+ ///
+ public DurableTestRunner RegisterFunction(
+ string functionNameOrArn,
+ Func> handler)
+ {
+ _registry.RegisterPlain(functionNameOrArn, handler);
+ return this;
+ }
+
+ ///
+ /// Registers a durable Lambda handler as a sibling function.
+ ///
+ public DurableTestRunner RegisterDurableFunction(
+ string functionNameOrArn,
+ Func> handler)
+ {
+ _registry.RegisterDurable(functionNameOrArn, handler);
+ return this;
+ }
+
+ ///
+ public async Task> RunAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ var orchestrator = CreateOrchestrator();
+ return await orchestrator.DriveToTerminalAsync(
+ _options.DurableExecutionArn,
+ input,
+ timeout ?? _options.DefaultTimeout,
+ cancellationToken);
+ }
+
+ ///
+ public async Task StartAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ var arn = _options.DurableExecutionArn;
+ var orchestrator = CreateOrchestrator();
+ _lastStartInput = input;
+ _lastOrchestrator = orchestrator;
+
+ var result = await orchestrator.DriveUntilSuspendedAsync(
+ arn, input, timeout ?? _options.DefaultTimeout, cancellationToken);
+
+ if (result is not null)
+ _completedResults[arn] = result;
+
+ return arn;
+ }
+
+ ///
+ public Task WaitForCallbackAsync(
+ string durableExecutionArn,
+ string? name = null,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ var ops = _store.GetAllOperations(durableExecutionArn);
+ foreach (var op in ops)
+ {
+ if (op.Type == OperationTypes.Callback
+ && op.Status == OperationStatuses.Started
+ && op.CallbackDetails?.CallbackId is { } cbId)
+ {
+ if (name is null || MatchesCallbackName(op.Name, name))
+ {
+ if (!_consumedCallbackIds.Contains(cbId))
+ {
+ _consumedCallbackIds.Add(cbId);
+ return Task.FromResult(cbId);
+ }
+ }
+ }
+ }
+
+ throw new InvalidOperationException(
+ $"No pending callback found{(name is not null ? $" with name '{name}'" : "")} for execution '{durableExecutionArn}'. " +
+ "Ensure the workflow has reached a WaitForCallbackAsync point before calling this method.");
+ }
+
+ private static bool MatchesCallbackName(string? opName, string name)
+ {
+ if (opName is null) return false;
+ // Exact match
+ if (string.Equals(opName, name, StringComparison.Ordinal)) return true;
+ // The runtime names inner callback ops as "{name}-callback"
+ if (string.Equals(opName, $"{name}-callback", StringComparison.Ordinal)) return true;
+ return false;
+ }
+
+ ///
+ public Task SendCallbackSuccessAsync(
+ string callbackId,
+ TResult result,
+ CancellationToken cancellationToken = default)
+ {
+ var (arn, op) = FindCallbackOperation(callbackId);
+ var serialized = SerializeToString(result);
+
+ op.Status = OperationStatuses.Succeeded;
+ op.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ op.CallbackDetails!.Result = serialized;
+ _store.Upsert(arn, op);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task SendCallbackFailureAsync(
+ string callbackId,
+ ErrorObject? error = null,
+ CancellationToken cancellationToken = default)
+ {
+ var (arn, op) = FindCallbackOperation(callbackId);
+
+ op.Status = OperationStatuses.Failed;
+ op.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ op.CallbackDetails!.Error = error;
+ _store.Upsert(arn, op);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task SendCallbackHeartbeatAsync(
+ string callbackId,
+ CancellationToken cancellationToken = default)
+ {
+ // Heartbeats are a no-op for local testing — just validate the callback exists
+ FindCallbackOperation(callbackId);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public async Task> WaitForResultAsync(
+ string durableExecutionArn,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (_completedResults.TryGetValue(durableExecutionArn, out var cached))
+ return cached;
+
+ var orchestrator = _lastOrchestrator ?? CreateOrchestrator();
+ var result = await orchestrator.DriveToTerminalAsync(
+ durableExecutionArn,
+ _lastStartInput!,
+ timeout ?? _options.DefaultTimeout,
+ cancellationToken);
+
+ _completedResults[durableExecutionArn] = result;
+ return result;
+ }
+
+ ///
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ private (string Arn, Operation Op) FindCallbackOperation(string callbackId)
+ {
+ var arn = _options.DurableExecutionArn;
+ var ops = _store.GetAllOperations(arn);
+ foreach (var op in ops)
+ {
+ if (op.Type == OperationTypes.Callback
+ && op.CallbackDetails?.CallbackId == callbackId)
+ {
+ return (arn, op);
+ }
+ }
+ throw new InvalidOperationException(
+ $"No callback operation found with ID '{callbackId}'.");
+ }
+
+ private string? SerializeToString(T value)
+ {
+ if (value is null) return null;
+ using var stream = new MemoryStream();
+ _serializer.Serialize(value, stream);
+ return System.Text.Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ private ExecutionOrchestrator CreateOrchestrator()
+ {
+ return new ExecutionOrchestrator(
+ _handler, _store, _serviceClient, _lambdaContext, _options, _serializer,
+ _processor, _registry);
+ }
+
+ private TestLambdaContext CreateLambdaContext()
+ {
+ var ctx = new TestLambdaContext
+ {
+ FunctionName = "test-durable-function",
+ FunctionVersion = "$LATEST",
+ RemainingTime = TimeSpan.FromMinutes(15),
+ };
+ ctx.Serializer = _serializer;
+ return ctx;
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/ExecutionOrchestrator.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/ExecutionOrchestrator.cs
new file mode 100644
index 000000000..bac7d9399
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/ExecutionOrchestrator.cs
@@ -0,0 +1,238 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution.Services;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Drives a durable workflow handler to a terminal state by repeatedly invoking
+/// the internal DurableFunction.WrapAsync overload with the in-memory service client.
+///
+internal sealed class ExecutionOrchestrator
+{
+ private readonly Func> _handler;
+ private readonly InMemoryOperationStore _store;
+ private readonly IDurableServiceClient _serviceClient;
+ private readonly ILambdaContext _lambdaContext;
+ private readonly TestRunnerOptions _options;
+ private readonly ILambdaSerializer _serializer;
+ private readonly CheckpointProcessor? _processor;
+ private readonly FunctionRegistry? _registry;
+
+ // Accumulates across DriveUntilSuspendedAsync + DriveToTerminalAsync so the
+ // reported InvocationCount reflects the whole run (including the invocations
+ // consumed before a callback suspension), not just the final drive.
+ private int _invocationCount;
+
+ public ExecutionOrchestrator(
+ Func> handler,
+ InMemoryOperationStore store,
+ IDurableServiceClient serviceClient,
+ ILambdaContext lambdaContext,
+ TestRunnerOptions options,
+ ILambdaSerializer serializer,
+ CheckpointProcessor? processor = null,
+ FunctionRegistry? registry = null)
+ {
+ _handler = handler;
+ _store = store;
+ _serviceClient = serviceClient;
+ _lambdaContext = lambdaContext;
+ _options = options;
+ _serializer = serializer;
+ _processor = processor;
+ _registry = registry;
+ }
+
+ public Task?> DriveUntilSuspendedAsync(
+ string arn,
+ TInput input,
+ TimeSpan timeout,
+ CancellationToken cancellationToken)
+ => DriveAsync(arn, input, timeout, stopAtSuspend: true, cancellationToken);
+
+ public async Task> DriveToTerminalAsync(
+ string arn,
+ TInput input,
+ TimeSpan timeout,
+ CancellationToken cancellationToken)
+ {
+ var result = await DriveAsync(arn, input, timeout, stopAtSuspend: false, cancellationToken);
+ // stopAtSuspend:false only returns null when the workflow suspended on a
+ // callback it cannot drive itself — surface that as a clear, actionable error.
+ return result ?? throw new InvalidOperationException(
+ "Workflow suspended waiting on a callback and cannot be driven to completion with RunAsync. " +
+ "Use the two-call pattern instead: StartAsync, then WaitForCallbackAsync + SendCallbackSuccessAsync/SendCallbackFailureAsync, then WaitForResultAsync.");
+ }
+
+ private async Task?> DriveAsync(
+ string arn,
+ TInput input,
+ TimeSpan timeout,
+ bool stopAtSuspend,
+ CancellationToken cancellationToken)
+ {
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(timeout);
+
+ SeedExecutionOperation(arn, input);
+
+ while (true)
+ {
+ timeoutCts.Token.ThrowIfCancellationRequested();
+
+ if (_invocationCount >= _options.MaxInvocations)
+ {
+ throw new TestExecutionLimitException(
+ _options.MaxInvocations, _store.OperationCount(arn));
+ }
+
+ var invocationInput = BuildInvocationInput(arn);
+
+ var output = await DurableFunction.WrapAsync(
+ _handler, invocationInput, _lambdaContext, _serviceClient);
+
+ _invocationCount++;
+
+ if (output.Status != InvocationStatus.Pending)
+ return BuildResult(arn, output, _invocationCount);
+
+ // Pending. Resolve any sibling invokes the workflow started this pass
+ // (the runtime suspends on a CHAINED_INVOKE START expecting an external
+ // system to run the target); then re-drive so replay sees them resolved.
+ if (await ResolvePendingInvokesAsync(arn, timeoutCts.Token))
+ continue;
+
+ // Genuinely suspended with nothing to resolve. A pending callback means
+ // the workflow is waiting on external input; the caller decides whether
+ // that is a suspend point (StartAsync) or an error (RunAsync). For other
+ // pending states (e.g. a real wait with SkipTime disabled) keep looping
+ // until the workflow progresses, hits MaxInvocations, or times out.
+ if (stopAtSuspend || HasPendingCallback(arn))
+ return null;
+ }
+ }
+
+ ///
+ /// Runs any chained-invoke siblings started during the last invocation through
+ /// the and stamps the result/error onto the
+ /// stored operation so the next replay resolves it. Returns true if at least
+ /// one invoke was resolved (i.e. the workflow should be re-driven).
+ ///
+ private async Task ResolvePendingInvokesAsync(string arn, CancellationToken cancellationToken)
+ {
+ if (_processor is null || _registry is null)
+ return false;
+
+ var pending = _processor.DrainPendingInvokes();
+ if (pending.Count == 0)
+ return false;
+
+ foreach (var invoke in pending)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // An unregistered sibling throws UnregisteredSiblingFunctionException
+ // (out of InvokeAsync) so it surfaces with actionable guidance rather
+ // than as an opaque MaxInvocations timeout.
+ var (result, error) = await _registry.InvokeAsync(
+ invoke.FunctionName, invoke.Payload ?? "null", _serializer, _lambdaContext);
+
+ var op = _store.GetOperation(arn, invoke.OperationId);
+ if (op is null)
+ continue;
+
+ op.ChainedInvokeDetails ??= new ChainedInvokeDetails();
+ op.EndTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ if (error is null)
+ {
+ op.Status = OperationStatuses.Succeeded;
+ op.ChainedInvokeDetails.Result = result;
+ op.ChainedInvokeDetails.Error = null;
+ }
+ else
+ {
+ op.Status = OperationStatuses.Failed;
+ op.ChainedInvokeDetails.Error = error;
+ }
+ _store.Upsert(arn, op);
+ }
+
+ return true;
+ }
+
+ private bool HasPendingCallback(string arn)
+ {
+ foreach (var op in _store.GetAllOperations(arn))
+ {
+ if (op.Type == OperationTypes.Callback && op.Status == OperationStatuses.Started)
+ return true;
+ }
+ return false;
+ }
+
+ private void SeedExecutionOperation(string arn, TInput input)
+ {
+ // Idempotent: re-driving (e.g. WaitForResultAsync after a callback) must
+ // not reset the EXECUTION op back to Started or clobber recorded state.
+ if (_store.GetOperation(arn, "exec-0") is not null)
+ return;
+
+ var serializedInput = SerializeToString(input);
+ _store.Upsert(arn, new Operation
+ {
+ Id = "exec-0",
+ Type = OperationTypes.Execution,
+ Status = OperationStatuses.Started,
+ ExecutionDetails = new ExecutionDetails { InputPayload = serializedInput }
+ });
+ }
+
+ private DurableExecutionInvocationInput BuildInvocationInput(string arn)
+ {
+ return new DurableExecutionInvocationInput
+ {
+ DurableExecutionArn = arn,
+ CheckpointToken = _store.CurrentToken(arn),
+ InitialExecutionState = new InitialExecutionState
+ {
+ Operations = _store.GetAllOperations(arn).ToList(),
+ NextMarker = null,
+ }
+ };
+ }
+
+ private string SerializeToString(TInput value)
+ {
+ using var stream = new MemoryStream();
+ _serializer.Serialize(value, stream);
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ private TestResult BuildResult(string arn, DurableExecutionInvocationOutput output, int invocationCount)
+ {
+ TOutput? result = default;
+ if (output.Status == InvocationStatus.Succeeded && output.Result is not null)
+ {
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(output.Result));
+ result = _serializer.Deserialize(stream);
+ }
+
+ var allOps = _store.GetAllOperations(arn);
+ var steps = allOps
+ .Where(o => o.Type != OperationTypes.Execution)
+ .Select(o => new TestStep(o, _serializer))
+ .ToList();
+
+ return new TestResult(
+ status: output.Status,
+ result: result,
+ error: output.Error,
+ durableExecutionArn: arn,
+ invocationCount: invocationCount,
+ steps: steps);
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/FunctionRegistry.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/FunctionRegistry.cs
new file mode 100644
index 000000000..26afd6314
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/FunctionRegistry.cs
@@ -0,0 +1,156 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+using Amazon.Lambda.Core;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Tracks registered sibling function handlers for use by the local test runner
+/// when a workflow calls InvokeAsync.
+///
+internal sealed class FunctionRegistry
+{
+ private readonly List _entries = new();
+ private readonly TestRunnerOptions _options;
+
+ public FunctionRegistry(TestRunnerOptions? options = null)
+ {
+ _options = options ?? new TestRunnerOptions();
+ }
+
+ public void RegisterPlain(
+ string functionNameOrArn,
+ Func> handler)
+ {
+ _entries.Add(new FunctionEntry(
+ ExtractName(functionNameOrArn),
+ functionNameOrArn,
+ IsDurable: false,
+ InvokeDelegate: (payload, serializer, lambdaContext) =>
+ InvokePlain(handler, payload, serializer, lambdaContext)));
+ }
+
+ public void RegisterDurable(
+ string functionNameOrArn,
+ Func> handler)
+ {
+ _entries.Add(new FunctionEntry(
+ ExtractName(functionNameOrArn),
+ functionNameOrArn,
+ IsDurable: true,
+ InvokeDelegate: (payload, serializer, lambdaContext) =>
+ InvokeDurable(handler, payload, serializer)));
+ }
+
+ public async Task<(string? Result, ErrorObject? Error)> InvokeAsync(
+ string functionNameOrArn,
+ string serializedPayload,
+ ILambdaSerializer serializer,
+ ILambdaContext lambdaContext)
+ {
+ var entry = Lookup(functionNameOrArn)
+ ?? throw new UnregisteredSiblingFunctionException(functionNameOrArn);
+
+ try
+ {
+ return await entry.InvokeDelegate(serializedPayload, serializer, lambdaContext);
+ }
+ catch (Exception ex)
+ {
+ return (null, ErrorObject.FromException(ex));
+ }
+ }
+
+ public bool IsRegistered(string functionNameOrArn) => Lookup(functionNameOrArn) is not null;
+
+ private FunctionEntry? Lookup(string functionNameOrArn)
+ {
+ foreach (var entry in _entries)
+ {
+ if (string.Equals(entry.OriginalName, functionNameOrArn, StringComparison.Ordinal))
+ return entry;
+ }
+
+ var extractedName = ExtractName(functionNameOrArn);
+ foreach (var entry in _entries)
+ {
+ if (string.Equals(entry.ShortName, extractedName, StringComparison.Ordinal))
+ return entry;
+ }
+
+ return null;
+ }
+
+ private static string ExtractName(string functionNameOrArn)
+ {
+ if (!functionNameOrArn.StartsWith("arn:", StringComparison.OrdinalIgnoreCase))
+ return functionNameOrArn;
+
+ var parts = functionNameOrArn.Split(':');
+ // ARN format: arn:aws:lambda:region:account:function:name[:qualifier]
+ if (parts.Length >= 7)
+ return parts[6];
+
+ return functionNameOrArn;
+ }
+
+ private static async Task<(string? Result, ErrorObject? Error)> InvokePlain(
+ Func> handler,
+ string serializedPayload,
+ ILambdaSerializer serializer,
+ ILambdaContext lambdaContext)
+ {
+ var payload = Deserialize(serializedPayload, serializer);
+ var result = await handler(payload, lambdaContext);
+ return (Serialize(result, serializer), null);
+ }
+
+ private async Task<(string? Result, ErrorObject? Error)> InvokeDurable(
+ Func> handler,
+ string serializedPayload,
+ ILambdaSerializer serializer)
+ {
+ // A durable sibling is itself a workflow, so drive it to completion in a
+ // nested runner that shares this runner's options (time-skipping,
+ // serializer, registered siblings) but gets its own isolated store/ARN.
+ var payload = Deserialize(serializedPayload, serializer);
+
+ var nestedOptions = _options with
+ {
+ DurableExecutionArn = _options.DurableExecutionArn + ":nested:" + Guid.NewGuid().ToString("N"),
+ };
+ var nested = new DurableTestRunner(handler, nestedOptions, registry: this);
+
+ var result = await nested.RunAsync(payload);
+ if (result.Status == InvocationStatus.Succeeded)
+ return (Serialize(result.Result, serializer), null);
+
+ return (null, result.Error ?? new ErrorObject
+ {
+ ErrorType = typeof(InvokeException).FullName,
+ ErrorMessage = $"Durable sibling '{typeof(TPayload).Name}->{typeof(TResult).Name}' did not succeed (status: {result.Status}).",
+ });
+ }
+
+ private static T Deserialize(string serialized, ILambdaSerializer serializer)
+ {
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
+ return serializer.Deserialize(stream);
+ }
+
+ private static string? Serialize(T value, ILambdaSerializer serializer)
+ {
+ if (value is null) return null;
+ using var stream = new MemoryStream();
+ serializer.Serialize(value, stream);
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ private sealed record FunctionEntry(
+ string ShortName,
+ string OriginalName,
+ bool IsDurable,
+ Func> InvokeDelegate);
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/IDurableTestRunner.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/IDurableTestRunner.cs
new file mode 100644
index 000000000..af9e0a6b0
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/IDurableTestRunner.cs
@@ -0,0 +1,73 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Common interface for local and cloud durable test runners. Tests written
+/// against this interface can run unchanged against either backend.
+///
+public interface IDurableTestRunner
+{
+ ///
+ /// Drives the workflow to a terminal state and returns the result. Registered
+ /// sibling functions (see RegisterFunction/RegisterDurableFunction on the local
+ /// runner) are resolved automatically. Throws
+ /// if the workflow suspends waiting on a callback — use the two-call pattern
+ /// ( + +
+ /// SendCallback* + ) for callback workflows.
+ ///
+ Task> RunAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Starts the workflow and returns the durable execution ARN.
+ /// Use with for callback workflows.
+ ///
+ Task StartAsync(
+ TInput input,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Blocks until the workflow reaches a callback point and returns the callback ID.
+ ///
+ Task WaitForCallbackAsync(
+ string durableExecutionArn,
+ string? name = null,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Sends a success result to a waiting callback.
+ ///
+ Task SendCallbackSuccessAsync(
+ string callbackId,
+ TResult result,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Sends a failure to a waiting callback.
+ ///
+ Task SendCallbackFailureAsync(
+ string callbackId,
+ ErrorObject? error = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Sends a heartbeat to keep a callback alive.
+ ///
+ Task SendCallbackHeartbeatAsync(
+ string callbackId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Waits for the workflow to reach a terminal state and returns the result.
+ ///
+ Task> WaitForResultAsync(
+ string durableExecutionArn,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryDurableServiceClient.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryDurableServiceClient.cs
new file mode 100644
index 000000000..a49f6c2b1
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryDurableServiceClient.cs
@@ -0,0 +1,52 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.DurableExecution.Services;
+using SdkOperationUpdate = Amazon.Lambda.Model.OperationUpdate;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// In-memory implementation of for local testing.
+/// Processes checkpoint updates against the in-memory store and returns operations
+/// to the runtime engine.
+///
+internal sealed class InMemoryDurableServiceClient : IDurableServiceClient
+{
+ private readonly InMemoryOperationStore _store;
+ private readonly CheckpointProcessor _processor;
+
+ public InMemoryDurableServiceClient(InMemoryOperationStore store, CheckpointProcessor processor)
+ {
+ _store = store;
+ _processor = processor;
+ }
+
+ public Task CheckpointAsync(
+ string durableExecutionArn,
+ string? checkpointToken,
+ IReadOnlyList pendingOperations,
+ Action>? onNewOperations = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (pendingOperations.Count == 0)
+ return Task.FromResult(checkpointToken);
+
+ var (newToken, newOps) = _processor.Process(durableExecutionArn, checkpointToken, pendingOperations);
+
+ if (onNewOperations is not null && newOps.Count > 0)
+ onNewOperations(newOps);
+
+ return Task.FromResult(newToken);
+ }
+
+ public Task<(List Operations, string? NextMarker)> GetExecutionStateAsync(
+ string durableExecutionArn,
+ string? checkpointToken,
+ string marker,
+ CancellationToken cancellationToken = default)
+ {
+ var allOps = _store.GetAllOperations(durableExecutionArn);
+ return Task.FromResult<(List, string?)>((allOps.ToList(), null));
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryOperationStore.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryOperationStore.cs
new file mode 100644
index 000000000..56ebe7dbe
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/InMemoryOperationStore.cs
@@ -0,0 +1,99 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// In-memory store for operations recorded during a test execution.
+/// Each execution (keyed by ARN) maintains its own isolated operation set.
+///
+internal sealed class InMemoryOperationStore
+{
+ private readonly Dictionary _executions = new();
+
+ // A single lock guards both the per-ARN dictionary and each ExecutionData's
+ // collections. Writes are normally serialized by the runtime's single-reader
+ // checkpoint batcher, but parallel/map workflows and the snapshot reads below
+ // can interleave, so we lock to keep the Dictionary/List internally consistent.
+ private readonly object _gate = new();
+
+ public string CurrentToken(string arn)
+ {
+ lock (_gate)
+ return GetOrCreate(arn).CheckpointToken;
+ }
+
+ ///
+ /// Returns a snapshot copy of the operations for the execution. The copy is
+ /// detached from the backing list so callers can iterate safely while the
+ /// store continues to mutate.
+ ///
+ public IReadOnlyList GetAllOperations(string arn)
+ {
+ lock (_gate)
+ return GetOrCreate(arn).Operations.ToList();
+ }
+
+ public Operation? GetOperation(string arn, string operationId)
+ {
+ lock (_gate)
+ {
+ var data = GetOrCreate(arn);
+ return data.OperationMap.TryGetValue(operationId, out var op) ? op : null;
+ }
+ }
+
+ public void Upsert(string arn, Operation operation)
+ {
+ lock (_gate)
+ {
+ var data = GetOrCreate(arn);
+ if (data.OperationMap.TryGetValue(operation.Id!, out var existing))
+ {
+ var index = data.Operations.IndexOf(existing);
+ data.Operations[index] = operation;
+ data.OperationMap[operation.Id!] = operation;
+ }
+ else
+ {
+ data.Operations.Add(operation);
+ data.OperationMap[operation.Id!] = operation;
+ }
+ }
+ }
+
+ public string IncrementToken(string arn)
+ {
+ lock (_gate)
+ {
+ var data = GetOrCreate(arn);
+ data.TokenCounter++;
+ data.CheckpointToken = data.TokenCounter.ToString();
+ return data.CheckpointToken;
+ }
+ }
+
+ public int OperationCount(string arn)
+ {
+ lock (_gate)
+ return GetOrCreate(arn).Operations.Count;
+ }
+
+ private ExecutionData GetOrCreate(string arn)
+ {
+ if (!_executions.TryGetValue(arn, out var data))
+ {
+ data = new ExecutionData();
+ _executions[arn] = data;
+ }
+ return data;
+ }
+
+ private sealed class ExecutionData
+ {
+ public readonly List Operations = new();
+ public readonly Dictionary OperationMap = new();
+ public string CheckpointToken = "0";
+ public int TokenCounter;
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationKind.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationKind.cs
new file mode 100644
index 000000000..b7fde87ef
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationKind.cs
@@ -0,0 +1,28 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Classifies a by the underlying operation type.
+///
+public enum OperationKind
+{
+ /// A step operation (user function call).
+ Step,
+
+ /// A wait/timer operation.
+ Wait,
+
+ /// A callback operation (external signal).
+ Callback,
+
+ /// A chained-invoke operation (durable-to-durable or durable-to-plain call).
+ ChainedInvoke,
+
+ /// A context operation (child context, parallel, map).
+ Context,
+
+ /// The top-level execution operation.
+ Execution
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationStatus.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationStatus.cs
new file mode 100644
index 000000000..7f21ab6ec
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/OperationStatus.cs
@@ -0,0 +1,41 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// String constants matching the wire-format operation statuses.
+/// Uses a static class instead of an enum so values stay in lockstep
+/// with from the runtime package.
+///
+///
+/// Each constant is defined in terms of the runtime
+/// counterpart rather than a hand-copied literal, so the two cannot silently drift:
+/// if the runtime renames or removes a status, this class fails to compile.
+///
+public static class OperationStatus
+{
+ /// The operation has started.
+ public const string Started = OperationStatuses.Started;
+
+ /// The operation completed successfully.
+ public const string Succeeded = OperationStatuses.Succeeded;
+
+ /// The operation failed.
+ public const string Failed = OperationStatuses.Failed;
+
+ /// The operation is pending.
+ public const string Pending = OperationStatuses.Pending;
+
+ /// The operation timed out.
+ public const string TimedOut = OperationStatuses.TimedOut;
+
+ /// The operation was cancelled.
+ public const string Cancelled = OperationStatuses.Cancelled;
+
+ /// The operation was stopped.
+ public const string Stopped = OperationStatuses.Stopped;
+
+ /// The operation is ready to resume.
+ public const string Ready = OperationStatuses.Ready;
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/README.md b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/README.md
new file mode 100644
index 000000000..7f0319b44
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/README.md
@@ -0,0 +1,200 @@
+# AWS Lambda Durable Execution Testing for .NET
+
+> **Preview.** `Amazon.Lambda.DurableExecution.Testing` tracks the `Amazon.Lambda.DurableExecution` runtime package (0.x). Public APIs may change before 1.0.
+
+`Amazon.Lambda.DurableExecution.Testing` lets you test [durable workflows](../Amazon.Lambda.DurableExecution/README.md) without deploying to AWS. It drives your workflow handler to a terminal state in-process using the real durable runtime engine backed by an in-memory store, and then exposes the result and every recorded operation for assertions.
+
+You write a test against the `IDurableTestRunner` interface; the same test runs unchanged against the in-memory `DurableTestRunner` (fast, no AWS) or the `CloudDurableTestRunner` (a deployed function).
+
+## Key Features
+
+- **No deployment** — exercise steps, waits, callbacks, child contexts, parallel branches, and sibling invokes in a unit test.
+- **Time-skipping** — day-long `WaitAsync` / `WaitForConditionAsync` waits complete in milliseconds by default, so the whole workflow runs instantly.
+- **Step-level inspection** — assert on each operation's status, attempt count, result, error, timing, and parent/child structure.
+- **Callback control** — start a workflow, wait for it to suspend on a callback, then send success / failure / heartbeat from the test.
+- **Sibling functions** — register plain or durable Lambda handlers so `ctx.InvokeAsync` resolves in-process.
+- **Portable tests** — code to `IDurableTestRunner` and switch between the local and cloud backends.
+
+## Installation
+
+```bash
+dotnet add package Amazon.Lambda.DurableExecution.Testing
+```
+
+Reference it from your test project only — it depends on `Amazon.Lambda.TestUtilities`.
+
+## Quick Start
+
+Construct a `DurableTestRunner` with your workflow handler, call `RunAsync`, and assert on the `TestResult`:
+
+```csharp
+using Amazon.Lambda.DurableExecution.Testing;
+using Xunit;
+
+public class OrderWorkflowTests
+{
+ [Fact]
+ public async Task MultiStepWorkflow_Succeeds()
+ {
+ await using var runner = new DurableTestRunner(
+ handler: async (input, ctx) =>
+ {
+ var doubled = await ctx.StepAsync(async (_, _) => input * 2, name: "double");
+ var result = await ctx.StepAsync(async (_, _) => doubled + 10, name: "add_ten");
+ return result;
+ });
+
+ var result = await runner.RunAsync(5);
+
+ result.EnsureSucceeded();
+ Assert.Equal(20, result.Result);
+
+ // Inspect individual steps
+ var doubleStep = result.GetStep("double");
+ Assert.Equal(OperationKind.Step, doubleStep.Kind);
+ Assert.Equal(OperationStatus.Succeeded, doubleStep.Status);
+ Assert.Equal(10, doubleStep.GetResult());
+ }
+}
+```
+
+`RunAsync` drives the workflow to a terminal state and resolves any registered sibling functions automatically. It throws `InvalidOperationException` if the workflow suspends on a callback — use the [callback pattern](#testing-callbacks) for those workflows.
+
+## The `TestResult`
+
+`RunAsync` (and `WaitForResultAsync`) return a `TestResult`:
+
+| Member | Description |
+|---|---|
+| `Status` | `InvocationStatus.Succeeded`, `Failed`, or `Pending`. |
+| `IsSucceeded` / `IsFailed` | Status shortcuts. |
+| `Result` | The typed workflow output when succeeded. |
+| `Error` | The `ErrorObject` when failed. |
+| `DurableExecutionArn` | The execution ARN for the run. |
+| `InvocationCount` | Handler invocations used to drive the workflow (local only; `-1` on the cloud runner). |
+| `Steps` | Every recorded operation except the top-level `EXECUTION` op. |
+| `EnsureSucceeded()` | Throws `TestExecutionFailedException` if not succeeded. |
+
+Query steps with `GetStep(name)` / `FindStep(name)` (returns null if absent), `GetSteps(name)` (all matches, e.g. parallel branches or map items), `GetStepById(id)`, `GetStepsByStatus(status)`, and `GetChildren(step)`.
+
+Each `TestStep` exposes `Id`, `Name`, `ParentId`, `Kind` (`Step`, `Wait`, `Callback`, `ChainedInvoke`, `Context`, `Execution`), `SubKind`, `Status`, `Attempt`, `StartedAt` / `EndedAt` / `Duration`, and `Children`. Use `GetResult()` and `GetError()` to read an operation's typed result or error.
+
+Asserting on a failed workflow:
+
+```csharp
+var result = await runner.RunAsync("test");
+
+Assert.Equal(InvocationStatus.Failed, result.Status);
+Assert.Equal("System.InvalidOperationException", result.Error!.ErrorType);
+```
+
+## Waits and Time-Skipping
+
+By default `TestRunnerOptions.SkipTime` is `true`, so `WaitAsync` timers and `WaitForConditionAsync` backoffs complete immediately — a workflow with a 30-day wait runs in milliseconds. The wait is still recorded as a step you can assert on:
+
+```csharp
+await using var runner = new DurableTestRunner(
+ handler: async (input, ctx) =>
+ {
+ await ctx.StepAsync(async (_, _) => "done", name: "before");
+ await ctx.WaitAsync(TimeSpan.FromDays(30), name: "long_wait");
+ return await ctx.StepAsync(async (_, _) => "completed", name: "after");
+ });
+
+var result = await runner.RunAsync("go");
+result.EnsureSucceeded();
+
+var wait = result.GetStep("long_wait");
+Assert.Equal(OperationKind.Wait, wait.Kind);
+```
+
+Set `SkipTime = false` to assert on real wait durations.
+
+## Testing Callbacks
+
+For workflows that suspend on a callback (approvals, webhooks, external signals), use the two-call pattern: `StartAsync` runs the workflow until it suspends, `WaitForCallbackAsync` returns the pending callback id, you send a result, then `WaitForResultAsync` drives it to completion.
+
+```csharp
+await using var runner = new DurableTestRunner(
+ handler: async (input, ctx) =>
+ {
+ var approval = await ctx.WaitForCallbackAsync(
+ async (callbackId, cbCtx, ct) => { /* submit to external system */ },
+ name: "approval");
+ return $"approved: {approval}";
+ });
+
+var arn = await runner.StartAsync("request-1");
+var callbackId = await runner.WaitForCallbackAsync(arn, name: "approval");
+
+await runner.SendCallbackSuccessAsync(callbackId, "yes");
+var result = await runner.WaitForResultAsync(arn);
+
+result.EnsureSucceeded();
+Assert.Equal("approved: yes", result.Result);
+```
+
+`SendCallbackFailureAsync(callbackId, error)` delivers a failure (surfaced in the workflow as `CallbackException`), and `SendCallbackHeartbeatAsync(callbackId)` keeps a callback alive (a no-op locally).
+
+## Sibling Functions
+
+When your workflow calls `ctx.InvokeAsync` to invoke another Lambda function, register that function on the runner so it resolves in-process. Functions can be registered by short name or full ARN (resolution matches on the short name).
+
+```csharp
+await using var runner = new DurableTestRunner(
+ handler: async (input, ctx) =>
+ {
+ var payment = await ctx.InvokeAsync(
+ "process-payment", new PaymentRequest { Amount = input }, name: "charge");
+ return $"charged: {payment.Status}";
+ });
+
+// A plain (non-durable) Lambda handler
+runner.RegisterFunction(
+ "process-payment",
+ (req, _) => Task.FromResult(new PaymentResult { Status = $"approved-{req.Amount}" }));
+
+// A durable sibling runs in its own nested runner with its own steps
+runner.RegisterDurableFunction(
+ "audit",
+ async (req, childCtx) => await childCtx.StepAsync(/* ... */));
+
+var result = await runner.RunAsync(100);
+result.EnsureSucceeded();
+
+var invoke = result.GetStep("charge");
+Assert.Equal(OperationKind.ChainedInvoke, invoke.Kind);
+```
+
+Invoking an unregistered function throws `UnregisteredSiblingFunctionException`. A sibling that throws is recorded as a failed `ChainedInvoke` step and surfaces in the workflow as `InvokeException`.
+
+## Configuration
+
+`TestRunnerOptions` (local runner):
+
+| Option | Default | Description |
+|---|---|---|
+| `SkipTime` | `true` | Complete waits and retry backoffs immediately. |
+| `MaxInvocations` | `100` | Handler invocation cap before `TestExecutionLimitException`. |
+| `DefaultTimeout` | `30s` | Wall-clock timeout for a single `RunAsync` / `WaitForResultAsync`. |
+| `Serializer` | `DefaultLambdaJsonSerializer` | `ILambdaSerializer` for payloads and results. |
+| `LoggerFactory` | none | Optional logging during execution. |
+| `DurableExecutionArn` | synthetic test ARN | Override for tests that assert on the ARN. |
+
+## Testing Against a Deployed Function
+
+`CloudDurableTestRunner` implements the same `IDurableTestRunner` interface against a real deployed durable function: it invokes the function, reads the durable execution ARN from the response, polls `GetDurableExecution` until terminal, and reconstructs the recorded operations from `GetDurableExecutionHistory` (the token-free, externally-pollable history API). Tests written against the interface run unchanged on either backend.
+
+```csharp
+await using var runner = new CloudDurableTestRunner(
+ functionArn: "arn:aws:lambda:us-east-1:123456789012:function:order-processor:live");
+
+var result = await runner.RunAsync(new Order(/* ... */));
+result.EnsureSucceeded();
+```
+
+`CloudTestRunnerOptions` configures `PollInterval` (default 2s), `DefaultTimeout` (default 5m), and `Serializer`. The cloud runner maps the service's `FAILED`, `TIMED_OUT`, and `STOPPED` terminal states onto `InvocationStatus.Failed` (inspect `Error` for detail) and does not track `InvocationCount` (it returns `-1`) — do not assert on `InvocationCount` in tests intended to run against both backends.
+
+## Related
+
+- [Amazon.Lambda.DurableExecution](../Amazon.Lambda.DurableExecution/README.md) — the durable execution runtime SDK this package tests.
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionFailedException.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionFailedException.cs
new file mode 100644
index 000000000..6441b77d0
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionFailedException.cs
@@ -0,0 +1,41 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Thrown by when the workflow
+/// did not complete successfully.
+///
+public sealed class TestExecutionFailedException : Exception
+{
+ /// The final status of the workflow.
+ public InvocationStatus FinalStatus { get; }
+
+ /// The error that caused the failure, if available.
+ public ErrorObject? FailureError { get; }
+
+ /// All recorded steps at the time of failure.
+ public IReadOnlyList Steps { get; }
+
+ internal TestExecutionFailedException(
+ InvocationStatus finalStatus,
+ ErrorObject? failureError,
+ IReadOnlyList steps)
+ : base(FormatMessage(finalStatus, failureError))
+ {
+ FinalStatus = finalStatus;
+ FailureError = failureError;
+ Steps = steps;
+ }
+
+ private static string FormatMessage(InvocationStatus status, ErrorObject? error)
+ {
+ var msg = $"Workflow execution did not succeed. Final status: {status}.";
+ if (error is not null)
+ {
+ msg += $" Error: [{error.ErrorType}] {error.ErrorMessage}";
+ }
+ return msg;
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionLimitException.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionLimitException.cs
new file mode 100644
index 000000000..333bc63f6
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestExecutionLimitException.cs
@@ -0,0 +1,40 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Thrown when the workflow does not reach a terminal state within the configured
+/// limit.
+///
+public sealed class TestExecutionLimitException : Exception
+{
+ /// The maximum invocations configured.
+ public int MaxInvocations { get; }
+
+ /// Total operations recorded at the time of the limit breach.
+ public int TotalOperations { get; }
+
+ internal TestExecutionLimitException(int maxInvocations, int totalOperations)
+ : base(FormatMessage(maxInvocations, totalOperations))
+ {
+ MaxInvocations = maxInvocations;
+ TotalOperations = totalOperations;
+ }
+
+ private static string FormatMessage(int maxInvocations, int totalOperations)
+ {
+ return $"""
+ Workflow did not reach a terminal state within {maxInvocations} invocations.
+
+ Possible causes:
+ - Workflow uses WaitForCallbackAsync — call StartAsync/WaitForCallbackAsync/SendCallbackSuccessAsync instead of RunAsync.
+ - Workflow uses InvokeAsync for a function that isn't registered — call runner.RegisterFunction("name", handler).
+ - Workflow has an infinite retry loop.
+ - Workflow uses WaitForConditionAsync that never returns true.
+
+ Set TestRunnerOptions.MaxInvocations to a higher value if your workflow is legitimately long.
+ Total operations recorded: {totalOperations}.
+ """;
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestResult.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestResult.cs
new file mode 100644
index 000000000..4dfebb1d3
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestResult.cs
@@ -0,0 +1,181 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// The outcome of a durable workflow execution, including the terminal result
+/// and every recorded operation for step-level inspection.
+///
+public sealed class TestResult
+{
+ ///
+ /// The terminal status of the workflow. The runtime's
+ /// has three values (Succeeded, Failed, Pending); the cloud runner maps the
+ /// service's finer terminal states (FAILED, TIMED_OUT, STOPPED) onto
+ /// . Inspect for the
+ /// underlying detail.
+ ///
+ public InvocationStatus Status { get; }
+
+ /// True when the workflow completed successfully.
+ public bool IsSucceeded => Status == InvocationStatus.Succeeded;
+
+ /// True when the workflow reached a failed terminal state.
+ public bool IsFailed => Status == InvocationStatus.Failed;
+
+ ///
+ /// The workflow result when is .
+ /// Default when not succeeded.
+ ///
+ public TOutput? Result { get; }
+
+ /// The error when is .
+ public ErrorObject? Error { get; }
+
+ /// The durable execution ARN for this run.
+ public string DurableExecutionArn { get; }
+
+ ///
+ /// Number of handler invocations the local runner used to drive the workflow
+ /// to completion. -1 when not tracked — the cloud runner never tracks
+ /// it, so do not assert on in tests intended to
+ /// run against both backends.
+ ///
+ public int InvocationCount { get; }
+
+ /// Every recorded operation except the top-level EXECUTION operation.
+ public IReadOnlyList Steps { get; }
+
+ internal TestResult(
+ InvocationStatus status,
+ TOutput? result,
+ ErrorObject? error,
+ string durableExecutionArn,
+ int invocationCount,
+ IReadOnlyList steps)
+ {
+ Status = status;
+ Result = result;
+ Error = error;
+ DurableExecutionArn = durableExecutionArn;
+ InvocationCount = invocationCount;
+ Steps = steps;
+
+ LinkChildren();
+ }
+
+ ///
+ /// Returns the first step matching .
+ /// Throws if no match is found.
+ ///
+ public TestStep GetStep(string name)
+ {
+ return FindStep(name)
+ ?? throw new InvalidOperationException(
+ $"No step with name '{name}' found. Available steps: [{string.Join(", ", Steps.Where(s => s.Name is not null).Select(s => s.Name))}]");
+ }
+
+ ///
+ /// Returns the first step matching , or null if not found.
+ ///
+ public TestStep? FindStep(string name)
+ {
+ foreach (var step in Steps)
+ {
+ if (string.Equals(step.Name, name, StringComparison.Ordinal))
+ return step;
+ }
+ return null;
+ }
+
+ ///
+ /// Returns all steps matching (e.g., parallel branches or map items).
+ ///
+ public IReadOnlyList GetSteps(string name)
+ {
+ var matches = new List();
+ foreach (var step in Steps)
+ {
+ if (string.Equals(step.Name, name, StringComparison.Ordinal))
+ matches.Add(step);
+ }
+ return matches;
+ }
+
+ ///
+ /// Returns the step with the exact operation ID.
+ /// Throws if not found.
+ ///
+ public TestStep GetStepById(string operationId)
+ {
+ foreach (var step in Steps)
+ {
+ if (string.Equals(step.Id, operationId, StringComparison.Ordinal))
+ return step;
+ }
+ throw new InvalidOperationException(
+ $"No step with ID '{operationId}' found.");
+ }
+
+ ///
+ /// Returns all direct children of (operations whose ParentId matches).
+ ///
+ public IReadOnlyList GetChildren(TestStep parent)
+ {
+ return parent.Children;
+ }
+
+ ///
+ /// Returns all steps whose equals
+ /// (use the constants,
+ /// e.g. or ).
+ ///
+ public IReadOnlyList GetStepsByStatus(string status)
+ {
+ var matches = new List();
+ foreach (var step in Steps)
+ {
+ if (string.Equals(step.Status, status, StringComparison.Ordinal))
+ matches.Add(step);
+ }
+ return matches;
+ }
+
+ ///
+ /// Throws if is not
+ /// .
+ ///
+ public void EnsureSucceeded()
+ {
+ if (Status != InvocationStatus.Succeeded)
+ {
+ throw new TestExecutionFailedException(Status, Error, Steps);
+ }
+ }
+
+ private void LinkChildren()
+ {
+ var childMap = new Dictionary>();
+ foreach (var step in Steps)
+ {
+ if (step.ParentId is not null)
+ {
+ if (!childMap.TryGetValue(step.ParentId, out var children))
+ {
+ children = new List();
+ childMap[step.ParentId] = children;
+ }
+ children.Add(step);
+ }
+ }
+
+ foreach (var step in Steps)
+ {
+ if (childMap.TryGetValue(step.Id, out var children))
+ {
+ step.Children = children;
+ }
+ }
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestRunnerOptions.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestRunnerOptions.cs
new file mode 100644
index 000000000..a0d7ff6df
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestRunnerOptions.cs
@@ -0,0 +1,65 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Configuration for the local durable test runner.
+///
+public sealed record TestRunnerOptions
+{
+ ///
+ /// When true, both WaitAsync timers and step/WaitForCondition retry
+ /// backoffs complete immediately rather than waiting for real wall-clock time,
+ /// so a workflow with day-long waits runs in milliseconds. Default: true.
+ ///
+ ///
+ /// The default differs from the JavaScript SDK (where time-skipping is opt-in).
+ /// In .NET it defaults to true so the common "drive the whole workflow to
+ /// completion" test does not block on real timers; set it to false to assert
+ /// on real wait durations.
+ ///
+ public bool SkipTime { get; init; } = true;
+
+ ///
+ /// Maximum number of handler invocations before throwing
+ /// . Default: 100.
+ ///
+ public int MaxInvocations
+ {
+ get => _maxInvocations;
+ init
+ {
+ if (value <= 0)
+ throw new ArgumentOutOfRangeException(nameof(MaxInvocations), value, "MaxInvocations must be greater than zero.");
+ _maxInvocations = value;
+ }
+ }
+ private readonly int _maxInvocations = 100;
+
+ ///
+ /// Wall-clock timeout for a single RunAsync or WaitForResultAsync call.
+ /// Default: 30 seconds.
+ ///
+ public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Serializer used for step result deserialization. When null, uses
+ /// DefaultLambdaJsonSerializer from Amazon.Lambda.Serialization.SystemTextJson.
+ ///
+ public ILambdaSerializer? Serializer { get; init; }
+
+ ///
+ /// Logger factory for runtime logging during test execution. Optional.
+ ///
+ public ILoggerFactory? LoggerFactory { get; init; }
+
+ ///
+ /// The durable execution ARN used in the test context. Override for tests that
+ /// assert on ARN values. Default: a synthetic test ARN.
+ ///
+ public string DurableExecutionArn { get; init; } = "arn:aws:lambda:us-east-1:123456789012:execution:test-fn:test-execution";
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestStep.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestStep.cs
new file mode 100644
index 000000000..3a6f0782e
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/TestStep.cs
@@ -0,0 +1,137 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+using Amazon.Lambda.Core;
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// A single operation recorded during workflow execution, exposed for test assertions.
+/// Wraps the internal with typed accessors.
+///
+public sealed class TestStep
+{
+ private readonly Operation _operation;
+ private readonly ILambdaSerializer _serializer;
+
+ internal TestStep(Operation operation, ILambdaSerializer serializer)
+ {
+ _operation = operation;
+ _serializer = serializer;
+ }
+
+ /// The operation's unique identifier.
+ public string Id => _operation.Id!;
+
+ /// User-supplied operation name (e.g., the step name).
+ public string? Name => _operation.Name;
+
+ /// Parent operation identifier, if this operation is nested.
+ public string? ParentId => _operation.ParentId;
+
+ /// The kind of operation (Step, Wait, Callback, etc.).
+ public OperationKind Kind => MapKind(_operation.Type);
+
+ ///
+ /// The sub-kind providing finer classification (e.g., "Parallel", "Map",
+ /// "WaitForCallback", "WaitForCondition"). Null when not applicable.
+ ///
+ public string? SubKind => _operation.SubType;
+
+ /// The terminal status of this operation.
+ public string Status => _operation.Status ?? OperationStatus.Pending;
+
+ ///
+ /// The attempt number (1-based) for step operations. 0 for non-step kinds.
+ ///
+ public int Attempt => _operation.StepDetails?.Attempt ?? 0;
+
+ /// When the operation started (null if not yet started).
+ public DateTimeOffset? StartedAt => _operation.StartTimestamp.HasValue
+ ? DateTimeOffset.FromUnixTimeMilliseconds(_operation.StartTimestamp.Value)
+ : null;
+
+ /// When the operation ended (null if not yet ended).
+ public DateTimeOffset? EndedAt => _operation.EndTimestamp.HasValue
+ ? DateTimeOffset.FromUnixTimeMilliseconds(_operation.EndTimestamp.Value)
+ : null;
+
+ /// Elapsed wall-clock duration, or null if timestamps are missing.
+ public TimeSpan? Duration => StartedAt.HasValue && EndedAt.HasValue
+ ? EndedAt - StartedAt
+ : null;
+
+ /// Child operations (linked by parent ID). Set externally by .
+ public IReadOnlyList Children { get; internal set; } = Array.Empty();
+
+ ///
+ /// Deserializes and returns the typed result from this operation.
+ /// Routes to the appropriate details property based on .
+ /// Returns default when no result is present.
+ ///
+ public T? GetResult()
+ {
+ var serialized = Kind switch
+ {
+ OperationKind.Step => _operation.StepDetails?.Result,
+ OperationKind.ChainedInvoke => _operation.ChainedInvokeDetails?.Result,
+ OperationKind.Context => _operation.ContextDetails?.Result,
+ OperationKind.Callback => _operation.CallbackDetails?.Result,
+ _ => null,
+ };
+
+ if (serialized is null) return default;
+
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
+ return _serializer.Deserialize(stream);
+ }
+
+ ///
+ /// Returns the error from this operation, or null if no error is present.
+ /// Routes to the appropriate details property based on .
+ ///
+ public ErrorObject? GetError()
+ {
+ return Kind switch
+ {
+ OperationKind.Step => _operation.StepDetails?.Error,
+ OperationKind.ChainedInvoke => _operation.ChainedInvokeDetails?.Error,
+ OperationKind.Context => _operation.ContextDetails?.Error,
+ OperationKind.Callback => _operation.CallbackDetails?.Error,
+ _ => null,
+ };
+ }
+
+ ///
+ /// Returns the scheduled end time for a wait operation, or null.
+ ///
+ public DateTimeOffset? GetWaitEndsAt()
+ {
+ var ts = _operation.WaitDetails?.ScheduledEndTimestamp;
+ return ts.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ts.Value) : null;
+ }
+
+ ///
+ /// Returns the callback identifier for a callback operation, or null.
+ ///
+ public string? GetCallbackId() => _operation.CallbackDetails?.CallbackId;
+
+ ///
+ /// Returns the function name for a chained-invoke operation, or null.
+ ///
+ public string? GetChainedInvokeFunctionName() => _operation.ChainedInvokeDetails is not null
+ ? _operation.Name
+ : null;
+
+ private static OperationKind MapKind(string? type) => type switch
+ {
+ OperationTypes.Step => OperationKind.Step,
+ OperationTypes.Wait => OperationKind.Wait,
+ OperationTypes.Callback => OperationKind.Callback,
+ OperationTypes.ChainedInvoke => OperationKind.ChainedInvoke,
+ OperationTypes.Context => OperationKind.Context,
+ OperationTypes.Execution => OperationKind.Execution,
+ _ => OperationKind.Step,
+ };
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution.Testing/UnregisteredSiblingFunctionException.cs b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/UnregisteredSiblingFunctionException.cs
new file mode 100644
index 000000000..6a32e82ea
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution.Testing/UnregisteredSiblingFunctionException.cs
@@ -0,0 +1,25 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.DurableExecution.Testing;
+
+///
+/// Thrown when a workflow calls InvokeAsync with a function name that has not
+/// been registered via RegisterFunction or RegisterDurableFunction.
+///
+public sealed class UnregisteredSiblingFunctionException : Exception
+{
+ /// The function name or ARN that was requested but not registered.
+ public string FunctionName { get; }
+
+ ///
+ /// Creates a new instance for the unregistered function.
+ ///
+ public UnregisteredSiblingFunctionException(string functionName)
+ : base($"No handler registered for function '{functionName}'. " +
+ $"Call runner.RegisterFunction(\"{functionName}\", handler) or " +
+ $"runner.RegisterDurableFunction(\"{functionName}\", handler) before running the workflow.")
+ {
+ FunctionName = functionName;
+ }
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj b/Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj
index cb03b2715..0e9fcd1f3 100644
--- a/Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj
@@ -26,6 +26,9 @@
<_Parameter1>Amazon.Lambda.DurableExecution.Tests, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4"
+
+ <_Parameter1>Amazon.Lambda.DurableExecution.Testing, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4"
+
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/DurableFunction.cs b/Libraries/src/Amazon.Lambda.DurableExecution/DurableFunction.cs
index 04ebee556..960be20b4 100644
--- a/Libraries/src/Amazon.Lambda.DurableExecution/DurableFunction.cs
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/DurableFunction.cs
@@ -37,7 +37,8 @@ public static Task WrapAsync(
Func> workflow,
DurableExecutionInvocationInput invocationInput,
ILambdaContext lambdaContext)
- => WrapAsyncCore(workflow, invocationInput, lambdaContext, _cachedLambdaClient.Value);
+ => WrapAsyncCore(workflow, invocationInput, lambdaContext,
+ new LambdaDurableServiceClient(_cachedLambdaClient.Value));
///
/// Wrap a workflow (typed input + output) with explicit Lambda client.
@@ -47,7 +48,8 @@ public static Task WrapAsync(
DurableExecutionInvocationInput invocationInput,
ILambdaContext lambdaContext,
IAmazonLambda lambdaClient)
- => WrapAsyncCore(workflow, invocationInput, lambdaContext, lambdaClient);
+ => WrapAsyncCore(workflow, invocationInput, lambdaContext,
+ new LambdaDurableServiceClient(lambdaClient));
///
/// Wrap a void workflow (typed input, no output).
@@ -68,20 +70,32 @@ public static Task WrapAsync(
IAmazonLambda lambdaClient)
=> WrapAsyncCore(
async (input, ctx) => { await workflow(input, ctx); return null; },
- invocationInput, lambdaContext, lambdaClient);
+ invocationInput, lambdaContext,
+ new LambdaDurableServiceClient(lambdaClient));
+
+ ///
+ /// Internal overload for the testing package — accepts an
+ /// directly so the testing SDK can
+ /// inject an in-memory implementation.
+ ///
+ internal static Task WrapAsync(
+ Func> workflow,
+ DurableExecutionInvocationInput invocationInput,
+ ILambdaContext lambdaContext,
+ IDurableServiceClient serviceClient)
+ => WrapAsyncCore(workflow, invocationInput, lambdaContext, serviceClient);
private static async Task WrapAsyncCore(
Func> workflow,
DurableExecutionInvocationInput invocationInput,
ILambdaContext lambdaContext,
- IAmazonLambda lambdaClient)
+ IDurableServiceClient serviceClient)
{
var serializer = LambdaSerializerHelper.GetRequired(lambdaContext);
var state = new ExecutionState();
state.LoadFromCheckpoint(invocationInput.InitialExecutionState);
- var serviceClient = new LambdaDurableServiceClient(lambdaClient);
var checkpointToken = invocationInput.CheckpointToken;
var nextMarker = invocationInput.InitialExecutionState?.NextMarker;
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/Services/IDurableServiceClient.cs b/Libraries/src/Amazon.Lambda.DurableExecution/Services/IDurableServiceClient.cs
new file mode 100644
index 000000000..8ccbf13c9
--- /dev/null
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/Services/IDurableServiceClient.cs
@@ -0,0 +1,27 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using SdkOperationUpdate = Amazon.Lambda.Model.OperationUpdate;
+
+namespace Amazon.Lambda.DurableExecution.Services;
+
+///
+/// Abstraction over the durable execution service RPCs. The production
+/// implementation () calls the real
+/// AWS Lambda APIs; the testing package injects an in-memory fake.
+///
+internal interface IDurableServiceClient
+{
+ Task CheckpointAsync(
+ string durableExecutionArn,
+ string? checkpointToken,
+ IReadOnlyList pendingOperations,
+ Action>? onNewOperations = null,
+ CancellationToken cancellationToken = default);
+
+ Task<(List Operations, string? NextMarker)> GetExecutionStateAsync(
+ string durableExecutionArn,
+ string? checkpointToken,
+ string marker,
+ CancellationToken cancellationToken = default);
+}
diff --git a/Libraries/src/Amazon.Lambda.DurableExecution/Services/LambdaDurableServiceClient.cs b/Libraries/src/Amazon.Lambda.DurableExecution/Services/LambdaDurableServiceClient.cs
index 7fae2ee10..818e95bd0 100644
--- a/Libraries/src/Amazon.Lambda.DurableExecution/Services/LambdaDurableServiceClient.cs
+++ b/Libraries/src/Amazon.Lambda.DurableExecution/Services/LambdaDurableServiceClient.cs
@@ -19,7 +19,7 @@ namespace Amazon.Lambda.DurableExecution.Services;
///
/// Calls the real AWS Lambda Durable Execution APIs via the AWSSDK.Lambda client.
///
-internal sealed class LambdaDurableServiceClient
+internal sealed class LambdaDurableServiceClient : IDurableServiceClient
{
private readonly IAmazonLambda _lambdaClient;
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Amazon.Lambda.DurableExecution.IntegrationTests.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Amazon.Lambda.DurableExecution.IntegrationTests.csproj
index 7eb196bf1..533025a48 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Amazon.Lambda.DurableExecution.IntegrationTests.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Amazon.Lambda.DurableExecution.IntegrationTests.csproj
@@ -38,6 +38,19 @@
+
+
+
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/DurableFunctionDeployment.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/DurableFunctionDeployment.cs
index 7d005d710..b095873aa 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/DurableFunctionDeployment.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/DurableFunctionDeployment.cs
@@ -73,7 +73,12 @@ internal sealed class DurableFunctionDeployment : IAsyncDisposable
private DurableFunctionDeployment(ITestOutputHelper output, string suffix)
{
- _output = output;
+ // xUnit buffers ITestOutputHelper and only flushes it when the test
+ // completes, so a long deploy/poll shows no live progress. When
+ // DURABLE_INTEG_TRACE is set, tee every line to an autoflushed file you
+ // can `tail -f` (DURABLE_INTEG_TRACE=, or "1"/"true" for a default
+ // path under the temp dir). Off by default — no behavior change.
+ _output = FileTracingTestOutputHelper.MaybeWrap(output);
_lambdaClient = new AmazonLambdaClient(DeploymentRegion);
_iamClient = new AmazonIdentityManagementServiceClient(DeploymentRegion);
@@ -96,12 +101,13 @@ public static async Task CreateAsync(
IDictionary? environment = null,
IReadOnlyList? invokeAllowedFunctionArns = null,
bool enableTenancy = false,
- string? handler = null)
+ string? handler = null,
+ int executionTimeoutSeconds = 60)
{
var deployment = new DurableFunctionDeployment(output, scenarioSuffix);
try
{
- await deployment.InitializeAsync(testFunctionDir, externalFunctionDir, environment, invokeAllowedFunctionArns, enableTenancy, handler);
+ await deployment.InitializeAsync(testFunctionDir, externalFunctionDir, environment, invokeAllowedFunctionArns, enableTenancy, handler, executionTimeoutSeconds);
}
catch
{
@@ -195,7 +201,8 @@ private async Task InitializeAsync(
IDictionary? environment,
IReadOnlyList? invokeAllowedFunctionArns,
bool enableTenancy,
- string? handler)
+ string? handler,
+ int executionTimeoutSeconds)
{
// 1. Create the workflow's IAM role.
_output.WriteLine($"Creating IAM role: {_roleName}");
@@ -365,7 +372,7 @@ await _lambdaClient.CreateFunctionAsync(new CreateFunctionRequest
Code = new FunctionCode { ZipFile = new MemoryStream(zipBytes) },
Timeout = 30,
MemorySize = 256,
- DurableConfig = new DurableConfig { ExecutionTimeout = 60 },
+ DurableConfig = new DurableConfig { ExecutionTimeout = executionTimeoutSeconds },
// Emit structured JSON logs so tests that parse log records (e.g.
// ReplayAwareLoggerTest) can assert on durable-execution scope keys.
LoggingConfig = new LoggingConfig { LogFormat = LogFormat.JSON }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/FileTracingTestOutputHelper.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/FileTracingTestOutputHelper.cs
new file mode 100644
index 000000000..2789c207b
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/FileTracingTestOutputHelper.cs
@@ -0,0 +1,74 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Xunit.Abstractions;
+
+namespace Amazon.Lambda.DurableExecution.IntegrationTests;
+
+///
+/// An that forwards to an inner helper and ALSO
+/// appends each line to an autoflushed file, so deploy/poll progress is visible
+/// live (via tail -f) instead of only when the test completes — xUnit
+/// buffers output until then.
+///
+///
+/// Opt-in through the DURABLE_INTEG_TRACE environment variable:
+/// set it to a file path, or to 1/true for a default path under
+/// the temp directory. Unset (the default) returns the original helper unchanged,
+/// so there is no behavior change for normal runs.
+///
+internal sealed class FileTracingTestOutputHelper : ITestOutputHelper
+{
+ private readonly ITestOutputHelper _inner;
+ private readonly string _path;
+ private static readonly object FileLock = new();
+
+ private FileTracingTestOutputHelper(ITestOutputHelper inner, string path)
+ {
+ _inner = inner;
+ _path = path;
+ }
+
+ public static ITestOutputHelper MaybeWrap(ITestOutputHelper inner)
+ {
+ var trace = Environment.GetEnvironmentVariable("DURABLE_INTEG_TRACE");
+ if (string.IsNullOrEmpty(trace))
+ return inner;
+
+ var path = trace is "1" or "true" or "TRUE"
+ ? Path.Combine(Path.GetTempPath(), "durable-integ-trace.log")
+ : trace;
+
+ return new FileTracingTestOutputHelper(inner, path);
+ }
+
+ public void WriteLine(string message)
+ {
+ _inner.WriteLine(message);
+ Append(message);
+ }
+
+ public void WriteLine(string format, params object[] args)
+ {
+ _inner.WriteLine(format, args);
+ Append(string.Format(format, args));
+ }
+
+ private void Append(string message)
+ {
+ // Serialize cross-thread writes; a single test class can build several
+ // deployments concurrently (e.g. CreateWithDownstreamAsync). Swallow IO
+ // errors — tracing must never fail a test.
+ try
+ {
+ lock (FileLock)
+ {
+ File.AppendAllText(_path, $"{DateTime.UtcNow:HH:mm:ss} {message}{System.Environment.NewLine}");
+ }
+ }
+ catch
+ {
+ // Best-effort diagnostics only.
+ }
+ }
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/PortableScenariosCloudTests.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/PortableScenariosCloudTests.cs
new file mode 100644
index 000000000..94b1fd08a
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/PortableScenariosCloudTests.cs
@@ -0,0 +1,445 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.DurableExecution.Testing;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Amazon.Lambda.DurableExecution.IntegrationTests;
+
+///
+/// Runs the shared against the deployed cloud
+/// backend (). These are the
+/// real-AWS half of the portable-test pair: the local half lives in
+/// Amazon.Lambda.DurableExecution.Testing.Tests.PortableScenariosLocalTests
+/// and runs the very same scenario bodies in-process. Passing both proves that a
+/// test coded to runs unchanged
+/// on either backend.
+///
+///
+/// Each test deploys its function(s) via
+/// (IAM role + zip on managed dotnet10), constructs a cloud runner against
+/// the deployed ARN, runs the scenario, then tears everything down. The runner's
+/// poll interval and timeout are widened because real durable waits and the
+/// settlement poll fire genuine service-side timers.
+///
+public class PortableScenariosCloudTests
+{
+ private readonly ITestOutputHelper _output;
+ public PortableScenariosCloudTests(ITestOutputHelper output) => _output = output;
+
+ private static CloudTestRunnerOptions CloudOptions() => new()
+ {
+ PollInterval = TimeSpan.FromSeconds(2),
+ DefaultTimeout = TimeSpan.FromMinutes(4),
+ };
+
+ [Fact]
+ public async Task Fulfillment_AllOperationKinds_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("FulfillmentFunction"),
+ "portable-fulfill", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.RunFulfillmentAsync(runner);
+ }
+
+ [Fact]
+ public async Task Approval_CallbackSuccessRoundTrip_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ApprovalFunction"),
+ "portable-approve", _output,
+ // The callback is delivered out-of-band by the test process, which
+ // adds history-poll latency on top of deploy time — give the
+ // execution generous headroom so it doesn't time out before delivery.
+ executionTimeoutSeconds: 300);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.ApproveAsync(runner);
+ }
+
+ [Fact]
+ public async Task Approval_CallbackFailure_RejectsButCompletes()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ApprovalFunction"),
+ "portable-reject", _output,
+ executionTimeoutSeconds: 300);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.RejectAsync(runner);
+ }
+
+ [Fact]
+ public async Task Retry_FlakyStepSucceedsOnFinalAttempt()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("RetryFunction"),
+ "portable-retry", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.RetryAsync(runner);
+ }
+
+ [Fact]
+ public async Task WaitOnly_NoSteps_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitOnlyFunction"),
+ "portable-waitonly", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.WaitOnlyAsync(runner);
+ }
+
+ [Fact]
+ public async Task MultipleSteps_AllCheckpointed_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MultipleStepsFunction"),
+ "portable-multi", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.MultipleStepsAsync(runner);
+ }
+
+ [Fact]
+ public async Task StepFails_PropagatesAsFailed()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("StepFailsFunction"),
+ "portable-stepfail", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.StepFailsAsync(runner);
+ }
+
+ [Fact]
+ public async Task StepWaitStep_ThreadsValueAcrossWait()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("StepWaitStepFunction"),
+ "portable-stepwait", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.StepWaitStepAsync(runner);
+ }
+
+ [Fact]
+ public async Task LongerWait_ThreadsValueAcrossWait()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("LongerWaitFunction"),
+ "portable-longwait", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.LongerWaitAsync(runner);
+ }
+
+ [Fact]
+ public async Task RetryExhaustion_FailsAfterAllAttempts()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("RetryExhaustionFunction"),
+ "portable-retryexh", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.RetryExhaustionAsync(runner);
+ }
+
+ [Fact]
+ public async Task WaitForCondition_HappyPath_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitForConditionHappyPathFunction"),
+ "portable-condhappy", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.WaitForConditionHappyAsync(runner);
+ }
+
+ [Fact]
+ public async Task WaitForCondition_MaxAttempts_Exhausts()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitForConditionMaxAttemptsFunction"),
+ "portable-condmax", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.WaitForConditionMaxAttemptsAsync(runner);
+ }
+
+ [Fact]
+ public async Task WaitForCondition_Exponential_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitForConditionExponentialFunction"),
+ "portable-condexp", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.WaitForConditionExponentialAsync(runner);
+ }
+
+ [Fact]
+ public async Task WaitForCondition_UserCheckThrows_Caught()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitForConditionUserCheckThrowsFunction"),
+ "portable-condthrow", _output);
+
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST",
+ deployment.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.WaitForConditionUserCheckThrowsAsync(runner);
+ }
+
+ [Fact]
+ public async Task Parallel_HappyPath_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ParallelHappyPathFunction"),
+ "portable-phappy", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ParallelHappyAsync(runner);
+ }
+
+ [Fact]
+ public async Task Parallel_PartialFailure_Tolerated()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ParallelPartialFailureFunction"),
+ "portable-ppartial", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ParallelPartialFailureAsync(runner);
+ }
+
+ [Fact]
+ public async Task Parallel_FirstSuccessful_ShortCircuits()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ParallelFirstSuccessfulFunction"),
+ "portable-pfirst", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ParallelFirstSuccessfulAsync(runner);
+ }
+
+ [Fact]
+ public async Task Parallel_FailureTolerance_Exceeded_Fails()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ParallelFailureToleranceFunction"),
+ "portable-ptol", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ParallelFailureToleranceAsync(runner);
+ }
+
+ [Fact]
+ public async Task Parallel_MaxConcurrency_AllComplete()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ParallelMaxConcurrencyFunction"),
+ "portable-pmax", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ParallelMaxConcurrencyAsync(runner);
+ }
+
+ [Fact]
+ public async Task Map_HappyPath_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MapHappyPathFunction"),
+ "portable-mhappy", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.MapHappyAsync(runner);
+ }
+
+ [Fact]
+ public async Task Map_PartialFailure_Tolerated()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MapPartialFailureFunction"),
+ "portable-mpartial", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.MapPartialFailureAsync(runner);
+ }
+
+ [Fact]
+ public async Task Map_FirstSuccessful_ShortCircuits()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MapFirstSuccessfulFunction"),
+ "portable-mfirst", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.MapFirstSuccessfulAsync(runner);
+ }
+
+ [Fact]
+ public async Task Map_FailureTolerance_Exceeded_Fails()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MapFailureToleranceFunction"),
+ "portable-mtol", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.MapFailureToleranceAsync(runner);
+ }
+
+ [Fact]
+ public async Task Map_MaxConcurrency_AllComplete()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("MapMaxConcurrencyFunction"),
+ "portable-mmax", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.MapMaxConcurrencyAsync(runner);
+ }
+
+ [Fact]
+ public async Task ChildContext_HappyPath_Succeeds()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ChildContextFunction"),
+ "portable-cchappy", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ChildContextHappyAsync(runner);
+ }
+
+ [Fact]
+ public async Task ChildContext_Fails_PropagatesFailure()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ChildContextFailsFunction"),
+ "portable-ccfail", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ChildContextFailsAsync(runner);
+ }
+
+ [Fact]
+ public async Task ChildContext_RetryFails_PropagatesFailure()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("ChildContextRetryFailsFunction"),
+ "portable-ccretryfail", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.ChildContextRetryFailsAsync(runner);
+ }
+
+ [Fact]
+ public async Task InvokeFailure_ParentCatchesAndSucceeds()
+ {
+ var (parent, downstream) = await DurableFunctionDeployment.CreateWithDownstreamAsync(
+ parentTestFunctionDir: DurableFunctionDeployment.FindTestFunctionDir("InvokeFailureParentFunction"),
+ downstreamTestFunctionDir: DurableFunctionDeployment.FindTestFunctionDir("InvokeFailureChildFunction"),
+ scenarioSuffix: "portable-invfail",
+ output: _output);
+
+ await using (downstream)
+ await using (parent)
+ {
+ await using var runner = new CloudDurableTestRunner(
+ parent.FunctionArn + ":$LATEST", parent.LambdaClient, CloudOptions());
+ await PortableScenarios.InvokeFailureAsync(runner);
+ }
+ }
+
+ [Fact]
+ public async Task CallbackSubmitterFails_PropagatesFailure()
+ {
+ await using var deployment = await DurableFunctionDeployment.CreateAsync(
+ DurableFunctionDeployment.FindTestFunctionDir("WaitForCallbackSubmitterFailsFunction"),
+ "portable-subfail", _output);
+ await using var runner = new CloudDurableTestRunner(
+ deployment.FunctionArn + ":$LATEST", deployment.LambdaClient, CloudOptions());
+ await PortableScenarios.CallbackSubmitterFailsAsync(runner);
+ }
+
+ [Fact]
+ public async Task ChainedInvoke_DeployedDownstream_Succeeds()
+ {
+ var (parent, downstream) = await DurableFunctionDeployment.CreateWithDownstreamAsync(
+ parentTestFunctionDir: DurableFunctionDeployment.FindTestFunctionDir("ChainedInvokeParentFunction"),
+ downstreamTestFunctionDir: DurableFunctionDeployment.FindTestFunctionDir("ChainedInvokeChildFunction"),
+ scenarioSuffix: "portable-chain",
+ output: _output);
+
+ await using (downstream)
+ await using (parent)
+ {
+ await using var runner = new CloudDurableTestRunner(
+ parent.FunctionArn + ":$LATEST",
+ parent.LambdaClient,
+ CloudOptions());
+
+ await PortableScenarios.ChainedInvokeAsync(runner);
+ }
+ }
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/RetryTest.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/RetryTest.cs
index 1dcf48249..ee8f5b550 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/RetryTest.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/RetryTest.cs
@@ -15,13 +15,16 @@ public class RetryTest
public RetryTest(ITestOutputHelper output) => _output = output;
///
- /// End-to-end retry: step throws on attempts 1 and 2, succeeds on attempt 3.
- /// Validates that the service honors the RETRY checkpoint, schedules the
- /// requested delay, and re-invokes the Lambda — none of which the unit
- /// tests can prove (they fake state transitions in-memory).
+ /// Cloud-only half of the retry scenario: validates what only the real
+ /// service can prove — that it honors the RETRY checkpoint, records a failed
+ /// event per attempt with the per-attempt message, schedules the requested
+ /// delay, and re-invokes the Lambda. The behavioral half (final result and
+ /// the step's attempt count) is covered portably on both backends by
+ /// PortableScenarios.RetryAsync and is intentionally not re-asserted
+ /// here.
///
[Fact]
- public async Task FlakyStep_RetriesAndSucceedsOnThirdAttempt()
+ public async Task FlakyStep_ServiceHonorsRetryEventsAndTiming()
{
await using var deployment = await DurableFunctionDeployment.CreateAsync(
DurableFunctionDeployment.FindTestFunctionDir("RetryFunction"),
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/ApprovalFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/ApprovalFunction.csproj
new file mode 100644
index 000000000..a320bc21e
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/ApprovalFunction.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ Exe
+ true
+ bootstrap
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/Function.cs
new file mode 100644
index 000000000..f8d35e0ee
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ApprovalFunction/Function.cs
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
+using Amazon.Lambda.RuntimeSupport;
+using Amazon.Lambda.Serialization.SystemTextJson;
+
+namespace DurableExecutionTestFunction;
+
+///
+/// Deployed entry point for . The workflow creates
+/// a callback and suspends; the cloud test harness plays the role of the external
+/// system, resolving the callback via the runner's SendCallback* methods.
+///
+public class Function
+{
+ public static async Task Main(string[] args)
+ {
+ var handler = new Function();
+ var serializer = new DefaultLambdaJsonSerializer();
+ using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(handler.Handler, serializer);
+ using var bootstrap = new LambdaBootstrap(handlerWrapper);
+ await bootstrap.RunAsync();
+ }
+
+ public Task Handler(
+ DurableExecutionInvocationInput input, ILambdaContext context)
+ => DurableFunction.WrapAsync(
+ ApprovalWorkflow.RunAsync, input, context);
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/ChainedInvokeChildFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/ChainedInvokeChildFunction.csproj
new file mode 100644
index 000000000..a320bc21e
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/ChainedInvokeChildFunction.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ Exe
+ true
+ bootstrap
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/Function.cs
new file mode 100644
index 000000000..f79c8b683
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeChildFunction/Function.cs
@@ -0,0 +1,31 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
+using Amazon.Lambda.RuntimeSupport;
+using Amazon.Lambda.Serialization.SystemTextJson;
+
+namespace DurableExecutionTestFunction;
+
+///
+/// Deployed entry point for
+/// (the callee). Invoked durably by the parent function.
+///
+public class Function
+{
+ public static async Task Main(string[] args)
+ {
+ var handler = new Function();
+ var serializer = new DefaultLambdaJsonSerializer();
+ using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(handler.Handler, serializer);
+ using var bootstrap = new LambdaBootstrap(handlerWrapper);
+ await bootstrap.RunAsync();
+ }
+
+ public Task Handler(
+ DurableExecutionInvocationInput input, ILambdaContext context)
+ => DurableFunction.WrapAsync(
+ ChainedInvokeWorkflow.DownstreamAsync, input, context);
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/ChainedInvokeParentFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/ChainedInvokeParentFunction.csproj
new file mode 100644
index 000000000..a320bc21e
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/ChainedInvokeParentFunction.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ Exe
+ true
+ bootstrap
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/Function.cs
new file mode 100644
index 000000000..5ea6b6545
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChainedInvokeParentFunction/Function.cs
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
+using Amazon.Lambda.RuntimeSupport;
+using Amazon.Lambda.Serialization.SystemTextJson;
+
+namespace DurableExecutionTestFunction;
+
+///
+/// Deployed entry point for (the
+/// caller). It durably invokes the downstream function whose ARN is supplied via
+/// the DOWNSTREAM_FUNCTION_ARN environment variable.
+///
+public class Function
+{
+ public static async Task Main(string[] args)
+ {
+ var handler = new Function();
+ var serializer = new DefaultLambdaJsonSerializer();
+ using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(handler.Handler, serializer);
+ using var bootstrap = new LambdaBootstrap(handlerWrapper);
+ await bootstrap.RunAsync();
+ }
+
+ public Task Handler(
+ DurableExecutionInvocationInput input, ILambdaContext context)
+ => DurableFunction.WrapAsync(
+ ChainedInvokeWorkflow.RunAsync, input, context);
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/ChildContextFailsFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/ChildContextFailsFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/ChildContextFailsFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/ChildContextFailsFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/Function.cs
index d62207f74..d7917d9a6 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFailsFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the inner ops re-parent under the CONTEXT op and the failure detail.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,28 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Throw inside a child context to validate the CONTEXT FAIL path: the
- // service must record a ContextFailed event with the error details and
- // mark the workflow FAILED.
- await context.RunInChildContextAsync(
- async (childCtx, _) =>
- {
- await childCtx.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"prepared-{input.OrderId}"; },
- name: "prepare");
-
- throw new InvalidOperationException("intentional child context failure for integration test");
- },
- name: "phase",
- config: new ChildContextConfig { SubType = "OrderProcessing" });
-
- return new TestResult { Status = "should_not_reach" };
- }
+ => DurableFunction.WrapAsync(
+ ChildContextWorkflows.FailsAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/ChildContextFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/ChildContextFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/ChildContextFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/ChildContextFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/Function.cs
index e62cca8c0..c435e66b0 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the inner ops re-parent under the CONTEXT op and the failure detail.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,34 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Run a child context that itself does step + wait + step. The child's
- // return value is checkpointed at the parent level as a CONTEXT
- // SUCCEED record, so on replay we'd see it returned from cache.
- var phaseResult = await context.RunInChildContextAsync(
- async (childCtx, _) =>
- {
- var validated = await childCtx.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"validated-{input.OrderId}"; },
- name: "validate");
-
- await childCtx.WaitAsync(TimeSpan.FromSeconds(2), name: "short_wait");
-
- var processed = await childCtx.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"processed-{validated}"; },
- name: "process");
-
- return processed;
- },
- name: "phase",
- config: new ChildContextConfig { SubType = "OrderProcessing" });
-
- return new TestResult { Status = "completed", Data = phaseResult };
- }
+ => DurableFunction.WrapAsync(
+ ChildContextWorkflows.HappyAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/ChildContextRetryFailsFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/ChildContextRetryFailsFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/ChildContextRetryFailsFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/ChildContextRetryFailsFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/Function.cs
index 7c7dd4974..d276d2142 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ChildContextRetryFailsFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the inner ops re-parent under the CONTEXT op and the failure detail.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,41 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // A retry-then-exhaust step inside a child context: every retry
- // checkpoint should be parented under the child, and the child should
- // close as ContextFailed when retries are exhausted — proving the
- // child is a single retry/error boundary.
- await context.RunInChildContextAsync(
- async (childCtx, _) =>
- {
- return await childCtx.StepAsync(
- async (ctx, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException(
- $"always-fails on attempt {ctx.AttemptNumber} for {input.OrderId}");
- },
- name: "always_fails",
- config: new StepConfig
- {
- RetryStrategy = RetryStrategy.Exponential(
- maxAttempts: 3,
- initialDelay: TimeSpan.FromSeconds(2),
- maxDelay: TimeSpan.FromSeconds(10),
- backoffRate: 2.0,
- jitter: JitterStrategy.None)
- });
- },
- name: "phase",
- config: new ChildContextConfig { SubType = "OrderProcessing" });
-
- return new TestResult { Status = "should_not_reach" };
- }
+ => DurableFunction.WrapAsync(
+ ChildContextWorkflows.RetryFailsAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/FulfillmentFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/FulfillmentFunction.csproj
new file mode 100644
index 000000000..a320bc21e
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/FulfillmentFunction.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ Exe
+ true
+ bootstrap
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/Function.cs
new file mode 100644
index 000000000..ea0294f2a
--- /dev/null
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/FulfillmentFunction/Function.cs
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
+using Amazon.Lambda.RuntimeSupport;
+using Amazon.Lambda.Serialization.SystemTextJson;
+
+namespace DurableExecutionTestFunction;
+
+///
+/// Deployed entry point for . The workflow body
+/// is shared verbatim with the local test backend so the portable cloud scenario
+/// asserts identically against either runner.
+///
+public class Function
+{
+ public static async Task Main(string[] args)
+ {
+ var handler = new Function();
+ var serializer = new DefaultLambdaJsonSerializer();
+ using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(handler.Handler, serializer);
+ using var bootstrap = new LambdaBootstrap(handlerWrapper);
+ await bootstrap.RunAsync();
+ }
+
+ public Task Handler(
+ DurableExecutionInvocationInput input, ILambdaContext context)
+ => DurableFunction.WrapAsync(
+ FulfillmentWorkflow.RunAsync, input, context);
+}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/Function.cs
index 291afbf2a..dac0ecb6d 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/Function.cs
@@ -3,11 +3,16 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed downstream entry point for .
+/// Always fails so the parent's chained invoke surfaces an InvokeException.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,22 +26,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(int input, IDurableContext context)
- {
- // Throw inside a step so the workflow records a step-failed event AND
- // surfaces a FAILED execution status. The parent's InvokeAsync sees a
- // FAILED chained invocation and raises InvokeFailedException with the
- // step's error type (System.InvalidOperationException) attached.
- await context.StepAsync(
- async (_, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("intentional child failure");
- },
- name: "fail_step");
-
- return "unreachable";
- }
+ => DurableFunction.WrapAsync(
+ InvokeFailureWorkflow.DownstreamAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/InvokeFailureChildFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/InvokeFailureChildFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/InvokeFailureChildFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureChildFunction/InvokeFailureChildFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/Function.cs
index 40bfa3079..05bb7e0d6 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed parent entry point for .
+/// Workflow body shared verbatim with the local backend; the cloud-only test
+/// additionally verifies the ChainedInvokeFailed event detail.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,36 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var downstreamArn = System.Environment.GetEnvironmentVariable("DOWNSTREAM_FUNCTION_ARN")
- ?? throw new InvalidOperationException("DOWNSTREAM_FUNCTION_ARN env var is not set.");
-
- try
- {
- await context.InvokeAsync(
- downstreamArn,
- payload: 1,
- name: "call_failing_child");
-
- // Should not reach — the child throws and the parent surfaces
- // InvokeFailedException on the resume.
- return new TestResult { Status = "unexpected_success", Data = null };
- }
- catch (InvokeFailedException ex)
- {
- // The parent catches and converts the exception into a normal result —
- // the workflow itself succeeds, even though the chained invoke failed.
- return new TestResult
- {
- Status = "completed",
- Data = $"parent-saw-{ex.ErrorType ?? "unknown"}"
- };
- }
- }
+ => DurableFunction.WrapAsync(
+ InvokeFailureWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/InvokeFailureParentFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/InvokeFailureParentFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/InvokeFailureParentFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/InvokeFailureParentFunction/InvokeFailureParentFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/Function.cs
index 7d241a02f..8b1347734 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only LongerWaitTest
+/// additionally verifies the real 15s wait duration.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,23 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var step1 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"started-{input.OrderId}"; },
- name: "before_wait");
-
- await context.WaitAsync(TimeSpan.FromSeconds(15), name: "long_wait");
-
- var step2 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"after_wait-{step1}"; },
- name: "after_wait");
-
- return new TestResult { Status = "completed", Data = step2 };
- }
+ => DurableFunction.WrapAsync(
+ LongerWaitWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/LongerWaitFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/LongerWaitFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/LongerWaitFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/LongerWaitFunction/LongerWaitFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/Function.cs
index db0f1ca4b..fa4b71adc 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,38 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Five items, two throw. ToleratedFailureCount = 1 means a second failure
- // exceeds tolerance and the map surfaces a MapException — terminating the
- // workflow FAILED.
- var items = new[] { "ok1", "bad1", "ok2", "bad2", "ok3" };
-
- var batch = await context.MapAsync(
- items,
- async (ctx, item, index, all, _) =>
- {
- await Task.CompletedTask;
- if (item.StartsWith("bad"))
- throw new InvalidOperationException($"{item} boom");
- return item;
- },
- name: "tolerance",
- config: new MapConfig
- {
- CompletionConfig = new CompletionConfig { ToleratedFailureCount = 1 }
- });
-
- // Should not reach here — the map must throw MapException.
- return new TestResult { Status = "should_not_reach", SuccessCount = batch.SuccessCount };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
+ => DurableFunction.WrapAsync(
+ MapWorkflows.FailureToleranceAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/MapFailureToleranceFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/MapFailureToleranceFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/MapFailureToleranceFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFailureToleranceFunction/MapFailureToleranceFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/Function.cs
index bfa60503c..2818dd232 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,46 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Four items, each waits a different (durable) duration. The shortest
- // wait should win and short-circuit the map via FirstSuccessful. Wait
- // durations are at least 1s (service timer granularity). The item value
- // IS the wait-seconds; the result is the item's index.
- var waitSeconds = new[] { 8, 1, 5, 6 };
-
- var batch = await context.MapAsync(
- waitSeconds,
- async (ctx, seconds, index, all, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(seconds), name: $"wait_{index}");
- return index;
- },
- name: "race",
- config: new MapConfig { CompletionConfig = CompletionConfig.FirstSuccessful() });
-
- var winner = batch.Succeeded.FirstOrDefault();
- return new TestResult
- {
- Status = "completed",
- WinnerIndex = winner?.Index ?? -1,
- WinnerName = winner?.Name,
- CompletionReason = batch.CompletionReason.ToString(),
- SuccessCount = batch.SuccessCount,
- StartedCount = batch.StartedCount
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int WinnerIndex { get; set; }
- public string? WinnerName { get; set; }
- public string? CompletionReason { get; set; }
- public int SuccessCount { get; set; }
- public int StartedCount { get; set; }
+ => DurableFunction.WrapAsync(
+ MapWorkflows.FirstSuccessfulAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/MapFirstSuccessfulFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/MapFirstSuccessfulFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/MapFirstSuccessfulFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapFirstSuccessfulFunction/MapFirstSuccessfulFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/Function.cs
index 52a3bb7dc..d18a46bdd 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,28 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var orders = new[] { "order-1", "order-2", "order-3" };
-
- // Each item is processed inside a step so the per-item child context
- // owns a leaf operation. ItemNamer gives each item a readable branch
- // name in the service-side history.
- var batch = await context.MapAsync(
- orders,
- async (ctx, orderId, index, all, _) =>
- await ctx.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"{orderId}-{input.OrderId}"; },
- name: "process"),
- name: "process_all",
- config: new MapConfig { ItemNamer = (item, index) => $"item-{item}" });
-
- var joined = string.Join(",", batch.GetResults());
- return new TestResult { Status = "completed", Data = joined };
- }
+ => DurableFunction.WrapAsync(
+ MapWorkflows.HappyAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/MapHappyPathFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/MapHappyPathFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/MapHappyPathFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapHappyPathFunction/MapHappyPathFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/Function.cs
index 0b0866ecc..8d16bb53c 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,44 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // 6 items, MaxConcurrency = 2. Each item does a 2-second durable wait
- // then captures the post-wait wall-clock as a unix-ms timestamp. The
- // expected outcome is 3 waves of 2 items; total elapsed ~6s. Use
- // IDurableContext.WaitAsync (not Task.Delay) — Task.Delay is NOT durable
- // and would skew this measurement under replay.
- var items = new[] { 0, 1, 2, 3, 4, 5 };
-
- var batch = await context.MapAsync(
- items,
- async (ctx, item, index, all, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(2), name: $"wait_{index}");
- return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
- },
- name: "throttled",
- config: new MapConfig
- {
- MaxConcurrency = 2,
- CompletionConfig = CompletionConfig.AllCompleted()
- });
-
- return new TestResult
- {
- Status = "completed",
- SuccessCount = batch.SuccessCount,
- Timestamps = batch.GetResults().ToArray()
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
- public long[]? Timestamps { get; set; }
+ => DurableFunction.WrapAsync(
+ MapWorkflows.MaxConcurrencyAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/MapMaxConcurrencyFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/MapMaxConcurrencyFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/MapMaxConcurrencyFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapMaxConcurrencyFunction/MapMaxConcurrencyFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/Function.cs
index 5b193b96c..1e68ffe08 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,46 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Three items, the middle one throws. Map's DEFAULT CompletionConfig is
- // AllCompleted() (permissive) — unlike Parallel's AllSuccessful() — so NO
- // config is supplied here and the map must still drive every item to a
- // terminal state without throwing. This is the key Map-vs-Parallel
- // behavioral difference, validated end-to-end.
- var items = new[] { "ok1", "boom", "ok2" };
-
- var batch = await context.MapAsync(
- items,
- async (ctx, item, index, all, _) =>
- {
- await Task.CompletedTask;
- if (item == "boom")
- throw new InvalidOperationException("intentional partial failure");
- return item;
- },
- name: "partial");
-
- var errors = batch.GetErrors();
- var errorSummary = string.Join("|", errors.Select(e => $"{e.GetType().Name}:{e.Message}"));
-
- return new TestResult
- {
- Status = "completed",
- SuccessCount = batch.SuccessCount,
- FailureCount = batch.FailureCount,
- ErrorSummary = errorSummary
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
- public int FailureCount { get; set; }
- public string? ErrorSummary { get; set; }
+ => DurableFunction.WrapAsync(
+ MapWorkflows.PartialFailureAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/MapPartialFailureFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/MapPartialFailureFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/MapPartialFailureFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MapPartialFailureFunction/MapPartialFailureFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/Function.cs
index 986126a3f..b1cbe96fa 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only MultipleStepsTest
+/// additionally verifies the exact StepStarted event count.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,33 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var step1 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"a-{input.OrderId}"; },
- name: "step_1");
-
- var step2 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"{step1}-b"; },
- name: "step_2");
-
- var step3 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"{step2}-c"; },
- name: "step_3");
-
- var step4 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"{step3}-d"; },
- name: "step_4");
-
- var step5 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"{step4}-e"; },
- name: "step_5");
-
- return new TestResult { Status = "completed", Data = step5 };
- }
+ => DurableFunction.WrapAsync(
+ MultipleStepsWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/MultipleStepsFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/MultipleStepsFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/MultipleStepsFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/MultipleStepsFunction/MultipleStepsFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/Function.cs
index 80bb39133..72a0b06e6 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,43 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Five branches, two throw. ToleratedFailureCount = 1 means a second
- // failure exceeds tolerance and the parallel surfaces a ParallelException.
- var batch = await context.ParallelAsync(
- new[]
- {
- new DurableBranch("ok1", async (_, _) => { await Task.CompletedTask; return "1"; }),
- new DurableBranch("bad1", async (_, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("bad1 boom");
- }),
- new DurableBranch("ok2", async (_, _) => { await Task.CompletedTask; return "2"; }),
- new DurableBranch("bad2", async (_, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("bad2 boom");
- }),
- new DurableBranch("ok3", async (_, _) => { await Task.CompletedTask; return "3"; }),
- },
- name: "tolerance",
- config: new ParallelConfig
- {
- CompletionConfig = new CompletionConfig { ToleratedFailureCount = 1 }
- });
-
- // Should not reach here — the parallel must throw ParallelException.
- return new TestResult { Status = "should_not_reach", SuccessCount = batch.SuccessCount };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
+ => DurableFunction.WrapAsync(
+ ParallelWorkflows.FailureToleranceAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/ParallelFailureToleranceFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/ParallelFailureToleranceFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/ParallelFailureToleranceFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFailureToleranceFunction/ParallelFailureToleranceFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/Function.cs
index 2a6e6161c..92d6558eb 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,62 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Four branches with different durable wait durations. The shortest
- // wait should win and short-circuit the parallel via FirstSuccessful.
- // Wait durations are at least 1s (service timer granularity).
- var batch = await context.ParallelAsync(
- new[]
- {
- new DurableBranch("slowest", async (ctx, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(8), name: "wait_3");
- return 3;
- }),
- new DurableBranch("fastest", async (ctx, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(1), name: "wait_0");
- return 0;
- }),
- new DurableBranch("mid1", async (ctx, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(5), name: "wait_1");
- return 1;
- }),
- new DurableBranch("mid2", async (ctx, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(6), name: "wait_2");
- return 2;
- }),
- },
- name: "race",
- config: new ParallelConfig { CompletionConfig = CompletionConfig.FirstSuccessful() });
-
- // The winner is whichever branch came back first. Surface the index +
- // its name so the test can assert one branch won.
- var winner = batch.Succeeded.FirstOrDefault();
- return new TestResult
- {
- Status = "completed",
- WinnerIndex = winner?.Index ?? -1,
- WinnerName = winner?.Name,
- CompletionReason = batch.CompletionReason.ToString(),
- SuccessCount = batch.SuccessCount,
- StartedCount = batch.StartedCount
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int WinnerIndex { get; set; }
- public string? WinnerName { get; set; }
- public string? CompletionReason { get; set; }
- public int SuccessCount { get; set; }
- public int StartedCount { get; set; }
+ => DurableFunction.WrapAsync(
+ ParallelWorkflows.FirstSuccessfulAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/ParallelFirstSuccessfulFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/ParallelFirstSuccessfulFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/ParallelFirstSuccessfulFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelFirstSuccessfulFunction/ParallelFirstSuccessfulFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/Function.cs
index dbcc7d2f9..657934cf4 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,23 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var batch = await context.ParallelAsync(
- new[]
- {
- new DurableBranch("alpha", async (_, _) => { await Task.CompletedTask; return $"alpha-{input.OrderId}"; }),
- new DurableBranch("beta", async (_, _) => { await Task.CompletedTask; return $"beta-{input.OrderId}"; }),
- new DurableBranch("gamma", async (_, _) => { await Task.CompletedTask; return $"gamma-{input.OrderId}"; }),
- },
- name: "fanout");
-
- var joined = string.Join(",", batch.GetResults());
- return new TestResult { Status = "completed", Data = joined };
- }
+ => DurableFunction.WrapAsync(
+ ParallelWorkflows.HappyAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/ParallelHappyPathFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/ParallelHappyPathFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/ParallelHappyPathFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelHappyPathFunction/ParallelHappyPathFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/Function.cs
index e36848ef3..c2a5d9379 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,50 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // 6 branches, MaxConcurrency = 2. Each branch does a 2-second durable
- // wait then captures the post-wait wall-clock as a unix-ms timestamp.
- // The expected outcome is 3 waves of 2 branches; total elapsed ~6s.
- // Use IDurableContext.WaitAsync (not Task.Delay) — Task.Delay is NOT
- // durable and would skew this measurement under replay.
- var branches = new DurableBranch[6];
- for (var i = 0; i < 6; i++)
- {
- var localIndex = i;
- branches[i] = new DurableBranch(
- $"b{localIndex}",
- async (ctx, _) =>
- {
- await ctx.WaitAsync(TimeSpan.FromSeconds(2), name: $"wait_{localIndex}");
- return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
- });
- }
-
- var batch = await context.ParallelAsync(
- branches,
- name: "throttled",
- config: new ParallelConfig
- {
- MaxConcurrency = 2,
- CompletionConfig = CompletionConfig.AllCompleted()
- });
-
- return new TestResult
- {
- Status = "completed",
- SuccessCount = batch.SuccessCount,
- Timestamps = batch.GetResults().ToArray()
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
- public long[]? Timestamps { get; set; }
+ => DurableFunction.WrapAsync(
+ ParallelWorkflows.MaxConcurrencyAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/ParallelMaxConcurrencyFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/ParallelMaxConcurrencyFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/ParallelMaxConcurrencyFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelMaxConcurrencyFunction/ParallelMaxConcurrencyFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/Function.cs
index fde9fde32..298f4f9ee 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the event-shape / timing concerns the runner cannot express.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,44 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var batch = await context.ParallelAsync(
- new[]
- {
- new DurableBranch("ok1", async (_, _) => { await Task.CompletedTask; return "first"; }),
- new DurableBranch("boom", async (_, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("intentional partial failure");
- }),
- new DurableBranch("ok2", async (_, _) => { await Task.CompletedTask; return "third"; }),
- },
- name: "partial",
- // AllCompleted: drive every branch to terminal state regardless of failure.
- // Without this, the default AllSuccessful() would throw on the first failure.
- config: new ParallelConfig { CompletionConfig = CompletionConfig.AllCompleted() });
-
- var errors = batch.GetErrors();
- var errorSummary = string.Join("|", errors.Select(e => $"{e.GetType().Name}:{e.Message}"));
-
- return new TestResult
- {
- Status = "completed",
- SuccessCount = batch.SuccessCount,
- FailureCount = batch.FailureCount,
- ErrorSummary = errorSummary
- };
- }
-}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int SuccessCount { get; set; }
- public int FailureCount { get; set; }
- public string? ErrorSummary { get; set; }
+ => DurableFunction.WrapAsync(
+ ParallelWorkflows.PartialFailureAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/ParallelPartialFailureFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/ParallelPartialFailureFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/ParallelPartialFailureFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ParallelPartialFailureFunction/ParallelPartialFailureFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/Function.cs
index 97602186e..3e0af930f 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only RetryExhaustionTest
+/// additionally verifies the per-attempt StepFailed events and timing.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,30 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var result = await context.StepAsync(
- async (ctx, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException($"always-fails attempt {ctx.AttemptNumber}");
- },
- name: "always_fails_step",
- config: new StepConfig
- {
- RetryStrategy = RetryStrategy.Exponential(
- maxAttempts: 3,
- initialDelay: TimeSpan.FromSeconds(2),
- maxDelay: TimeSpan.FromSeconds(10),
- backoffRate: 2.0,
- jitter: JitterStrategy.None)
- });
-
- return new TestResult { Status = "completed", Data = result };
- }
+ => DurableFunction.WrapAsync(
+ RetryExhaustionWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/RetryExhaustionFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/RetryExhaustionFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/RetryExhaustionFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryExhaustionFunction/RetryExhaustionFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/Function.cs
index 5f81ca7dd..377a2081d 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/Function.cs
@@ -3,11 +3,19 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . The workflow body is
+/// shared verbatim with the local backend (see PortableScenariosLocalTests) so
+/// the behavioral half of the retry scenario asserts identically on both, while
+/// this deployed function additionally lets the cloud-only test observe the real
+/// per-attempt retry events and timing.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,32 +29,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var result = await context.StepAsync(
- async (ctx, _) =>
- {
- await Task.CompletedTask;
- if (ctx.AttemptNumber < 3)
- throw new InvalidOperationException($"flake on attempt {ctx.AttemptNumber}");
- return $"ok on attempt {ctx.AttemptNumber}";
- },
- name: "flaky_step",
- config: new StepConfig
- {
- RetryStrategy = RetryStrategy.Exponential(
- maxAttempts: 3,
- initialDelay: TimeSpan.FromSeconds(2),
- maxDelay: TimeSpan.FromSeconds(10),
- backoffRate: 2.0,
- jitter: JitterStrategy.None)
- });
-
- return new TestResult { Status = "completed", Data = result };
- }
+ => DurableFunction.WrapAsync(
+ RetryWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/RetryFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/RetryFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/RetryFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/RetryFunction/RetryFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/Function.cs
index 293b83424..1c05efcfb 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body shared
+/// verbatim with the local backend; the cloud-only StepFailsTest additionally
+/// verifies the StepFailed history event detail.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,21 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- await context.StepAsync(
- async (_, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("intentional failure for integration test");
- },
- name: "fail_step");
-
- return new TestResult { Status = "should_not_reach" };
- }
+ => DurableFunction.WrapAsync(
+ StepFailsWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/StepFailsFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/StepFailsFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/StepFailsFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepFailsFunction/StepFailsFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/Function.cs
index 7de143800..db9e8c1e6 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow body
+/// shared verbatim with the local backend; the cloud-only StepWaitStepTest
+/// additionally verifies the real wait duration and suspend/resume cycle.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,23 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- var step1 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"validated-{input.OrderId}"; },
- name: "validate");
-
- await context.WaitAsync(TimeSpan.FromSeconds(3), name: "short_wait");
-
- var step2 = await context.StepAsync(
- async (_, _) => { await Task.CompletedTask; return $"processed-{step1}"; },
- name: "process");
-
- return new TestResult { Status = "completed", Data = step2 };
- }
+ => DurableFunction.WrapAsync(
+ StepWaitStepWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult { public string? Status { get; set; } public string? Data { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/StepWaitStepFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/StepWaitStepFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/StepWaitStepFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/StepWaitStepFunction/StepWaitStepFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/Function.cs
index b9851d5ea..a4e3c311c 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/Function.cs
@@ -3,11 +3,17 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for .
+/// The submitter throws with no retries, so the workflow fails with
+/// CallbackSubmitterException. Workflow body shared verbatim with the local backend.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -21,26 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // The submitter throws on every attempt. With RetryStrategy.None the
- // SDK should fail terminally on the first attempt and surface the
- // failure as CallbackSubmitterException. The workflow does not catch
- // it, so the durable execution surfaces FAILED with that exception.
- var result = await context.WaitForCallbackAsync(
- submitter: async (callbackId, cbCtx, _) =>
- {
- await Task.CompletedTask;
- throw new InvalidOperationException("submitter intentional failure");
- },
- name: "approve",
- config: new WaitForCallbackConfig { RetryStrategy = RetryStrategy.None });
-
- return result;
- }
+ => DurableFunction.WrapAsync(
+ CallbackSubmitterFailsWorkflow.RunAsync, input, context);
}
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class MyResult { public string? Status { get; set; } public string? ApprovedBy { get; set; } }
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/WaitForCallbackSubmitterFailsFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/WaitForCallbackSubmitterFailsFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/WaitForCallbackSubmitterFailsFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForCallbackSubmitterFailsFunction/WaitForCallbackSubmitterFailsFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/Function.cs
index f3aad3f52..d81f6dc22 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for .
+/// Workflow body shared verbatim with the local backend; the cloud-only test
+/// additionally verifies the real exponential inter-poll delays.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,49 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Exponential strategy with no jitter so the timing is predictable.
- // Done flips on attempt 3 (1-based). With initialDelay=1s,
- // backoffRate=1.5, maxDelay=4s, no jitter: delays are 1s, 1.5s
- // (which the SDK ceilings to 2s due to 1s timer granularity).
- var finalState = await context.WaitForConditionAsync(
- check: async (state, ctx, _) =>
- {
- await Task.CompletedTask;
- var done = ctx.AttemptNumber >= 3;
- return new State(done, ctx.AttemptNumber);
- },
- config: new WaitForConditionConfig
- {
- InitialState = new State(false, 0),
- WaitStrategy = WaitStrategy.Exponential(
- maxAttempts: 5,
- initialDelay: TimeSpan.FromSeconds(1),
- maxDelay: TimeSpan.FromSeconds(4),
- backoffRate: 1.5,
- jitter: JitterStrategy.None,
- isDone: s => s.Done)
- },
- name: "exp_poll");
-
- return new TestResult
- {
- Status = "completed",
- AttemptsTaken = finalState.AttemptNumber,
- Done = finalState.Done
- };
- }
-}
-
-public record State(bool Done, int AttemptNumber);
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int AttemptsTaken { get; set; }
- public bool Done { get; set; }
+ => DurableFunction.WrapAsync(
+ WaitForConditionExponentialWorkflow.RunAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/WaitForConditionExponentialFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/WaitForConditionExponentialFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/WaitForConditionExponentialFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionExponentialFunction/WaitForConditionExponentialFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/Function.cs
index 00d68b4c3..2661a2b5e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for . Workflow
+/// body shared verbatim with the local backend; the cloud-only test additionally
+/// verifies the real inter-poll delays.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,44 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Counter increments every poll. isDone fires once it hits 3.
- // Each poll iteration is a separate Lambda invocation; the state is
- // carried across iterations via the RETRY checkpoint payload.
- var finalState = await context.WaitForConditionAsync(
- check: async (state, ctx, _) =>
- {
- await Task.CompletedTask;
- return new State(state.Counter + 1, ctx.AttemptNumber);
- },
- config: new WaitForConditionConfig
- {
- InitialState = new State(0, 0),
- WaitStrategy = WaitStrategy.Fixed(
- delay: TimeSpan.FromSeconds(2),
- maxAttempts: 10,
- isDone: s => s.Counter >= 3)
- },
- name: "happy_poll");
-
- return new TestResult
- {
- Status = "completed",
- Counter = finalState.Counter,
- AttemptsTaken = finalState.AttemptNumber
- };
- }
-}
-
-public record State(int Counter, int AttemptNumber);
-
-public class TestEvent { public string? OrderId { get; set; } }
-public class TestResult
-{
- public string? Status { get; set; }
- public int Counter { get; set; }
- public int AttemptsTaken { get; set; }
+ => DurableFunction.WrapAsync(
+ WaitForConditionHappyWorkflow.RunAsync, input, context);
}
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/WaitForConditionHappyPathFunction.csproj b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/WaitForConditionHappyPathFunction.csproj
index f8bf7fd0c..a320bc21e 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/WaitForConditionHappyPathFunction.csproj
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionHappyPathFunction/WaitForConditionHappyPathFunction.csproj
@@ -15,4 +15,9 @@
+
+
+
+
+
diff --git a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionMaxAttemptsFunction/Function.cs b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionMaxAttemptsFunction/Function.cs
index 8bdda540a..868551453 100644
--- a/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionMaxAttemptsFunction/Function.cs
+++ b/Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/WaitForConditionMaxAttemptsFunction/Function.cs
@@ -1,10 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
+using Amazon.Lambda.DurableExecution.Testing.Shared;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
namespace DurableExecutionTestFunction;
+///
+/// Deployed entry point for .
+/// Workflow body shared verbatim with the local backend; the cloud-only test
+/// additionally verifies the FAILED checkpoint records the WaitForConditionException.
+///
public class Function
{
public static async Task Main(string[] args)
@@ -18,45 +27,6 @@ public static async Task Main(string[] args)
public Task Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
- => DurableFunction.WrapAsync(Workflow, input, context);
-
- private async Task Workflow(TestEvent input, IDurableContext context)
- {
- // Condition is never satisfied (isDone is always false), so the
- // strategy will eventually exhaust maxAttempts and the operation will
- // throw WaitForConditionException. The workflow catches it and
- // surfaces AttemptsExhausted in the result so the test can assert on
- // it without inspecting the FAILED status.
- try
- {
- await context.WaitForConditionAsync