mirror of
https://github.com/microsoft/autogen.git
synced 2025-07-25 18:01:03 +00:00

Factors out RunContext management into separate classes: * Defines a LifecycleObject abstraction to manage initialization / deinitialization * Defines RunContextStack to enable a stack of init/deinit layers * Defines a RunManager to own ensuring proper semantics for performing a "single execution at a time" call while enabling a polymorphic base. Implements Reset Closes #5799 Unblocks #5800 (needs RunContext refactor too)
265 lines
9.1 KiB
C#
265 lines
9.1 KiB
C#
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// RunContextStackTests.cs
|
|
|
|
using FluentAssertions;
|
|
using Microsoft.AutoGen.AgentChat.GroupChat;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AutoGen.AgentChat.Tests;
|
|
|
|
public class RunContextStackTests
|
|
{
|
|
public static IRunContextLayer CreateLayer(Action<Mock<IRunContextLayer>>? setupAction = null)
|
|
{
|
|
Mock<IRunContextLayer> layer = new();
|
|
|
|
if (setupAction != null)
|
|
{
|
|
setupAction(layer);
|
|
}
|
|
else
|
|
{
|
|
layer.Setup(l => l.InitializeAsync()).Returns(ValueTask.CompletedTask);
|
|
layer.Setup(l => l.DeinitializeAsync()).Returns(ValueTask.CompletedTask);
|
|
}
|
|
|
|
return layer.Object;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Initialize_SucceedsWithNoLayers()
|
|
{
|
|
// Arrange
|
|
RunContextStack stack = new RunContextStack();
|
|
|
|
// Act
|
|
Func<Task> func = async () => await stack.InitializeAsync();
|
|
|
|
// Assert
|
|
await func.Should().NotThrowAsync("RunContextStack should work without context frames");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Deinitialize_SucceedsWithNoLayers()
|
|
{
|
|
// Arrange
|
|
RunContextStack stack = new RunContextStack();
|
|
await stack.InitializeAsync();
|
|
|
|
// Act
|
|
Func<Task> func = async () => await stack.DeinitializeAsync();
|
|
|
|
// Assert
|
|
await func.Should().NotThrowAsync("RunContextStack should work without context frames");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushLayer_FailsWhenInitialized()
|
|
{
|
|
// Arrange
|
|
RunContextStack stack = new RunContextStack();
|
|
await stack.InitializeAsync();
|
|
|
|
// Act
|
|
Action pushLayerAction = () => stack.PushLayer(CreateLayer());
|
|
|
|
// Assert
|
|
pushLayerAction.Should().Throw<InvalidOperationException>("RunContextStack should not allow pushing layers when initialized");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopLayer_FailsWhenInitialized()
|
|
{
|
|
// Arrange
|
|
RunContextStack stack = new RunContextStack();
|
|
await stack.InitializeAsync();
|
|
|
|
// Act
|
|
Action popLayerAction = stack.PopLayer;
|
|
|
|
// Assert
|
|
popLayerAction.Should().Throw<InvalidOperationException>("RunContextStack should not allow popping layers when initialized");
|
|
}
|
|
|
|
[Fact]
|
|
public Task InitializeDeinitialize_ShouldInvokeLayersInOrder_WhenPushed()
|
|
{
|
|
return PrepareAndRun_LayerOrderTest(Arrange);
|
|
|
|
static RunContextStack Arrange(IEnumerable<IRunContextLayer> layers)
|
|
{
|
|
RunContextStack stack = new RunContextStack();
|
|
|
|
foreach (IRunContextLayer layer in layers)
|
|
{
|
|
stack.PushLayer(layer);
|
|
}
|
|
|
|
return stack;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public Task InitializeDeinitialize_ShouldInvokeLayersInOrder_WhenConstructed()
|
|
{
|
|
return PrepareAndRun_LayerOrderTest(Arrange);
|
|
|
|
static RunContextStack Arrange(IEnumerable<IRunContextLayer> layers)
|
|
{
|
|
return new RunContextStack([.. layers]);
|
|
}
|
|
}
|
|
|
|
private async Task PrepareAndRun_LayerOrderTest(Func<IEnumerable<IRunContextLayer>, RunContextStack> arrangeStack)
|
|
{
|
|
bool bottomLayerInit = false;
|
|
bool bottomLayerDeinit = false;
|
|
|
|
bool topLayerInit = false;
|
|
bool topLayerDeinit = false;
|
|
|
|
// Arrange
|
|
IRunContextLayer topLayer = CreateLayer(mock =>
|
|
{
|
|
mock.Setup(l => l.InitializeAsync()).Callback(
|
|
() =>
|
|
{
|
|
topLayerInit.Should().BeFalse("Top Layer should not have been initialized yet");
|
|
bottomLayerInit.Should().BeFalse("Bottom Layer should not have been initialized yet");
|
|
|
|
topLayerInit = true;
|
|
}
|
|
).Returns(ValueTask.CompletedTask).Verifiable();
|
|
mock.Setup(l => l.DeinitializeAsync()).Callback(
|
|
() =>
|
|
{
|
|
topLayerInit.Should().BeTrue("Top Layer should have been initialized");
|
|
bottomLayerInit.Should().BeTrue("Bottom Layer should have been initialized");
|
|
|
|
bottomLayerDeinit.Should().BeTrue("Bottom Layer should be deinitialized before Top Layer");
|
|
topLayerDeinit.Should().BeFalse("Top Layer should not have been deinitialized yet");
|
|
|
|
topLayerDeinit = true;
|
|
}).Returns(ValueTask.CompletedTask).Verifiable();
|
|
});
|
|
|
|
IRunContextLayer bottomLayer = CreateLayer(mock =>
|
|
{
|
|
mock.Setup(l => l.InitializeAsync()).Callback(
|
|
() =>
|
|
{
|
|
topLayerInit.Should().BeTrue("Top Layer should have been initialized before Bottom Layer");
|
|
bottomLayerInit.Should().BeFalse("Bottom Layer should not have been initialized yet");
|
|
|
|
bottomLayerInit = true;
|
|
}
|
|
).Returns(ValueTask.CompletedTask).Verifiable();
|
|
mock.Setup(l => l.DeinitializeAsync()).Callback(
|
|
() =>
|
|
{
|
|
topLayerInit.Should().BeTrue("Top Layer should have been initialized");
|
|
bottomLayerInit.Should().BeTrue("Bottom Layer should have been initialized");
|
|
|
|
bottomLayerDeinit.Should().BeFalse("Bottom Layer should not have been deinitialized yet");
|
|
topLayerDeinit.Should().BeFalse("Top Layer should not have been deinitialized yet");
|
|
|
|
bottomLayerDeinit = true;
|
|
}).Returns(ValueTask.CompletedTask).Verifiable();
|
|
});
|
|
|
|
RunContextStack stack = arrangeStack([bottomLayer, topLayer]);
|
|
|
|
// Act
|
|
await stack.InitializeAsync();
|
|
|
|
// Assert
|
|
Mock.Get(topLayer).Verify(l => l.InitializeAsync(), Times.Once);
|
|
Mock.Get(bottomLayer).Verify(l => l.InitializeAsync(), Times.Once);
|
|
|
|
bottomLayerInit.Should().BeTrue("Top Layer should have been initialized");
|
|
topLayerInit.Should().BeTrue("Bottom Layer should have been initialized");
|
|
|
|
// Act 2
|
|
await stack.DeinitializeAsync();
|
|
|
|
// Assert 2
|
|
Mock.Get(bottomLayer).Verify(l => l.DeinitializeAsync(), Times.Once);
|
|
Mock.Get(topLayer).Verify(l => l.DeinitializeAsync(), Times.Once);
|
|
|
|
topLayerDeinit.Should().BeTrue("Bottom Layer should have been deinitialized");
|
|
bottomLayerDeinit.Should().BeTrue("Top Layer should have been deinitialized");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateOverrides_GetsInvokedOnError()
|
|
{
|
|
int initializeErrors = 0;
|
|
int deinitializeErrors = 0;
|
|
|
|
// Arrange
|
|
IRunContextLayer overrides = RunContextStack.OverrideErrors(
|
|
initializeError: () => initializeErrors++,
|
|
deinitializeError: () => deinitializeErrors++);
|
|
|
|
RunContextStack stack = new RunContextStack(overrides);
|
|
|
|
// Act
|
|
Func<Task> deinitializeAction = async () => await stack.DeinitializeAsync();
|
|
|
|
// Assert
|
|
// The first Deinitialize should throw because we only override after the top layer it initialized
|
|
await deinitializeAction.Should().ThrowAsync<InvalidOperationException>("Deinitialize should throw an exception");
|
|
|
|
// Act 2
|
|
await stack.InitializeAsync();
|
|
Func<Task> initializeAgainAction = async () => await stack.InitializeAsync();
|
|
|
|
// Assert 2
|
|
// The second Initialize should not throw, because the overrides should be applied
|
|
await initializeAgainAction.Should().NotThrowAsync("Initialize should not throw an exception");
|
|
|
|
initializeErrors.Should().Be(1, "There should be one initialization error");
|
|
deinitializeErrors.Should().Be(0, "There should not have been an overriden invocation of a deinitialize error.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Enter_DisposableWorksIdempotently()
|
|
{
|
|
int initializeCount = 0;
|
|
int deinitializeCount = 0;
|
|
|
|
// Arrange
|
|
IRunContextLayer layer = CreateLayer(mock =>
|
|
{
|
|
mock.Setup(l => l.InitializeAsync()).Callback(() => initializeCount++).Returns(ValueTask.CompletedTask);
|
|
mock.Setup(l => l.DeinitializeAsync()).Callback(() => deinitializeCount++).Returns(ValueTask.CompletedTask);
|
|
});
|
|
|
|
RunContextStack stack = new RunContextStack(layer);
|
|
|
|
// Act
|
|
IAsyncDisposable exitDisposable = await stack.Enter();
|
|
|
|
// Assert
|
|
initializeCount.Should().Be(1, "Layer should have been initialized once");
|
|
deinitializeCount.Should().Be(0, "Layer should not have been deinitialized yet");
|
|
|
|
// Act 2
|
|
await exitDisposable.DisposeAsync();
|
|
|
|
// Assert 2
|
|
initializeCount.Should().Be(1, "Layer should have been initialized once");
|
|
deinitializeCount.Should().Be(1, "Layer should have been deinitialized once");
|
|
|
|
// Act 3
|
|
Func<Task> disposeAgain = async () => await exitDisposable.DisposeAsync();
|
|
|
|
// Assert 3
|
|
await disposeAgain.Should().NotThrowAsync("Dispose should be idempotent");
|
|
|
|
initializeCount.Should().Be(1, "Layer should have been initialized once");
|
|
deinitializeCount.Should().Be(1, "Layer should have been deinitialized once");
|
|
}
|
|
}
|