.NET Workflows - Add unit tests for ForeachExecutor (Declarative Workflows) (#3835)

* Initial plan

* Add comprehensive unit tests for ForeachExecutor

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Formatting

* Checkpoint

* Checkpoint

* Updated test capabilities for non-discrete

* Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Consistency

* Cleanup test

* Fixed

---------

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>
This commit is contained in:
Copilot
2026-02-12 16:29:54 +00:00
committed by GitHub
co-authored by crickman Copilot copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Chris Rickman
parent be7b55f99b
commit 69dcfe31ee
12 changed files with 440 additions and 60 deletions
@@ -65,7 +65,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
this._values = [evaluatedValue];
}
await this.ResetAsync(context, null, cancellationToken).ConfigureAwait(false);
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
return default;
}
@@ -84,9 +84,19 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
AssignVariable(this.Index, "this._index", tightFormat: true);
}
this.Write("\n\n this._index++;\n }\n }\n\n public async ValueTask ResetAsy" +
"nc(IWorkflowContext context, object? _, CancellationToken cancellationToken)\n " +
" {");
this.Write(@"
this._index++;
}
}
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
}
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
{");
AssignVariable(this.Value, "UnassignedValue.Instance", tightFormat: true);
@@ -36,7 +36,7 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session) : ActionE
this._values = [evaluatedValue];
}
await this.ResetAsync(context, null, cancellationToken).ConfigureAwait(false);
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
return default;
}
@@ -59,7 +59,12 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session) : ActionE
}
}
public async ValueTask ResetAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
}
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
{<#
AssignVariable(this.Value, "UnassignedValue.Instance", tightFormat: true);
@@ -201,7 +201,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
// Transition to end of inner actions
string endActionsId = ForeachExecutor.Steps.End(action.Id);
this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.ResetAsync), action.Id);
this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.CompleteAsync), action.Id);
// Transition to select the next item
this._workflowModel.AddLink(endActionsId, loopId);
}
@@ -163,7 +163,7 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
{
// Transition to end of inner actions
string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation
this.ContinueWith(new EmptyTemplate(endActionsId, this._rootId, $"{action.Id.FormatName()}.{nameof(ForeachExecutor.ResetAsync)}"), action.Id);
this.ContinueWith(new EmptyTemplate(endActionsId, this._rootId, $"{action.Id.FormatName()}.{nameof(ForeachExecutor.CompleteAsync)}"), action.Id);
// Transition to select the next item
this._workflowModel.AddLink(endActionsId, loopId);
}
@@ -37,27 +37,21 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(this.Model.Items, $"{nameof(this.Model)}.{nameof(this.Model.Items)}");
this._index = 0;
if (this.Model.Items is null)
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Items);
if (expressionResult.Value is TableDataValue tableValue)
{
this._values = [];
this.HasValue = false;
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())];
}
else
{
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Items);
if (expressionResult.Value is TableDataValue tableValue)
{
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())];
}
else
{
this._values = [expressionResult.Value.ToFormula()];
}
this._values = [expressionResult.Value.ToFormula()];
}
await this.ResetAsync(context, null, cancellationToken).ConfigureAwait(false);
await this.ResetStateAsync(context, cancellationToken).ConfigureAwait(false);
return default;
}
@@ -79,19 +73,24 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
}
}
public async ValueTask ResetAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
try
{
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value), cancellationToken).ConfigureAwait(false);
if (this.Model.Index is not null)
{
await context.QueueStateResetAsync(this.Model.Index, cancellationToken).ConfigureAwait(false);
}
await this.ResetStateAsync(context, cancellationToken).ConfigureAwait(false);
}
finally
{
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
}
private async Task ResetStateAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value), cancellationToken).ConfigureAwait(false);
if (this.Model.Index is not null)
{
await context.QueueStateResetAsync(this.Model.Index, cancellationToken).ConfigureAwait(false);
}
}
}