diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs index a1f430242fb..aaf8cf22bf9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableExecutor.cs @@ -34,7 +34,7 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st EvaluationResult addResult = this.Evaluator.GetValue(addItemValue); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); + await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false); break; case TableChangeType.Remove: ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs index b06a5ebd365..da43a0e0ff6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs @@ -33,7 +33,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat EvaluationResult expressionResult = this.Evaluator.GetValue(addItemValue); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.ItemsVariable, newRecord, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false); } else if (changeType is ClearItemsOperation) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs index 77e7f45ff67..d27470d4a9f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs @@ -33,13 +33,44 @@ await this.ExecuteTestAsync( changeType: TableChangeType.Add, value: new RecordDataValue([new("id", new NumberDataValue(7))])); - // Verify the variable now contains the added record + // Verify the variable remains a table containing the added record FormulaValue resultValue = this.State.Get("MyTable"); - RecordValue resultRecord = Assert.IsAssignableFrom(resultValue); - DecimalValue idValue = Assert.IsType(resultRecord.GetField("id")); + TableValue resultTable = Assert.IsAssignableFrom(resultValue); + Assert.Equal(2, resultTable.Rows.Count()); + DecimalValue idValue = Assert.IsType(resultTable.Rows.Last().Value.GetField("id")); Assert.Equal(7, idValue.Value); } + [Fact] + public async Task ConsecutiveAddsPreserveTableAsync() + { + // Arrange + FormulaValue tableValue = this.State.Engine.Eval("[{id: 1}]"); + this.State.Set("MyTable", tableValue); + + EditTable firstAdd = this.CreateModel( + nameof(ConsecutiveAddsPreserveTableAsync), + "MyTable", + TableChangeType.Add, + new RecordDataValue([new("id", new NumberDataValue(2))])); + EditTable secondAdd = this.CreateModel( + nameof(ConsecutiveAddsPreserveTableAsync), + "MyTable", + TableChangeType.Add, + new RecordDataValue([new("id", new NumberDataValue(3))])); + + // Act + await this.ExecuteAsync(new EditTableExecutor(firstAdd, this.State)); + await this.ExecuteAsync(new EditTableExecutor(secondAdd, this.State)); + + // Assert + TableValue resultTable = Assert.IsAssignableFrom(this.State.Get("MyTable")); + decimal[] ids = resultTable.Rows + .Select(row => Assert.IsType(row.Value.GetField("id")).Value) + .ToArray(); + Assert.Equal([1, 2, 3], ids); + } + [Fact] public async Task AddItemWithMultipleFieldsAsync() { @@ -57,9 +88,11 @@ await this.ExecuteTestAsync( new("name", new StringDataValue("Second")) ])); - // Verify the variable now contains the added record + // Verify the variable remains a table containing the added record FormulaValue resultValue = this.State.Get("MyTable"); - RecordValue resultRecord = Assert.IsAssignableFrom(resultValue); + TableValue resultTable = Assert.IsAssignableFrom(resultValue); + Assert.Equal(2, resultTable.Rows.Count()); + RecordValue resultRecord = resultTable.Rows.Last().Value; DecimalValue idValue = Assert.IsType(resultRecord.GetField("id")); Assert.Equal(2, idValue.Value); StringValue nameValue = Assert.IsType(resultRecord.GetField("name")); @@ -83,9 +116,10 @@ await this.ExecuteTestAsync( changeType: TableChangeType.Add, value: new RecordDataValue([new("id", new NumberDataValue(1))])); - // Verify the variable now contains the added record + // Verify the variable remains a table containing the added record FormulaValue resultValue = this.State.Get("MyTable"); - RecordValue resultRecord = Assert.IsAssignableFrom(resultValue); + TableValue resultTable = Assert.IsAssignableFrom(resultValue); + RecordValue resultRecord = Assert.Single(resultTable.Rows).Value; DecimalValue idValue = Assert.IsType(resultRecord.GetField("id")); Assert.Equal(1, idValue.Value); } @@ -345,11 +379,12 @@ public async Task AddWithExpressionAsync() EditTableExecutor action = new(model, this.State); await this.ExecuteAsync(action); - // Assert - Variable should contain the newly added record + // Assert - Variable should remain a table containing the newly added record VerifyModel(model, action); FormulaValue resultValue = this.State.Get("MyTable"); - RecordValue resultRecord = Assert.IsAssignableFrom(resultValue); - DecimalValue idValue = Assert.IsType(resultRecord.GetField("id")); + TableValue resultTable = Assert.IsAssignableFrom(resultValue); + Assert.Equal(2, resultTable.Rows.Count()); + DecimalValue idValue = Assert.IsType(resultTable.Rows.Last().Value.GetField("id")); Assert.Equal(10, idValue.Value); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs index bb4442507c8..a7ecc696662 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; @@ -144,7 +145,7 @@ public async Task AddItemOperationWithSingleFieldRecordAsync() this.State.Set("TestTable", tableValue); // Arrange, Act, Assert - await this.ExecuteTestAsync( + await this.ExecuteTestAsync( displayName: nameof(AddItemOperationWithSingleFieldRecordAsync), variableName: "TestTable", changeType: this.CreateAddItemOperation(new RecordDataValue.Builder @@ -154,8 +155,8 @@ await this.ExecuteTestAsync( ["Name"] = new StringDataValue("John") } }.Build()), - verifyAction: (variableName, recordValue) => - Assert.Equal("John", recordValue.GetField("Name").ToObject()) + verifyAction: (variableName, resultTable) => + Assert.Equal("John", Assert.Single(resultTable.Rows).Value.GetField("Name").ToObject()) ); } @@ -168,15 +169,46 @@ public async Task AddItemOperationWithScalarValueAsync() this.State.Set("TestTable", tableValue); // Act & Assert - await this.ExecuteTestAsync( + await this.ExecuteTestAsync( displayName: nameof(AddItemOperationWithScalarValueAsync), variableName: "TestTable", changeType: this.CreateAddItemOperation(new StringDataValue("TestValue")), - verifyAction: (variableName, recordValue) => - Assert.Equal("TestValue", recordValue.GetField("Value").ToObject()) + verifyAction: (variableName, resultTable) => + Assert.Equal("TestValue", Assert.Single(resultTable.Rows).Value.GetField("Value").ToObject()) ); } + [Fact] + public async Task ConsecutiveAddItemOperationsPreserveTableAsync() + { + // Arrange + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue initialRecord = FormulaValue.NewRecordFromFields( + recordType, + new NamedValue("Value", FormulaValue.New("Initial"))); + this.State.Set("TestTable", FormulaValue.NewTable(recordType, initialRecord)); + + EditTableV2 firstAdd = this.CreateModel( + nameof(ConsecutiveAddItemOperationsPreserveTableAsync), + "TestTable", + this.CreateAddItemOperation(new StringDataValue("First"))); + EditTableV2 secondAdd = this.CreateModel( + nameof(ConsecutiveAddItemOperationsPreserveTableAsync), + "TestTable", + this.CreateAddItemOperation(new StringDataValue("Second"))); + + // Act + await this.ExecuteAsync(new EditTableV2Executor(firstAdd, this.State)); + await this.ExecuteAsync(new EditTableV2Executor(secondAdd, this.State)); + + // Assert + TableValue resultTable = Assert.IsAssignableFrom(this.State.Get("TestTable")); + string[] values = resultTable.Rows + .Select(row => Assert.IsType(row.Value.GetField("Value")).Value) + .ToArray(); + Assert.Equal(["Initial", "First", "Second"], values); + } + [Fact] public async Task ClearItemsOperationAsync() {