mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Expand Workflow Unit Test Coverage (#5390)
* refactor: remove dead code * refactor: remove ignore YieldsMessageAttribute - the correct one to use is YieldsOutputAttribute - fixes a comment that mistakenly refers to `.YieldsMessage()` which does not exist. * fix: ChatForwardingExecutor does not use correct role for string messages - make ChatForwardingExecutor use its configured role for string messages rather than always use ChatRole.User - add ChatForwardingExecutor tests * fixup: remove unused attribute * test: Add tests for failure when .AsAgent used on a non-ChatProtocol workflow * test: Add FunctionExecutor tests - also fixes Send and YieldOutput type registration for synchronous output-returning delegates * test: Suppress CodeCoverage for obsolete names * fix: Re-add Obsolete attributes - avoid hard-breaking change - properly notify users that these attributes get ignored
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
internal enum ChatRoleType
|
||||
{
|
||||
None,
|
||||
User,
|
||||
Assistant,
|
||||
Custom
|
||||
}
|
||||
|
||||
internal static class ChatRoleTestingExtensions
|
||||
{
|
||||
public const string CustomChatRoleName = nameof(CustomChatRole);
|
||||
|
||||
public static ChatRole CustomChatRole { get; } = new(CustomChatRoleName);
|
||||
|
||||
public static ChatRole? ToChatRole(this ChatRoleType type)
|
||||
=> type switch
|
||||
{
|
||||
ChatRoleType.None => null,
|
||||
ChatRoleType.User => ChatRole.User,
|
||||
ChatRoleType.Assistant => ChatRole.Assistant,
|
||||
ChatRoleType.Custom => CustomChatRole,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(type),
|
||||
type,
|
||||
$"Invalid ChatRoleType {type}; expecting one of {string.Join(",",
|
||||
[null,
|
||||
ChatRole.User,
|
||||
ChatRole.Assistant,
|
||||
CustomChatRole])}")
|
||||
};
|
||||
}
|
||||
|
||||
public class ChatForwardingExecutorTests
|
||||
{
|
||||
private async Task<TestWorkflowContext> RunForwardMessageTestAsync<TMessage>(ChatForwardingExecutor executor, TMessage message)
|
||||
where TMessage : notnull
|
||||
{
|
||||
// Ensure that we have constructed the Protocol (and registered the handlers)
|
||||
_ = executor.Protocol;
|
||||
|
||||
TestWorkflowContext testContext = new(executor.Id);
|
||||
object? callResult = await executor.ExecuteCoreAsync(message, new TypeId(typeof(TMessage)), testContext);
|
||||
|
||||
callResult.Should().BeNull(); // ChatForwardingExecutor's do not have a return type
|
||||
|
||||
return testContext;
|
||||
}
|
||||
|
||||
private const string TestMessageContent = nameof(TestMessageContent);
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_DoesNotForwardStringByDefaultAsync()
|
||||
{
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
|
||||
// Act
|
||||
Func<Task<TestWorkflowContext>> action = () => this.RunForwardMessageTestAsync(executor, TestMessageContent);
|
||||
await action.Should().ThrowAsync<NotSupportedException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ChatRoleType.None)]
|
||||
[InlineData(ChatRoleType.User)]
|
||||
[InlineData(ChatRoleType.Assistant)]
|
||||
[InlineData(ChatRoleType.Custom)]
|
||||
internal async Task Test_ChatForwardingExecutor_ForwardsStringIfConfiguredAsync(ChatRoleType chatRoleType)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutorOptions options = new()
|
||||
{
|
||||
StringMessageChatRole = chatRoleType.ToChatRole()
|
||||
};
|
||||
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor), options);
|
||||
|
||||
// Act
|
||||
Func<Task<TestWorkflowContext>> action = () => this.RunForwardMessageTestAsync(executor, TestMessageContent);
|
||||
|
||||
// Assert
|
||||
if (options.StringMessageChatRole is ChatRole chatRole)
|
||||
{
|
||||
TestWorkflowContext testContext = await action();
|
||||
|
||||
testContext.SentMessages.Should().HaveCount(1)
|
||||
.And.BeEquivalentTo([new ChatMessage(chatRole, TestMessageContent)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
await action.Should().ThrowAsync<NotSupportedException>();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageUnmodifiedAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ChatMessage testMessage = new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent);
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessage);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(message => ReferenceEquals(message, testMessage));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageListUnmodifiedAsync(bool sendAsIEnumerable)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
List<ChatMessage> testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext
|
||||
= sendAsIEnumerable
|
||||
? await this.RunForwardMessageTestAsync<IEnumerable<ChatMessage>>(executor, testMessages)
|
||||
: await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => ReferenceEquals(messages, testMessages));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageArrayUnchangedAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ChatMessage[] testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => ReferenceEquals(messages, testMessages));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsMessageCollectionAsListAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ConcurrentBag<ChatMessage> testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => !ReferenceEquals(messages, testMessages))
|
||||
.And.Subject.Single().Should().BeEquivalentTo(testMessages);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsTurnTokenUnmodifiedAsync(bool? emitEvents)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
TurnToken testTurnToken = new(emitEvents);
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testTurnToken);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().BeEquivalentTo([testTurnToken]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
public class ExecutorTestsBase
|
||||
{
|
||||
public sealed record TextMessage(string Text);
|
||||
|
||||
public const string TestMessageContent = nameof(TestMessage);
|
||||
public static TextMessage TestMessage { get; } = new(TestMessageContent);
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Test Object")]
|
||||
public sealed record DataMessage(string Base64Bytes)
|
||||
{
|
||||
private static string ToBase64String(string text, Encoding? expectedEncoding)
|
||||
{
|
||||
byte[] bytes = (expectedEncoding ?? Encoding.UTF8).GetBytes(text);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
public DataMessage(TextMessage textMessage, Encoding? expectedEncoding = null) : this(ToBase64String(textMessage.Text, expectedEncoding))
|
||||
{ }
|
||||
}
|
||||
|
||||
public const string DataMessageContent = nameof(DataMessage);
|
||||
public static DataMessage TestDataMessage { get; } = new(TestMessage);
|
||||
|
||||
public sealed class InvocationEvent<TMessage>(TMessage message) : WorkflowEvent(message)
|
||||
{
|
||||
public TMessage Message => message;
|
||||
}
|
||||
|
||||
internal sealed record ExecutorTestResult(TestWorkflowContext Context, object? CallResult);
|
||||
|
||||
internal async ValueTask<ExecutorTestResult> Run_FunctionExecutor_MessageHandlerTestAsync<TMessage>(Executor executor, TMessage message, CancellationToken cancellationToken = default)
|
||||
where TMessage : notnull
|
||||
{
|
||||
TestWorkflowContext workflowContext = this.CreateWorkflowContext(executor);
|
||||
_ = executor.DescribeProtocol();
|
||||
|
||||
object? result = await executor.ExecuteCoreAsync(message, new(typeof(TMessage)), workflowContext, cancellationToken);
|
||||
|
||||
return new(workflowContext, result);
|
||||
}
|
||||
|
||||
internal static void CheckInvoked<TMessage>(ExecutorTestResult result, TMessage expectedInput, object? expectedCallResult = null)
|
||||
where TMessage : class
|
||||
{
|
||||
result.CallResult.Should().Be(expectedCallResult);
|
||||
|
||||
result.Context.EmittedEvents.Should().Contain(evt => evt is ExecutorInvokedEvent
|
||||
&& ((ExecutorInvokedEvent)evt).Data as TMessage == expectedInput)
|
||||
.And.Contain(evt => evt is ExecutorCompletedEvent
|
||||
&& ((ExecutorCompletedEvent)evt).Data == expectedCallResult);
|
||||
}
|
||||
|
||||
internal static void CheckInvoked<TMessage, TOutput>(ExecutorTestResult result, TMessage expectedInput, TOutput expectedCallResult)
|
||||
where TMessage : class
|
||||
where TOutput : class
|
||||
{
|
||||
result.CallResult.Should().Be(expectedCallResult);
|
||||
|
||||
result.Context.EmittedEvents.Should().Contain(evt => evt is ExecutorInvokedEvent
|
||||
&& ((ExecutorInvokedEvent)evt).Data as TMessage == expectedInput)
|
||||
.And.Contain(evt => evt is ExecutorCompletedEvent
|
||||
&& ((ExecutorCompletedEvent)evt).Data as TOutput == expectedCallResult);
|
||||
}
|
||||
|
||||
internal TestWorkflowContext CreateWorkflowContext(Executor executor) => new(executor.Id);
|
||||
}
|
||||
|
||||
public class FunctionExecutorTests : ExecutorTestsBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_FunctionExecutor__1_InvokesDelegateSuccessfullyAsync(bool useAsync)
|
||||
{
|
||||
// Arrange
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage);
|
||||
|
||||
// Helpers
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> default;
|
||||
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken) { }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_FunctionExecutor__2_InvokesDelegateSuccessfullyAsync(bool useAsync)
|
||||
{
|
||||
// Arrange
|
||||
FunctionExecutor<TextMessage, DataMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<,>), MessageHandlerAsync)
|
||||
: new(nameof(FunctionExecutor<,>), MessageHandler);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage, TestDataMessage);
|
||||
|
||||
// Helpers
|
||||
ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(new DataMessage(message));
|
||||
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__1_SendTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
IEnumerable<Type>? sendTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(TextMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, sentMessageTypes: sendTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, sentMessageTypes: sendTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(TextMessage)]);
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
ValueTask MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
void MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__2_SendTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = false,
|
||||
AutoYieldOutputHandlerResultObject = false
|
||||
};
|
||||
|
||||
IEnumerable<Type>? sendTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(TextMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor
|
||||
= useAsync
|
||||
? new(nameof(FunctionExecutor<,>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, options, sentMessageTypes: sendTypes)
|
||||
: new(nameof(FunctionExecutor<,>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, options, sentMessageTypes: sendTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(TextMessage)]);
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
ValueTask<DataMessage> MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
async ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.SendMessageAsync(message, cancellationToken);
|
||||
return new(message);
|
||||
}
|
||||
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
DataMessage MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
return new(message);
|
||||
}
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__1_YieldTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
IEnumerable<Type>? yieldTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(DataMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, outputTypes: yieldTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, outputTypes: yieldTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
ValueTask MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.YieldOutputAsync(new DataMessage(message), cancellationToken);
|
||||
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
void MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.YieldOutputAsync(new DataMessage(message), cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__2_YieldTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = false,
|
||||
AutoYieldOutputHandlerResultObject = false
|
||||
};
|
||||
|
||||
IEnumerable<Type>? yieldTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(DataMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor
|
||||
= useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, options, outputTypes: yieldTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, options, outputTypes: yieldTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
ValueTask<DataMessage> MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
async ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.YieldOutputAsync(new DataMessage(message), cancellationToken);
|
||||
return new(message);
|
||||
}
|
||||
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
DataMessage MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.YieldOutputAsync(new DataMessage(message), cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
return new(message);
|
||||
}
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false)]
|
||||
[InlineData(false, false, true)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
[InlineData(true, true, true)]
|
||||
public void Test_FunctionExecutor__1_ExecutorOptionsAreNoOp(bool useAsync, bool autoSendReturnValue, bool autoYieldReturnValue)
|
||||
{
|
||||
// Because FunctionExecutor<TInput> does not have a rail for a returned value, setting up options for it will
|
||||
// not register any output types
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = autoSendReturnValue,
|
||||
AutoYieldOutputHandlerResultObject = autoYieldReturnValue
|
||||
};
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync, options)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler, options);
|
||||
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false)]
|
||||
[InlineData(false, false, true)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
[InlineData(true, true, true)]
|
||||
public async Task Test_FunctionExecutor__2_ExecutorOptionsCauseCorrectRegistration_AndAutoBehaviorAsync(bool useAsync, bool autoSendReturnValue, bool autoYieldReturnValue)
|
||||
{
|
||||
// Arrange
|
||||
// Because FunctionExecutor<TInput> does not have a rail for a returned value, setting up options for it will
|
||||
// not register any output types
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = autoSendReturnValue,
|
||||
AutoYieldOutputHandlerResultObject = autoYieldReturnValue
|
||||
};
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync, options)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler, options);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage, TestDataMessage);
|
||||
if (autoSendReturnValue)
|
||||
{
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
result.Context.SentMessages.Should().ContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
result.Context.SentMessages.Should().NotContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
|
||||
if (autoYieldReturnValue)
|
||||
{
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
result.Context.YieldedOutputs.Should().ContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
result.Context.YieldedOutputs.Should().NotContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(new DataMessage(message));
|
||||
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(message);
|
||||
}
|
||||
}
|
||||
@@ -206,6 +206,14 @@ internal sealed class TurnTrackingStartExecutor : ChatProtocolExecutor
|
||||
}
|
||||
}
|
||||
|
||||
public class NonChatProtocolExecutor() : Executor<string>(nameof(NonChatProtocolExecutor))
|
||||
{
|
||||
public override ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkflowHostSmokeTests : AIAgentHostingExecutorTestsBase
|
||||
{
|
||||
private sealed class AlwaysFailsAIAgent(bool failByThrowing) : AIAgent
|
||||
@@ -732,6 +740,25 @@ public class WorkflowHostSmokeTests : AIAgentHostingExecutorTestsBase
|
||||
.BeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_AsAgent_FailsWhenNotChatProtocolAsync(bool runAsync)
|
||||
{
|
||||
// Arrange
|
||||
NonChatProtocolExecutor executor = new();
|
||||
executor.DescribeProtocol().IsChatProtocol().Should().BeFalse();
|
||||
|
||||
Workflow workflow = new WorkflowBuilder(executor).Build();
|
||||
AIAgent workflowAsAgent = workflow.AsAIAgent();
|
||||
|
||||
Func<Task> action = runAsync
|
||||
? () => workflowAsAgent.RunStreamingAsync().ToAgentResponseAsync()
|
||||
: () => workflowAsAgent.RunAsync();
|
||||
|
||||
await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
private async Task Run_AsAgent_OutgoingMessagesInHistoryAsync(Workflow workflow, bool runAsync)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Reference in New Issue
Block a user