Skip to content

Commit c14beed

Browse files
authored
test: Add Handoff composability test (#5208)
1 parent 43d9897 commit c14beed

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed

dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffAgentExecutorTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System;
4+
using System.Collections.Generic;
35
using System.Linq;
6+
using System.Runtime.CompilerServices;
7+
using System.Threading;
48
using System.Threading.Tasks;
9+
using FluentAssertions;
510
using Microsoft.Agents.AI.Workflows.Specialized;
11+
using Microsoft.Extensions.AI;
612

713
namespace Microsoft.Agents.AI.Workflows.UnitTests;
814

@@ -68,4 +74,98 @@ public async Task Test_HandoffAgentExecutor_EmitsResponseIFFConfiguredAsync(bool
6874
AgentResponseEvent[] updates = testContext.Events.OfType<AgentResponseEvent>().ToArray();
6975
CheckResponseEventsAgainstTestMessages(updates, expectingResponse: executorSetting, agent.GetDescriptiveId());
7076
}
77+
78+
[Fact]
79+
public async Task Test_HandoffAgentExecutor_PreservesExistingInstructionsAndToolsAsync()
80+
{
81+
// Arrange
82+
const string BaseInstructions = "BaseInstructions";
83+
const string HandoffInstructions = "HandoffInstructions";
84+
85+
AITool someTool = AIFunctionFactory.CreateDeclaration("BaseTool", null, AIFunctionFactory.Create(() => { }).JsonSchema);
86+
87+
OptionValidatingChatClient chatClient = new(BaseInstructions, HandoffInstructions, someTool);
88+
AIAgent handoffAgent = chatClient.AsAIAgent(BaseInstructions, tools: [someTool]);
89+
AIAgent targetAgent = new TestEchoAgent();
90+
91+
HandoffAgentExecutorOptions options = new(HandoffInstructions, false, null, HandoffToolCallFilteringBehavior.None);
92+
HandoffTarget handoff = new(targetAgent);
93+
HandoffAgentExecutor executor = new(handoffAgent, [handoff], options);
94+
95+
TestWorkflowContext testContext = new(executor.Id);
96+
HandoffState state = new(new(false), null, [], null);
97+
98+
// Act / Assert
99+
Func<Task> runStreamingAsync = async () => await executor.HandleAsync(state, testContext);
100+
await runStreamingAsync.Should().NotThrowAsync();
101+
}
102+
103+
private sealed class OptionValidatingChatClient(string baseInstructions, string handoffInstructions, AITool baseTool) : IChatClient
104+
{
105+
public void Dispose()
106+
{
107+
}
108+
109+
private void CheckOptions(ChatOptions? options)
110+
{
111+
options.Should().NotBeNull();
112+
113+
options.Instructions.Should().NotBeNullOrEmpty("Handoff orchestration should preserve and augment instructions.")
114+
.And.Contain(baseInstructions, because: "Handoff orchestration should preserve existing instructions.")
115+
.And.Contain(handoffInstructions, because: "Handoff orchestration should inject handoff instructions.");
116+
117+
options.Tools.Should().NotBeNullOrEmpty("Handoff orchestration should preserve and augment tools.")
118+
.And.Contain(tool => tool.Name == baseTool.Name, "Handoff orchestration should preserve existing tools.")
119+
.And.Contain(tool => tool.Name.StartsWith(HandoffWorkflowBuilder.FunctionPrefix, StringComparison.Ordinal),
120+
because: "Handoff orchestration should inject handoff tools.");
121+
}
122+
123+
private List<ChatMessage> ResponseMessages =>
124+
[
125+
new ChatMessage(ChatRole.Assistant, "Ok")
126+
{
127+
MessageId = Guid.NewGuid().ToString(),
128+
AuthorName = nameof(OptionValidatingChatClient)
129+
}
130+
];
131+
132+
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
133+
{
134+
this.CheckOptions(options);
135+
136+
ChatResponse response = new(this.ResponseMessages)
137+
{
138+
ResponseId = Guid.NewGuid().ToString("N"),
139+
CreatedAt = DateTimeOffset.Now
140+
};
141+
142+
return Task.FromResult(response);
143+
}
144+
145+
public object? GetService(Type serviceType, object? serviceKey = null)
146+
{
147+
if (serviceType == typeof(OptionValidatingChatClient))
148+
{
149+
return this;
150+
}
151+
152+
return null;
153+
}
154+
155+
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
156+
{
157+
this.CheckOptions(options);
158+
159+
string responseId = Guid.NewGuid().ToString("N");
160+
foreach (ChatMessage message in this.ResponseMessages)
161+
{
162+
yield return new(message.Role, message.Contents)
163+
{
164+
ResponseId = responseId,
165+
MessageId = message.MessageId,
166+
CreatedAt = DateTimeOffset.Now
167+
};
168+
}
169+
}
170+
}
71171
}

0 commit comments

Comments
 (0)