Files
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

359 lines
13 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="InvokeAzureAgentExecutor"/>.
/// </summary>
public sealed class InvokeAzureAgentExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public void InvokeAzureAgentThrowsWhenModelInvalid() =>
// Arrange, Act & Assert
Assert.Throws<DeclarativeModelException>(() => new InvokeAzureAgentExecutor(new InvokeAzureAgent(), new CapturingAgentProvider("text"), this.State));
#region Input argument binding
[Fact]
public async Task MultipleNamedArgumentsAreAllBoundAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("acknowledged");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(MultipleNamedArgumentsAreAllBoundAsync),
agentName: "BrainCombine",
arguments:
[
("a", ValueExpression.Literal(new StringDataValue("alpha"))),
("b", ValueExpression.Literal(new StringDataValue("beta"))),
]);
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
Assert.NotNull(provider.CapturedArguments);
Assert.Equal("alpha", provider.CapturedArguments!["a"]);
Assert.Equal("beta", provider.CapturedArguments!["b"]);
}
[Fact]
public async Task RecordValuedArgumentIsBoundAsRecordAsync()
{
// Arrange
this.State.InitializeSystem();
this.State.Set(
"R",
FormulaValue.NewRecordFromFields(
new NamedValue("a", FormulaValue.New("alpha")),
new NamedValue("b", FormulaValue.New("beta"))));
CapturingAgentProvider provider = new("acknowledged");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(RecordValuedArgumentIsBoundAsRecordAsync),
agentName: "BrainTest",
arguments:
[
("input", ValueExpression.Variable(PropertyPath.TopicVariable("R"))),
]);
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
Assert.NotNull(provider.CapturedArguments);
IDictionary<string, object?> record = Assert.IsAssignableFrom<IDictionary<string, object?>>(provider.CapturedArguments!["input"]);
Assert.Equal("alpha", record["a"]);
Assert.Equal("beta", record["b"]);
}
[Fact]
public async Task InlineRecordExpressionArgumentIsBoundAsRecordAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("acknowledged");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(InlineRecordExpressionArgumentIsBoundAsRecordAsync),
agentName: "BrainInlineRecord",
arguments:
[
("input", ValueExpression.Expression("""{ a: "alpha", b: "beta" }""")),
]);
// Act (reporter repro (b): a single argument whose value is an inline record literal)
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
Assert.NotNull(provider.CapturedArguments);
IDictionary<string, object?> record = Assert.IsAssignableFrom<IDictionary<string, object?>>(provider.CapturedArguments!["input"]);
Assert.Equal("alpha", record["a"]);
Assert.Equal("beta", record["b"]);
}
#endregion
#region Response object parsing
[Fact]
public async Task JsonObjectOutputAssignsRecordAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("""{ "a": "alpha", "b": "beta" }""");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(JsonObjectOutputAssignsRecordAsync),
agentName: "BrainObject",
responseObjectVariable: "Result");
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
RecordValue record = Assert.IsAssignableFrom<RecordValue>(this.State.Get("Result"));
Assert.Equal("alpha", ((StringValue)record.GetField("a")).Value);
}
[Fact]
public async Task JsonArrayOutputAssignsListAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("""["alpha","beta"]""");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(JsonArrayOutputAssignsListAsync),
agentName: "BrainArray",
responseObjectVariable: "Result");
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
TableValue table = Assert.IsAssignableFrom<TableValue>(this.State.Get("Result"));
Assert.Equal(2, table.Rows.Count());
}
[Fact]
public async Task EmptyJsonArrayOutputAssignsEmptyListAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("[]");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(EmptyJsonArrayOutputAssignsEmptyListAsync),
agentName: "BrainEmptyArray",
responseObjectVariable: "Result");
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
TableValue table = Assert.IsAssignableFrom<TableValue>(this.State.Get("Result"));
Assert.Empty(table.Rows);
}
[Fact]
public async Task JsonScalarOutputAssignsScalarAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("42");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(JsonScalarOutputAssignsScalarAsync),
agentName: "BrainScalar",
responseObjectVariable: "Result");
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
NumberValue number = Assert.IsAssignableFrom<NumberValue>(this.State.Get("Result"));
Assert.Equal(42d, number.Value);
}
[Fact]
public async Task MixedJsonArrayOutputSkipsAssignmentAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("""["alpha",1]""");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(MixedJsonArrayOutputSkipsAssignmentAsync),
agentName: "BrainMixedArray",
responseObjectVariable: "Result");
// Act (must not throw despite non-convertible JSON)
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
this.VerifyUndefined("Result");
}
[Fact]
public async Task NestedJsonArrayOutputSkipsAssignmentAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("[[1,2],[3,4]]");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(NestedJsonArrayOutputSkipsAssignmentAsync),
agentName: "BrainNestedArray",
responseObjectVariable: "Result");
// Act (a nested array parses but is not convertible to a workflow value — must not throw)
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
this.VerifyUndefined("Result");
}
[Fact]
public async Task PlainTextOutputSkipsAssignmentAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("hello world");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(PlainTextOutputSkipsAssignmentAsync),
agentName: "BrainText",
responseObjectVariable: "Result");
// Act
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
// Assert
this.VerifyUndefined("Result");
}
[Fact]
public async Task NonObjectJsonOutputWithoutResponseObjectDoesNotThrowAsync()
{
// Arrange
this.State.InitializeSystem();
CapturingAgentProvider provider = new("""["alpha","beta"]""");
InvokeAzureAgent model =
this.CreateModel(
displayName: nameof(NonObjectJsonOutputWithoutResponseObjectDoesNotThrowAsync),
agentName: "BrainNoOutput");
// Act & Assert (reporter repro shape — no output block; must not throw)
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
}
#endregion
#region Helpers
private InvokeAzureAgent CreateModel(
string displayName,
string agentName,
IReadOnlyList<(string Key, ValueExpression Value)>? arguments = null,
string? responseObjectVariable = null)
{
InvokeAzureAgent.Builder builder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Agent =
new AzureAgentUsage.Builder
{
Name = new StringExpression.Builder(StringExpression.Literal(agentName)),
},
};
if (arguments is not null)
{
AzureAgentInput.Builder inputBuilder = new();
foreach ((string key, ValueExpression value) in arguments)
{
inputBuilder.Arguments.Add(key, value);
}
builder.Input = inputBuilder;
}
if (responseObjectVariable is not null)
{
builder.Output =
new AzureAgentOutput.Builder
{
AutoSend = new BoolExpression.Builder(BoolExpression.Literal(false)),
ResponseObject = new InitializablePropertyPath(PropertyPath.TopicVariable(responseObjectVariable), isInitializer: false),
};
}
return AssignParent<InvokeAzureAgent>(builder);
}
/// <summary>
/// Minimal <see cref="ResponseAgentProvider"/> that returns a single configured text response and
/// captures the input arguments supplied to <see cref="InvokeAgentAsync"/>.
/// </summary>
private sealed class CapturingAgentProvider(string responseText) : ResponseAgentProvider
{
public IDictionary<string, object?>? CapturedArguments { get; private set; }
public override IAsyncEnumerable<AgentResponseUpdate> InvokeAgentAsync(
string agentId,
string? agentVersion,
string? conversationId,
IEnumerable<ChatMessage>? messages,
IDictionary<string, object?>? inputArguments,
CancellationToken cancellationToken = default)
{
this.CapturedArguments = inputArguments;
return YieldAsync(responseText);
}
public override Task<string> CreateConversationAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(Guid.NewGuid().ToString("N"));
public override Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) =>
Task.FromResult(conversationMessage);
public override Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public override async IAsyncEnumerable<ChatMessage> GetMessagesAsync(
string conversationId,
int? limit = null,
string? after = null,
string? before = null,
bool newestFirst = false,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask.ConfigureAwait(false);
yield break;
}
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(string text)
{
yield return new AgentResponseUpdate(ChatRole.Assistant, text);
await Task.CompletedTask.ConfigureAwait(false);
}
}
#endregion
}