// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
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 ForeachExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public void ForeachThrowsWhenModelInvalid() =>
// Arrange, Act & Assert
Assert.Throws(() => new ForeachExecutor(new Foreach(), this.State));
[Fact]
public void ForeachNamingConvention()
{
// Arrange
string testId = this.CreateActionId().Value;
// Act
string startStep = ForeachExecutor.Steps.Start(testId);
string nextStep = ForeachExecutor.Steps.Next(testId);
string endStep = ForeachExecutor.Steps.End(testId);
// Assert
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.Start)}", startStep);
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.Next)}", nextStep);
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.End)}", endStep);
}
[Fact]
public async Task ForeachInvokedWithSingleValueAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
// Act & Assert
await this.ExecuteTestAsync(
displayName: nameof(ForeachInvokedWithSingleValueAsync),
items: ValueExpression.Literal(new NumberDataValue(42)),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachInvokedWithTableValueAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
// Act & Assert
await this.ExecuteTestAsync(
displayName: nameof(ForeachInvokedWithTableValueAsync),
items: ValueExpression.Literal(DataValue.EmptyTable),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachInvokedWithIndexAsync()
{
// Arrange
this.SetVariableState("CurrentValue", "CurrentIndex");
TableDataValue tableValue = DataValue.TableFromRecords(
DataValue.RecordFromFields(new KeyValuePair("item", new NumberDataValue(1))),
DataValue.RecordFromFields(new KeyValuePair("item", new NumberDataValue(2))),
DataValue.RecordFromFields(new KeyValuePair("item", new NumberDataValue(3))));
// Act & Assert
await this.ExecuteTestAsync(
displayName: nameof(ForeachInvokedWithIndexAsync),
items: ValueExpression.Literal(tableValue),
valueName: "CurrentValue",
indexName: "CurrentIndex");
}
[Fact]
public async Task ForeachInvokedWithExpressionAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
this.State.Set("SourceArray", FormulaValue.NewTable(RecordType.Empty()));
// Act & Assert
await this.ExecuteTestAsync(
displayName: nameof(ForeachInvokedWithExpressionAsync),
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachTakeNextAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
this.State.Set(
"SourceArray",
FormulaValue.NewTable(
RecordType.Empty(),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10))),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(20))),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(30)))));
// Act & Assert
await this.TakeNextTestAsync(
displayName: nameof(ForeachTakeNextAsync),
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachTakeNextWithIndexAsync()
{
// Arrange
this.SetVariableState("CurrentValue", "CurrentIndex");
this.State.Set(
"SourceArray",
FormulaValue.NewTable(
RecordType.Empty(),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10))),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(20))),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(30)))));
// Act & Assert
await this.TakeNextTestAsync(
displayName: nameof(ForeachTakeNextWithIndexAsync),
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
valueName: "CurrentValue",
indexName: "CurrentIndex");
}
[Fact]
public async Task ForeachTakeLastAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
this.State.Set(
"SourceArray",
FormulaValue.NewTable(
RecordType.Empty(),
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10)))));
// Act & Assert
await this.TakeNextTestAsync(
displayName: nameof(ForeachTakeLastAsync),
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachTakeNextWhenDoneAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
// Act & Assert
await this.TakeNextTestAsync(
displayName: nameof(ForeachTakeNextWhenDoneAsync),
items: ValueExpression.Literal(DataValue.EmptyTable),
valueName: "CurrentValue",
indexName: null,
expectValue: false);
}
[Fact]
public async Task ForeachCompletedWithoutIndexAsync()
{
// Arrange
this.SetVariableState("CurrentValue");
// Act & Assert
await this.CompletedTestAsync(
displayName: nameof(ForeachCompletedWithoutIndexAsync),
valueName: "CurrentValue",
indexName: null);
}
[Fact]
public async Task ForeachCompletedWithIndexAsync()
{
// Arrange
this.SetVariableState("CurrentValue", "CurrentIndex");
// Act & Assert
await this.CompletedTestAsync(
displayName: nameof(ForeachCompletedWithIndexAsync),
valueName: "CurrentValue",
indexName: "CurrentIndex");
}
private void SetVariableState(string valueName, string? indexName = null, FormulaValue? valueState = null)
{
this.State.Set(valueName, valueState ?? FormulaValue.New("something"));
if (indexName is not null)
{
this.State.Set(indexName, FormulaValue.New(33));
}
}
private async Task ExecuteTestAsync(
string displayName,
ValueExpression items,
string valueName,
string? indexName,
bool expectValue = false)
{
// Arrange
Foreach model = this.CreateModel(displayName, items, valueName, indexName);
ForeachExecutor action = new(model, this.State);
// Act
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
// Assert
VerifyModel(model, action);
VerifyInvocationEvent(events);
// IsDiscreteAction should be false for Foreach
VerifyIsDiscrete(action, isDiscrete: false);
// Verify HasValue state after execution
Assert.Equal(expectValue, action.HasValue);
// Verify value was reset at the end
this.VerifyUndefined(valueName);
// Verify index was reset at the end if it was used
if (indexName is not null)
{
this.VerifyUndefined(indexName);
}
}
private async Task TakeNextTestAsync(
string displayName,
ValueExpression items,
string valueName,
string? indexName,
bool expectValue = true)
{
// Arrange
Foreach model = this.CreateModel(displayName, items, valueName, indexName);
ForeachExecutor action = new(model, this.State);
// Act
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
// Assert
VerifyModel(model, action);
// Verify HasValue state after execution
Assert.Equal(expectValue, action.HasValue);
}
private async Task CompletedTestAsync(
string displayName,
string valueName,
string? indexName)
{
// Arrange
Foreach model = this.CreateModel(displayName, ValueExpression.Literal(DataValue.EmptyTable), valueName, indexName);
ForeachExecutor action = new(model, this.State);
// Act
WorkflowEvent[] events = await this.ExecuteAsync(ForeachExecutor.Steps.End(action.Id), action.CompleteAsync);
// Assert
VerifyModel(model, action);
VerifyCompletionEvent(events);
// Verify HasValue state after completion
Assert.False(action.HasValue);
// Verify value was reset at the end
this.VerifyUndefined(valueName);
// Verify index was reset at the end if it was used
if (indexName is not null)
{
this.VerifyUndefined(indexName);
}
}
private Foreach CreateModel(
string displayName,
ValueExpression items,
string valueName,
string? indexName)
{
Foreach.Builder actionBuilder = new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Items = items,
Value = PropertyPath.Create(FormatVariablePath(valueName)),
};
if (indexName is not null)
{
actionBuilder.Index = PropertyPath.Create(FormatVariablePath(indexName));
}
return AssignParent(actionBuilder);
}
}