chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
/// <summary>
/// Tests for <see cref="AgentProviderExtensions.InvokeAgentAsync"/>.
/// </summary>
public sealed class AgentProviderExtensionsTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
private const string WorkflowConversationId = "workflow-conv-id";
private const string AgentName = "test-agent";
[Fact]
public Task AutoSendFalseOnWorkflowConversationSuppressesResponseEventsAsync() =>
this.RunAsync(autoSend: false, conversationId: WorkflowConversationId, expectResponseEvents: false);
[Fact]
public Task AutoSendTrueOnWorkflowConversationEmitsResponseEventsAsync() =>
this.RunAsync(autoSend: true, conversationId: WorkflowConversationId, expectResponseEvents: true);
[Fact]
public Task AutoSendFalseOnExternalConversationSuppressesResponseEventsAsync() =>
this.RunAsync(autoSend: false, conversationId: "other-conv-id", expectResponseEvents: false);
[Fact]
public Task AutoSendTrueOnExternalConversationEmitsResponseEventsAndCopiesMessagesAsync() =>
this.RunAsync(
autoSend: true,
conversationId: "other-conv-id",
expectResponseEvents: true,
expectCrossConversationCopy: true);
private async Task RunAsync(
bool autoSend,
string conversationId,
bool expectResponseEvents,
bool expectCrossConversationCopy = false)
{
// Arrange: seed the workflow conversation id so IsWorkflowConversation can recognize it.
this.State.Set(
SystemScope.Names.ConversationId,
FormulaValue.New(WorkflowConversationId),
VariableScopeNames.System);
MockAgentProvider mockProvider = new();
AgentResponseUpdate[] updates =
[
new(ChatRole.Assistant, "hello "),
new(ChatRole.Assistant, "world"),
];
mockProvider
.Setup(p => p.InvokeAgentAsync(
AgentName,
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<IEnumerable<ChatMessage>?>(),
It.IsAny<IDictionary<string, object?>?>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(updates));
List<(string ConversationId, ChatMessage Message)> copiedMessages = [];
mockProvider
.Setup(p => p.CreateMessageAsync(
It.IsAny<string>(),
It.IsAny<ChatMessage>(),
It.IsAny<CancellationToken>()))
.Returns<string, ChatMessage, CancellationToken>(
(convId, msg, _) =>
{
copiedMessages.Add((convId, msg));
return Task.FromResult(msg);
});
string actionId = this.CreateActionId().Value;
// Act
WorkflowEvent[] events =
await this.ExecuteAsync(
actionId,
async (IWorkflowContext context, ActionExecutorResult _, CancellationToken cancellationToken) =>
{
await mockProvider.Object.InvokeAgentAsync(
actionId,
context,
AgentName,
conversationId,
autoSend,
cancellationToken: cancellationToken).ConfigureAwait(false);
});
// Assert
int updateEventCount = events.OfType<AgentResponseUpdateEvent>().Count();
int responseEventCount = events.OfType<AgentResponseEvent>().Count();
if (expectResponseEvents)
{
Assert.Equal(updates.Length, updateEventCount);
Assert.Equal(1, responseEventCount);
}
else
{
Assert.Equal(0, updateEventCount);
Assert.Equal(0, responseEventCount);
}
if (expectCrossConversationCopy)
{
Assert.NotEmpty(copiedMessages);
Assert.All(copiedMessages, c => Assert.Equal(WorkflowConversationId, c.ConversationId));
}
else
{
Assert.Empty(copiedMessages);
}
}
private static async IAsyncEnumerable<AgentResponseUpdate> ToAsyncEnumerableAsync(IEnumerable<AgentResponseUpdate> updates)
{
foreach (AgentResponseUpdate update in updates)
{
yield return update;
}
await Task.CompletedTask;
}
}
@@ -0,0 +1,933 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ChatMessageExtensionsTests
{
[Fact]
public void ToRecordWithSimpleTextMessage()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello World");
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Role);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Text);
FormulaValue roleField = result.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(roleField);
Assert.Equal(ChatRole.User.Value, roleValue.Value);
}
[Fact]
public void ToRecordWithAssistantMessage()
{
// Arrange
ChatMessage message = new(ChatRole.Assistant, "I can help you");
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Role);
FormulaValue roleField = result.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(roleField);
Assert.Equal(ChatRole.Assistant.Value, roleValue.Value);
}
[Fact]
public void ToRecordIncludesAllStandardFields()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Test")
{
MessageId = "msg-123"
};
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result.GetField(TypeSchema.Discriminator));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Id));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Role));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Content));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Text));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Metadata));
}
[Fact]
public void ToTableWithMultipleMessages()
{
// Arrange
IEnumerable<ChatMessage> messages =
[
new(ChatRole.User, "First message"),
new(ChatRole.Assistant, "Second message"),
new(ChatRole.User, "Third message")
];
// Act
TableValue result = messages.ToTable();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Rows.Count());
}
[Fact]
public void ToTableWithEmptyMessages()
{
// Arrange
IEnumerable<ChatMessage> messages = [];
// Act
TableValue result = messages.ToTable();
// Assert
Assert.NotNull(result);
Assert.Empty(result.Rows);
}
[Fact]
public void ToChatMessagesWithNull()
{
// Arrange
DataValue? value = null;
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessagesWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessagesWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Hello");
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal("Hello", message.Text);
}
[Fact]
public void ToChatMessagesWithRecordDataValue()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
DataValue record = source.ToRecord().ToDataValue();
// Act
IEnumerable<ChatMessage>? result = record.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(source.Role, message.Role);
Assert.Equal(source.Text, message.Text);
}
[Fact]
public void ToChatMessagesWithTableDataValue()
{
// Arrange
ChatMessage[] source = [new(ChatRole.User, "Test")];
DataValue table = source.ToTable().ToDataValue();
// Act
IEnumerable<ChatMessage>? result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(source[0].Role, message.Role);
Assert.Equal(source[0].Text, message.Text);
}
[Fact]
public void ToChatMessagesWithTableOfDataValue()
{
// Arrange
TableDataValue table = DataValue.TableFromValues([new StringDataValue("test")]);
// Act
IEnumerable<ChatMessage>? result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal("test", message.Text);
}
[Fact]
public void ToChatMessagesWithUnsupportedValue()
{
// Arrange
BooleanDataValue booleanValue = new(true);
// Act
IEnumerable<ChatMessage>? messages = booleanValue.ToChatMessages();
// Assert
Assert.Null(messages);
}
[Fact]
public void ToChatMessageFromStringDataValue()
{
// Arrange
StringDataValue value = StringDataValue.Create("Test message");
// Act
ChatMessage result = value.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test message", result.Text);
}
[Fact]
public void ToChatMessageFromDataValueRecord()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test", result.Text);
}
[Fact]
public void ToChatMessageFromDataValueString()
{
// Arrange
DataValue value = StringDataValue.Create("Test message");
// Act
ChatMessage? result = value.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test message", result.Text);
}
[Fact]
public void ToChatMessageFromBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
ChatMessage? result = value.ToChatMessage();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessageFromUnsupportedValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act & Assert
Assert.Throws<DeclarativeActionException>(() => value.ToChatMessage());
}
[Fact]
public void ToChatMessageFromRecordDataValue()
{
// Arrange
// Note: Use "Agent" not "Assistant" - AgentMessageRole.Agent maps to ChatRole.Assistant
RecordDataValue record = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("Agent")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
// Act
ChatMessage result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
}
[Fact]
public void ToChatMessageWithImpliedRole()
{
// Arrange
RecordValue source =
FormulaValue.NewRecordFromFields(
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(string.Empty)),
new NamedValue(
TypeSchema.Message.Fields.Content,
FormulaValue.NewTable(
TypeSchema.MessageContent.RecordType,
FormulaValue.NewRecordFromFields(
new NamedValue(TypeSchema.MessageContent.Fields.Type, TypeSchema.MessageContent.ContentTypes.Text.ToFormula()),
new NamedValue(TypeSchema.MessageContent.Fields.Value, FormulaValue.New("Test"))))));
RecordDataValue record = source.ToRecord();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test", result.Text);
}
[Fact]
public void ToChatMessageWithImageUrlContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageUrl.ToContent("https://example.com/image.jpg")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<UriContent>(content);
}
[Fact]
public void ToChatMessageWithWithImageDataContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageUrl.ToContent("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<DataContent>(content);
}
[Fact]
public void ToChatMessageWithWithImageFileContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageFile.ToContent("file-id-123")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<HostedFileContent>(content);
}
[Fact]
public void ToChatMessageWithUnsupportedContent()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
RecordDataValue record = source.ToRecord().ToRecord();
DataValue contentValue = record.Properties[TypeSchema.Message.Fields.Content];
TableDataValue contentValues = Assert.IsType<TableDataValue>(contentValue, exactMatch: false);
RecordDataValue badContent = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.MessageContent.Fields.Type, StringDataValue.Create(TypeSchema.MessageContent.ContentTypes.Text)),
new KeyValuePair<string, DataValue>(TypeSchema.MessageContent.Fields.Value, BooleanDataValue.Create(true)));
contentValues.Values.Add(badContent);
// Act
ChatMessage message = record.ToChatMessage();
// Assert
Assert.Single(message.Contents);
Assert.Equal("Test", message.Text);
}
[Fact]
public void ToChatMessageWithEmptyContent()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
source.Contents.Add(new TextContent(string.Empty));
RecordDataValue record = source.ToRecord().ToRecord();
// Act
ChatMessage message = record.ToChatMessage();
// Assert
Assert.Single(message.Contents);
Assert.Equal("Test", message.Text);
}
[Fact]
public void ToMetadataWithNull()
{
// Arrange
RecordDataValue? metadata = null;
// Act
AdditionalPropertiesDictionary? result = metadata.ToMetadata();
// Assert
Assert.Null(result);
}
[Fact]
public void ToMetadataWithProperties()
{
// Arrange
RecordDataValue metadata = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("key1", StringDataValue.Create("value1")),
new KeyValuePair<string, DataValue>("key2", NumberDataValue.Create(42)));
// Act
AdditionalPropertiesDictionary? result = metadata.ToMetadata();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.Equal("value1", result["key1"]);
Assert.Equal(42m, result["key2"]);
}
[Fact]
public void ToChatRoleFromAgentMessageRole()
{
// Act & Assert
Assert.Equal(ChatRole.Assistant, AgentMessageRole.Agent.ToChatRole());
Assert.Equal(ChatRole.User, AgentMessageRole.User.ToChatRole());
Assert.Equal(ChatRole.User, ((AgentMessageRole)99).ToChatRole());
Assert.Equal(ChatRole.User, ((AgentMessageRole?)null).ToChatRole());
}
[Fact]
public void AgentMessageContentTypeToContentMissing()
{
// Act & Assert
Assert.Null(AgentMessageContentType.Text.ToContent(string.Empty));
Assert.Null(AgentMessageContentType.Text.ToContent(null));
}
[Fact]
public void AgentMessageContentTypeToContentText()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.Text.ToContent("Sample text");
// Assert
Assert.NotNull(result);
TextContent textContent = Assert.IsType<TextContent>(result);
Assert.Equal("Sample text", textContent.Text);
}
[Fact]
public void ToContentWithImageUrlContentType()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageUrl.ToContent("https://example.com/image.jpg");
// Assert
Assert.NotNull(result);
UriContent uriContent = Assert.IsType<UriContent>(result);
Assert.Equal("https://example.com/image.jpg", uriContent.Uri.ToString());
}
[Fact]
public void ToContentWithImageUrlContentTypeDataUri()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageUrl.ToContent("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA");
// Assert
Assert.NotNull(result);
DataContent dataContent = Assert.IsType<DataContent>(result);
Assert.False(dataContent.Data.IsEmpty);
}
[Fact]
public void ToContentWithImageFileContentType()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageFile.ToContent("file-id-123");
// Assert
Assert.NotNull(result);
HostedFileContent fileContent = Assert.IsType<HostedFileContent>(result);
Assert.Equal("file-id-123", fileContent.FileId);
}
[Fact]
public void ToChatMessageFromFunctionResultContents()
{
// Arrange
IEnumerable<FunctionResultContent> functionResults =
[
new(callId: "call1", result: "Result 1"),
new(callId: "call2", result: "Result 2")
];
// Act
ChatMessage result = functionResults.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Tool, result.Role);
Assert.Equal(2, result.Contents.Count);
}
[Fact]
public void ToChatMessagesFromTableDataValueWithStrings()
{
// Arrange
TableDataValue table =
DataValue.TableFromValues(
[
StringDataValue.Create("Message 1"),
StringDataValue.Create("Message 2")
]);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count());
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
}
[Fact]
public void ToChatMessagesFromTableDataValueWithRecords()
{
// Arrange
RecordDataValue record1 = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("User")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
RecordDataValue record2 = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("Assistant")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
TableDataValue table = DataValue.TableFromRecords(record1, record2);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count());
}
[Fact]
public void ToChatMessagesFromTableDataValueWithSingleColumnRecords()
{
// Arrange
RecordDataValue innerRecord = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("User")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
RecordDataValue wrappedRecord = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Value", innerRecord));
TableDataValue table = DataValue.TableFromRecords(wrappedRecord);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
}
[Fact]
public void ToRecordWithMessageContainingMultipleContentItems()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("First part"),
new TextContent("Second part")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Equal(2, contentTable.Rows.Count());
}
[Fact]
public void ToRecordWithMessageContainingUriContent()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new UriContent("https://example.com/image.jpg", "image/*")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Single(contentTable.Rows);
}
[Fact]
public void ToRecordWithMessageContainingHostedFileContent()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new HostedFileContent("file-123")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Single(contentTable.Rows);
}
[Fact]
public void ToRecordWithMessageContainingMetadata()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Test message")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
["custom_key"] = "custom_value",
["count"] = 5
}
};
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue metadataField = result.GetField(TypeSchema.Message.Fields.Metadata);
RecordValue metadataRecord = Assert.IsType<RecordValue>(metadataField, exactMatch: false);
Assert.Equal(2, metadataRecord.Fields.Count());
}
[Fact]
public void RoundTripChatMessageAsRecord()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("Test message"),
new UriContent("https://example.com/image.jpg", "image/jpeg"),
new HostedFileContent("file_123abc"),
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
])
{
MessageId = "msg-001"
};
// Act
RecordValue result = message.ToRecord();
DataValue resultValue = result.ToDataValue();
ChatMessage? messageCopy = resultValue.ToChatMessage();
// Assert
Assert.NotNull(messageCopy);
Assert.Equal(message.Role, messageCopy.Role);
Assert.Equal(message.MessageId, messageCopy.MessageId);
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
foreach (AIContent contentCopy in messageCopy.Contents)
{
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
AssertAIContentEquivalent(sourceContent, contentCopy);
}
}
[Fact]
public void RoundTripChatMessageAsTable()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("Test message"),
new UriContent("https://example.com/image.jpg", "image/jpeg"),
new HostedFileContent("file_123abc"),
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
])
{
MessageId = "msg-001"
};
IEnumerable<ChatMessage> messages = [message];
// Act
TableValue result = messages.ToTable();
TableDataValue resultValue = result.ToTable();
ChatMessage[] messagesCopy = resultValue.ToChatMessages().ToArray();
// Assert
Assert.NotNull(messagesCopy);
ChatMessage messageCopy = Assert.Single(messagesCopy);
Assert.Equal(message.Role, messageCopy.Role);
Assert.Equal(message.MessageId, messageCopy.MessageId);
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
foreach (AIContent contentCopy in messageCopy.Contents)
{
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
AssertAIContentEquivalent(sourceContent, contentCopy);
}
}
/// <summary>
/// Compares two AIContent instances for equivalence without using Assert.Equivalent,
/// which fails on .NET Framework 4.7.2 due to ReadOnlySpan.GetHashCode() not being supported.
/// </summary>
private static void AssertAIContentEquivalent(AIContent expected, AIContent actual)
{
Assert.Equal(expected.GetType(), actual.GetType());
switch (expected)
{
case TextContent expectedText:
TextContent actualText = Assert.IsType<TextContent>(actual);
Assert.Equal(expectedText.Text, actualText.Text);
break;
case UriContent expectedUri:
UriContent actualUri = Assert.IsType<UriContent>(actual);
Assert.Equal(expectedUri.Uri, actualUri.Uri);
Assert.Equal(expectedUri.MediaType, actualUri.MediaType);
break;
case HostedFileContent expectedFile:
HostedFileContent actualFile = Assert.IsType<HostedFileContent>(actual);
Assert.Equal(expectedFile.FileId, actualFile.FileId);
break;
case DataContent expectedData:
DataContent actualData = Assert.IsType<DataContent>(actual);
Assert.Equal(expectedData.MediaType, actualData.MediaType);
Assert.Equal(expectedData.Data.ToArray(), actualData.Data.ToArray());
break;
default:
Assert.Fail($"Unexpected AIContent type: {expected.GetType().Name}");
break;
}
}
[Fact]
public void MergeForLastMessageReturnsInputWhenInputMessageIsNull()
{
// Arrange
ChatMessage input = new(ChatRole.User, "hello") { MessageId = "local" };
// Act
ChatMessage result = input.MergeForLastMessage(null);
// Assert
Assert.Same(input, result);
}
[Fact]
public void MergeForLastMessageReturnsSameInstanceAsRoundTripped()
{
// Arrange: returning the round-tripped instance keeps the merge forward-compatible
// with future ChatMessage properties (e.g., new metadata fields) without explicit copies.
ChatMessage input = new(ChatRole.User, "original");
ChatMessage roundTripped = new(ChatRole.User, "stripped") { MessageId = "server" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert
Assert.Same(roundTripped, result);
}
[Fact]
public void MergeForLastMessagePrefersOriginalTextOverRoundTrippedText()
{
// Arrange
ChatMessage input = new(ChatRole.User, "original text");
ChatMessage roundTripped = new(ChatRole.User, "stripped") { MessageId = "server-id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert
Assert.Equal("server-id", result.MessageId);
Assert.Equal("original text", result.Text);
TextContent text = Assert.IsType<TextContent>(Assert.Single(result.Contents));
Assert.Equal("original text", text.Text);
}
[Fact]
public void MergeForLastMessageReplacesTextInPlaceAndKeepsServerMedia()
{
// Arrange
HostedFileContent serverRef = new("file-abc");
ChatMessage input = new(ChatRole.User, [new TextContent("look at this:"), new DataContent("data:image/jpeg;base64,QUJD", "image/jpeg")]);
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped"), serverRef]) { MessageId = "server-id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert: server's text slot is replaced with original text; server's media reference is preserved.
Assert.Equal("server-id", result.MessageId);
Assert.Collection(result.Contents,
c => Assert.Equal("look at this:", Assert.IsType<TextContent>(c).Text),
c => Assert.Same(serverRef, c));
}
[Fact]
public void MergeForLastMessageAppendsOriginalTextWhenRoundTripHasNoTextSlot()
{
// Arrange: round-tripped message has only media (no text slot to replace).
HostedFileContent serverRef = new("file-1");
ChatMessage input = new(ChatRole.User, [new TextContent("middle"), new DataContent("data:image/jpeg;base64,QUE=", "image/jpeg")]);
ChatMessage roundTripped = new(ChatRole.User, [serverRef]) { MessageId = "id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert: media kept; original text appended at end.
Assert.Collection(result.Contents,
c => Assert.Same(serverRef, c),
c => Assert.Equal("middle", Assert.IsType<TextContent>(c).Text));
}
[Fact]
public void MergeForLastMessageReplacesMultipleTextSlotsInOrder()
{
// Arrange: input has two text items; round-tripped has two text slots interleaved with media.
HostedFileContent firstRef = new("file-1");
HostedFileContent secondRef = new("file-2");
ChatMessage input = new(ChatRole.User, [new TextContent("first"), new TextContent("second")]);
ChatMessage roundTripped = new(ChatRole.User, [firstRef, new TextContent("a"), secondRef, new TextContent("b")]) { MessageId = "id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert
Assert.Collection(result.Contents,
c => Assert.Same(firstRef, c),
c => Assert.Equal("first", Assert.IsType<TextContent>(c).Text),
c => Assert.Same(secondRef, c),
c => Assert.Equal("second", Assert.IsType<TextContent>(c).Text));
}
[Fact]
public void MergeForLastMessageFallsBackToInputTextWhenInputHasNoTextContent()
{
// Arrange: ChatMessage(role, "string") populates Text but no explicit TextContent
// when Contents is initially empty in some construction paths. Verify we still
// recover the original Text via input.Text.
ChatMessage input = new(ChatRole.User, "fallback text");
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped")]) { MessageId = "id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert
Assert.Equal("fallback text", Assert.IsType<TextContent>(Assert.Single(result.Contents)).Text);
}
[Fact]
public void MergeForLastMessagePreservesServerAuthoredProperties()
{
// Arrange: server (round-trip) is authoritative for metadata. Returning the
// round-tripped instance means any future ChatMessage property is automatically
// preserved without code changes here.
ChatMessage input = new(ChatRole.User, "hi")
{
AuthorName = "client-side",
AdditionalProperties = new AdditionalPropertiesDictionary { ["client"] = "value" },
};
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped")])
{
MessageId = "server",
AuthorName = "server-side",
AdditionalProperties = new AdditionalPropertiesDictionary { ["server"] = "value" },
};
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert
Assert.Equal("server", result.MessageId);
Assert.Equal("server-side", result.AuthorName);
Assert.NotNull(result.AdditionalProperties);
Assert.True(result.AdditionalProperties.ContainsKey("server"));
Assert.False(result.AdditionalProperties.ContainsKey("client"));
}
[Fact]
public void MergeForLastMessageHandlesEmptyInputContents()
{
// Arrange
ChatMessage input = new(ChatRole.User, new List<AIContent>());
HostedFileContent serverRef = new("file-only");
ChatMessage roundTripped = new(ChatRole.User, [serverRef]) { MessageId = "id" };
// Act
ChatMessage result = input.MergeForLastMessage(roundTripped);
// Assert: nothing to splice; round-tripped returned unchanged.
Assert.Same(roundTripped, result);
Assert.Equal("file-only", Assert.IsType<HostedFileContent>(Assert.Single(result.Contents)).FileId);
}
}
@@ -0,0 +1,845 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.ObjectModel;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class DataValueExtensionsTests
{
[Fact]
public void ToDataValueWithNull()
{
// Arrange
object? value = null;
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.IsType<BlankDataValue>(result);
}
[Fact]
public void ToDataValueWithUnassignedValue()
{
// Arrange
object value = UnassignedValue.Instance;
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.IsType<BlankDataValue>(result);
}
[Fact]
public void ToDataValueWithBooleanTrue()
{
// Arrange
const bool Value = true;
// Act
DataValue result = Value.ToDataValue();
// Assert
BooleanDataValue boolValue = Assert.IsType<BooleanDataValue>(result);
Assert.True(boolValue.Value);
}
[Fact]
public void ToDataValueWithBooleanFalse()
{
// Arrange
const bool Value = false;
// Act
DataValue result = Value.ToDataValue();
// Assert
BooleanDataValue boolValue = Assert.IsType<BooleanDataValue>(result);
Assert.False(boolValue.Value);
}
[Fact]
public void ToDataValueWithInt()
{
// Arrange
const int Value = 42;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(42, numberValue.Value);
}
[Fact]
public void ToDataValueWithLong()
{
// Arrange
const long Value = 9876543210L;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(9876543210L, numberValue.Value);
}
[Fact]
public void ToDataValueWithFloat()
{
// Arrange
const float Value = 3.14f;
// Act
DataValue result = Value.ToDataValue();
// Assert
FloatDataValue floatValue = Assert.IsType<FloatDataValue>(result);
Assert.Equal(3.14f, floatValue.Value, precision: 2);
}
[Fact]
public void ToDataValueWithDecimal()
{
// Arrange
const decimal Value = 123.456m;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(123.456m, numberValue.Value);
}
[Fact]
public void ToDataValueWithDouble()
{
// Arrange
const double Value = 2.71828;
// Act
DataValue result = Value.ToDataValue();
// Assert
FloatDataValue floatValue = Assert.IsType<FloatDataValue>(result);
Assert.Equal(2.71828, floatValue.Value, precision: 5);
}
[Fact]
public void ToDataValueWithString()
{
// Arrange
const string Value = "Test String";
// Act
DataValue result = Value.ToDataValue();
// Assert
StringDataValue stringValue = Assert.IsType<StringDataValue>(result);
Assert.Equal("Test String", stringValue.Value);
}
[Fact]
public void ToDataValueWithDateTimeZeroTime()
{
// Arrange
DateTime value = new(2025, 10, 17, 0, 0, 0);
// Act
DataValue result = value.ToDataValue();
// Assert
DateDataValue dateValue = Assert.IsType<DateDataValue>(result);
Assert.Equal(new DateTime(2025, 10, 17), dateValue.Value);
}
[Fact]
public void ToDataValueWithDateTimeNonZeroTime()
{
// Arrange
DateTime value = new(2025, 10, 17, 14, 30, 45);
// Act
DataValue result = value.ToDataValue();
// Assert
DateTimeDataValue dateTimeValue = Assert.IsType<DateTimeDataValue>(result);
Assert.Equal(new DateTime(2025, 10, 17, 14, 30, 45), dateTimeValue.Value.DateTime);
}
[Fact]
public void ToDataValueWithTimeSpan()
{
// Arrange
TimeSpan value = TimeSpan.FromHours(2.5);
// Act
DataValue result = value.ToDataValue();
// Assert
TimeDataValue timeValue = Assert.IsType<TimeDataValue>(result);
Assert.Equal(TimeSpan.FromHours(2.5), timeValue.Value);
}
[Fact]
public void ToDataValueWithDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Already a DataValue");
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.Same(value, result);
}
[Fact]
public void ToDataValueWithFormulaValue()
{
// Arrange
FormulaValue value = FormulaValue.New(123);
// Act
DataValue result = value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(123, numberValue.Value);
}
[Fact]
public void ToFormulaWithNull()
{
// Arrange
DataValue? value = null;
// Act
FormulaValue result = value.ToFormula();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToFormulaWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
FormulaValue result = value.ToFormula();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToFormulaWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
FormulaValue result = value.ToFormula();
// Assert
BooleanValue boolValue = Assert.IsType<BooleanValue>(result);
Assert.True(boolValue.Value);
}
[Fact]
public void ToFormulaWithNumberDataValue()
{
// Arrange
DataValue value = NumberDataValue.Create(99.5m);
// Act
FormulaValue result = value.ToFormula();
// Assert
DecimalValue decimalValue = Assert.IsType<DecimalValue>(result);
Assert.Equal(99.5m, decimalValue.Value);
}
[Fact]
public void ToFormulaWithFloatDataValue()
{
// Arrange
DataValue value = FloatDataValue.Create(1.23);
// Act
FormulaValue result = value.ToFormula();
// Assert
NumberValue numberValue = Assert.IsType<NumberValue>(result);
Assert.Equal(1.23, numberValue.Value, precision: 2);
}
[Fact]
public void ToFormulaWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Test");
// Act
FormulaValue result = value.ToFormula();
// Assert
StringValue stringValue = Assert.IsType<StringValue>(result);
Assert.Equal("Test", stringValue.Value);
}
[Fact]
public void ToFormulaWithDateTimeDataValue()
{
// Arrange
DateTime dateTime = new(2025, 10, 17, 12, 0, 0);
DataValue value = DateTimeDataValue.Create(dateTime);
// Act
FormulaValue result = value.ToFormula();
// Assert
DateTimeValue dateTimeValue = Assert.IsType<DateTimeValue>(result);
Assert.Equal(dateTime, dateTimeValue.GetConvertedValue(TimeZoneInfo.Utc));
}
[Fact]
public void ToFormulaWithDateDataValue()
{
// Arrange
DateTime date = new(2025, 10, 17);
DataValue value = DateDataValue.Create(date);
// Act
FormulaValue result = value.ToFormula();
// Assert
DateValue dateValue = Assert.IsType<DateValue>(result);
Assert.Equal(date, dateValue.GetConvertedValue(TimeZoneInfo.Utc));
}
[Fact]
public void ToFormulaWithTimeDataValue()
{
// Arrange
TimeSpan time = TimeSpan.FromHours(3);
DataValue value = TimeDataValue.Create(time);
// Act
FormulaValue result = value.ToFormula();
// Assert
TimeValue timeValue = Assert.IsType<TimeValue>(result);
Assert.Equal(time, timeValue.Value);
}
[Fact]
public void ToFormulaWithRecordDataValue()
{
// Arrange
DataValue value = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Name", StringDataValue.Create("John")),
new KeyValuePair<string, DataValue>("Age", NumberDataValue.Create(30)));
// Act
FormulaValue result = value.ToFormula();
// Assert
RecordValue recordValue = Assert.IsType<RecordValue>(result, exactMatch: false);
Assert.Equal(2, recordValue.Fields.Count());
}
[Fact]
public void ToFormulaWithTableDataValue()
{
// Arrange
RecordDataValue record = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Field", StringDataValue.Create("Value")));
DataValue value = DataValue.TableFromRecords(ImmutableArray.Create(record));
// Act
FormulaValue result = value.ToFormula();
// Assert
TableValue tableValue = Assert.IsType<TableValue>(result, exactMatch: false);
Assert.Single(tableValue.Rows);
}
[Fact]
public void ToFormulaTypeWithNull()
{
// Arrange
DataValue? value = null;
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Blank, result);
}
[Fact]
public void ToFormulaTypeWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Boolean, result);
}
[Fact]
public void ToFormulaTypeWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Test");
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.String, result);
}
[Fact]
public void DataTypeToFormulaTypeWithNull()
{
// Arrange
DataType? type = null;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Blank, result);
}
[Fact]
public void DataTypeToFormulaTypeWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Boolean, result);
}
[Fact]
public void DataTypeToFormulaTypeWithNumberDataType()
{
// Arrange
DataType type = NumberDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Decimal, result);
}
[Fact]
public void DataTypeToFormulaTypeWithFloatDataType()
{
// Arrange
DataType type = FloatDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Number, result);
}
[Fact]
public void DataTypeToFormulaTypeWithStringDataType()
{
// Arrange
DataType type = StringDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.String, result);
}
[Fact]
public void DataTypeToFormulaTypeWithDateTimeDataType()
{
// Arrange
DataType type = DateTimeDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.DateTime, result);
}
[Fact]
public void DataTypeToFormulaTypeWithDateDataType()
{
// Arrange
DataType type = DateDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Date, result);
}
[Fact]
public void DataTypeToFormulaTypeWithTimeDataType()
{
// Arrange
DataType type = TimeDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Time, result);
}
[Fact]
public void ToObjectWithNull()
{
// Arrange
DataValue? value = null;
// Act
object? result = value.ToObject();
// Assert
Assert.Null(result);
}
[Fact]
public void ToObjectWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
object? result = value.ToObject();
// Assert
Assert.Null(result);
}
[Fact]
public void ToObjectWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<bool>(result);
Assert.True((bool)result);
}
[Fact]
public void ToObjectWithNumberDataValue()
{
// Arrange
DataValue value = NumberDataValue.Create(42.5m);
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<decimal>(result);
Assert.Equal(42.5m, (decimal)result);
}
[Fact]
public void ToObjectWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Hello");
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<string>(result);
Assert.Equal("Hello", (string)result);
}
[Fact]
public void ToClrTypeWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(bool), result);
}
[Fact]
public void ToClrTypeWithNumberDataType()
{
// Arrange
DataType type = NumberDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(decimal), result);
}
[Fact]
public void ToClrTypeWithFloatDataType()
{
// Arrange
DataType type = FloatDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(double), result);
}
[Fact]
public void ToClrTypeWithStringDataType()
{
// Arrange
DataType type = StringDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(string), result);
}
[Fact]
public void ToClrTypeWithDateTimeDataType()
{
// Arrange
DataType type = DateTimeDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(DateTime), result);
}
[Fact]
public void ToClrTypeWithTimeDataType()
{
// Arrange
DataType type = TimeDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(TimeSpan), result);
}
[Fact]
public void AsListWithNull()
{
// Arrange
DataValue? value = null;
// Act
IList<string>? result = value.AsList<string>();
// Assert
Assert.Null(result);
}
[Fact]
public void AsListWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
IList<string>? result = value.AsList<string>();
// Assert
Assert.Null(result);
}
[Fact]
public void NewBlankWithNullDataType()
{
// Arrange
DataType? type = null;
// Act
FormulaValue result = type.NewBlank();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void NewBlankWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
FormulaValue result = type.NewBlank();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToRecordValueWithRecordDataValue()
{
// Arrange
RecordDataValue recordDataValue = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Field1", StringDataValue.Create("Value1")),
new KeyValuePair<string, DataValue>("Field2", NumberDataValue.Create(123)));
// Act
RecordValue result = recordDataValue.ToRecordValue();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Fields.Count());
Assert.NotNull(result.GetField("Field1"));
Assert.NotNull(result.GetField("Field2"));
}
[Fact]
public void ToRecordTypeWithRecordDataType()
{
// Arrange
RecordDataType recordDataType = new RecordDataType.Builder
{
Properties =
{
["Name"] = new PropertyInfo.Builder
{
Type = StringDataType.Instance
}.Build(),
["Count"] = new PropertyInfo.Builder
{
Type = NumberDataType.Instance
}.Build()
}
}.Build();
// Act
RecordType result = recordDataType.ToRecordType();
// Assert
Assert.NotNull(result);
IEnumerable<NamedFormulaType> fieldTypes = result.GetFieldTypes();
List<NamedFormulaType> fieldTypesList = fieldTypes.ToList();
Assert.Equal(2, fieldTypesList.Count);
IEnumerable<string> fieldNames = fieldTypesList.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("Count", fieldNames);
NamedFormulaType nameField = fieldTypesList.First(f => f.Name.Value == "Name");
NamedFormulaType countField = fieldTypesList.First(f => f.Name.Value == "Count");
Assert.Equal(FormulaType.String, nameField.Type);
Assert.Equal(FormulaType.Decimal, countField.Type);
}
[Fact]
public void ToRecordValueWithDictionary()
{
// Arrange
IDictionary dictionary = new Dictionary<string, object>
{
["Key1"] = "Value1",
["Key2"] = 42
};
// Act
RecordDataValue result = dictionary.ToRecordValue();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Properties.Count);
Assert.True(result.Properties.ContainsKey("Key1"));
Assert.True(result.Properties.ContainsKey("Key2"));
}
[Fact]
public void ToTableValueWithEmptyEnumerable()
{
// Arrange
IEnumerable enumerable = Array.Empty<object>();
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
Assert.Empty(result.Values);
}
[Fact]
public void ToTableValueWithDictionaryEnumerable()
{
// Arrange
IEnumerable enumerable = new List<IDictionary>
{
new Dictionary<string, object> { ["Name"] = "Alice", ["Age"] = 30 },
new Dictionary<string, object> { ["Name"] = "Bob", ["Age"] = 25 }
};
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
}
[Fact]
public void ToTableValueWithPrimitiveEnumerable()
{
// Arrange
IEnumerable enumerable = new List<int> { 1, 2, 3 };
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Values.Length);
}
}
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.PowerFx;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class DeclarativeWorkflowOptionsExtensionsTests
{
[Fact]
public void NullContext_UsesDefaultMaximumExpressionLength()
{
// Arrange
DeclarativeWorkflowOptions? options = null;
// Act
RecalcEngine engine = options.CreateRecalcEngine();
// Assert
Assert.NotNull(engine);
Assert.Equal(10000, engine.Config.MaximumExpressionLength);
}
[Fact]
public void OptionsWithoutLimits_UsesDefaults()
{
// Arrange
DeclarativeWorkflowOptions options = CreateOptions();
// Act
RecalcEngine engine = options.CreateRecalcEngine();
// Assert
Assert.NotNull(engine);
Assert.Equal(10000, engine.Config.MaximumExpressionLength);
Assert.True(engine.Config.MaxCallDepth >= 0);
}
[Fact]
public void OptionsWithBothLimits()
{
// Arrange
const int ExpectedLength = 5000;
const int ExpectedDepth = 12;
DeclarativeWorkflowOptions context = CreateOptions(ExpectedLength, ExpectedDepth);
// Act
RecalcEngine engine = context.CreateRecalcEngine();
// Assert
Assert.Equal(ExpectedLength, engine.Config.MaximumExpressionLength);
Assert.Equal(ExpectedDepth, engine.Config.MaxCallDepth);
}
// Factory for creating options and mock provider
private static DeclarativeWorkflowOptions CreateOptions(
int? maximumExpressionLength = null,
int? maximumCallDepth = null)
{
Mock<ResponseAgentProvider> providerMock = new(MockBehavior.Strict);
return
new(providerMock.Object)
{
MaximumExpressionLength = maximumExpressionLength,
MaximumCallDepth = maximumCallDepth
};
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.ObjectModel;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
/// <summary>
/// Tests for <see cref="DialogBaseExtensions"/>.
/// </summary>
public sealed class DialogBaseExtensionsTests
{
[Fact]
public void WrapWithBotCreatesValidBotDefinition()
{
// Arrange
AdaptiveDialog dialog = new AdaptiveDialog.Builder()
{
BeginDialog = new OnActivity.Builder()
{
Id = "test_dialog",
},
}.Build();
// Assert
Assert.False(dialog.HasSchemaName);
// Act
AdaptiveDialog wrappedDialog = dialog.WrapWithBot();
// Assert
VerifyWrappedDialog(wrappedDialog);
// Act & Assert
VerifyWrappedDialog(wrappedDialog.WrapWithBot());
}
private static void VerifyWrappedDialog(AdaptiveDialog wrappedDialog)
{
Assert.NotNull(wrappedDialog);
Assert.NotNull(wrappedDialog.BeginDialog);
Assert.Equal("test_dialog", wrappedDialog.BeginDialog.Id);
Assert.True(wrappedDialog.HasSchemaName);
}
}
@@ -0,0 +1,217 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ExpandoObjectExtensionsTests
{
[Fact]
public void ToRecordTypeWithEmptyExpandoObject()
{
// Arrange
ExpandoObject expando = new();
// Act
RecordType recordType = expando.ToRecordType();
// Assert
Assert.NotNull(recordType);
Assert.Empty(recordType.GetFieldTypes());
}
[Fact]
public void ToRecordTypeWithStringProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "John Doe";
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Single(fieldTypes);
NamedFormulaType field = fieldTypes.First();
Assert.Equal("Name", field.Name.Value);
Assert.Equal(FormulaType.String, field.Type);
}
[Fact]
public void ToRecordTypeWithMultipleProperties()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Alice";
expando.Age = 30;
expando.IsActive = true;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Equal(3, fieldTypes.Count());
IEnumerable<string> fieldNames = fieldTypes.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("Age", fieldNames);
Assert.Contains("IsActive", fieldNames);
}
[Fact]
public void ToRecordTypeWithNullProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Test";
expando.NullValue = null;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Equal(2, fieldTypes.Count());
IEnumerable<string> fieldNames = fieldTypes.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("NullValue", fieldNames);
}
[Fact]
public void ToRecordWithEmptyExpandoObject()
{
// Arrange
ExpandoObject expando = new();
// Act
RecordValue recordValue = expando.ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Empty(recordValue.Fields);
}
[Fact]
public void ToRecordWithStringProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Message = "Hello World";
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Single(recordValue.Fields);
NamedValue field = recordValue.Fields.First();
Assert.Equal("Message", field.Name);
StringValue stringValue = Assert.IsType<StringValue>(field.Value);
Assert.Equal("Hello World", stringValue.Value);
}
[Fact]
public void ToRecordWithMultiplePropertiesOfDifferentTypes()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Bob";
expando.Count = 42;
expando.Active = true;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(3, recordValue.Fields.Count());
FormulaValue nameField = recordValue.GetField("Name");
StringValue nameValue = Assert.IsType<StringValue>(nameField);
Assert.Equal("Bob", nameValue.Value);
FormulaValue countField = recordValue.GetField("Count");
DecimalValue countValue = Assert.IsType<DecimalValue>(countField);
Assert.Equal(42, countValue.Value);
FormulaValue activeField = recordValue.GetField("Active");
BooleanValue activeValue = Assert.IsType<BooleanValue>(activeField);
Assert.True(activeValue.Value);
}
[Fact]
public void ToRecordWithNestedExpandoObject()
{
// Arrange
dynamic nested = new ExpandoObject();
nested.InnerValue = "Inner";
dynamic expando = new ExpandoObject();
expando.Outer = "Outer";
expando.Nested = nested;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(2, recordValue.Fields.Count());
Assert.NotNull(recordValue.GetField("Outer"));
FormulaValue nestedField = recordValue.GetField("Nested");
Assert.NotNull(nestedField);
RecordValue nestedRecord = Assert.IsType<RecordValue>(nestedField, exactMatch: false);
Assert.Single(nestedRecord.Fields);
}
[Fact]
public void ToRecordWithNullProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Test";
expando.NullValue = null;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(2, recordValue.Fields.Count());
FormulaValue nullField = recordValue.GetField("NullValue");
Assert.IsType<BlankValue>(nullField);
}
[Fact]
public void ToRecordTypeAndToRecordAreConsistent()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.StringField = "Value";
expando.IntField = 123;
expando.BoolField = false;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
List<NamedFormulaType> fieldTypesList = recordType.GetFieldTypes().ToList();
Assert.Equal(fieldTypesList.Count, recordValue.Fields.Count());
foreach (NamedFormulaType fieldType in fieldTypesList)
{
Assert.Contains(recordValue.Fields, f => f.Name == fieldType.Name.Value);
}
}
}
@@ -0,0 +1,215 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.ObjectModel;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public class FormulaValueExtensionsTests
{
[Fact]
public void BooleanValue()
{
BooleanValue formulaValue = FormulaValue.New(true);
DataValue dataValue = formulaValue.ToDataValue();
BooleanDataValue typedValue = Assert.IsType<BooleanDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
BooleanValue formulaCopy = Assert.IsType<BooleanValue>(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(bool.TrueString, formulaValue.Format());
}
[Fact]
public void StringValues()
{
StringValue formulaValue = FormulaValue.New("test value");
Assert.Equal(StringDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
StringDataValue typedValue = Assert.IsType<StringDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
StringValue formulaCopy = Assert.IsType<StringValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(formulaValue.Value, formulaValue.Format());
}
[Fact]
public void DecimalValues()
{
DecimalValue formulaValue = FormulaValue.New(45.3m);
Assert.Equal(NumberDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
NumberDataValue typedValue = Assert.IsType<NumberDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
DecimalValue formulaCopy = Assert.IsType<DecimalValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("45.3", formulaValue.Format());
}
[Fact]
public void NumberValues()
{
NumberValue formulaValue = FormulaValue.New(3.1415926535897);
Assert.Equal(FloatDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
FloatDataValue typedValue = Assert.IsType<FloatDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
NumberValue formulaCopy = Assert.IsType<NumberValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("3.1415926535897", formulaValue.Format());
}
[Fact]
public void BlankValues()
{
BlankValue formulaValue = FormulaValue.NewBlank();
Assert.Equal(DataType.Blank, formulaValue.GetDataType());
Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
Assert.Equal(string.Empty, formulaValue.Format());
}
[Fact]
public void VoidValues()
{
VoidValue formulaValue = FormulaValue.NewVoid();
Assert.Equal(DataType.Unspecified, formulaValue.GetDataType());
Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
}
[Fact]
public void DateValues()
{
DateTime timestamp = DateTime.UtcNow.Date;
DateValue formulaValue = FormulaValue.NewDateOnly(timestamp);
Assert.Equal(DataType.Date, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
DateDataValue typedValue = Assert.IsType<DateDataValue>(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
DateValue formulaCopy = Assert.IsType<DateValue>(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
}
[Fact]
public void DateTimeValues()
{
DateTime timestamp = DateTime.UtcNow;
DateTimeValue formulaValue = FormulaValue.New(timestamp);
Assert.Equal(DataType.DateTime, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
DateTimeDataValue typedValue = Assert.IsType<DateTimeDataValue>(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
DateTimeValue formulaCopy = Assert.IsType<DateTimeValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
}
[Fact]
public void TimeValues()
{
TimeValue formulaValue = FormulaValue.New(TimeSpan.Parse("10:35"));
Assert.Equal(DataType.Time, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
TimeDataValue typedValue = Assert.IsType<TimeDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
TimeValue formulaCopy = Assert.IsType<TimeValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("10:35:00", formulaValue.Format());
}
[Fact]
public void RecordValues()
{
RecordValue formulaValue = FormulaValue.NewRecordFromFields(
new NamedValue("FieldA", FormulaValue.New("Value1")),
new NamedValue("FieldB", FormulaValue.New("Value2")),
new NamedValue("FieldC", FormulaValue.New("Value3")));
Assert.Equal(DataType.EmptyRecord, formulaValue.GetDataType());
RecordDataValue dataValue = formulaValue.ToRecord();
Assert.Equal(formulaValue.Fields.Count(), dataValue.Properties.Count);
foreach (KeyValuePair<string, DataValue> property in dataValue.Properties)
{
Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name));
}
RecordValue formulaCopy = Assert.IsType<RecordValue>(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count);
foreach (NamedValue field in formulaCopy.Fields)
{
Assert.Contains(field.Name, dataValue.Properties.Keys);
}
Assert.Equal(
"""
{
"FieldA": "Value1",
"FieldB": "Value2",
"FieldC": "Value3"
}
""",
formulaValue.Format().Replace(Environment.NewLine, "\n"));
Dictionary<string, int> source =
new()
{
["FieldA"] = 1,
["FieldB"] = 2,
["FieldC"] = 3
};
FormulaValue formula = source.ToFormula();
Assert.IsType<RecordValue>(formula, exactMatch: false);
}
[Fact]
public void TableValues()
{
RecordValue recordValue = FormulaValue.NewRecordFromFields(
new NamedValue("FieldA", FormulaValue.New("Value1")),
new NamedValue("FieldB", FormulaValue.New("Value2")),
new NamedValue("FieldC", FormulaValue.New("Value3")));
TableValue formulaValue = FormulaValue.NewTable(recordValue.Type, [recordValue]);
TableDataValue dataValue = formulaValue.ToTable();
Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length);
TableValue formulaCopy = Assert.IsType<TableValue>(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length);
Assert.Equal(
"""
[
{
"FieldA": "Value1",
"FieldB": "Value2",
"FieldC": "Value3"
}
]
""",
formulaValue.Format().Replace(Environment.NewLine, "\n"));
}
}
@@ -0,0 +1,764 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class JsonDocumentExtensionsTests
{
[Fact]
public void ParseRecord_Object_PrimitiveFields_Succeeds()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string)),
("numberInt", typeof(int)),
("numberLong", typeof(long)),
("numberDecimal", typeof(decimal)),
("numberDouble", typeof(double)),
("flag", typeof(bool)),
("date", typeof(DateTime)),
("time", typeof(TimeSpan))
]);
DateTime expectedDateTime = new(2024, 10, 01, 12, 34, 56, DateTimeKind.Utc);
TimeSpan expectedTimeSpan = new(12, 34, 56);
JsonDocument document = JsonDocument.Parse(
"""
{
"text": "hello",
"numberInt": 7,
"numberLong": 9223372036854775807,
"numberDecimal": 12.5,
"numberDouble": 3.99E99,
"flag": true,
"date": "2024-10-01T12:34:56Z",
"time": "12:34:56"
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Equal("hello", result["text"]);
Assert.Equal(7, result["numberInt"]);
Assert.Equal(9223372036854775807L, result["numberLong"]);
Assert.Equal(12.5m, result["numberDecimal"]);
Assert.Equal(3.99E99, result["numberDouble"]);
Assert.Equal(true, result["flag"]);
Assert.Equal(expectedDateTime, result["date"]);
Assert.Equal(expectedTimeSpan, result["time"]);
}
[Fact]
public void ParseRecord_Object_NoSchema_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
{
"text": "hello",
"numberInt": 7,
"numberLong": 9223372036854775807,
"numberDecimal": 12.5,
"numberDouble": 3.99E99,
"flag": true,
"date": "2024-10-01T12:34:56Z",
"time": "12:34:56"
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(VariableType.RecordType);
// Assert
Assert.Equal("hello", result["text"]);
Assert.Equal(7, result["numberInt"]);
Assert.Equal(9223372036854775807L, result["numberLong"]);
Assert.Equal(12.5m, result["numberDecimal"]);
Assert.Equal(3.99E99, result["numberDouble"]);
Assert.Equal(true, result["flag"]);
Assert.Equal("2024-10-01T12:34:56Z", result["date"]);
Assert.Equal("12:34:56", result["time"]);
}
[Fact]
public void ParseRecord_Object_NestedRecord_Succeeds()
{
// Arrange
VariableType innerRecord =
VariableType.Record(
[
("innerText", typeof(string)),
("innerNumber", typeof(int))
]);
VariableType outerRecord =
VariableType.Record(
[
("outerText", typeof(string)),
("nested", innerRecord)
]);
JsonDocument document = JsonDocument.Parse(
"""
{
"outerText": "outer",
"nested": {
"innerText": "inner",
"innerNumber": 42
}
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(outerRecord);
// Assert
Assert.Equal("outer", result["outerText"]);
Dictionary<string, object?> nested = (Dictionary<string, object?>)result["nested"]!;
Assert.NotNull(nested);
Assert.True(nested.ContainsKey("innerText"));
Assert.Equal("inner", nested["innerText"]);
Assert.Equal(42, nested["innerNumber"]);
}
[Fact]
public void ParseRecord_NullRoot_ReturnsEmpty()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string))
]);
JsonDocument document = JsonDocument.Parse("null");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Empty(result);
}
[Fact]
public void ParseRecord_ArrayWithSingleRecord_Succeeds()
{
// Arrange
VariableType listType =
VariableType.List(
[
("name", typeof(string)),
("value", typeof(int))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{
"name": "item",
"value": 5
}
]
""");
// Act
List<object?> result = document.ParseList(listType);
// Assert
Assert.Single(result);
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result[0]);
Assert.Equal("item", element["name"]);
Assert.Equal(5, element["value"]);
}
[Fact]
public void ParseRecord_ArrayWithMultipleRecords_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("id", typeof(int))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{ "id": 1 },
{ "id": 2 }
]
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_InvalidTargetType_Throws()
{
// Arrange
VariableType notARecord = typeof(string);
JsonDocument document = JsonDocument.Parse(
"""
{ "x": 1 }
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(notARecord));
}
[Fact]
public void ParseRecord_InvalidRootKind_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string))
]);
JsonDocument document = JsonDocument.Parse(@"""not-an-object""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_UnsupportedPropertyType_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("unsupported", typeof(Guid))
]);
JsonDocument document = JsonDocument.Parse(
"""
{ "unsupported": "C2556C11-210E-4BB6-BF18-4A8968CB45A8" }
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_MissingRequiredProperty_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("required", typeof(bool))
]);
JsonDocument document = JsonDocument.Parse("{}");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_MissingNullableProperty_Succeeds()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("required", typeof(string))
]);
JsonDocument document = JsonDocument.Parse("{}");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Single(result);
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result);
Assert.Null(element["required"]);
}
[Fact]
public void ParseList_NullRoot_ReturnsEmpty()
{
// Arrange
JsonDocument document = JsonDocument.Parse("null");
// Act
List<object?> result = document.ParseList(typeof(int[]));
// Assert
Assert.Empty(result);
}
[Fact]
public void ParseList_Array_Primitives_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,2,3]");
// Act
List<object?> result = document.ParseList(typeof(int[]));
// Assert
Assert.Equal(3, result.Count);
Assert.Equal(1, result[0]);
Assert.Equal(2, result[1]);
Assert.Equal(3, result[2]);
}
[Fact]
public void ParseList_PrimitiveRoot_WrappedAsSingleElement_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse("7");
// Act
List<object?> result = document.ParseList(typeof(int));
// Assert
Assert.Single(result);
Assert.Equal(7, result[0]);
}
[Fact]
public void ParseList_Array_Records_Succeeds()
{
// Arrange
VariableType listType =
VariableType.List(
[
("id", typeof(int)),
("name", typeof(string))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{ "id": 1, "name": "a" },
{ "id": 2, "name": "b" }
]
""");
// Act
List<object?> result = document.ParseList(listType);
// Assert
Assert.Equal(2, result.Count);
Dictionary<string, object?> first = (Dictionary<string, object?>)result[0]!;
Dictionary<string, object?> second = (Dictionary<string, object?>)result[1]!;
Assert.NotNull(first);
Assert.Equal(1, first["id"]);
Assert.Equal("a", first["name"]);
Assert.NotNull(second);
Assert.Equal(2, second["id"]);
Assert.Equal("b", second["name"]);
}
[Fact]
public void ParseList_InvalidTargetType_Throws()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,2]");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int)));
}
[Fact]
public void ParseList_Array_MixedTypes_Throws()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,\"two\",3]");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int[])));
}
/// <summary>
/// Regression test for #4195: When a JSON object contains an array of objects
/// and is parsed with <c>VariableType.RecordType</c> (no schema), the nested
/// object properties must be preserved. Before the fix, DetermineElementType()
/// created an empty-schema VariableType, causing ParseRecord to take the
/// ParseSchema path (zero fields) and return empty dictionaries.
/// </summary>
[Fact]
public void ParseRecord_ObjectWithArrayOfObjects_NoSchema_PreservesNestedProperties()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
{
"items": [
{ "name": "Alice", "role": "Engineer" },
{ "name": "Bob", "role": "Designer" },
{ "name": "Carol", "role": "PM" }
]
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(VariableType.RecordType);
// Assert
Assert.True(result.ContainsKey("items"));
List<object?> items = Assert.IsType<List<object?>>(result["items"]);
Assert.Equal(3, items.Count);
Dictionary<string, object?> first = Assert.IsType<Dictionary<string, object?>>(items[0]);
Assert.Equal("Alice", first["name"]);
Assert.Equal("Engineer", first["role"]);
Dictionary<string, object?> second = Assert.IsType<Dictionary<string, object?>>(items[1]);
Assert.Equal("Bob", second["name"]);
Assert.Equal("Designer", second["role"]);
Dictionary<string, object?> third = Assert.IsType<Dictionary<string, object?>>(items[2]);
Assert.Equal("Carol", third["name"]);
Assert.Equal("PM", third["role"]);
}
/// <summary>
/// Regression test for #4195: When a JSON array of objects is parsed directly
/// via <c>ParseList</c> with <c>VariableType.ListType</c> (no schema), all
/// object properties must be preserved in each element.
/// </summary>
[Fact]
public void ParseList_ArrayOfObjects_NoSchema_PreservesProperties()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[
{ "name": "Alice", "role": "Engineer" },
{ "name": "Bob", "role": "Designer" }
]
""");
// Act
List<object?> result = document.ParseList(VariableType.ListType);
// Assert
Assert.Equal(2, result.Count);
Dictionary<string, object?> first = Assert.IsType<Dictionary<string, object?>>(result[0]);
Assert.Equal("Alice", first["name"]);
Assert.Equal("Engineer", first["role"]);
Dictionary<string, object?> second = Assert.IsType<Dictionary<string, object?>>(result[1]);
Assert.Equal("Bob", second["name"]);
Assert.Equal("Designer", second["role"]);
}
[Fact]
public void GetListTypeFromJson_EmptyArray_ReturnsFallbackListType()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[]");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.Equal(VariableType.ListType, result.Type);
Assert.False(result.HasSchema);
}
[Fact]
public void GetListTypeFromJson_ArrayOfPrimitives_ReturnsFallbackListType()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1, 2, 3]");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.Equal(VariableType.ListType, result.Type);
Assert.False(result.HasSchema);
}
[Fact]
public void GetListTypeFromJson_ObjectWithStringField_InfersStringType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "name": "hello" }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("name"));
Assert.Equal(typeof(string), result.Schema["name"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithNumberField_InfersDecimalType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "value": 42 }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("value"));
Assert.Equal(typeof(decimal), result.Schema["value"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithBooleanTrueField_InfersBoolType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "flag": true }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("flag"));
Assert.Equal(typeof(bool), result.Schema["flag"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithBooleanFalseField_InfersBoolType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "flag": false }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("flag"));
Assert.Equal(typeof(bool), result.Schema["flag"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithNestedObjectField_InfersRecordType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "child": { "inner": 1 } }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("child"));
Assert.Equal(VariableType.RecordType, result.Schema["child"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithNestedArrayField_InfersListType()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "items": [1, 2, 3] }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("items"));
Assert.Equal(VariableType.ListType, result.Schema["items"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithNullField_InfersStringTypeDefault()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{ "missing": null }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("missing"));
Assert.Equal(typeof(string), result.Schema["missing"].Type);
}
[Fact]
public void GetListTypeFromJson_SkipsNonObjectElements_InfersFromFirstObject()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[1, "text", { "id": 99 }]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.True(result.Schema!.ContainsKey("id"));
Assert.Equal(typeof(decimal), result.Schema["id"].Type);
}
[Fact]
public void GetListTypeFromJson_ObjectWithAllFieldTypes_InfersCorrectTypes()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
[{
"text": "hello",
"count": 5,
"enabled": true,
"disabled": false,
"nested": { "x": 1 },
"list": [1, 2],
"empty": null
}]
""");
// Act
VariableType result = document.RootElement.GetListTypeFromJson();
// Assert
Assert.True(result.HasSchema);
Assert.Equal(7, result.Schema!.Count);
Assert.Equal(typeof(string), result.Schema["text"].Type);
Assert.Equal(typeof(decimal), result.Schema["count"].Type);
Assert.Equal(typeof(bool), result.Schema["enabled"].Type);
Assert.Equal(typeof(bool), result.Schema["disabled"].Type);
Assert.Equal(VariableType.RecordType, result.Schema["nested"].Type);
Assert.Equal(VariableType.ListType, result.Schema["list"].Type);
Assert.Equal(typeof(string), result.Schema["empty"].Type);
}
[Fact]
public void ParseJsonValue_Object_ReturnsRecord()
{
// Arrange
JsonDocument document = JsonDocument.Parse("""{ "a": "alpha", "b": "beta" }""");
// Act
object? result = document.ParseJsonValue("""{ "a": "alpha", "b": "beta" }""");
// Assert
Dictionary<string, object?> record = Assert.IsType<Dictionary<string, object?>>(result);
Assert.Equal("alpha", record["a"]);
Assert.Equal("beta", record["b"]);
}
[Fact]
public void ParseJsonValue_Array_ReturnsList()
{
// Arrange
const string Json = """["alpha","beta"]""";
JsonDocument document = JsonDocument.Parse(Json);
// Act
object? result = document.ParseJsonValue(Json);
// Assert
List<object?> list = Assert.IsType<List<object?>>(result);
Assert.Equal(new object?[] { "alpha", "beta" }, list);
}
[Fact]
public void ParseJsonValue_EmptyArray_ReturnsEmptyList()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[]");
// Act
object? result = document.ParseJsonValue("[]");
// Assert
List<object?> list = Assert.IsType<List<object?>>(result);
Assert.Empty(list);
}
[Fact]
public void ParseJsonValue_String_ReturnsString()
{
// Arrange
JsonDocument document = JsonDocument.Parse("\"hello\"");
// Act
object? result = document.ParseJsonValue("\"hello\"");
// Assert
Assert.Equal("hello", result);
}
[Fact]
public void ParseJsonValue_Number_ReturnsNumericValue()
{
// Arrange
JsonDocument document = JsonDocument.Parse("42");
// Act
object? result = document.ParseJsonValue("42");
// Assert
Assert.Equal(42d, Assert.IsType<double>(result));
}
[Theory]
[InlineData("true", true)]
[InlineData("false", false)]
public void ParseJsonValue_Boolean_ReturnsBoolean(string json, bool expected)
{
// Arrange
JsonDocument document = JsonDocument.Parse(json);
// Act
object? result = document.ParseJsonValue(json);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void ParseJsonValue_Null_ReturnsNull()
{
// Arrange
JsonDocument document = JsonDocument.Parse("null");
// Act
object? result = document.ParseJsonValue("null");
// Assert
Assert.Null(result);
}
}
@@ -0,0 +1,124 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ObjectExtensionsTests
{
[Fact]
public void AsListWithNullInput()
{
object[]? nullList = null;
IList<string>? result = nullList.AsList<string>();
Assert.Null(result);
}
[Fact]
public void AsListWithEmptyInput()
{
IList<string>? result = Array.Empty<int>().AsList<string>();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void AsListWithSingleElement()
{
const string Value = "Test";
IList<string>? result = Value.AsList<string>();
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(Value, result[0]);
}
[Fact]
public void AsListWithMultipleInput()
{
object[] inputs = ["33.3", "test"];
IList<string>? result = inputs.AsList<string>();
Assert.NotNull(result);
Assert.Equal(2, result.Count);
}
[Fact]
public void ConvertSame()
{
VerifyConversion(true, typeof(bool), true);
VerifyConversion(32, typeof(int), 32);
VerifyConversion("Test", typeof(string), "Test");
DateTime now = DateTime.Now;
VerifyConversion(now, typeof(DateTime), now);
VerifyConversion(now.TimeOfDay, typeof(TimeSpan), now.TimeOfDay);
}
[Fact]
public void ConvertFailure()
{
VerifyInvalid(32, VariableType.RecordType);
VerifyInvalid(true, VariableType.RecordType);
VerifyInvalid(Guid.NewGuid(), typeof(Guid));
}
[Fact]
public void ConvertToString()
{
VerifyConversion(true, typeof(string), bool.TrueString);
VerifyConversion(32, typeof(string), "32");
VerifyConversion(3.14d, typeof(string), "3.14");
DateTime now = DateTime.Now;
VerifyConversion(now, typeof(string), $"{now:o}");
VerifyConversion(now.TimeOfDay, typeof(string), $"{now.TimeOfDay:c}");
}
[Fact]
public void ConvertFromString()
{
VerifyConversion("true", typeof(bool), true);
VerifyConversion("32", typeof(int), 32);
VerifyConversion("3.14", typeof(double), 3.14D);
DateTime now = DateTime.Now;
VerifyConversion($"{now:o}", typeof(DateTime), now);
VerifyConversion($"{now.TimeOfDay:c}", typeof(TimeSpan), now.TimeOfDay);
}
[Fact]
public void ConvertJson()
{
const string Json =
"""
{
"id": "item1",
"count": 5
}
""";
Dictionary<string, object?> expected =
new()
{
{ "id", "item1"},
{ "count", 5},
};
VerifyConversion(Json, VariableType.Record(("id", typeof(string)), ("count", typeof(int))), expected);
}
private static void VerifyConversion(object? sourceValue, VariableType targetType, object? expectedValue)
{
object? actualValue = sourceValue.ConvertType(targetType);
if (expectedValue is IDictionary<string, object?> or DateTime)
{
Assert.Equivalent(expectedValue, actualValue);
}
else
{
Assert.Equal(expectedValue, actualValue);
}
}
private static void VerifyInvalid(object? sourceValue, VariableType targetType)
{
Assert.Throws<DeclarativeActionException>(() => sourceValue.ConvertType(targetType));
}
}
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class PortableValueExtensionsTests
{
[Fact]
public void InvalidType() => TestInvalidType(IPAddress.Loopback);
[Fact]
public void NullType() => TestValidType<object>(null, FormulaType.Blank);
[Fact]
public void BooleanType() => TestValidType(true, FormulaType.Boolean);
[Fact]
public void StringType() => TestValidType("Hello, World!", FormulaType.String);
[Fact]
public void IntType() => TestValidType(int.MinValue, FormulaType.Decimal);
[Fact]
public void LongType() => TestValidType(long.MaxValue, FormulaType.Decimal);
[Fact]
public void DecimalType() => TestValidType(decimal.MaxValue, FormulaType.Decimal);
[Fact]
public void FloatType() => TestValidType(float.MaxValue, FormulaType.Number);
[Fact]
public void DoubleType() => TestValidType(double.MinValue, FormulaType.Number);
[Fact]
public void DateType() => TestValidType(DateTime.UtcNow.Date, FormulaType.Date);
[Fact]
public void DateTimeType() => TestValidType(DateTime.UtcNow, FormulaType.DateTime);
[Fact]
public void TimeSpanType() => TestValidType(DateTime.UtcNow.TimeOfDay, FormulaType.Time);
[Fact]
public void ChatMessageType() => TestValidType(new ChatMessage(ChatRole.User, "input"), RecordType.Empty());
[Fact]
public void ListEmptyType()
{
TableValue convertedValue = (TableValue)TestValidType(Array.Empty<int>(), TableType.Empty());
Assert.Equal(0, convertedValue.Count());
}
[Fact]
public void ListSimpleType()
{
TableValue convertedValue = (TableValue)TestValidType(new List<int> { 1, 2, 3 }, TableType.Empty());
Assert.Equal(3, convertedValue.Count());
RecordValue firstElement = convertedValue.Rows.First().Value;
NamedValue recordElement = Assert.Single(firstElement.Fields);
Assert.Equal("Value", recordElement.Name);
DecimalValue recordValue = Assert.IsType<DecimalValue>(recordElement.Value);
Assert.Equal(1, recordValue.Value);
}
[Fact]
public void ListComplexType()
{
TableValue convertedValue = (TableValue)TestValidType(new List<ChatMessage> { new(ChatRole.User, "input"), new(ChatRole.Assistant, "output") }, TableType.Empty());
Assert.Equal(2, convertedValue.Count());
RecordValue firstElement = convertedValue.Rows.First().Value;
StringValue typeValue = Assert.IsType<StringValue>(firstElement.GetField(TypeSchema.Discriminator));
Assert.Equal(nameof(ChatMessage), typeValue.Value);
StringValue textValue = Assert.IsType<StringValue>(firstElement.GetField(TypeSchema.Message.Fields.Text));
Assert.Equal("input", textValue.Value);
}
[Fact]
public void DictionaryType()
{
RecordValue convertedValue = (RecordValue)TestValidType(new Dictionary<string, int> { { "A", 1 }, { "B", 2 } }, RecordType.Empty());
Assert.Equal(2, convertedValue.Fields.Count());
NamedValue firstElement = convertedValue.Fields.First();
Assert.Equal("A", firstElement.Name);
DecimalValue firstElementValue = Assert.IsType<DecimalValue>(firstElement.Value);
Assert.Equal(1, firstElementValue.Value);
}
[Fact]
public void ObjectType()
{
RecordValue convertedValue = (RecordValue)TestValidType(FormulaValue.NewRecordFromFields(new NamedValue("key", FormulaValue.New(3))).ToDataValue().ToObject(), RecordType.Empty());
Assert.Single(convertedValue.Fields);
NamedValue firstElement = convertedValue.Fields.First();
Assert.Equal("key", firstElement.Name);
DecimalValue firstElementValue = Assert.IsType<DecimalValue>(firstElement.Value);
Assert.Equal(3, firstElementValue.Value);
}
private static void TestInvalidType(object? sourceValue)
{
Assert.Throws<DeclarativeModelException>(() => sourceValue.AsPortable());
PortableValue portableValue = new(sourceValue ?? UnassignedValue.Instance);
Assert.Throws<DeclarativeModelException>(() => portableValue.ToFormula());
}
private static FormulaValue TestValidType<TValue>(TValue? sourceValue, FormulaType expectedType) where TValue : notnull
{
object portableObject = sourceValue.AsPortable();
Assert.IsNotType<PortableValue>(portableObject);
PortableValue portableValue = new(portableObject);
FormulaValue formulaValue = portableValue.ToFormula();
Assert.NotNull(formulaValue);
Assert.Equal(expectedType.GetType(), formulaValue.Type.GetType());
return formulaValue;
}
}
@@ -0,0 +1,162 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class StringExtensionsTests
{
[Fact]
public void TrimJsonWithDelimiter()
{
// Arrange
const string Input =
"""
```json
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithPadding()
{
// Arrange
const string Input =
"""
```json
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithUnqualifiedDelimiter()
{
// Arrange
const string Input =
"""
```
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithoutDelimiter()
{
// Arrange
const string Input =
"""
{
"key": "value"
}
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithoutDelimiterWithPadding()
{
// Arrange
const string Input =
"""
{
"key": "value"
}
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimMissingWithDelimiter()
{
// Arrange
const string Input =
"""
```json
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void TrimEmptyString()
{
// Act
string result = string.Empty.TrimJsonDelimiter();
// Assert
Assert.Equal(string.Empty, result);
}
}
@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.ObjectModel;
using Microsoft.PowerFx;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class TemplateExtensionsTests
{
[Fact]
public void FormatTemplateWithTextSegments()
{
// Arrange
RecalcEngine engine = new();
IEnumerable<TemplateLine> template =
[
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Hello " },
new TextSegment.Builder { Value = "World" }
}
}.Build()
];
// Act
string result = engine.Format(template);
// Assert
Assert.Equal("Hello World", result);
}
[Fact]
public void FormatTemplateWithMultipleLines()
{
// Arrange
RecalcEngine engine = new();
IEnumerable<TemplateLine> template =
[
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Line 1" }
}
}.Build(),
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Line 2" }
}
}.Build()
];
// Act
string result = engine.Format(template);
// Assert
Assert.Equal("Line 1Line 2", result);
}
[Fact]
public void FormatSingleTemplateLineWithNullValue()
{
// Arrange
RecalcEngine engine = new();
TemplateLine? line = null;
// Act
string result = engine.Format(line);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatSingleTemplateLineWithTextSegment()
{
// Arrange
RecalcEngine engine = new();
TemplateLine line = new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Test" }
}
}.Build();
// Act
string result = engine.Format(line);
// Assert
Assert.Equal("Test", result);
}
[Fact]
public void FormatTextSegmentWithNullValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = null }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTextSegmentWithEmptyValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = "" }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTextSegmentWithValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = "Hello World" }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal("Hello World", result);
}
}
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class TypeExtensionsTests
{
[Fact]
public void ReferenceType() => VerifyIsNullable(typeof(string));
[Fact]
public void ClassType() => VerifyIsNullable(typeof(object));
[Fact]
public void InterfaceType() => VerifyIsNullable(typeof(IDisposable));
[Fact]
public void ArrayType() => VerifyIsNullable(typeof(int[]));
[Fact]
public void NonNullableValueType() => VerifyNotNullable(typeof(int));
[Fact]
public void NonNullableStructType() => VerifyNotNullable(typeof(DateTime));
[Fact]
public void NonNullableEnumType() => VerifyNotNullable(typeof(DayOfWeek));
[Fact]
public void NullableInt() => VerifyIsNullable(typeof(int?));
[Fact]
public void NullableDateTime() => VerifyIsNullable(typeof(DateTime?));
[Fact]
public void NullableEnum() => VerifyIsNullable(typeof(DayOfWeek?));
[Fact]
public void NullableCustomStruct() => VerifyIsNullable(typeof(TestStruct?));
private static void VerifyNotNullable(Type targetType)
{
// Act
bool result = targetType.IsNullable();
// Assert
Assert.False(result);
}
private static void VerifyIsNullable(Type targetType)
{
// Act
bool result = targetType.IsNullable();
// Assert
Assert.True(result);
}
private struct TestStruct
{
public int Value { get; set; }
}
}