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
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:
+139
@@ -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;
|
||||
}
|
||||
}
|
||||
+933
@@ -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);
|
||||
}
|
||||
}
|
||||
+845
@@ -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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
+217
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -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"));
|
||||
}
|
||||
}
|
||||
+764
@@ -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);
|
||||
}
|
||||
}
|
||||
+124
@@ -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));
|
||||
}
|
||||
}
|
||||
+127
@@ -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;
|
||||
}
|
||||
}
|
||||
+162
@@ -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);
|
||||
}
|
||||
}
|
||||
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
+65
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user