mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - Add unit tests for ConditionGroupExecutor (#3893)
* Initial plan * Add ConditionGroupExecutorTest with comprehensive test coverage Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Address code review feedback - extract reflection helper and improve comments Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Ready * Namespace * Cleanup --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> Co-authored-by: Chris Rickman <crickman@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e633f208d9
commit
349d645cfc
+2
-1
@@ -22,11 +22,12 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
{
|
||||
return conditionItem.Id;
|
||||
}
|
||||
|
||||
int index = model.Conditions.IndexOf(conditionItem);
|
||||
return $"{model.Id}_Items{index}";
|
||||
}
|
||||
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value ?? $"{model.Id}_Else";
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value;
|
||||
}
|
||||
|
||||
public ConditionGroupExecutor(ConditionGroup model, WorkflowFormulaState state)
|
||||
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ConditionGroupExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ConditionGroupExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void ConditionGroupThrowsWhenModelInvalid() =>
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new ConditionGroupExecutor(new ConditionGroup(), this.State));
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupDefaultNaming()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupDefaultNaming), [false], includeElse: true, defineActionIds: false);
|
||||
ConditionItem condition = model.Conditions[0];
|
||||
|
||||
// Act
|
||||
string conditionStepId = ConditionGroupExecutor.Steps.Item(model, condition);
|
||||
string elseStepId = ConditionGroupExecutor.Steps.Else(model);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{model.Id}_Items0", conditionStepId);
|
||||
Assert.Equal(model.ElseActions.Id.Value, elseStepId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupExplicitNaming()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupExplicitNaming), [false], includeElse: true);
|
||||
ConditionItem condition = model.Conditions[0];
|
||||
|
||||
// Act
|
||||
string conditionStepId = ConditionGroupExecutor.Steps.Item(model, condition);
|
||||
string elseStepId = ConditionGroupExecutor.Steps.Else(model);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(condition.Id, conditionStepId);
|
||||
Assert.Equal(model.ElseActions.Id.Value, elseStepId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupFirstConditionTrueAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupFirstConditionTrueAsync),
|
||||
conditions: [true, false]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupSecondConditionTrueAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupSecondConditionTrueAsync),
|
||||
conditions: [false, true]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupFirstConditionNullAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupFirstConditionNullAsync),
|
||||
conditions: [null, true]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupElseBranchAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupElseBranchAsync),
|
||||
conditions: [false, false],
|
||||
includeElse: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupDoneAsync()
|
||||
{
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupDoneAsync), [true]);
|
||||
ConditionGroupExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync("condition_done_id", action.DoneAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
Assert.NotEmpty(events);
|
||||
VerifyCompletionEvent(events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsMatchTrue()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsMatchTrue), [true]);
|
||||
ConditionItem firstCondition = model.Conditions[0];
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Item(model, firstCondition));
|
||||
|
||||
// Act
|
||||
bool isMatch = executor.IsMatch(firstCondition, result);
|
||||
|
||||
// Assert
|
||||
Assert.True(isMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsMatchFalse()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsMatchFalse), [true, false]);
|
||||
ConditionItem firstCondition = model.Conditions[0];
|
||||
ConditionItem secondCondition = model.Conditions[1];
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Item(model, secondCondition));
|
||||
|
||||
// Act
|
||||
bool isMatch = executor.IsMatch(firstCondition, result);
|
||||
|
||||
// Assert
|
||||
Assert.False(isMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsElseTrue()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsElseTrue), [false]);
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Else(model));
|
||||
|
||||
// Act
|
||||
bool isElse = executor.IsElse(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(isElse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsElseFalse()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsElseFalse), [false]);
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, "different_step");
|
||||
|
||||
// Act
|
||||
bool isElse = executor.IsElse(result);
|
||||
|
||||
// Assert
|
||||
Assert.False(isElse);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
bool?[] conditions,
|
||||
bool includeElse = false)
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(displayName, conditions, includeElse);
|
||||
ConditionGroupExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
Assert.NotEmpty(events);
|
||||
VerifyInvocationEvent(events);
|
||||
|
||||
VerifyIsDiscrete(action, isDiscrete: false);
|
||||
}
|
||||
|
||||
private ConditionGroup CreateModel(
|
||||
string displayName,
|
||||
bool?[] conditions,
|
||||
bool includeElse = false,
|
||||
bool defineActionIds = true)
|
||||
{
|
||||
ConditionGroup.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
};
|
||||
|
||||
for (int index = 0; index < conditions.Length; ++index)
|
||||
{
|
||||
bool? condition = conditions[index];
|
||||
|
||||
ConditionItem.Builder conditionBuilder = new()
|
||||
{
|
||||
Id = defineActionIds ? $"condition_{index}" : null,
|
||||
Actions = this.CreateActions(defineActionIds ? $"condition_actions_{index}" : null),
|
||||
Condition = condition is null ? null : BoolExpression.Literal(condition.Value).ToBuilder(),
|
||||
};
|
||||
|
||||
actionBuilder.Conditions.Add(conditionBuilder);
|
||||
}
|
||||
|
||||
if (includeElse)
|
||||
{
|
||||
actionBuilder.ElseActions = this.CreateActions(defineActionIds ? "else_actions" : null);
|
||||
}
|
||||
|
||||
return AssignParent<ConditionGroup>(actionBuilder);
|
||||
}
|
||||
|
||||
private ActionScope.Builder CreateActions(string? actionScopeId)
|
||||
{
|
||||
ActionScope.Builder actions = [];
|
||||
|
||||
if (actionScopeId is not null)
|
||||
{
|
||||
actions.Id = new ActionId(actionScopeId);
|
||||
}
|
||||
|
||||
actions.Actions.Add(
|
||||
new SendActivity.Builder
|
||||
{
|
||||
Id = $"{actionScopeId ?? "action"}_send_activity",
|
||||
Activity = new MessageActivityTemplate(),
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -227,11 +227,7 @@ public sealed class ForeachExecutorTest(ITestOutputHelper output) : WorkflowActi
|
||||
VerifyInvocationEvent(events);
|
||||
|
||||
// IsDiscreteAction should be false for Foreach
|
||||
Assert.Equal(
|
||||
false,
|
||||
action.GetType().BaseType?
|
||||
.GetProperty("IsDiscreteAction", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
|
||||
.GetValue(action));
|
||||
VerifyIsDiscrete(action, isDiscrete: false);
|
||||
|
||||
// Verify HasValue state after execution
|
||||
Assert.Equal(expectValue, action.HasValue);
|
||||
|
||||
+15
-3
@@ -27,13 +27,16 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
|
||||
|
||||
internal Task<WorkflowEvent[]> ExecuteAsync(string actionId, DelegateAction<ActionExecutorResult> executorAction) =>
|
||||
this.ExecuteAsync(new DelegateActionExecutor(actionId, this.State, executorAction), isDiscrete: false);
|
||||
this.ExecuteAsync([new DelegateActionExecutor(actionId, this.State, executorAction)], isDiscrete: false);
|
||||
|
||||
internal Task<WorkflowEvent[]> ExecuteAsync(Executor executor, string actionId, DelegateAction<ActionExecutorResult> executorAction) =>
|
||||
this.ExecuteAsync([executor, new DelegateActionExecutor(actionId, this.State, executorAction)], isDiscrete: false);
|
||||
|
||||
internal Task<WorkflowEvent[]> ExecuteAsync(Executor executor, bool isDiscrete = true) =>
|
||||
this.ExecuteAsync([executor], isDiscrete);
|
||||
internal async Task<WorkflowEvent[]> ExecuteAsync(DeclarativeActionExecutor executor, bool isDiscrete = true)
|
||||
{
|
||||
VerifyIsDiscrete(executor, isDiscrete);
|
||||
return await this.ExecuteAsync([executor], isDiscrete);
|
||||
}
|
||||
|
||||
internal async Task<WorkflowEvent[]> ExecuteAsync(Executor[] executors, bool isDiscrete)
|
||||
{
|
||||
@@ -79,6 +82,15 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
Assert.Equal(model, action.Model);
|
||||
}
|
||||
|
||||
internal static void VerifyIsDiscrete(DeclarativeActionExecutor action, bool isDiscrete = true)
|
||||
{
|
||||
Assert.Equal(
|
||||
isDiscrete,
|
||||
action.GetType().BaseType?
|
||||
.GetProperty("IsDiscreteAction", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
|
||||
.GetValue(action));
|
||||
}
|
||||
|
||||
protected static void VerifyInvocationEvent(WorkflowEvent[] events) =>
|
||||
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user