Files
agent-framework/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs
T
Stephen ToubandGitHub 456e7d1b65 Round 2 of cleanup for agent runtime and orchestrations (#164)
- Moved InProcessRuntime type into abstractions package and deleted InProcess package.
- Moved several members of IAgentRuntime to be extension methods instead, e.g. multiple GetActorAsync overloads.
- Added synchronous RegisterMessageHandler overloads and used them to avoid unnecessary async usage at call sites.
- Removed unnecessary surface area from InProcessRuntime, e.g. StopAsync, RunUntilIdleAsync, etc.
- Fixed spin loop in InProcessRuntime that would consume an entire core for the duration of the orchestration's operation.
- Removed a bunch of allocation from InProcessRuntime.
- Made a runtime optional for orchestrations, defaulting to using a temporary InProcessRuntime if none is provided.
- Removed custom delegate types from orchestrations.
- Consolidated namespaces.
- Used records to simplify message classes.
- Tweaked naming on AgentActor to make purpose of protected methods more clear.
- Removed invocation in AgentActor.InvokeAsync of empty update / isFinal parameter.
- Changed OrchestrationHandoffs to avoid needing to pass in agents duplicatively.
- Made various extension methods, such as those on OrchestrationHandoffsExtensions, into instance methods.
2025-07-11 11:12:18 -04:00

254 lines
8.6 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Moq;
namespace Microsoft.Agents.Orchestration.UnitTest;
public class HandoffsTests
{
[Fact]
public void EmptyConstructorsCreateEmptyCollections()
{
AgentHandoffs agentHandoffs = [];
Assert.Empty(agentHandoffs);
OrchestrationHandoffs orchestrationHandoffs = new("first");
Assert.Empty(orchestrationHandoffs);
Assert.Equal("first", orchestrationHandoffs.FirstAgentName);
}
[Fact]
public void DictionaryConstructorsInvalidFirstAgent()
{
Assert.Throws<ArgumentNullException>(() => new OrchestrationHandoffs((string)null!));
Assert.Throws<ArgumentException>(() => new OrchestrationHandoffs(string.Empty));
Assert.Throws<ArgumentException>(() => new OrchestrationHandoffs(" "));
}
[Fact]
public void AddWithAgentObjectsCreatesHandoffRelationships()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent1 = CreateAgent("target1", "Target Agent 1");
Agent targetAgent2 = CreateAgent("target2", "Target Agent 2");
// Act
handoffs.Add(sourceAgent, targetAgent1, targetAgent2);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
Assert.True(handoffs.ContainsKey("source"));
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Equal(2, sourceHandoffs.Count);
Assert.Equal("Target Agent 1", sourceHandoffs["target1"]);
Assert.Equal("Target Agent 2", sourceHandoffs["target2"]);
}
[Fact]
public void AddWithAgentAndCustomDescriptionUsesCustomDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent = CreateAgent("target", "Target Agent");
string customDescription = "Custom handoff description";
// Act
handoffs.Add(sourceAgent, targetAgent, customDescription);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal(customDescription, sourceHandoffs["target"]);
}
[Fact]
public void AddWithAgentAndTargetNameAddsHandoffWithDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
string targetName = "targetName";
string description = "Target description";
// Act
handoffs.Add(sourceAgent, targetName, description);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal(description, sourceHandoffs[targetName]);
}
[Fact]
public void AddWithSourceNameAndTargetNameAddsHandoffWithDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("sourceName");
string sourceName = "sourceName";
string targetName = "targetName";
string description = "Target description";
// Act
handoffs.Add(sourceName, targetName, description);
// Assert
Assert.Single(handoffs);
Assert.Equal("sourceName", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs[sourceName];
Assert.Single(sourceHandoffs);
Assert.Equal(description, sourceHandoffs[targetName]);
}
[Fact]
public void AddWithMultipleSourcesAndTargetsCreatesCorrectStructure()
{
// Arrange
OrchestrationHandoffs handoffs = new("source1");
Agent source1 = CreateAgent("source1", "Source Agent 1");
Agent source2 = CreateAgent("source2", "Source Agent 2");
Agent target1 = CreateAgent("target1", "Target Agent 1");
Agent target2 = CreateAgent("target2", "Target Agent 2");
Agent target3 = CreateAgent("target3", "Target Agent 3");
// Act
handoffs.Add(source1, target1, target2);
handoffs.Add(source2, target2, target3);
handoffs.Add(source1, target3, "Custom description");
// Assert
Assert.Equal(2, handoffs.Count);
Assert.Equal("source1", handoffs.FirstAgentName);
// Check source1's targets
AgentHandoffs source1Handoffs = handoffs["source1"];
Assert.Equal(3, source1Handoffs.Count);
Assert.Equal("Target Agent 1", source1Handoffs["target1"]);
Assert.Equal("Target Agent 2", source1Handoffs["target2"]);
Assert.Equal("Custom description", source1Handoffs["target3"]);
// Check source2's targets
AgentHandoffs source2Handoffs = handoffs["source2"];
Assert.Equal(2, source2Handoffs.Count);
Assert.Equal("Target Agent 2", source2Handoffs["target2"]);
Assert.Equal("Target Agent 3", source2Handoffs["target3"]);
}
[Fact]
public void StaticAddCreatesNewOrchestrationHandoffs()
{
// Arrange
Agent source = CreateAgent("source", "Source Agent");
Agent target1 = CreateAgent("target1", "Target Agent 1");
Agent target2 = CreateAgent("target2", "Target Agent 2");
// Act
OrchestrationHandoffs handoffs =
OrchestrationHandoffs
.StartWith(source)
.Add(source, target1, target2);
// Assert
Assert.NotNull(handoffs);
Assert.Equal(source.Id, handoffs.FirstAgentName);
Assert.Single(handoffs);
Assert.True(handoffs.ContainsKey("source"));
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Equal(2, sourceHandoffs.Count);
Assert.Equal("Target Agent 1", sourceHandoffs["target1"]);
Assert.Equal("Target Agent 2", sourceHandoffs["target2"]);
}
[Fact]
public void AddWithAgentsWithNoNameUsesId()
{
// Arrange
OrchestrationHandoffs handoffs = new("source-id");
Agent sourceAgent = CreateAgent(id: "source-id", name: null);
Agent targetAgent = CreateAgent(id: "target-id", name: null, description: "Target Description");
// Act
handoffs.Add(sourceAgent, targetAgent);
// Assert
Assert.Single(handoffs);
Assert.Equal("source-id", handoffs.FirstAgentName);
Assert.True(handoffs.ContainsKey("source-id"));
AgentHandoffs sourceHandoffs = handoffs["source-id"];
Assert.Single(sourceHandoffs);
Assert.Equal("Target Description", sourceHandoffs["target-id"]);
}
[Fact]
public void AddWithAgentWithNoDescriptionUsesName()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent1 = CreateAgent("target1", name: "target 1");
// Act
handoffs.Add(sourceAgent, targetAgent1);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
Assert.True(handoffs.ContainsKey("source"));
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal("target 1", sourceHandoffs["target 1"]);
}
[Fact]
public void AddWithAgentWithNoDescriptionOrNameThrows()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent1 = CreateAgent("target1");
// Act
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() => handoffs.Add(sourceAgent, targetAgent1));
// Assert
Assert.Equal("The provided target agent with Id 'target1' has no description or name, and no handoff description has been provided. At least one of these are required to register a handoff so that the appropriate target agent can be chosen.", ex.Message);
}
private static ChatClientAgent CreateAgent(string id, string? description = null, string? name = null)
{
Mock<IChatClient> mockClient = new(MockBehavior.Loose);
ChatClientAgentOptions options =
new()
{
Id = id,
Name = name,
Description = description,
};
ChatClientAgent mockAgent = new(mockClient.Object, options);
return mockAgent;
}
}