mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
ae6b23c8d8
- Add UTF-8 BOM to 20 .cs files across DurableTask, AzureFunctions, unit tests, and workflow samples - Remove unnecessary using directive in 07_SubWorkflows/Executors.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
505 lines
15 KiB
C#
505 lines
15 KiB
C#
// 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
|
|
}
|