mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
dc2b109b50
* Upgrade to .NET 10 - Require .NET 10 SDK - Include net10.0 assets in all assemblies - Move net9.0-only targets to net10.0 - Update LangVersion to latest - Remove complicated distinctions between debug target TFMs and release target TFMs - Remove unnecessary package dependencies when built into netcoreapp - Clean up some ifdefs - Clean up some analyzer warnings * Fix CI
572 lines
28 KiB
C#
572 lines
28 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
|
using Microsoft.Agents.AI.Workflows.Execution;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
|
|
|
public class StateManagerTests
|
|
{
|
|
[Fact]
|
|
public async Task Test_SharedScope_ReadKeysAsync()
|
|
{
|
|
const string? ScopeName = "sharedScope";
|
|
await RunScopeKeysTestAsync(ScopeName, isSharedScope: true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_PrivateScope_ReadKeysAsync()
|
|
{
|
|
const string? ScopeName = null;
|
|
await RunScopeKeysTestAsync(ScopeName, isSharedScope: false);
|
|
}
|
|
|
|
private static async Task RunScopeKeysTestAsync(string? scopeName, bool isSharedScope)
|
|
{
|
|
const string SelfExecutorId = "executor1";
|
|
const string OtherExecutorId = "executor2";
|
|
const string Key1 = "key1";
|
|
HashSet<string> ExpectedAfterWrite = [Key1];
|
|
|
|
StateManager manager = new();
|
|
ScopeId sharedScopeSelfView = new(SelfExecutorId, scopeName);
|
|
ScopeId sharedScopeOtherView = new(OtherExecutorId, scopeName);
|
|
|
|
// Assert baseline: neither executor sees any keys
|
|
HashSet<string> selfKeys = await manager.ReadKeysAsync(sharedScopeSelfView);
|
|
selfKeys.Should().BeEmpty("there should be no keys in an empty StateManager");
|
|
|
|
HashSet<string> otherKeys = await manager.ReadKeysAsync(sharedScopeOtherView);
|
|
otherKeys.Should().BeEmpty("there should be no keys in an empty StateManager");
|
|
|
|
// Act 1: Write a key from the self executor's view of the shared scope
|
|
|
|
await manager.WriteStateAsync(sharedScopeSelfView, Key1, "value1");
|
|
|
|
// Assert 1: The self executor should see the key immediately, but the other executor should not
|
|
selfKeys = await manager.ReadKeysAsync(sharedScopeSelfView);
|
|
selfKeys.SetEquals(ExpectedAfterWrite).Should().BeTrue("writes should be visible immediately to the writing executor");
|
|
|
|
otherKeys = await manager.ReadKeysAsync(sharedScopeOtherView);
|
|
otherKeys.Should().BeEmpty(isSharedScope ? "writes should not be visible to other executors until published"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
// Act 2: Publish the updates
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert 2: Both executors should see the key now, if sharedScope
|
|
selfKeys = await manager.ReadKeysAsync(sharedScopeSelfView);
|
|
selfKeys.SetEquals(ExpectedAfterWrite).Should().BeTrue("published writes should be visible to all executors");
|
|
|
|
otherKeys = await manager.ReadKeysAsync(sharedScopeOtherView);
|
|
|
|
if (isSharedScope)
|
|
{
|
|
otherKeys.SetEquals(ExpectedAfterWrite).Should().BeTrue("published writes should be visible to all executors");
|
|
}
|
|
else
|
|
{
|
|
otherKeys.Should().BeEmpty("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
// Act 3: Clear the state from the self executor's view of the shared scope
|
|
await manager.WriteStateAsync<string?>(sharedScopeSelfView, Key1, null);
|
|
|
|
// Assert 3: The self executor should not see the key immediately, but the other executor should still see it if sharedScope
|
|
selfKeys = await manager.ReadKeysAsync(sharedScopeSelfView);
|
|
selfKeys.Should().BeEmpty("deletes should be visible immediately to the writing executor");
|
|
|
|
otherKeys = await manager.ReadKeysAsync(sharedScopeOtherView);
|
|
if (isSharedScope)
|
|
{
|
|
otherKeys.SetEquals(ExpectedAfterWrite).Should().BeTrue("published writes should be visible to all executors");
|
|
}
|
|
else
|
|
{
|
|
otherKeys.Should().BeEmpty("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
// Act 4: Publish the updates
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert 4: Neither executor should see the key now
|
|
selfKeys = await manager.ReadKeysAsync(sharedScopeSelfView);
|
|
selfKeys.Should().BeEmpty("published deletes should be visible to all executors");
|
|
|
|
otherKeys = await manager.ReadKeysAsync(sharedScopeOtherView);
|
|
otherKeys.Should().BeEmpty(isSharedScope ? "published deletes should be visible to all executors"
|
|
: "writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_SharedScope_ValueLifecycleAsync()
|
|
{
|
|
const string? ScopeName = "sharedScope";
|
|
await RunValueLifecycleTestAsync(ScopeName, isSharedScope: true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_PrivateScope_ValueLifecycleAsync()
|
|
{
|
|
const string? ScopeName = null;
|
|
await RunValueLifecycleTestAsync(ScopeName, isSharedScope: false);
|
|
}
|
|
|
|
private static async Task RunValueLifecycleTestAsync(string? scopeName, bool isSharedScope)
|
|
{
|
|
const string SelfExecutorId = "executor1";
|
|
const string OtherExecutorId = "executor2";
|
|
const string Key1 = "key1", Key2 = "key2";
|
|
const string Value1 = "value1", Value2 = "value2";
|
|
|
|
StateManager manager = new();
|
|
ScopeId scopeSelfView = new(SelfExecutorId, scopeName);
|
|
ScopeId scopeOtherView = new(OtherExecutorId, scopeName);
|
|
|
|
isSharedScope.Should().Be(scopeSelfView == scopeOtherView);
|
|
|
|
// Assert baseline: neither executor sees any keys or values
|
|
string? selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
string? selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue1.Should().BeNull("there should be no values in an empty StateManager");
|
|
selfValue2.Should().BeNull("there should be no values in an empty StateManager");
|
|
|
|
string? otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
string? otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue1.Should().BeNull("there should be no values in an empty StateManager");
|
|
otherValue2.Should().BeNull("there should be no values in an empty StateManager");
|
|
|
|
// Act 1: Write a value from the self executor's view of the shared scope
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, Value1);
|
|
|
|
// Assert 1: The self executor should see the value immediately, but the other executor should not
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().Be(Value1, "writes should be visible immediately to the writing executor");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull("uninvolved keys' state/value should not change after a write");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "writes should not be visible to other executors until published (key1: written by self, read by other)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().BeNull("uninvolved keys' state/value should not change after a write");
|
|
|
|
// Act 2: Write a value from the other executor's view of the shared scope
|
|
await manager.WriteStateAsync(scopeOtherView, Key2, Value2);
|
|
|
|
// Assert 2: The other executor should see the value immediately, but the self executor should not
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().Be(Value1, "uninvolved keys' state/value should not change after a write");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull(isSharedScope ? "writes should not be visible to other executors until published (key2: written by other, read by self)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "writes should not be visible to other executors until published (key1: written by self, read by other)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().Be(Value2, "writes should be visible immediately to the writing executor");
|
|
|
|
// Act 3: Publish the updates
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert 3: Both executors should see both values now, if the scope is shared
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().Be(Value1, "published writes should be visible to all executors (key1: written by self, read by self)");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
if (isSharedScope)
|
|
{
|
|
selfValue2.Should().Be(Value2, "published writes should be visible to all executors (key2: written by other, read by self)");
|
|
}
|
|
else
|
|
{
|
|
selfValue2.Should().BeNull("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
if (isSharedScope)
|
|
{
|
|
otherValue1.Should().Be(Value1, "published writes should be visible to all executors (key1: written by self, read by other)");
|
|
}
|
|
else
|
|
{
|
|
otherValue1.Should().BeNull("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().Be(Value2, "published writes should be visible to all executors (key2: written by other, read by other)");
|
|
|
|
// Act 4: Clear the value from the self executor's view of the shared scope
|
|
await manager.ClearStateAsync(scopeSelfView);
|
|
|
|
// Assert 4: The self executor should not see either value immediately, but the other executor should still see both
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().BeNull("clears should be visible immediately to the writing executor");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull(isSharedScope ? "clears should be visible immediately to the writing executor"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
if (isSharedScope)
|
|
{
|
|
otherValue1.Should().Be(Value1, "clears should not be visible to other executors until published (key2: written by self, read by other)");
|
|
}
|
|
else
|
|
{
|
|
otherValue1.Should().BeNull("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().Be(Value2, isSharedScope ? "clears should not be visible to other executors until published (key2: written by self, read by other)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
// Act 5: Publish the updates
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert 5: Neither executor should see either value now
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().BeNull("published clears should be visible to all executors");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull(isSharedScope ? "published clears should be visible to all executors"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "published clears should be visible to all executors"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
if (isSharedScope)
|
|
{
|
|
otherValue2.Should().BeNull("published clears should be visible to all executors");
|
|
}
|
|
else
|
|
{
|
|
otherValue2.Should().Be(Value2, "writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
// Restore the written state of both keys
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, Value1);
|
|
await manager.WriteStateAsync(scopeOtherView, Key2, Value2);
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Act 6: Delete Key1 from the other executor's view of the shared scope
|
|
await manager.WriteStateAsync<string?>(scopeOtherView, Key1, null);
|
|
|
|
// Assert 6: The other executor should not see Key1 immediately, but should still see Key2. The self executor should still see both.
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().Be(Value1, isSharedScope ? "deletes should not be visible to other executors until published (key1: written by other, read by self)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
if (isSharedScope)
|
|
{
|
|
selfValue2.Should().Be(Value2, "uninvolved keys' state/value should not change after a delete");
|
|
}
|
|
else
|
|
{
|
|
selfValue2.Should().BeNull("writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "deletes should be visible immediately to the writing executor"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().Be(Value2, "uninvolved keys' state/value should not change after a delete");
|
|
|
|
// Act 7: Delete Key2 from the self executor's view of the shared scope
|
|
await manager.WriteStateAsync<string?>(scopeSelfView, Key2, null);
|
|
|
|
// Assert 7: The self executor should not see Key2 immediately, but should still see Key1.
|
|
// The other executor should not see Key1, but should still see Key2.
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
selfValue1.Should().Be(Value1, isSharedScope ? "deletes should not be visible to other executors until published (key1: written by other, read by self)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull(isSharedScope ? "deletes should be visible immediately to the writing executor"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "deletes should be visible immediately to the writing executor"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
otherValue2.Should().Be(Value2, isSharedScope ? "deletes should not be visible to other executors until published (key2: written by self, read by other)"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
// Act 8: Publish the updates
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert 8: Neither executor should see either value now
|
|
selfValue1 = await manager.ReadStateAsync<string>(scopeSelfView, Key1);
|
|
if (isSharedScope)
|
|
{
|
|
selfValue1.Should().BeNull("published deletes should be visible to all executors");
|
|
}
|
|
else
|
|
{
|
|
selfValue1.Should().Be(Value1, "writes to private scopes should not be visible across executors");
|
|
}
|
|
|
|
selfValue2 = await manager.ReadStateAsync<string>(scopeSelfView, Key2);
|
|
selfValue2.Should().BeNull(isSharedScope ? "published deletes should be visible to all executors"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue1 = await manager.ReadStateAsync<string>(scopeOtherView, Key1);
|
|
otherValue1.Should().BeNull(isSharedScope ? "published deletes should be visible to all executors"
|
|
: "writes to private scopes should not be visible across executors");
|
|
|
|
otherValue2 = await manager.ReadStateAsync<string>(scopeOtherView, Key2);
|
|
if (isSharedScope)
|
|
{
|
|
otherValue2.Should().BeNull("published deletes should be visible to all executors");
|
|
}
|
|
else
|
|
{
|
|
otherValue2.Should().Be(Value2, "writes to private scopes should not be visible across executors");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_SharedScope_ConflictingUpdatesAsync()
|
|
{
|
|
const string? ScopeName = "sharedScope";
|
|
await RunConflictingUpdatesTest_WriteVsWriteAsync(ScopeName, isSharedScope: true);
|
|
await RunConflictingUpdatesTest_WriteVsDeleteAsync(ScopeName, isSharedScope: true);
|
|
await RunConflictingUpdatesTest_WriteVsClearAsync(ScopeName, isSharedScope: true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_PrivateScope_ConflictingUpdatesAsync()
|
|
{
|
|
const string? ScopeName = null;
|
|
await RunConflictingUpdatesTest_WriteVsWriteAsync(ScopeName, isSharedScope: false);
|
|
await RunConflictingUpdatesTest_WriteVsDeleteAsync(ScopeName, isSharedScope: false);
|
|
await RunConflictingUpdatesTest_WriteVsClearAsync(ScopeName, isSharedScope: false);
|
|
}
|
|
|
|
private static async Task RunConflictingUpdatesTest_WriteVsWriteAsync(string? scopeName, bool isSharedScope)
|
|
{
|
|
const string SelfExecutorId = "executor1";
|
|
const string OtherExecutorId = "executor2";
|
|
const string Key1 = "key1";
|
|
const string Value1 = "value", Value2 = "value";
|
|
|
|
// Arrange
|
|
StateManager manager = new();
|
|
ScopeId scopeSelfView = new(SelfExecutorId, scopeName);
|
|
ScopeId scopeOtherView = new(OtherExecutorId, scopeName);
|
|
isSharedScope.Should().Be(scopeSelfView == scopeOtherView);
|
|
|
|
// Act 1: Write a conflicting value from the self executor's view of the shared scope
|
|
// Note that conflicting means update to the same key, not that the values are necessarily different.
|
|
// We do not have any logic to resolve equivalent updates from different executors as idempotent.
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, Value1);
|
|
await manager.WriteStateAsync(scopeOtherView, Key1, Value2);
|
|
|
|
Func<Task> act = async () => await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
if (isSharedScope)
|
|
{
|
|
await act.Should().ThrowAsync<InvalidOperationException>("conflicting writes to the same key should raise an exception when published");
|
|
}
|
|
else
|
|
{
|
|
await act.Should().NotThrowAsync("writes to private scopes should not be visible across executors");
|
|
}
|
|
}
|
|
|
|
private static async Task RunConflictingUpdatesTest_WriteVsDeleteAsync(string? scopeName, bool isSharedScope)
|
|
{
|
|
const string SelfExecutorId = "executor1";
|
|
const string OtherExecutorId = "executor2";
|
|
const string Key1 = "key1", Key2 = "key2";
|
|
const string Value1 = "value", Value2 = "value";
|
|
|
|
// Arrange
|
|
StateManager manager = new();
|
|
ScopeId scopeSelfView = new(SelfExecutorId, scopeName);
|
|
ScopeId scopeOtherView = new(OtherExecutorId, scopeName);
|
|
isSharedScope.Should().Be(scopeSelfView == scopeOtherView);
|
|
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, Value1);
|
|
await manager.WriteStateAsync(scopeOtherView, Key2, Value2);
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Act: Update the key from one executor and delete it from another
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, "newValue");
|
|
await manager.ClearStateAsync(scopeOtherView, Key1);
|
|
Func<Task> act = async () => await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
if (isSharedScope)
|
|
{
|
|
await act.Should().ThrowAsync<InvalidOperationException>("conflicting writes (update vs delete) should raise an exception when published");
|
|
}
|
|
else
|
|
{
|
|
await act.Should().NotThrowAsync("writes to private scopes should not be visible across executors");
|
|
}
|
|
}
|
|
|
|
private static async Task RunConflictingUpdatesTest_WriteVsClearAsync(string? scopeName, bool isSharedScope)
|
|
{
|
|
const string SelfExecutorId = "executor1";
|
|
const string OtherExecutorId = "executor2";
|
|
const string Key1 = "key1", Key2 = "key2";
|
|
const string Value1 = "value", Value2 = "value";
|
|
|
|
// Arrange
|
|
StateManager manager = new();
|
|
ScopeId scopeSelfView = new(SelfExecutorId, scopeName);
|
|
ScopeId scopeOtherView = new(OtherExecutorId, scopeName);
|
|
isSharedScope.Should().Be(scopeSelfView == scopeOtherView);
|
|
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, Value1);
|
|
await manager.WriteStateAsync(scopeOtherView, Key2, Value2);
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Act: Update the key from one, and clear the entire scope from another
|
|
await manager.WriteStateAsync(scopeSelfView, Key1, "newValue");
|
|
await manager.ClearStateAsync(scopeOtherView);
|
|
Func<Task> act = async () => await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
// Assert
|
|
if (isSharedScope)
|
|
{
|
|
await act.Should().ThrowAsync<InvalidOperationException>("conflicting writes (update vs clear) should raise an exception when published");
|
|
}
|
|
else
|
|
{
|
|
await act.Should().NotThrowAsync("writes to private scopes should not be visible across executors");
|
|
}
|
|
}
|
|
|
|
private static void VerifyIs<TExpectedType>(PortableValue? candidatePV, TExpectedType value)
|
|
{
|
|
candidatePV.Should().NotBeNull();
|
|
candidatePV.Is(out TExpectedType? candidateValue).Should().BeTrue();
|
|
candidateValue.Should().Be(value);
|
|
}
|
|
|
|
private static void VerifyIsNot<TExpectedType>(PortableValue? candidatePV)
|
|
{
|
|
candidatePV.Should().NotBeNull();
|
|
candidatePV.Is(out TExpectedType? _).Should().BeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task Test_LoadPortableValueStateAsync(bool publishStateUpdates)
|
|
{
|
|
ScopeId scope = new("executor1");
|
|
const string StringValue = "string";
|
|
const int IntValue = 42;
|
|
ScopeKey ScopeKey = new("executor1", "scope", "key");
|
|
PortableValue PortableValueValue = new(StringValue);
|
|
|
|
// Arrange
|
|
StateManager manager = new();
|
|
await manager.WriteStateAsync(scope, nameof(StringValue), StringValue);
|
|
await manager.WriteStateAsync(scope, nameof(IntValue), IntValue);
|
|
await manager.WriteStateAsync(scope, nameof(ScopeKey), ScopeKey);
|
|
await manager.WriteStateAsync(scope, nameof(PortableValueValue), PortableValueValue);
|
|
|
|
if (publishStateUpdates)
|
|
{
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
}
|
|
|
|
// Act & Assert - Read as the original types
|
|
PortableValue? stringAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(StringValue));
|
|
VerifyIs(stringAsPV, StringValue);
|
|
VerifyIsNot<int>(stringAsPV);
|
|
VerifyIsNot<ChatMessage>(stringAsPV);
|
|
VerifyIsNot<PortableValue>(stringAsPV);
|
|
|
|
PortableValue? intAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(IntValue));
|
|
VerifyIsNot<string>(intAsPV);
|
|
VerifyIs(intAsPV, IntValue);
|
|
VerifyIsNot<ChatMessage>(intAsPV);
|
|
VerifyIsNot<PortableValue>(intAsPV);
|
|
|
|
PortableValue? scopeKeyAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(ScopeKey));
|
|
VerifyIsNot<string>(scopeKeyAsPV);
|
|
VerifyIsNot<int>(scopeKeyAsPV);
|
|
VerifyIs(scopeKeyAsPV, ScopeKey);
|
|
VerifyIsNot<PortableValue>(scopeKeyAsPV);
|
|
|
|
PortableValue? pvAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(PortableValueValue));
|
|
VerifyIs(pvAsPV, StringValue);
|
|
VerifyIsNot<int>(pvAsPV);
|
|
VerifyIsNot<ChatMessage>(pvAsPV);
|
|
|
|
// Check that we don't double-wrap stored PortableValues on the out path
|
|
VerifyIsNot<PortableValue>(pvAsPV);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test_LoadPortableValueState_AfterSerializationAsync()
|
|
{
|
|
ScopeId scope = new("executor1");
|
|
const string StringValue = "string";
|
|
const int IntValue = 42;
|
|
ScopeKey ScopeKey = new("executor1", "scope", "key");
|
|
PortableValue PortableValueValue = new(StringValue);
|
|
|
|
// Arrange
|
|
StateManager manager = new();
|
|
await manager.WriteStateAsync(scope, nameof(StringValue), StringValue);
|
|
await manager.WriteStateAsync(scope, nameof(IntValue), IntValue);
|
|
await manager.WriteStateAsync(scope, nameof(ScopeKey), ScopeKey);
|
|
await manager.WriteStateAsync(scope, nameof(PortableValueValue), PortableValueValue);
|
|
|
|
await manager.PublishUpdatesAsync(tracer: null);
|
|
|
|
Dictionary<ScopeKey, PortableValue> exportedState = await manager.ExportStateAsync();
|
|
Dictionary<ScopeKey, PortableValue> serializedState = JsonSerializationTests.RunJsonRoundtrip(exportedState);
|
|
Checkpoint testCheckpoint = new(0, JsonSerializationTests.CreateTestWorkflowInfo(), new([], [], []), serializedState, []);
|
|
|
|
manager = new();
|
|
await manager.ImportStateAsync(testCheckpoint);
|
|
|
|
// Act & Assert - Read as the original types
|
|
PortableValue? stringAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(StringValue));
|
|
VerifyIs(stringAsPV, StringValue);
|
|
VerifyIsNot<int>(stringAsPV);
|
|
VerifyIsNot<ChatMessage>(stringAsPV);
|
|
|
|
PortableValue? intAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(IntValue));
|
|
VerifyIsNot<string>(intAsPV);
|
|
VerifyIs(intAsPV, IntValue);
|
|
VerifyIsNot<ChatMessage>(intAsPV);
|
|
|
|
PortableValue? scopeKeyAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(ScopeKey));
|
|
VerifyIsNot<string>(scopeKeyAsPV);
|
|
VerifyIsNot<int>(scopeKeyAsPV);
|
|
VerifyIs(scopeKeyAsPV, ScopeKey);
|
|
VerifyIsNot<PortableValue>(scopeKeyAsPV);
|
|
|
|
PortableValue? pvAsPV = await manager.ReadStateAsync<PortableValue>(scope, nameof(PortableValueValue));
|
|
VerifyIs(pvAsPV, StringValue);
|
|
VerifyIsNot<int>(pvAsPV);
|
|
VerifyIsNot<ChatMessage>(pvAsPV);
|
|
|
|
// Check that we don't double-wrap stored PortableValues on the out path
|
|
VerifyIsNot<PortableValue>(pvAsPV);
|
|
}
|
|
}
|