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
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:
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
+235
@@ -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);
|
||||
}
|
||||
+814
@@ -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; }
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+504
@@ -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
|
||||
}
|
||||
+90
@@ -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!));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user