// Copyright (c) Microsoft Corporation. All rights reserved. // SendMessageTests.cs using System.Diagnostics; using System.Reflection; using FluentAssertions; using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.Logging; using Xunit; namespace Microsoft.AutoGen.Core.Tests; [Trait("Category", "UnitV2")] public class SendMessageTests { private sealed class BasicMessage { public string Content { get; set; } = string.Empty; } private sealed class SendOnAgent : BaseAgent, IHandle { private IList targetKeys; public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList targetKeys, ILogger? logger = null) : base(id, runtime, description, logger) { this.targetKeys = targetKeys; } public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) { foreach (Guid targetKey in targetKeys) { AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); BasicMessage message = new BasicMessage { Content = $"@{targetKey}: item.Content" }; await this.Runtime.SendMessageAsync(message, targetId); } } } private sealed class ReceiverAgent : BaseAgent, IHandle { public List Messages { get; } = new(); public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) : base(id, runtime, description, logger) { } public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) { this.Messages.Add(item); return ValueTask.CompletedTask; } } private sealed class ProcessorAgent : BaseAgent, IHandle { private Func ProcessFunc { get; } public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func processFunc, string description, ILogger? logger = null) : base(id, runtime, description, logger) { this.ProcessFunc = processFunc; } public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) { BasicMessage result = new() { Content = this.ProcessFunc(item.Content) }; return ValueTask.FromResult(result); } } private sealed class TestException : Exception { } private sealed class CancelAgent : BaseAgent, IHandle { public CancelAgent(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) : base(id, runtime, description, logger) { } public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) { CancellationToken cancelledToken = new CancellationToken(canceled: true); cancelledToken.ThrowIfCancellationRequested(); return ValueTask.FromResult(item); } } private sealed class ErrorAgent : BaseAgent, IHandle { public ErrorAgent(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) : base(id, runtime, description, logger) { } public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) { throw new TestException(); } } private sealed class SendTestFixture { private Dictionary AgentsTypeMap { get; } = new(); public InProcessRuntime Runtime { get; private set; } = new(); public ValueTask RegisterFactoryMapInstances(AgentType type, Func> factory) where TAgent : IHostableAgent { Func> wrappedFactory = async (id, runtime) => { TAgent agent = await factory(id, runtime); this.GetAgentInstances()[id] = agent; return agent; }; return this.Runtime.RegisterAgentFactoryAsync(type, wrappedFactory); } public Dictionary GetAgentInstances() where TAgent : IHostableAgent { if (!AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) || maybeAgentMap is not Dictionary result) { this.AgentsTypeMap[typeof(TAgent)] = result = new Dictionary(); } return result; } public async ValueTask RunTestAsync(AgentId sendTarget, object message, string? messageId = null) { messageId ??= Guid.NewGuid().ToString(); await this.Runtime.StartAsync(); object? result = await this.Runtime.SendMessageAsync(message, sendTarget, messageId: messageId); await this.Runtime.RunUntilIdleAsync(); return result; } } [Fact] public async Task Test_SendMessage_ReturnsValue() { Func ProcessFunc = (s) => $"Processed({s})"; SendTestFixture fixture = new SendTestFixture(); await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent), (id, runtime) => ValueTask.FromResult(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty))); AgentId targetAgent = new AgentId(nameof(ProcessorAgent), Guid.NewGuid().ToString()); object? maybeResult = await fixture.RunTestAsync(targetAgent, new BasicMessage { Content = "1" }); maybeResult.Should().NotBeNull() .And.BeOfType() .And.Match(m => m.Content == "Processed(1)"); } [Fact] public async Task Test_SendMessage_Cancellation() { SendTestFixture fixture = new SendTestFixture(); await fixture.RegisterFactoryMapInstances(nameof(CancelAgent), (id, runtime) => ValueTask.FromResult(new CancelAgent(id, runtime, string.Empty))); AgentId targetAgent = new AgentId(nameof(CancelAgent), Guid.NewGuid().ToString()); Func testAction = () => fixture.RunTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask(); // TODO: Do we want to do the unwrap in this case? await testAction.Should().ThrowAsync(); } [Fact] public async Task Test_SendMessage_Error() { SendTestFixture fixture = new SendTestFixture(); await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent), (id, runtime) => ValueTask.FromResult(new ErrorAgent(id, runtime, string.Empty))); AgentId targetAgent = new AgentId(nameof(ErrorAgent), Guid.NewGuid().ToString()); Func testAction = () => fixture.RunTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask(); (await testAction.Should().ThrowAsync()) .WithInnerException(); } [Fact] public async Task TesT_SendMessage_FromSendMessageHandler() { Guid[] targetGuids = [Guid.NewGuid(), Guid.NewGuid()]; SendTestFixture fixture = new SendTestFixture(); Dictionary sendAgents = fixture.GetAgentInstances(); Dictionary receiverAgents = fixture.GetAgentInstances(); await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent), (id, runtime) => ValueTask.FromResult(new SendOnAgent(id, runtime, string.Empty, targetGuids))); await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent), (id, runtime) => ValueTask.FromResult(new ReceiverAgent(id, runtime, string.Empty))); AgentId targetAgent = new AgentId(nameof(SendOnAgent), Guid.NewGuid().ToString()); Task testTask = fixture.RunTestAsync(targetAgent, new BasicMessage { Content = "Hello" }).AsTask(); // We do not actually expect to wait the timeout here, but it is still better than waiting the 10 min // timeout that the tests default to. A failure will fail regardless of what timeout value we set. TimeSpan timeout = Debugger.IsAttached ? TimeSpan.FromSeconds(120) : TimeSpan.FromSeconds(10); Task timeoutTask = Task.Delay(timeout); Task completedTask = await Task.WhenAny([testTask, timeoutTask]); completedTask.Should().Be(testTask, "SendOnAgent should complete before timeout"); // Check that each of the target agents received the message foreach (var targetKey in targetGuids) { var targetId = new AgentId(nameof(ReceiverAgent), targetKey.ToString()); receiverAgents[targetId].Messages.Should().ContainSingle(m => m.Content == $"@{targetKey}: item.Content"); } } }