// 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(); } }