File: Contents\ToolApprovalRequestContentTests.cs
Project: ..\..\..\test\Libraries\Microsoft.Extensions.AI.Abstractions.Tests\Microsoft.Extensions.AI.Abstractions.Tests.csproj (Microsoft.Extensions.AI.Abstractions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Text.Json;
using Xunit;
 
namespace Microsoft.Extensions.AI.Contents;
 
public class ToolApprovalRequestContentTests
{
    [Fact]
    public void Constructor_InvalidArguments_Throws()
    {
        Assert.Throws<ArgumentNullException>("requestId", () => new ToolApprovalRequestContent(null!, new FunctionCallContent("FCC1", "TestFunction")));
        Assert.Throws<ArgumentException>("requestId", () => new ToolApprovalRequestContent("", new FunctionCallContent("FCC1", "TestFunction")));
        Assert.Throws<ArgumentException>("requestId", () => new ToolApprovalRequestContent("\r\t\n ", new FunctionCallContent("FCC1", "TestFunction")));
        Assert.Throws<ArgumentNullException>("toolCall", () => new ToolApprovalRequestContent("id", null!));
    }
 
    public static TheoryData<ToolCallContent> ToolCallContentInstances => new()
    {
        new FunctionCallContent("FCC1", "TestFunction", new Dictionary<string, object?> { { "param1", 123 } }),
        new McpServerToolCallContent("MCC1", "TestTool", "TestServer") { Arguments = new Dictionary<string, object?> { { "arg1", "value1" } } },
        new CodeInterpreterToolCallContent("CI1") { Inputs = [new DataContent("print('hello')"u8.ToArray(), "text/x-python")] },
        new ImageGenerationToolCallContent("IG1"),
    };
 
    [Theory]
    [MemberData(nameof(ToolCallContentInstances), DisableDiscoveryEnumeration = true)]
    public void Constructor_Roundtrips(ToolCallContent toolCall)
    {
        string id = "req-1";
        ToolApprovalRequestContent content = new(id, toolCall);
 
        Assert.Same(id, content.RequestId);
        Assert.Same(toolCall, content.ToolCall);
    }
 
    [Theory]
    [MemberData(nameof(ToolCallContentInstances), DisableDiscoveryEnumeration = true)]
    public void CreateResponse_ReturnsExpectedResponse(ToolCallContent toolCall)
    {
        string id = "req-1";
        ToolApprovalRequestContent content = new(id, toolCall);
 
        var response = content.CreateResponse(approved: true);
 
        Assert.NotNull(response);
        Assert.Same(id, response.RequestId);
        Assert.True(response.Approved);
        Assert.Same(toolCall, response.ToolCall);
        Assert.Null(response.Reason);
    }
 
    [Theory]
    [InlineData(true, "Approved for testing")]
    [InlineData(false, "Rejected due to security concerns")]
    [InlineData(true, null)]
    [InlineData(false, null)]
    public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, string? reason)
    {
        string id = "req-1";
        FunctionCallContent functionCall = new("FCC1", "TestFunction");
 
        ToolApprovalRequestContent content = new(id, functionCall);
 
        var response = content.CreateResponse(approved, reason);
 
        Assert.NotNull(response);
        Assert.Same(id, response.RequestId);
        Assert.Equal(approved, response.Approved);
        Assert.Same(functionCall, response.ToolCall);
        Assert.Equal(reason, response.Reason);
    }
 
    [Theory]
    [MemberData(nameof(ToolCallContentInstances), DisableDiscoveryEnumeration = true)]
    public void Serialization_Roundtrips(ToolCallContent toolCall)
    {
        var content = new ToolApprovalRequestContent("request123", toolCall);
 
        AssertSerializationRoundtrips<ToolApprovalRequestContent>(content);
        AssertSerializationRoundtrips<InputRequestContent>(content);
        AssertSerializationRoundtrips<AIContent>(content);
 
        static void AssertSerializationRoundtrips<T>(ToolApprovalRequestContent content)
            where T : AIContent
        {
            T contentAsT = (T)(object)content;
            string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
            T? deserialized = JsonSerializer.Deserialize<T>(json, AIJsonUtilities.DefaultOptions);
            Assert.NotNull(deserialized);
            var deserializedContent = Assert.IsType<ToolApprovalRequestContent>(deserialized);
            Assert.Equal(content.RequestId, deserializedContent.RequestId);
            Assert.NotNull(deserializedContent.ToolCall);
            Assert.IsType(content.ToolCall.GetType(), deserializedContent.ToolCall);
            Assert.Equal(content.ToolCall.CallId, deserializedContent.ToolCall.CallId);
        }
    }
 
    [Fact]
    public void JsonDeserialization_KnownPayload()
    {
        const string Json = """
            {
              "$type": "toolApprovalRequest",
              "requestId": "req-abc123",
              "toolCall": {
                "$type": "functionCall",
                "callId": "call1",
                "name": "myFunc"
              },
              "additionalProperties": {
                "key": "val"
              }
            }
            """;
 
        AIContent? result = JsonSerializer.Deserialize<AIContent>(Json, AIJsonUtilities.DefaultOptions);
 
        Assert.NotNull(result);
        var approvalRequest = Assert.IsType<ToolApprovalRequestContent>(result);
        Assert.Equal("req-abc123", approvalRequest.RequestId);
        Assert.NotNull(approvalRequest.ToolCall);
        var funcCall = Assert.IsType<FunctionCallContent>(approvalRequest.ToolCall);
        Assert.Equal("call1", funcCall.CallId);
        Assert.Equal("myFunc", funcCall.Name);
        Assert.NotNull(approvalRequest.AdditionalProperties);
        Assert.Equal("val", approvalRequest.AdditionalProperties["key"]?.ToString());
    }
 
    [Fact]
    public void RequiresConfirmation_DefaultsToTrue()
    {
        var content = new ToolApprovalRequestContent("req-1", new FunctionCallContent("call1", "Func"));
        Assert.True(content.RequiresConfirmation);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void RequiresConfirmation_RoundtripsThroughJson(bool value)
    {
        var content = new ToolApprovalRequestContent("req-1", new FunctionCallContent("call1", "Func"))
        {
            RequiresConfirmation = value,
        };
 
        string json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
        var deserialized = JsonSerializer.Deserialize<ToolApprovalRequestContent>(json, AIJsonUtilities.DefaultOptions);
 
        Assert.NotNull(deserialized);
        Assert.Equal(value, deserialized!.RequiresConfirmation);
    }
}