chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
/// <summary>
/// Verifies that <see cref="DurableActivityExecutor.ResolveInputType(string?, ISet{Type})"/>
/// matches persisted assembly-qualified type-name strings against the executor's supported
/// input types even when the persisted name carries a different assembly version, culture,
/// or public key token than the loaded assemblies.
/// </summary>
public sealed class DurableActivityExecutorResolveInputTypeTests
{
[Fact]
public void ResolveInputType_NullInput_ReturnsFirstSupportedType()
{
Type result = DurableActivityExecutor.ResolveInputType(null, new HashSet<Type> { typeof(int) });
Assert.Equal(typeof(int), result);
}
[Fact]
public void ResolveInputType_EmptyInputAndNoSupportedTypes_FallsBackToString()
{
Type result = DurableActivityExecutor.ResolveInputType(string.Empty, new HashSet<Type>());
Assert.Equal(typeof(string), result);
}
[Fact]
public void ResolveInputType_LoadedAssemblyQualifiedName_ReturnsSupportedType()
{
Type supported = typeof(ChatMessage);
ISet<Type> supportedTypes = new HashSet<Type> { supported };
Type result = DurableActivityExecutor.ResolveInputType(supported.AssemblyQualifiedName, supportedTypes);
Assert.Same(supported, result);
}
[Fact]
public void ResolveInputType_MutatedAssemblyVersion_ReturnsSupportedType()
{
Type supported = typeof(ChatMessage);
string simpleAssemblyName = supported.Assembly.GetName().Name!;
string mutated = $"{supported.FullName}, {simpleAssemblyName}, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null";
ISet<Type> supportedTypes = new HashSet<Type> { supported };
Type result = DurableActivityExecutor.ResolveInputType(mutated, supportedTypes);
Assert.Same(supported, result);
}
[Fact]
public void ResolveInputType_MutatedGenericArgumentVersion_ReturnsSupportedType()
{
Type supported = typeof(List<ChatMessage>);
string outerSimple = supported.Assembly.GetName().Name!;
string innerSimple = typeof(ChatMessage).Assembly.GetName().Name!;
string mutated =
$"System.Collections.Generic.List`1[[Microsoft.Extensions.AI.ChatMessage, {innerSimple}, " +
"Version=99.0.0.0, Culture=neutral, PublicKeyToken=null]], " +
$"{outerSimple}, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null";
ISet<Type> supportedTypes = new HashSet<Type> { supported };
Type result = DurableActivityExecutor.ResolveInputType(mutated, supportedTypes);
Assert.Same(supported, result);
}
[Fact]
public void ResolveInputType_ShortNameMatch_ReturnsSupportedType()
{
Type supported = typeof(ChatMessage);
ISet<Type> supportedTypes = new HashSet<Type> { supported };
Type result = DurableActivityExecutor.ResolveInputType(supported.Name, supportedTypes);
Assert.Same(supported, result);
}
[Fact]
public void ResolveInputType_FullNameMatch_ReturnsSupportedType()
{
Type supported = typeof(ChatMessage);
ISet<Type> supportedTypes = new HashSet<Type> { supported };
Type result = DurableActivityExecutor.ResolveInputType(supported.FullName, supportedTypes);
Assert.Same(supported, result);
}
[Fact]
public void ResolveInputType_StringFallback_ReturnsFirstSupportedTypeWhenStringUnsupported()
{
ISet<Type> supportedTypes = new HashSet<Type> { typeof(int) };
Type result = DurableActivityExecutor.ResolveInputType(typeof(string).AssemblyQualifiedName, supportedTypes);
Assert.Equal(typeof(int), result);
}
}
@@ -0,0 +1,235 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.AI.DurableTask.Workflows;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
public sealed class DurableActivityExecutorTests
{
private static readonly JsonSerializerOptions s_camelCaseOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
#region DeserializeInput
[Fact]
public void DeserializeInput_StringType_ReturnsInputAsIs()
{
// Arrange
const string Input = "hello world";
// Act
object result = DurableActivityExecutor.DeserializeInput(Input, typeof(string));
// Assert
Assert.Equal("hello world", result);
}
[Fact]
public void DeserializeInput_SimpleObject_DeserializesCorrectly()
{
// Arrange
string input = JsonSerializer.Serialize(new TestRecord("EXP-001", 100.50m), s_camelCaseOptions);
// Act
object result = DurableActivityExecutor.DeserializeInput(input, typeof(TestRecord));
// Assert
TestRecord record = Assert.IsType<TestRecord>(result);
Assert.Equal("EXP-001", record.Id);
Assert.Equal(100.50m, record.Amount);
}
[Fact]
public void DeserializeInput_StringArray_DeserializesDirectly()
{
// Arrange
string input = JsonSerializer.Serialize((string[])["a", "b", "c"]);
// Act
object result = DurableActivityExecutor.DeserializeInput(input, typeof(string[]));
// Assert
string[] array = Assert.IsType<string[]>(result);
Assert.Equal(["a", "b", "c"], array);
}
[Fact]
public void DeserializeInput_TypedArrayFromFanIn_DeserializesEachElement()
{
// Arrange — fan-in produces a JSON array of serialized strings
TestRecord r1 = new("EXP-001", 100m);
TestRecord r2 = new("EXP-002", 200m);
string[] serializedElements =
[
JsonSerializer.Serialize(r1, s_camelCaseOptions),
JsonSerializer.Serialize(r2, s_camelCaseOptions)
];
string input = JsonSerializer.Serialize(serializedElements);
// Act
object result = DurableActivityExecutor.DeserializeInput(input, typeof(TestRecord[]));
// Assert
TestRecord[] records = Assert.IsType<TestRecord[]>(result);
Assert.Equal(2, records.Length);
Assert.Equal("EXP-001", records[0].Id);
Assert.Equal(100m, records[0].Amount);
Assert.Equal("EXP-002", records[1].Id);
Assert.Equal(200m, records[1].Amount);
}
[Fact]
public void DeserializeInput_TypedArrayWithSingleElement_DeserializesCorrectly()
{
// Arrange
TestRecord r1 = new("EXP-001", 50m);
string[] serializedElements = [JsonSerializer.Serialize(r1, s_camelCaseOptions)];
string input = JsonSerializer.Serialize(serializedElements);
// Act
object result = DurableActivityExecutor.DeserializeInput(input, typeof(TestRecord[]));
// Assert
TestRecord[] records = Assert.IsType<TestRecord[]>(result);
Assert.Single(records);
Assert.Equal("EXP-001", records[0].Id);
}
[Fact]
public void DeserializeInput_TypedArrayWithNullElement_ThrowsInvalidOperationException()
{
// Arrange — one element is "null"
string input = JsonSerializer.Serialize((string[])["null"]);
// Act & Assert
Assert.Throws<InvalidOperationException>(
() => DurableActivityExecutor.DeserializeInput(input, typeof(TestRecord[])));
}
[Fact]
public void DeserializeInput_InvalidJson_ThrowsJsonException()
{
// Arrange
const string Input = "not valid json";
// Act & Assert
Assert.ThrowsAny<JsonException>(
() => DurableActivityExecutor.DeserializeInput(Input, typeof(TestRecord)));
}
#endregion
#region ResolveInputType
[Fact]
public void ResolveInputType_NullTypeName_ReturnsFirstSupportedType()
{
// Arrange
HashSet<Type> supportedTypes = [typeof(TestRecord), typeof(string)];
// Act
Type result = DurableActivityExecutor.ResolveInputType(null, supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord), result);
}
[Fact]
public void ResolveInputType_EmptyTypeName_ReturnsFirstSupportedType()
{
// Arrange
HashSet<Type> supportedTypes = [typeof(TestRecord)];
// Act
Type result = DurableActivityExecutor.ResolveInputType(string.Empty, supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord), result);
}
[Fact]
public void ResolveInputType_EmptySupportedTypes_DefaultsToString()
{
// Arrange
HashSet<Type> supportedTypes = [];
// Act
Type result = DurableActivityExecutor.ResolveInputType(null, supportedTypes);
// Assert
Assert.Equal(typeof(string), result);
}
[Fact]
public void ResolveInputType_MatchesByFullName()
{
// Arrange
HashSet<Type> supportedTypes = [typeof(TestRecord)];
// Act
Type result = DurableActivityExecutor.ResolveInputType(typeof(TestRecord).FullName, supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord), result);
}
[Fact]
public void ResolveInputType_MatchesByName()
{
// Arrange
HashSet<Type> supportedTypes = [typeof(TestRecord)];
// Act
Type result = DurableActivityExecutor.ResolveInputType("TestRecord", supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord), result);
}
[Fact]
public void ResolveInputType_StringArrayFallsBackToSupportedType()
{
// Arrange — fan-in sends string[] but executor expects TestRecord[]
HashSet<Type> supportedTypes = [typeof(TestRecord[])];
// Act
Type result = DurableActivityExecutor.ResolveInputType(typeof(string[]).FullName, supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord[]), result);
}
[Fact]
public void ResolveInputType_StringFallsBackToSupportedType()
{
// Arrange — executor doesn't support string
HashSet<Type> supportedTypes = [typeof(TestRecord)];
// Act
Type result = DurableActivityExecutor.ResolveInputType(typeof(string).FullName, supportedTypes);
// Assert
Assert.Equal(typeof(TestRecord), result);
}
[Fact]
public void ResolveInputType_StringArrayRetainedWhenSupported()
{
// Arrange — executor explicitly supports string[]
HashSet<Type> supportedTypes = [typeof(string[])];
// Act
Type result = DurableActivityExecutor.ResolveInputType(typeof(string[]).FullName, supportedTypes);
// Assert
Assert.Equal(typeof(string[]), result);
}
#endregion
private sealed record TestRecord(string Id, decimal Amount);
}
@@ -0,0 +1,814 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Workflows;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Moq;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
public sealed class DurableStreamingWorkflowRunTests
{
private const string InstanceId = "test-instance-123";
private const string WorkflowTestName = "TestWorkflow";
private static Workflow CreateTestWorkflow() =>
new WorkflowBuilder(new FunctionExecutor<string>("start", (_, _, _) => default))
.WithName(WorkflowTestName)
.Build();
private static OrchestrationMetadata CreateMetadata(
OrchestrationRuntimeStatus status,
string? serializedCustomStatus = null,
string? serializedOutput = null,
TaskFailureDetails? failureDetails = null)
{
return new OrchestrationMetadata(WorkflowTestName, InstanceId)
{
RuntimeStatus = status,
SerializedCustomStatus = serializedCustomStatus,
SerializedOutput = serializedOutput,
FailureDetails = failureDetails,
};
}
private static string SerializeCustomStatus(List<string> events)
{
DurableWorkflowLiveStatus status = new() { Events = events };
return JsonSerializer.Serialize(status, DurableSerialization.Options);
}
private static string SerializeCustomStatusWithPendingEvents(
List<string> events,
List<PendingRequestPortStatus> pendingEvents)
{
DurableWorkflowLiveStatus status = new() { Events = events, PendingEvents = pendingEvents };
return JsonSerializer.Serialize(status, DurableSerialization.Options);
}
private static Workflow CreateTestWorkflowWithRequestPort(string requestPortId)
{
FunctionExecutor<string> start = new("start", (_, _, _) => default);
RequestPort<string, string> requestPort = RequestPort.Create<string, string>(requestPortId);
FunctionExecutor<string> end = new("end", (_, _, _) => default);
return new WorkflowBuilder(start)
.WithName(WorkflowTestName)
.AddEdge(start, requestPort)
.AddEdge(requestPort, end)
.Build();
}
private static string SerializeWorkflowResult(string? result, List<string> events)
{
DurableWorkflowResult workflowResult = new() { Result = result, Events = events };
return JsonSerializer.Serialize(workflowResult, DurableWorkflowJsonContext.Default.DurableWorkflowResult);
}
private static string SerializeEvent(WorkflowEvent evt)
{
Type eventType = evt.GetType();
TypedPayload wrapper = new()
{
TypeName = eventType.AssemblyQualifiedName,
Data = JsonSerializer.Serialize(evt, eventType, DurableSerialization.Options)
};
return JsonSerializer.Serialize(wrapper, DurableWorkflowJsonContext.Default.TypedPayload);
}
#region Constructor and Properties
[Fact]
public void Constructor_SetsRunIdAndWorkflowName()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
// Act
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Assert
Assert.Equal(InstanceId, run.RunId);
Assert.Equal(WorkflowTestName, run.WorkflowName);
}
[Fact]
public void Constructor_NoWorkflowName_SetsEmptyString()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
Workflow workflow = new WorkflowBuilder(new FunctionExecutor<string>("start", (_, _, _) => default)).Build();
// Act
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, workflow);
// Assert
Assert.Equal(string.Empty, run.WorkflowName);
}
#endregion
#region GetStatusAsync
[Theory]
[InlineData(OrchestrationRuntimeStatus.Pending, DurableRunStatus.Pending)]
[InlineData(OrchestrationRuntimeStatus.Running, DurableRunStatus.Running)]
[InlineData(OrchestrationRuntimeStatus.Completed, DurableRunStatus.Completed)]
[InlineData(OrchestrationRuntimeStatus.Failed, DurableRunStatus.Failed)]
[InlineData(OrchestrationRuntimeStatus.Terminated, DurableRunStatus.Terminated)]
[InlineData(OrchestrationRuntimeStatus.Suspended, DurableRunStatus.Suspended)]
public async Task GetStatusAsync_MapsRuntimeStatusCorrectlyAsync(
OrchestrationRuntimeStatus runtimeStatus,
DurableRunStatus expectedStatus)
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(runtimeStatus));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
DurableRunStatus status = await run.GetStatusAsync();
// Assert
Assert.Equal(expectedStatus, status);
}
[Fact]
public async Task GetStatusAsync_InstanceNotFound_ReturnsNotFoundAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, false, It.IsAny<CancellationToken>()))
.ReturnsAsync((OrchestrationMetadata?)null);
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
DurableRunStatus status = await run.GetStatusAsync();
// Assert
Assert.Equal(DurableRunStatus.NotFound, status);
}
#endregion
#region WatchStreamAsync
[Fact]
public async Task WatchStreamAsync_InstanceNotFound_YieldsNoEventsAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync((OrchestrationMetadata?)null);
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Empty(events);
}
[Fact]
public async Task WatchStreamAsync_CompletedWithResult_YieldsCompletedEventAsync()
{
// Arrange
string serializedOutput = SerializeWorkflowResult("done", []);
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Single(events);
DurableWorkflowCompletedEvent completedEvent = Assert.IsType<DurableWorkflowCompletedEvent>(events[0]);
Assert.Equal("done", completedEvent.Data);
}
[Fact]
public async Task WatchStreamAsync_CompletedWithEventsInOutput_YieldsEventsAndCompletionAsync()
{
// Arrange
DurableHaltRequestedEvent haltEvent = new("executor-1");
string serializedEvent = SerializeEvent(haltEvent);
string serializedOutput = SerializeWorkflowResult("result", [serializedEvent]);
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Equal(2, events.Count);
DurableHaltRequestedEvent haltResult = Assert.IsType<DurableHaltRequestedEvent>(events[0]);
Assert.Equal("executor-1", haltResult.ExecutorId);
DurableWorkflowCompletedEvent completedResult = Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
Assert.Equal("result", completedResult.Result);
}
[Fact]
public async Task WatchStreamAsync_CompletedWithoutWrapper_YieldsFailedEventAsync()
{
// Arrange — output not wrapped in DurableWorkflowResult (indicates a bug)
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: "\"raw output\""));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert — yields a failed event with diagnostic message instead of crashing
Assert.Single(events);
DurableWorkflowFailedEvent failedEvent = Assert.IsType<DurableWorkflowFailedEvent>(events[0]);
Assert.Contains("could not be parsed", failedEvent.ErrorMessage);
}
[Fact]
public async Task WatchStreamAsync_Failed_YieldsFailedEventAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
TaskFailureDetails failureDetails = new("ErrorType", "Something went wrong", null, null, null);
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(
OrchestrationRuntimeStatus.Failed,
failureDetails: failureDetails));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Single(events);
DurableWorkflowFailedEvent failedEvent = Assert.IsType<DurableWorkflowFailedEvent>(events[0]);
Assert.Equal("Something went wrong", failedEvent.ErrorMessage);
Assert.NotNull(failedEvent.FailureDetails);
Assert.Equal("ErrorType", failedEvent.FailureDetails.ErrorType);
Assert.Equal("Something went wrong", failedEvent.FailureDetails.ErrorMessage);
}
[Fact]
public async Task WatchStreamAsync_FailedWithNoDetails_YieldsDefaultMessageAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Failed));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Single(events);
DurableWorkflowFailedEvent failedEvent = Assert.IsType<DurableWorkflowFailedEvent>(events[0]);
Assert.Equal("Workflow execution failed.", failedEvent.ErrorMessage);
Assert.Null(failedEvent.FailureDetails);
}
[Fact]
public async Task WatchStreamAsync_Terminated_YieldsFailedEventAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Terminated));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Single(events);
DurableWorkflowFailedEvent failedEvent = Assert.IsType<DurableWorkflowFailedEvent>(events[0]);
Assert.Equal("Workflow was terminated.", failedEvent.ErrorMessage);
Assert.Null(failedEvent.FailureDetails);
}
[Fact]
public async Task WatchStreamAsync_EventsInCustomStatus_YieldsEventsBeforeCompletionAsync()
{
// Arrange
DurableHaltRequestedEvent haltEvent = new("exec-1");
string serializedEvent = SerializeEvent(haltEvent);
string customStatus = SerializeCustomStatus([serializedEvent]);
string serializedOutput = SerializeWorkflowResult("final", []);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
if (callCount == 1)
{
return CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus);
}
return CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput);
});
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Equal(2, events.Count);
DurableHaltRequestedEvent haltResult = Assert.IsType<DurableHaltRequestedEvent>(events[0]);
Assert.Equal("exec-1", haltResult.ExecutorId);
DurableWorkflowCompletedEvent completedResult = Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
Assert.Equal("final", completedResult.Result);
}
[Fact]
public async Task WatchStreamAsync_EventTypeNameHasMutatedAssemblyVersion_StillDeserializesAsync()
{
// Arrange — model what happens after a package upgrade: the persisted TypedPayload.TypeName
// carries an assembly Version= that no longer matches any loaded assembly.
DurableHaltRequestedEvent haltEvent = new("exec-1");
Type eventType = haltEvent.GetType();
string outerSimpleName = eventType.Assembly.GetName().Name!;
string mutatedTypeName = $"{eventType.FullName}, {outerSimpleName}, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null";
TypedPayload wrapper = new()
{
TypeName = mutatedTypeName,
Data = JsonSerializer.Serialize(haltEvent, eventType, DurableSerialization.Options)
};
string serializedEvent = JsonSerializer.Serialize(wrapper, DurableWorkflowJsonContext.Default.TypedPayload);
string customStatus = SerializeCustomStatus([serializedEvent]);
string serializedOutput = SerializeWorkflowResult("final", []);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
if (callCount == 1)
{
return CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus);
}
return CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput);
});
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Equal(2, events.Count);
DurableHaltRequestedEvent haltResult = Assert.IsType<DurableHaltRequestedEvent>(events[0]);
Assert.Equal("exec-1", haltResult.ExecutorId);
DurableWorkflowCompletedEvent completedResult = Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
Assert.Equal("final", completedResult.Result);
}
[Fact]
public async Task WatchStreamAsync_IncrementalEvents_YieldsOnlyNewEventsPerPollAsync()
{
// Arrange — simulate 3 poll cycles where events accumulate in custom status,
// then a final completion poll. This validates:
// 1. Events arriving across multiple poll cycles are yielded incrementally
// 2. Already-seen events are not re-yielded (lastReadEventIndex dedup)
// 3. Completion event follows all streamed events
DurableHaltRequestedEvent event1 = new("executor-1");
DurableHaltRequestedEvent event2 = new("executor-2");
DurableHaltRequestedEvent event3 = new("executor-3");
string serializedEvent1 = SerializeEvent(event1);
string serializedEvent2 = SerializeEvent(event2);
string serializedEvent3 = SerializeEvent(event3);
// Poll 1: 1 event in custom status
string customStatus1 = SerializeCustomStatus([serializedEvent1]);
// Poll 2: same event + 1 new event (accumulating list)
string customStatus2 = SerializeCustomStatus([serializedEvent1, serializedEvent2]);
// Poll 3: all 3 events accumulated
string customStatus3 = SerializeCustomStatus([serializedEvent1, serializedEvent2, serializedEvent3]);
// Poll 4: completed, all events also in output
string serializedOutput = SerializeWorkflowResult("done", [serializedEvent1, serializedEvent2, serializedEvent3]);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
return callCount switch
{
1 => CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus1),
2 => CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus2),
3 => CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus3),
_ => CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput),
};
});
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert — exactly 4 events: 3 incremental halt events + 1 completion
Assert.Equal(4, events.Count);
DurableHaltRequestedEvent halt1 = Assert.IsType<DurableHaltRequestedEvent>(events[0]);
DurableHaltRequestedEvent halt2 = Assert.IsType<DurableHaltRequestedEvent>(events[1]);
DurableHaltRequestedEvent halt3 = Assert.IsType<DurableHaltRequestedEvent>(events[2]);
Assert.Equal("executor-1", halt1.ExecutorId);
Assert.Equal("executor-2", halt2.ExecutorId);
Assert.Equal("executor-3", halt3.ExecutorId);
DurableWorkflowCompletedEvent completed = Assert.IsType<DurableWorkflowCompletedEvent>(events[3]);
Assert.Equal("done", completed.Data);
}
[Fact]
public async Task WatchStreamAsync_NoNewEventsOnRepoll_DoesNotDuplicateAsync()
{
// Arrange — simulate polling where custom status doesn't change between polls,
// validating that events are not duplicated when the list is unchanged.
DurableHaltRequestedEvent event1 = new("executor-1");
string serializedEvent1 = SerializeEvent(event1);
string customStatus = SerializeCustomStatus([serializedEvent1]);
string serializedOutput = SerializeWorkflowResult("result", [serializedEvent1]);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
return callCount switch
{
// First 3 polls return the same custom status (no new events after first)
<= 3 => CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus),
_ => CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput),
};
});
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert — event1 appears exactly once despite 3 polls with the same status
Assert.Equal(2, events.Count);
DurableHaltRequestedEvent haltResult = Assert.IsType<DurableHaltRequestedEvent>(events[0]);
Assert.Equal("executor-1", haltResult.ExecutorId);
DurableWorkflowCompletedEvent completedResult = Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
Assert.Equal("result", completedResult.Result);
}
[Fact]
public async Task WatchStreamAsync_Cancellation_EndsGracefullyAsync()
{
// Arrange
using CancellationTokenSource cts = new();
int pollCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
if (++pollCount >= 2)
{
cts.Cancel();
}
return CreateMetadata(OrchestrationRuntimeStatus.Running);
});
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cts.Token))
{
events.Add(evt);
}
// Assert — no exception thrown, stream ends cleanly
Assert.Empty(events);
}
[Fact]
public async Task WatchStreamAsync_PendingRequestPort_YieldsWaitingForInputEventAsync()
{
// Arrange
string customStatus = SerializeCustomStatusWithPendingEvents(
[],
[new PendingRequestPortStatus("ApprovalPort", """{"amount":100}""")]);
string serializedOutput = SerializeWorkflowResult("approved", []);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
return callCount == 1
? CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus)
: CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput);
});
Workflow workflow = CreateTestWorkflowWithRequestPort("ApprovalPort");
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, workflow);
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
Assert.Equal(2, events.Count);
DurableWorkflowWaitingForInputEvent waitingEvent = Assert.IsType<DurableWorkflowWaitingForInputEvent>(events[0]);
Assert.Equal("ApprovalPort", waitingEvent.RequestPort.Id);
Assert.Contains("amount", waitingEvent.Input);
DurableWorkflowCompletedEvent completedEvent = Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
Assert.Equal("approved", completedEvent.Result);
}
[Fact]
public async Task WatchStreamAsync_PendingRequestPort_DoesNotDuplicateOnSubsequentPollsAsync()
{
// Arrange — same pending event across 2 polls, then completion
string customStatus = SerializeCustomStatusWithPendingEvents(
[],
[new PendingRequestPortStatus("ApprovalPort", """{"amount":100}""")]);
string serializedOutput = SerializeWorkflowResult("done", []);
int callCount = 0;
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.GetInstanceAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
callCount++;
return callCount switch
{
<= 2 => CreateMetadata(OrchestrationRuntimeStatus.Running, serializedCustomStatus: customStatus),
_ => CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput),
};
});
Workflow workflow = CreateTestWorkflowWithRequestPort("ApprovalPort");
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, workflow);
// Act
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert — WaitingForInputEvent yielded only once despite 2 polls
Assert.Equal(2, events.Count);
Assert.IsType<DurableWorkflowWaitingForInputEvent>(events[0]);
Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
}
#endregion
#region SendResponseAsync
[Fact]
public async Task SendResponseAsync_SerializesAndRaisesEventAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.RaiseEventAsync(
InstanceId,
"ApprovalPort",
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
RequestPort approvalPort = RequestPort.Create<string, string>("ApprovalPort");
DurableWorkflowWaitingForInputEvent requestEvent = new("""{"amount":100}""", approvalPort);
Workflow workflow = CreateTestWorkflowWithRequestPort("ApprovalPort");
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, workflow);
// Act
await run.SendResponseAsync(requestEvent, new { approved = true, comments = "Looks good" });
// Assert
mockClient.Verify(c => c.RaiseEventAsync(
InstanceId,
"ApprovalPort",
It.Is<string>(s => s.Contains("approved") && s.Contains("true")),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task SendResponseAsync_NullRequestEvent_ThrowsAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() =>
run.SendResponseAsync(null!, "response").AsTask());
}
#endregion
#region WaitForCompletionAsync
[Fact]
public async Task WaitForCompletionAsync_Completed_ReturnsResultAsync()
{
// Arrange
string serializedOutput = SerializeWorkflowResult("hello world", []);
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.WaitForInstanceCompletionAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Completed, serializedOutput: serializedOutput));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act
string? result = await run.WaitForCompletionAsync<string>();
// Assert
Assert.Equal("hello world", result);
}
[Fact]
public async Task WaitForCompletionAsync_Failed_ThrowsTaskFailedExceptionAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.WaitForInstanceCompletionAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(
OrchestrationRuntimeStatus.Failed,
failureDetails: new TaskFailureDetails("Error", "kaboom", null, null, null)));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act & Assert
TaskFailedException ex = await Assert.ThrowsAsync<TaskFailedException>(
() => run.WaitForCompletionAsync<string>().AsTask());
Assert.Equal("kaboom", ex.FailureDetails.ErrorMessage);
}
[Fact]
public async Task WaitForCompletionAsync_UnexpectedStatus_ThrowsAsync()
{
// Arrange
Mock<DurableTaskClient> mockClient = new("test");
mockClient.Setup(c => c.WaitForInstanceCompletionAsync(InstanceId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(CreateMetadata(OrchestrationRuntimeStatus.Terminated));
DurableStreamingWorkflowRun run = new(mockClient.Object, InstanceId, CreateTestWorkflow());
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => run.WaitForCompletionAsync<string>().AsTask());
}
#endregion
#region ExtractResult
[Fact]
public void ExtractResult_NullOutput_ReturnsDefault()
{
// Act
string? result = DurableStreamingWorkflowRun.ExtractResult<string>(null);
// Assert
Assert.Null(result);
}
[Fact]
public void ExtractResult_WrappedStringResult_ReturnsUnwrappedString()
{
// Arrange
string serializedOutput = SerializeWorkflowResult("hello", []);
// Act
string? result = DurableStreamingWorkflowRun.ExtractResult<string>(serializedOutput);
// Assert
Assert.Equal("hello", result);
}
[Fact]
public void ExtractResult_UnwrappedOutput_ThrowsInvalidOperationException()
{
// Arrange — raw output not wrapped in DurableWorkflowResult
string serializedOutput = JsonSerializer.Serialize("raw value");
// Act & Assert
Assert.Throws<InvalidOperationException>(
() => DurableStreamingWorkflowRun.ExtractResult<string>(serializedOutput));
}
[Fact]
public void ExtractResult_WrappedObjectResult_DeserializesCorrectly()
{
// Arrange
TestPayload original = new() { Name = "test", Value = 42 };
string resultJson = JsonSerializer.Serialize(original);
string serializedOutput = SerializeWorkflowResult(resultJson, []);
// Act
TestPayload? result = DurableStreamingWorkflowRun.ExtractResult<TestPayload>(serializedOutput);
// Assert
Assert.NotNull(result);
Assert.Equal("test", result.Name);
Assert.Equal(42, result.Value);
}
[Fact]
public void ExtractResult_CamelCaseSerializedObject_DeserializesToPascalCaseMembers()
{
// Arrange — executor outputs are serialized with DurableSerialization.Options (camelCase)
TestPayload original = new() { Name = "camel", Value = 99 };
string resultJson = JsonSerializer.Serialize(original, DurableSerialization.Options);
string serializedOutput = SerializeWorkflowResult(resultJson, []);
// Act
TestPayload? result = DurableStreamingWorkflowRun.ExtractResult<TestPayload>(serializedOutput);
// Assert
Assert.NotNull(result);
Assert.Equal("camel", result.Name);
Assert.Equal(99, result.Value);
}
#endregion
private sealed class TestPayload
{
public string? Name { get; set; }
public int Value { get; set; }
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
/// <summary>
/// Verifies that <see cref="DurableTaskTypeResolver.Resolve(string)"/> resolves persisted
/// assembly-qualified type-name strings to a loaded <see cref="Type"/> across assembly
/// version, culture, and public key token mutations.
/// </summary>
public sealed class DurableTaskTypeResolverTests
{
[Fact]
public void Resolve_LoadedAssemblyQualifiedName_ReturnsLiveType()
{
Type live = typeof(List<ChatMessage>);
string aqn = live.AssemblyQualifiedName!;
Type? resolved = DurableTaskTypeResolver.Resolve(aqn);
Assert.Same(live, resolved);
}
[Fact]
public void Resolve_MutatedOuterAssemblyVersion_ReturnsLiveType()
{
Type live = typeof(ChatMessage);
string outerSimpleName = live.Assembly.GetName().Name!;
string mutated = $"{live.FullName}, {outerSimpleName}, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null";
Type? resolved = DurableTaskTypeResolver.Resolve(mutated);
Assert.Same(live, resolved);
}
[Fact]
public void Resolve_MutatedGenericArgumentVersion_ReturnsLiveType()
{
Type live = typeof(List<ChatMessage>);
string outerSimpleName = live.Assembly.GetName().Name!;
string innerSimpleName = typeof(ChatMessage).Assembly.GetName().Name!;
string mutated =
$"System.Collections.Generic.List`1[[Microsoft.Extensions.AI.ChatMessage, {innerSimpleName}, " +
"Version=99.0.0.0, Culture=neutral, PublicKeyToken=null]], " +
$"{outerSimpleName}, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null";
Type? resolved = DurableTaskTypeResolver.Resolve(mutated);
Assert.Same(live, resolved);
}
[Fact]
public void Resolve_UnknownType_ReturnsNull()
{
Type? resolved = DurableTaskTypeResolver.Resolve(
"Some.Unknown.Namespace.MissingType, Some.Unloaded.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
Assert.Null(resolved);
}
[Fact]
public void Resolve_CachesResults()
{
Type live = typeof(ChatMessage);
string aqn = live.AssemblyQualifiedName!;
Type? first = DurableTaskTypeResolver.Resolve(aqn);
Type? second = DurableTaskTypeResolver.Resolve(aqn);
Assert.Same(first, second);
Assert.Same(live, second);
}
}
@@ -0,0 +1,504 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Workflows;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
public sealed class DurableWorkflowContextTests
{
private static FunctionExecutor<string> CreateTestExecutor(string id = "test-executor")
=> new(id, (_, _, _) => default, outputTypes: [typeof(string)]);
#region ReadStateAsync
[Fact]
public async Task ReadStateAsync_KeyExistsInInitialState_ReturnsValueAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:counter"] = "42" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
// Act
int? result = await context.ReadStateAsync<int>("counter");
// Assert
Assert.Equal(42, result);
}
[Fact]
public async Task ReadStateAsync_KeyDoesNotExist_ReturnsNullAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
string? result = await context.ReadStateAsync<string>("missing");
// Assert
Assert.Null(result);
}
[Fact]
public async Task ReadStateAsync_LocalUpdateTakesPriorityOverInitialStateAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:key"] = "\"old\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
await context.QueueStateUpdateAsync("key", "new");
// Act
string? result = await context.ReadStateAsync<string>("key");
// Assert
Assert.Equal("new", result);
}
[Fact]
public async Task ReadStateAsync_ScopeCleared_IgnoresInitialStateAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:key"] = "\"value\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
await context.QueueClearScopeAsync();
// Act
string? result = await context.ReadStateAsync<string>("key");
// Assert
Assert.Null(result);
}
[Fact]
public async Task ReadStateAsync_WithNamedScope_ReadsFromCorrectScopeAsync()
{
// Arrange
Dictionary<string, string> state = new()
{
["scopeA:key"] = "\"fromA\"",
["scopeB:key"] = "\"fromB\""
};
DurableWorkflowContext context = new(state, CreateTestExecutor());
// Act
string? resultA = await context.ReadStateAsync<string>("key", "scopeA");
string? resultB = await context.ReadStateAsync<string>("key", "scopeB");
// Assert
Assert.Equal("fromA", resultA);
Assert.Equal("fromB", resultB);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task ReadStateAsync_NullOrEmptyKey_ThrowsArgumentExceptionAsync(string? key)
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act & Assert
await Assert.ThrowsAnyAsync<ArgumentException>(() => context.ReadStateAsync<string>(key!).AsTask());
}
#endregion
#region ReadOrInitStateAsync
[Fact]
public async Task ReadOrInitStateAsync_KeyDoesNotExist_CallsFactoryAndQueuesUpdateAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
string result = await context.ReadOrInitStateAsync("key", () => "initialized");
// Assert
Assert.Equal("initialized", result);
Assert.True(context.StateUpdates.ContainsKey("__default__:key"));
}
[Fact]
public async Task ReadOrInitStateAsync_KeyExists_ReturnsExistingValueAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:key"] = "\"existing\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
bool factoryCalled = false;
// Act
string result = await context.ReadOrInitStateAsync("key", () =>
{
factoryCalled = true;
return "should-not-be-used";
});
// Assert
Assert.Equal("existing", result);
Assert.False(factoryCalled);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task ReadOrInitStateAsync_NullOrEmptyKey_ThrowsArgumentExceptionAsync(string? key)
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act & Assert
await Assert.ThrowsAnyAsync<ArgumentException>(
() => context.ReadOrInitStateAsync(key!, () => "value").AsTask());
}
[Fact]
public async Task ReadOrInitStateAsync_ValueType_MissingKey_CallsFactoryAsync()
{
// Arrange
// Validates that ReadStateAsync<int> returns null (not 0) for missing keys,
// because the return type is int? (Nullable<int>). This ensures the factory
// is correctly invoked for value types when the key does not exist.
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
int result = await context.ReadOrInitStateAsync("counter", () => 42);
// Assert
Assert.Equal(42, result);
Assert.True(context.StateUpdates.ContainsKey("__default__:counter"));
}
[Fact]
public async Task ReadOrInitStateAsync_NullFactory_ThrowsArgumentNullExceptionAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => context.ReadOrInitStateAsync<string>("key", null!).AsTask());
}
#endregion
#region QueueStateUpdateAsync
[Fact]
public async Task QueueStateUpdateAsync_SetsValue_VisibleToSubsequentReadAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
await context.QueueStateUpdateAsync("key", "hello");
string? result = await context.ReadStateAsync<string>("key");
// Assert
Assert.Equal("hello", result);
}
[Fact]
public async Task QueueStateUpdateAsync_NullValue_RecordsDeletionAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:key"] = "\"value\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
// Act
await context.QueueStateUpdateAsync<string>("key", null);
// Assert
Assert.True(context.StateUpdates.ContainsKey("__default__:key"));
Assert.Null(context.StateUpdates["__default__:key"]);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task QueueStateUpdateAsync_NullOrEmptyKey_ThrowsArgumentExceptionAsync(string? key)
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act & Assert
await Assert.ThrowsAnyAsync<ArgumentException>(
() => context.QueueStateUpdateAsync(key!, "value").AsTask());
}
#endregion
#region QueueClearScopeAsync
[Fact]
public async Task QueueClearScopeAsync_DefaultScope_ClearsStateAndPendingUpdatesAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:key"] = "\"value\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
await context.QueueStateUpdateAsync("pending", "data");
// Act
await context.QueueClearScopeAsync();
// Assert
Assert.Contains("__default__", context.ClearedScopes);
Assert.Empty(context.StateUpdates);
}
[Fact]
public async Task QueueClearScopeAsync_NamedScope_OnlyClearsThatScopeAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
await context.QueueStateUpdateAsync("keyA", "valueA", scopeName: "scopeA");
await context.QueueStateUpdateAsync("keyB", "valueB", scopeName: "scopeB");
// Act
await context.QueueClearScopeAsync("scopeA");
// Assert
Assert.DoesNotContain("scopeA:keyA", context.StateUpdates.Keys);
Assert.Contains("scopeB:keyB", context.StateUpdates.Keys);
}
#endregion
#region ReadStateKeysAsync
[Fact]
public async Task ReadStateKeysAsync_ReturnsKeysFromInitialStateAsync()
{
// Arrange
Dictionary<string, string> state = new()
{
["__default__:alpha"] = "\"a\"",
["__default__:beta"] = "\"b\""
};
DurableWorkflowContext context = new(state, CreateTestExecutor());
// Act
HashSet<string> keys = await context.ReadStateKeysAsync();
// Assert
Assert.Equal(2, keys.Count);
Assert.Contains("alpha", keys);
Assert.Contains("beta", keys);
}
[Fact]
public async Task ReadStateKeysAsync_MergesLocalUpdatesAndDeletionsAsync()
{
// Arrange
Dictionary<string, string> state = new()
{
["__default__:existing"] = "\"val\"",
["__default__:toDelete"] = "\"val\""
};
DurableWorkflowContext context = new(state, CreateTestExecutor());
await context.QueueStateUpdateAsync("newKey", "value");
await context.QueueStateUpdateAsync<string>("toDelete", null);
// Act
HashSet<string> keys = await context.ReadStateKeysAsync();
// Assert
Assert.Contains("existing", keys);
Assert.Contains("newKey", keys);
Assert.DoesNotContain("toDelete", keys);
}
[Fact]
public async Task ReadStateKeysAsync_AfterClearScope_ExcludesInitialStateAsync()
{
// Arrange
Dictionary<string, string> state = new() { ["__default__:old"] = "\"val\"" };
DurableWorkflowContext context = new(state, CreateTestExecutor());
await context.QueueClearScopeAsync();
await context.QueueStateUpdateAsync("new", "value");
// Act
HashSet<string> keys = await context.ReadStateKeysAsync();
// Assert
Assert.DoesNotContain("old", keys);
Assert.Contains("new", keys);
}
[Fact]
public async Task ReadStateKeysAsync_WithNamedScope_OnlyReturnsKeysFromThatScopeAsync()
{
// Arrange
Dictionary<string, string> state = new()
{
["scopeA:key1"] = "\"val\"",
["scopeB:key2"] = "\"val\""
};
DurableWorkflowContext context = new(state, CreateTestExecutor());
// Act
HashSet<string> keysA = await context.ReadStateKeysAsync("scopeA");
// Assert
Assert.Single(keysA);
Assert.Contains("key1", keysA);
}
#endregion
#region AddEventAsync
[Fact]
public async Task AddEventAsync_AddsEventToCollectionAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
WorkflowEvent evt = new ExecutorInvokedEvent("test", "test-data");
// Act
await context.AddEventAsync(evt);
// Assert
Assert.Single(context.OutboundEvents);
Assert.Same(evt, context.OutboundEvents[0]);
}
[Fact]
public async Task AddEventAsync_NullEvent_DoesNotAddAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await context.AddEventAsync(null);
#pragma warning restore CS8625
// Assert
Assert.Empty(context.OutboundEvents);
}
#endregion
#region SendMessageAsync
[Fact]
public async Task SendMessageAsync_SerializesMessageWithTypeNameAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
await context.SendMessageAsync("hello");
// Assert
Assert.Single(context.SentMessages);
Assert.Equal(typeof(string).AssemblyQualifiedName, context.SentMessages[0].TypeName);
Assert.NotNull(context.SentMessages[0].Data);
}
[Fact]
public async Task SendMessageAsync_NullMessage_DoesNotAddAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await context.SendMessageAsync(null);
#pragma warning restore CS8625
// Assert
Assert.Empty(context.SentMessages);
}
#endregion
#region YieldOutputAsync
[Fact]
public async Task YieldOutputAsync_AddsWorkflowOutputEventAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
await context.YieldOutputAsync("result");
// Assert
Assert.Single(context.OutboundEvents);
WorkflowOutputEvent outputEvent = Assert.IsType<WorkflowOutputEvent>(context.OutboundEvents[0]);
Assert.Equal("result", outputEvent.Data);
}
[Fact]
public async Task YieldOutputAsync_NullOutput_DoesNotAddAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await context.YieldOutputAsync(null);
#pragma warning restore CS8625
// Assert
Assert.Empty(context.OutboundEvents);
}
#endregion
#region RequestHaltAsync
[Fact]
public async Task RequestHaltAsync_SetsHaltRequestedAndAddsEventAsync()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Act
await context.RequestHaltAsync();
// Assert
Assert.True(context.HaltRequested);
Assert.Single(context.OutboundEvents);
Assert.IsType<DurableHaltRequestedEvent>(context.OutboundEvents[0]);
}
#endregion
#region Properties
[Fact]
public void TraceContext_ReturnsNull()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Assert
Assert.Null(context.TraceContext);
}
[Fact]
public void ConcurrentRunsEnabled_ReturnsFalse()
{
// Arrange
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Assert
Assert.False(context.ConcurrentRunsEnabled);
}
[Fact]
public async Task Constructor_NullInitialState_CreatesEmptyStateAsync()
{
// Arrange & Act
DurableWorkflowContext context = new(null, CreateTestExecutor());
// Assert
string? result = await context.ReadStateAsync<string>("anything");
Assert.Null(result);
}
#endregion
}
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.Workflows;
namespace Microsoft.Agents.AI.DurableTask.UnitTests.Workflows;
public sealed class WorkflowNamingHelperTests
{
[Fact]
public void ToOrchestrationFunctionName_ValidWorkflowName_ReturnsPrefixedName()
{
string result = WorkflowNamingHelper.ToOrchestrationFunctionName("MyWorkflow");
Assert.Equal("dafx-MyWorkflow", result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void ToOrchestrationFunctionName_NullOrEmpty_ThrowsArgumentException(string? workflowName)
{
Assert.ThrowsAny<ArgumentException>(() => WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName!));
}
[Fact]
public void ToWorkflowName_ValidOrchestrationFunctionName_ReturnsWorkflowName()
{
string result = WorkflowNamingHelper.ToWorkflowName("dafx-MyWorkflow");
Assert.Equal("MyWorkflow", result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void ToWorkflowName_NullOrEmpty_ThrowsArgumentException(string? orchestrationFunctionName)
{
Assert.ThrowsAny<ArgumentException>(() => WorkflowNamingHelper.ToWorkflowName(orchestrationFunctionName!));
}
[Theory]
[InlineData("MyWorkflow")]
[InlineData("invalid-prefix-MyWorkflow")]
[InlineData("dafx")]
[InlineData("dafx-")]
public void ToWorkflowName_InvalidOrMissingPrefix_ThrowsArgumentException(string orchestrationFunctionName)
{
Assert.Throws<ArgumentException>(() => WorkflowNamingHelper.ToWorkflowName(orchestrationFunctionName));
}
[Fact]
public void GetExecutorName_SimpleExecutorId_ReturnsSameName()
{
string result = WorkflowNamingHelper.GetExecutorName("OrderParser");
Assert.Equal("OrderParser", result);
}
[Fact]
public void GetExecutorName_ExecutorIdWithGuidSuffix_ReturnsNameWithoutSuffix()
{
string result = WorkflowNamingHelper.GetExecutorName("Physicist_8884e71021334ce49517fa2b17b1695b");
Assert.Equal("Physicist", result);
}
[Fact]
public void GetExecutorName_NameWithUnderscoresAndGuidSuffix_ReturnsFullName()
{
string result = WorkflowNamingHelper.GetExecutorName("my_agent_8884e71021334ce49517fa2b17b1695b");
Assert.Equal("my_agent", result);
}
[Fact]
public void GetExecutorName_NameWithUnderscoreButNoGuidSuffix_ReturnsSameName()
{
string result = WorkflowNamingHelper.GetExecutorName("my_custom_executor");
Assert.Equal("my_custom_executor", result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void GetExecutorName_NullOrEmpty_ThrowsArgumentException(string? executorId)
{
Assert.ThrowsAny<ArgumentException>(() => WorkflowNamingHelper.GetExecutorName(executorId!));
}
}