// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading; using System.Threading.Tasks; using FluentAssertions; namespace Microsoft.Agents.Workflows.UnitTests; public class InProcessStateTests { private sealed class TurnToken { public int Count { get; } public TurnToken() : this(0) { } private TurnToken(int count) { this.Count = count; } public TurnToken Next => new(this.Count + 1); } private sealed class StateTestExecutor : TestingExecutor { private static Func>[] WrapActions(ScopeKey stateKey, Func[] stateActions) { Func>[] result = new Func>[stateActions.Length]; for (int i = 0; i < stateActions.Length; i++) { result[i] = CreateWrapperAsync(stateActions[i]); } return result; Func> CreateWrapperAsync(Func action) { return async (turn, context, cancellation) => { TState? state = await context.ReadStateAsync(stateKey.Key, stateKey.ScopeId.ScopeName) .ConfigureAwait(false); state = action(state); await context.QueueStateUpdateAsync(stateKey.Key, state, stateKey.ScopeId.ScopeName); return turn.Next; }; } } public ScopeKey StateKey { get; } public StateTestExecutor(ScopeKey stateKey, bool loop = false, params Func[] stateActions) : base(stateKey.ScopeId.ExecutorId, loop, WrapActions(stateKey, stateActions)) { this.StateKey = stateKey; } } private static Func CreateOrIncrement(int defaultValue = default) => currState => currState.HasValue ? currState + 1 : defaultValue; private static Func ValidateState(int expectedValue, string? because = null, params object[] becauseArgs) => (int? currState) => { currState.Should().Be(expectedValue, because, becauseArgs); return currState; }; private static Func MaxTurns(int maxTurns) => (object? maybeTurn) => maybeTurn is not TurnToken turn || turn.Count < maxTurns; [Fact] public async Task InProcessRun_StateShouldPersist_NotCheckpointedAsync() { StateTestExecutor writer = new( new ScopeKey("Writer", "TestScope", "TestKey"), loop: false, CreateOrIncrement(), CreateOrIncrement() ); StateTestExecutor validator = new( new ScopeKey("Validator", "TestScope", "TestKey"), loop: false, ValidateState(0), ValidateState(1) ); Workflow workflow = new WorkflowBuilder(writer) .AddEdge(writer, validator, MaxTurns(4)) .AddEdge(validator, writer, MaxTurns(4)).Build(); Run run = await InProcessExecution.RunAsync(workflow, new()); run.Status.Should().Be(RunStatus.Idle); } [Fact] public async Task InProcessRun_StateShouldPersist_CheckpointedAsync() { StateTestExecutor writer = new( new ScopeKey("Writer", "TestScope", "TestKey"), loop: false, CreateOrIncrement(), CreateOrIncrement() ); StateTestExecutor validator = new( new ScopeKey("Validator", "TestScope", "TestKey"), loop: false, ValidateState(0), ValidateState(1) ); Workflow workflow = new WorkflowBuilder(writer) .AddEdge(writer, validator, MaxTurns(4)) .AddEdge(validator, writer, MaxTurns(4)).Build(); Checkpointed checkpointed = await InProcessExecution.RunAsync(workflow, new(), new CheckpointManager()); checkpointed.Checkpoints.Should().HaveCount(6); checkpointed.Run.Status.Should().Be(RunStatus.Idle); } [Fact] public async Task InProcessRun_StateShouldError_TwoExecutorsAsync() { ForwardMessageExecutor forward = new(); using StateTestExecutor testExecutor = new( new ScopeKey("StateTestExecutor", "TestScope", "TestKey"), loop: false, CreateOrIncrement() ); using StateTestExecutor testExecutor2 = new( new ScopeKey("StateTestExecutor2", "TestScope", "TestKey"), loop: false, CreateOrIncrement() ); Workflow workflow = new WorkflowBuilder(forward) .AddFanOutEdge(forward, targets: [testExecutor, testExecutor2]) .Build(); var act = async () => await InProcessExecution.RunAsync(workflow, new()); var result = await act.Should() .ThrowAsync("multiple writers to the same shared scope key"); } }