mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
3256baa8b6
* Adding support for events & shared state in durable workflows. * PR feedback fixes * PR feedback fixes. * Add YieldOutputAsync calls to 05_WorkflowEvents sample executors The integration test asserts that WorkflowOutputEvent is found in the stream, but the sample executors only used AddEventAsync for custom events and never called YieldOutputAsync. Since WorkflowOutputEvent is only emitted via explicit YieldOutputAsync calls, the assertion would fail. Added YieldOutputAsync to each executor to match the test expectation and demonstrate the API in the sample. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix deserialization to use shared serializer options. * PR feedback updates. * Sample cleanup * PR feedback fixes * Addressing PR review feedback for DurableStreamingWorkflowRun - Use -1 instead of 0 for taskId in TaskFailedException when task ID is not relevant. - Add [NotNullWhen(true)] to TryParseWorkflowResult out parameter following .NET TryXXX conventions. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
626 lines
24 KiB
C#
626 lines
24 KiB
C#
// 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)
|
|
{
|
|
DurableWorkflowCustomStatus status = new() { Events = events };
|
|
return JsonSerializer.Serialize(status, DurableWorkflowJsonContext.Default.DurableWorkflowCustomStatus);
|
|
}
|
|
|
|
private static string SerializeWorkflowResult(string? result, List<string> events)
|
|
{
|
|
DurableWorkflowResult workflowResult = new() { Result = result, Events = events };
|
|
string inner = JsonSerializer.Serialize(workflowResult, DurableWorkflowJsonContext.Default.DurableWorkflowResult);
|
|
return JsonSerializer.Serialize(inner);
|
|
}
|
|
|
|
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_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);
|
|
}
|
|
|
|
#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; }
|
|
}
|
|
}
|