// 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;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
///
/// Tests for .
///
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(() => 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(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(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(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(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(
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(
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(
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(
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(
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(
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(
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(
displayName: nameof(TakeFirstItemOperationEmptyTableAsync),
variableName: "TestTable",
changeType: new TakeFirstItemOperation.Builder().Build());
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
EditTableOperation changeType,
Action? 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(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(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();
}
}