mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
9f4c5f3faa
* Initial plan * Add comprehensive unit tests for EditTableV2Executor - Test AddItemOperation with record and scalar values - Test ClearItemsOperation - Test RemoveItemOperation - Test TakeLastItemOperation (with items and empty table) - Test TakeFirstItemOperation (with items and empty table) - Test error cases (null ItemsVariable, non-table variable) - Include ExecuteTestAsync and CreateModel helper methods - All 10 tests passing Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Add comprehensive unit tests for EditTableV2Executor - complete with 100% coverage - Added 13 comprehensive tests covering all code paths - Test AddItemOperation with record and scalar values - Test ClearItemsOperation - Test RemoveItemOperation (including non-table value case) - Test TakeLastItemOperation (with items and empty table) - Test TakeFirstItemOperation (with items and empty table) - Test error cases (null ItemsVariable, non-table variable, null operation values) - Include ExecuteTestAsync and CreateModel helper methods - 100% line and branch coverage achieved Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Update tests / refine product code * Checkpoint * Updated * Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address code review feedback - Fix typo: rename metadataExpresssion to metadataExpression - Fix test name in AddMessageWithMetadataAsync (was using wrong test name) - Fix test name in ClearGlobalScopeAsync (was using wrong test name) - Remove pre-population in SetTextVariableExecutorTest that made tests ineffective - Use explicit .Where() filter in SetMultipleVariablesExecutorTest foreach loop Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> --------- 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> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
346 lines
14 KiB
C#
346 lines
14 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
|
using Microsoft.Agents.ObjectModel;
|
|
using Microsoft.PowerFx.Types;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="EditTableV2Executor"/>.
|
|
/// </summary>
|
|
public sealed class EditTableV2ExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
|
{
|
|
[Fact]
|
|
public void InvalidModelNullItemsVariable()
|
|
{
|
|
// Arrange
|
|
EditTableV2 model = new EditTableV2.Builder
|
|
{
|
|
Id = this.CreateActionId(),
|
|
DisplayName = this.FormatDisplayName(nameof(InvalidModelNullItemsVariable)),
|
|
ItemsVariable = null,
|
|
ChangeType = new AddItemOperation.Builder
|
|
{
|
|
Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test")))
|
|
}.Build()
|
|
}.Build();
|
|
|
|
// Act, Assert
|
|
DeclarativeModelException exception = Assert.Throws<DeclarativeModelException>(() => new EditTableV2Executor(model, this.State));
|
|
Assert.Contains("required", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvalidModelVariableNotTableAsync()
|
|
{
|
|
// Arrange
|
|
this.State.Set("NotATable", FormulaValue.New("I am a string"));
|
|
|
|
EditTableV2 model = this.CreateModel(
|
|
nameof(InvalidModelVariableNotTableAsync),
|
|
"NotATable",
|
|
new AddItemOperation.Builder
|
|
{
|
|
Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test")))
|
|
}.Build());
|
|
|
|
EditTableV2Executor action = new(model, this.State);
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvalidModelAddItemOperationNullValueAsync()
|
|
{
|
|
// Arrange
|
|
EditTableV2 model = new EditTableV2.Builder
|
|
{
|
|
Id = this.CreateActionId(),
|
|
DisplayName = this.FormatDisplayName(nameof(InvalidModelAddItemOperationNullValueAsync)),
|
|
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
|
ChangeType = new AddItemOperation.Builder
|
|
{
|
|
Value = null
|
|
}.Build()
|
|
}.Build();
|
|
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act, Assert
|
|
EditTableV2Executor action = new(model, this.State);
|
|
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvalidModelRemoveItemOperationNullValueAsync()
|
|
{
|
|
// Arrange
|
|
EditTableV2 model = new EditTableV2.Builder
|
|
{
|
|
Id = this.CreateActionId(),
|
|
DisplayName = this.FormatDisplayName(nameof(InvalidModelRemoveItemOperationNullValueAsync)),
|
|
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
|
ChangeType = new RemoveItemOperation.Builder
|
|
{
|
|
Value = null
|
|
}.Build()
|
|
}.Build();
|
|
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act, Assert
|
|
EditTableV2Executor action = new(model, this.State);
|
|
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoveItemOperationNonTableValueAsync()
|
|
{
|
|
// Arrange
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
|
TableValue tableValue = FormulaValue.NewTable(recordType, record1);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Set a string value instead of a table for removal
|
|
this.State.Set("RemoveItems", FormulaValue.New("NotATable"));
|
|
|
|
EditTableV2 model = new EditTableV2.Builder
|
|
{
|
|
Id = this.CreateActionId(),
|
|
DisplayName = this.FormatDisplayName(nameof(RemoveItemOperationNonTableValueAsync)),
|
|
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
|
ChangeType = new RemoveItemOperation.Builder
|
|
{
|
|
Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems")))
|
|
}.Build()
|
|
}.Build();
|
|
|
|
// Act
|
|
EditTableV2Executor action = new(model, this.State);
|
|
await this.ExecuteAsync(action);
|
|
|
|
// Assert: When the remove value is not a table, no removal occurs, so the table should be unchanged
|
|
FormulaValue value = this.State.Get("TestTable");
|
|
Assert.IsAssignableFrom<TableValue>(value);
|
|
TableValue resultTable = (TableValue)value;
|
|
Assert.Single(resultTable.Rows);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddItemOperationWithSingleFieldRecordAsync()
|
|
{
|
|
// Arrange: Create an empty table with single field
|
|
RecordType recordType = RecordType.Empty().Add("Name", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Arrange, Act, Assert
|
|
await this.ExecuteTestAsync<RecordValue>(
|
|
displayName: nameof(AddItemOperationWithSingleFieldRecordAsync),
|
|
variableName: "TestTable",
|
|
changeType: this.CreateAddItemOperation(new RecordDataValue.Builder
|
|
{
|
|
Properties =
|
|
{
|
|
["Name"] = new StringDataValue("John")
|
|
}
|
|
}.Build()),
|
|
verifyAction: (variableName, recordValue) =>
|
|
Assert.Equal("John", recordValue.GetField("Name").ToObject())
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddItemOperationWithScalarValueAsync()
|
|
{
|
|
// Arrange: Create an empty table with single field
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act & Assert
|
|
await this.ExecuteTestAsync<RecordValue>(
|
|
displayName: nameof(AddItemOperationWithScalarValueAsync),
|
|
variableName: "TestTable",
|
|
changeType: this.CreateAddItemOperation(new StringDataValue("TestValue")),
|
|
verifyAction: (variableName, recordValue) =>
|
|
Assert.Equal("TestValue", recordValue.GetField("Value").ToObject())
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClearItemsOperationAsync()
|
|
{
|
|
// Arrange: Create a table with some items
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
|
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
|
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act & Assert
|
|
await this.ExecuteTestAsync<BlankValue>(
|
|
displayName: nameof(ClearItemsOperationAsync),
|
|
variableName: "TestTable",
|
|
changeType: new ClearItemsOperation.Builder().Build());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoveItemOperationAsync()
|
|
{
|
|
// Arrange: Create a table with some items
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
|
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
|
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act & Assert
|
|
await this.ExecuteTestAsync<BlankValue>(
|
|
displayName: nameof(RemoveItemOperationAsync),
|
|
variableName: "TestTable",
|
|
changeType: this.CreateRemoveItemOperation("Item1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TakeLastItemOperationWithItemsAsync()
|
|
{
|
|
// Arrange: Create a table with some items
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
|
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
|
RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3")));
|
|
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Arrange, Act, Assert
|
|
await this.ExecuteTestAsync<RecordValue>(
|
|
displayName: nameof(TakeLastItemOperationWithItemsAsync),
|
|
variableName: "TestTable",
|
|
changeType: new TakeLastItemOperation.Builder().Build(),
|
|
verifyAction: (variableName, recordValue) =>
|
|
Assert.Equal("Item3", recordValue.GetField("Value").ToObject())
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TakeLastItemOperationEmptyTableAsync()
|
|
{
|
|
// Arrange: Create an empty table
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Arrange, Act, Assert
|
|
await this.ExecuteTestAsync<TableValue>(
|
|
displayName: nameof(TakeLastItemOperationEmptyTableAsync),
|
|
variableName: "TestTable",
|
|
changeType: new TakeLastItemOperation.Builder().Build());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TakeFirstItemOperationWithItemsAsync()
|
|
{
|
|
// Arrange: Create a table with some items
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
|
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
|
RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3")));
|
|
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act & Assert
|
|
await this.ExecuteTestAsync<RecordValue>(
|
|
displayName: nameof(TakeFirstItemOperationWithItemsAsync),
|
|
variableName: "TestTable",
|
|
changeType: new TakeFirstItemOperation.Builder().Build(),
|
|
verifyAction: (variableName, recordValue) =>
|
|
Assert.Equal("Item1", recordValue.GetField("Value").ToObject())
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TakeFirstItemOperationEmptyTableAsync()
|
|
{
|
|
// Arrange: Create an empty table
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
TableValue tableValue = FormulaValue.NewTable(recordType);
|
|
this.State.Set("TestTable", tableValue);
|
|
|
|
// Act & Assert
|
|
await this.ExecuteTestAsync<TableValue>(
|
|
displayName: nameof(TakeFirstItemOperationEmptyTableAsync),
|
|
variableName: "TestTable",
|
|
changeType: new TakeFirstItemOperation.Builder().Build());
|
|
}
|
|
|
|
private async Task ExecuteTestAsync<TValue>(
|
|
string displayName,
|
|
string variableName,
|
|
EditTableOperation changeType,
|
|
Action<string, TValue>? verifyAction = null) where TValue : FormulaValue
|
|
{
|
|
// Arrange
|
|
EditTableV2 model = this.CreateModel(displayName, variableName, changeType);
|
|
|
|
EditTableV2Executor action = new(model, this.State);
|
|
|
|
// Act
|
|
await this.ExecuteAsync(action);
|
|
|
|
// Assert
|
|
VerifyModel(model, action);
|
|
FormulaValue value = this.State.Get(variableName);
|
|
TValue typedValue = Assert.IsAssignableFrom<TValue>(value);
|
|
verifyAction?.Invoke(variableName, typedValue);
|
|
}
|
|
|
|
private EditTableV2 CreateModel(string displayName, string variableName, EditTableOperation changeType)
|
|
{
|
|
EditTableV2.Builder actionBuilder = new()
|
|
{
|
|
Id = this.CreateActionId(),
|
|
DisplayName = this.FormatDisplayName(displayName),
|
|
ItemsVariable = PropertyPath.Create(FormatVariablePath(variableName)),
|
|
ChangeType = changeType
|
|
};
|
|
|
|
return AssignParent<EditTableV2>(actionBuilder);
|
|
}
|
|
|
|
private AddItemOperation CreateAddItemOperation(DataValue value)
|
|
{
|
|
return new AddItemOperation.Builder
|
|
{
|
|
Value = new ValueExpression.Builder(ValueExpression.Literal(value))
|
|
}.Build();
|
|
}
|
|
|
|
private RemoveItemOperation CreateRemoveItemOperation(string itemValue)
|
|
{
|
|
// Create a table with the item to remove
|
|
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
|
RecordValue recordToRemove = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New(itemValue)));
|
|
TableValue tableToRemove = FormulaValue.NewTable(recordType, recordToRemove);
|
|
|
|
// Store in state for expression evaluation
|
|
this.State.Set("RemoveItems", tableToRemove);
|
|
this.State.Bind();
|
|
|
|
return new RemoveItemOperation.Builder
|
|
{
|
|
Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems")))
|
|
}.Build();
|
|
}
|
|
}
|