test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="AddConversationMessageExecutor"/>.
/// </summary>
public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Theory]
[InlineData(AgentMessageRole.User)]
[InlineData(AgentMessageRole.Agent)]
public async Task AddMessageSuccessfullyAsync(AgentMessageRole role)
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(AddMessageSuccessfullyAsync),
variableName: "TestMessage",
role: AgentMessageRoleWrapper.Get(role),
messageText: $"Hello from {role}");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
AgentMessageRoleWrapper role,
string messageText)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
AddConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
role,
messageText);
AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private AddConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
AgentMessageRoleWrapper role,
string messageText)
{
AddConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
Role = role,
};
actionBuilder.Content.Add(new AddConversationMessageContent.Builder
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse(messageText)
});
return AssignParent<AddConversationMessage>(actionBuilder);
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ClearAllVariablesExecutor"/>.
/// </summary>
public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ClearWorkflowScopeAsync()
{
// Arrange
this.State.Set("NoVar", FormulaValue.New("Old value"));
this.State.Bind();
ClearAllVariables model =
this.CreateModel(
this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)),
VariablesToClear.ConversationScopedVariables);
// Act
ClearAllVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("NoVar");
}
[Fact]
public async Task ClearUndefinedScopeAsync()
{
// Arrange
this.State.Set("NoVar", FormulaValue.New("Old value"));
this.State.Bind();
// Arrange
ClearAllVariables model =
this.CreateModel(
this.FormatDisplayName(nameof(ClearUndefinedScopeAsync)),
VariablesToClear.UserScopedVariables);
// Act
ClearAllVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("NoVar", FormulaValue.New("Old value"));
}
private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget)
{
ClearAllVariables.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variables = EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClearWrapper.Get(variableTarget)),
};
return AssignParent<ClearAllVariables>(actionBuilder);
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="CreateConversationExecutor "/>.
/// </summary>
public sealed class CreateConversationExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task CreateNewConversationAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(CreateNewConversationAsync),
"TestConversationId",
executionIteration: 1);
}
[Fact]
public async Task CreateMultipleConversationsAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(CreateMultipleConversationsAsync),
"TestConversationId",
executionIteration: 4);
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
int executionIteration)
{
// Arrange
// Initialize state to simulate workflow environment.
this.State.InitializeSystem();
CreateConversation model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName));
MockAgentProvider mockAgentProvider = new();
CreateConversationExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
int expectedIterationCount = executionIteration;
while (executionIteration-- > 0)
{
await this.ExecuteAsync(action);
}
// Assert
VerifyModel(model, action);
Assert.Equal(expected: expectedIterationCount, actual: mockAgentProvider.ExistingConversationIds.Count);
this.VerifyState("TestConversationId", FormulaValue.New(mockAgentProvider.ExistingConversationIds.Last()));
}
private CreateConversation CreateModel(string displayName, string conversationIdVariable)
{
CreateConversation.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = PropertyPath.Create(conversationIdVariable)
};
return AssignParent<CreateConversation>(actionBuilder);
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ParseValueExecutor"/>.
/// </summary>
public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ParseRecordAsync()
{
// Arrange
RecordDataType.Builder recordBuilder =
new()
{
Properties =
{
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
}
};
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseRecordAsync)),
recordBuilder,
@"{ ""key1"": ""val1"" }");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
}
[Fact]
public async Task ParseTableAsync()
{
// Arrange
RecordDataType.Builder recordBuilder =
new()
{
Properties =
{
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
}
};
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseTableAsync)),
DataType.EmptyTable,
@"[""apple"",""banana"",""cat""]");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat")));
}
[Fact]
public async Task ParseBooleanAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseTableAsync)),
new BooleanDataType.Builder(),
"True");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New(true));
}
[Fact]
public async Task ParseNumberAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseNumberAsync)),
new NumberDataType.Builder(),
"42");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New(42));
}
[Fact]
public async Task ParseStringAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseStringAsync)),
new StringDataType.Builder(),
"Hello, World!");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New("Hello, World!"));
}
private ParseValue CreateModel(string displayName, DataType.Builder typeBuilder, string sourceText)
{
ParseValue.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
ValueType = typeBuilder,
Variable = PropertyPath.TopicVariable("Target"),
Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))),
};
return AssignParent<ParseValue>(actionBuilder);
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ResetVariableExecutor"/>.
/// </summary>
public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ResetDefinedValueAsync()
{
// Arrange
this.State.Set("MyVar1", FormulaValue.New("Value #1"));
this.State.Set("MyVar2", FormulaValue.New("Value #2"));
ResetVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(ResetDefinedValueAsync)),
FormatVariablePath("MyVar1"));
// Act
ResetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("MyVar1");
this.VerifyState("MyVar2", FormulaValue.New("Value #2"));
}
[Fact]
public async Task ResetUndefinedValueAsync()
{
// Arrange
this.State.Set("MyVar1", FormulaValue.New("Value #1"));
ResetVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(ResetUndefinedValueAsync)),
FormatVariablePath("NoVar"));
// Act
ResetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("NoVar");
this.VerifyState("MyVar1", FormulaValue.New("Value #1"));
}
private ResetVariable CreateModel(string displayName, string variablePath)
{
ResetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
};
return AssignParent<ResetVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="RetrieveConversationMessageExecutor"/>.
/// </summary>
public sealed class RetrieveConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task RetrieveMessageSuccessfullyAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(RetrieveMessageSuccessfullyAsync),
"TestMessage");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
RetrieveConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
"DefaultMessageId");
RetrieveConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private RetrieveConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
string messageId)
{
RetrieveConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
MessageId = StringExpression.Literal(messageId)
};
return AssignParent<RetrieveConversationMessage>(actionBuilder);
}
}

View File

@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="RetrieveConversationMessagesExecutor"/>.
/// </summary>
public sealed class RetrieveConversationMessagesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task RetrieveAllMessagesSuccessfullyAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
nameof(RetrieveAllMessagesSuccessfullyAsync),
"TestMessages",
"TestConversationId");
}
[Fact]
public async Task RetrieveMessagesWithOptionalValuesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
nameof(RetrieveMessagesWithOptionalValuesAsync),
"TestMessages",
"TestConversationId",
limit: IntExpression.Literal(2),
after: StringExpression.Literal("11/01/2025"),
before: StringExpression.Literal("12/01/2025"),
sortOrder: EnumExpression<AgentMessageSortOrderWrapper>.Literal(AgentMessageSortOrderWrapper.Get(AgentMessageSortOrder.NewestFirst)));
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
string conversationId,
IntExpression? limit = null,
StringExpression? after = null,
StringExpression? before = null,
EnumExpression<AgentMessageSortOrderWrapper>? sortOrder = null)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
RetrieveConversationMessages model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
conversationId,
limit,
after,
before,
sortOrder);
RetrieveConversationMessagesExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
var testMessages = mockAgentProvider.TestMessages;
Assert.NotNull(testMessages);
VerifyModel(model, action);
this.VerifyState(variableName, testMessages.ToTable());
}
private RetrieveConversationMessages CreateModel(
string displayName,
string variableName,
string conversationId,
IntExpression? limit,
StringExpression? after,
StringExpression? before,
EnumExpression<AgentMessageSortOrderWrapper>? sortOrder)
{
RetrieveConversationMessages.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Messages = PropertyPath.Create(variableName),
ConversationId = StringExpression.Literal(conversationId)
};
if (limit is not null)
{
actionBuilder.Limit = limit;
}
if (after is not null)
{
actionBuilder.MessageAfter = after;
}
if (before is not null)
{
actionBuilder.MessageBefore = before;
}
if (sortOrder is not null)
{
actionBuilder.SortOrder = sortOrder;
}
return AssignParent<RetrieveConversationMessages>(actionBuilder);
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SendActivityExecutor"/>.
/// </summary>
public sealed class SendActivityExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task CaptureActivityAsync()
{
// Arrange
SendActivity model =
this.CreateModel(
this.FormatDisplayName(nameof(CaptureActivityAsync)),
"Test activity message");
// Act
SendActivityExecutor action = new(model, this.State);
WorkflowEvent[] events = await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
Assert.Contains(events, e => e is MessageActivityEvent);
}
private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null)
{
MessageActivityTemplate.Builder activityBuilder =
new()
{
Summary = summary,
Text = { TemplateLine.Parse(activityMessage) },
};
SendActivity.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Activity = activityBuilder.Build(),
};
return AssignParent<SendActivity>(actionBuilder);
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetMultipleVariablesExecutor"/>.
/// </summary>
public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task SetMultipleVariablesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesAsync),
assignments: [
new AssignmentCase("Variable1", new NumberDataValue(42), FormulaValue.New(42)),
new AssignmentCase("Variable2", new StringDataValue("Test"), FormulaValue.New("Test")),
new AssignmentCase("Variable3", new BooleanDataValue(true), FormulaValue.New(true))
]);
}
[Fact]
public async Task SetMultipleVariablesWithExpressionsAsync()
{
// Arrange
this.State.Set("SourceNumber", FormulaValue.New(10));
this.State.Set("SourceText", FormulaValue.New("Hello"));
this.State.Bind();
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithExpressionsAsync),
assignments: [
new AssignmentCase("CalcVariable", ValueExpression.Expression("Local.SourceNumber * 2"), FormulaValue.New(20)),
new AssignmentCase("ConcatVariable", ValueExpression.Expression(@"Concatenate(Local.SourceText, "" World"")"), FormulaValue.New("Hello World")),
new AssignmentCase("BoolVariable", ValueExpression.Expression("Local.SourceNumber > 5"), FormulaValue.New(true))
]);
}
[Fact]
public async Task SetMultipleVariablesWithVariableReferencesAsync()
{
// Arrange
this.State.Set("Source1", FormulaValue.New(123));
this.State.Set("Source2", FormulaValue.New("Reference"));
this.State.Bind();
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithVariableReferencesAsync),
assignments: [
new AssignmentCase("Target1", ValueExpression.Variable(PropertyPath.TopicVariable("Source1")), FormulaValue.New(123)),
new AssignmentCase("Target2", ValueExpression.Variable(PropertyPath.TopicVariable("Source2")), FormulaValue.New("Reference"))
]);
}
[Fact]
public async Task SetMultipleVariablesWithNullValuesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithNullValuesAsync),
assignments: [
new AssignmentCase("NullVar1", null, FormulaValue.NewBlank()),
new AssignmentCase("NormalVar", new StringDataValue("NotNull"), FormulaValue.New("NotNull")),
new AssignmentCase("NullVar2", null, FormulaValue.NewBlank())
]);
}
[Fact]
public async Task SetMultipleVariablesUpdateExistingAsync()
{
// Arrange
this.State.Set("ExistingVar1", FormulaValue.New(999));
this.State.Set("ExistingVar2", FormulaValue.New("OldValue"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesUpdateExistingAsync),
assignments: [
new AssignmentCase("ExistingVar1", new NumberDataValue(111), FormulaValue.New(111)),
new AssignmentCase("ExistingVar2", new StringDataValue("NewValue"), FormulaValue.New("NewValue")),
new AssignmentCase("NewVar", new BooleanDataValue(false), FormulaValue.New(false))
]);
}
[Fact]
public async Task SetMultipleVariablesEmptyAssignmentsAsync()
{
// Arrange
SetMultipleVariables model = this.CreateModel(nameof(SetMultipleVariablesEmptyAssignmentsAsync), []);
// Arrange, Act, Assert
Assert.Throws<DeclarativeModelException>(() =>
{
// Empty variables assignment should fail RequiredProperties validation.
_ = new SetMultipleVariablesExecutor(model, this.State);
});
}
private async Task ExecuteTestAsync(string displayName, AssignmentCase[] assignments)
{
// Arrange
SetMultipleVariables model = this.CreateModel(displayName, assignments);
// Act
SetMultipleVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
foreach (AssignmentCase assignment in assignments)
{
this.VerifyState(assignment.VariableName, assignment.ExpectedValue);
}
}
private SetMultipleVariables CreateModel(string displayName, AssignmentCase[] assignments)
{
SetMultipleVariables.Builder actionBuilder = new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
};
foreach (AssignmentCase assignment in assignments)
{
ValueExpression.Builder? valueExpressionBuilder = assignment.ValueExpression switch
{
null => null,
DataValue dataValue => new ValueExpression.Builder(ValueExpression.Literal(dataValue)),
ValueExpression valueExpression => new ValueExpression.Builder(valueExpression),
_ => throw new System.ArgumentException($"Unsupported value type: {assignment.ValueExpression?.GetType().Name}")
};
actionBuilder.Assignments.Add(new VariableAssignment.Builder()
{
Variable = PropertyPath.Create(FormatVariablePath(assignment.VariableName)),
Value = valueExpressionBuilder,
});
}
return AssignParent<SetMultipleVariables>(actionBuilder);
}
private sealed record AssignmentCase(string VariableName, object? ValueExpression, FormulaValue ExpectedValue);
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetTextVariableExecutor"/>.
/// </summary>
public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task SetLiteralValueAsync()
{
// Arrange
SetTextVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(SetLiteralValueAsync)),
FormatVariablePath("TextVar"),
"Text variable value");
// Act
SetTextVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("TextVar", FormulaValue.New("Text variable value"));
}
[Fact]
public async Task UpdateExistingValueAsync()
{
// Arrange
this.State.Set("TextVar", FormulaValue.New("Old value"));
SetTextVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(UpdateExistingValueAsync)),
FormatVariablePath("TextVar"),
"New value");
// Act
SetTextVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("TextVar", FormulaValue.New("New value"));
}
private SetTextVariable CreateModel(string displayName, string variablePath, string textValue)
{
SetTextVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
Value = TemplateLine.Parse(textValue),
};
return AssignParent<SetTextVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetVariableExecutor"/>.
/// </summary>
public sealed class SetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public void InvalidModel() =>
// Arrange, Act, Assert
Assert.Throws<DeclarativeModelException>(() => new SetVariableExecutor(new SetVariable(), this.State));
[Fact]
public async Task SetNumericValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetNumericValueAsync),
variableName: "TestVariable",
variableValue: new NumberDataValue(42),
expectedValue: FormulaValue.New(42));
[Fact]
public async Task SetStringValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetStringValueAsync),
variableName: "TestVariable",
variableValue: new StringDataValue("Text"),
expectedValue: FormulaValue.New("Text"));
[Fact]
public async Task SetBooleanValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanValueAsync),
variableName: "TestVariable",
variableValue: new BooleanDataValue(true),
expectedValue: FormulaValue.New(true));
[Fact]
public async Task SetBooleanExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("true || false"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(true));
}
[Fact]
public async Task SetNumberExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("9 - 3"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(6));
}
[Fact]
public async Task SetStringExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression(@"Concatenate(""A"", ""B"", ""C"")"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New("ABC"));
}
[Fact]
public async Task SetBooleanVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New(true));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(true));
}
[Fact]
public async Task SetNumberVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New(321));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(321));
}
[Fact]
public async Task SetStringVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New("Test"));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New("Test"));
}
[Fact]
public async Task UpdateExistingValueAsync()
{
// Arrange
this.State.Set("VarA", FormulaValue.New(33));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(UpdateExistingValueAsync),
variableName: "VarA",
variableValue: new NumberDataValue(42),
expectedValue: FormulaValue.New(42));
}
private Task ExecuteTestAsync(
string displayName,
string variableName,
DataValue variableValue,
FormulaValue expectedValue)
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Literal(variableValue));
// Act & Assert
return this.ExecuteTestAsync(displayName, variableName, expressionBuilder, expectedValue);
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
ValueExpression.Builder valueExpression,
FormulaValue expectedValue)
{
// Arrange
SetVariable model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
valueExpression);
this.State.Set(variableName, FormulaValue.New(33));
// Act
SetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState(variableName, expectedValue);
}
private SetVariable CreateModel(string displayName, string variablePath, ValueExpression.Builder valueExpression)
{
SetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
Value = valueExpression,
};
return AssignParent<SetVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Base test class for <see cref="DeclarativeActionExecutor"/> implementations.
/// </summary>
public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output)
{
internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create());
protected ActionId CreateActionId() => new($"{this.GetType().Name}_{Guid.NewGuid():N}");
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
internal async Task<WorkflowEvent[]> ExecuteAsync(DeclarativeActionExecutor executor)
{
TestWorkflowExecutor workflowExecutor = new();
WorkflowBuilder workflowBuilder = new(workflowExecutor);
workflowBuilder.AddEdge(workflowExecutor, executor);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State);
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
Assert.Contains(events, e => e is DeclarativeActionCompletedEvent);
ExecutorFailedEvent[] failureEvents = events.OfType<ExecutorFailedEvent>().ToArray();
switch (failureEvents.Length)
{
case 0:
break;
case 1:
throw failureEvents[0].Data ?? new XunitException("Executor failed without exception data.");
default:
AggregateException aggregateException = new("One or more executor failures occurred.", failureEvents.Select(e => e.Data).Where(e => e is not null).Cast<Exception>());
throw aggregateException;
}
return events;
}
internal static void VerifyModel(DialogAction model, DeclarativeActionExecutor action)
{
Assert.Equal(model.Id, action.Id);
Assert.Equal(model, action.Model);
}
protected void VerifyState(string variableName, FormulaValue expectedValue) => this.VerifyState(variableName, WorkflowFormulaState.DefaultScopeName, expectedValue);
internal void VerifyState(string variableName, string scopeName, FormulaValue expectedValue)
{
FormulaValue actualValue = this.State.Get(variableName, scopeName);
Assert.Equal(expectedValue.Format(), actualValue.Format());
}
internal void VerifyUndefined(string variableName, string? scopeName = null) =>
Assert.IsType<BlankValue>(this.State.Get(variableName, scopeName));
protected static TAction AssignParent<TAction>(DialogAction.Builder actionBuilder) where TAction : DialogAction
{
OnActivity.Builder activityBuilder =
new()
{
Id = new("root"),
};
activityBuilder.Actions.Add(actionBuilder);
OnActivity model = activityBuilder.Build();
return (TAction)model.Actions[0];
}
internal sealed class TestWorkflowExecutor() : Executor<WorkflowFormulaState>("test_workflow")
{
public override async ValueTask HandleAsync(WorkflowFormulaState message, IWorkflowContext context, CancellationToken cancellationToken) =>
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
}