File: Backchannel\BackchannelJsonSerializerContextTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Text.Json;
using Aspire.Cli.Backchannel;
using ModelContextProtocol.Protocol;
 
namespace Aspire.Cli.Tests.Backchannel;
 
public class BackchannelJsonSerializerContextTests
{
    [Fact]
    public void JsonSerializerOptionsCanSerializeAndDeserializeResourceSnapshotMcpServers()
    {
        var options = BackchannelJsonSerializerContext.CreateJsonSerializerOptions();
 
        var servers = new Aspire.Cli.Backchannel.ResourceSnapshotMcpServer[]
        {
            new()
            {
                EndpointUrl = "http://localhost:8000",
                Tools =
                [
                    new Tool
                    {
                        Name = "query",
                        Description = "Runs a SQL query",
                        InputSchema = JsonDocument.Parse("{\"type\":\"object\",\"properties\":{\"sql\":{\"type\":\"string\"}}}").RootElement
                    }
                ]
            }
        };
 
        var json = JsonSerializer.Serialize(servers, options);
        var roundTripped = JsonSerializer.Deserialize<Aspire.Cli.Backchannel.ResourceSnapshotMcpServer[]>(json, options);
 
        Assert.NotNull(roundTripped);
        Assert.Single(roundTripped);
        Assert.Equal("http://localhost:8000", roundTripped[0].EndpointUrl);
        Assert.Single(roundTripped[0].Tools);
        Assert.Equal("query", roundTripped[0].Tools[0].Name);
    }
 
    [Fact]
    public void JsonSerializerOptionsCanSerializeAndDeserializeDictionaryStringJsonElement()
    {
        var options = BackchannelJsonSerializerContext.CreateJsonSerializerOptions();
 
        var payload = new Dictionary<string, JsonElement>(StringComparer.Ordinal)
        {
            ["sql"] = JsonDocument.Parse("\"select 1\"").RootElement,
            ["limit"] = JsonDocument.Parse("1").RootElement
        };
 
        var json = JsonSerializer.Serialize(payload, options);
        var roundTripped = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json, options);
 
        Assert.NotNull(roundTripped);
        Assert.Equal("select 1", roundTripped["sql"].GetString());
        Assert.Equal(1, roundTripped["limit"].GetInt32());
    }
 
    [Fact]
    public void JsonSerializerOptionsCanDeserializePublishingActivityWithoutHierarchyMetadata()
    {
        var options = BackchannelJsonSerializerContext.CreateJsonSerializerOptions();
        var json =
            """
            {
              "Type": "step",
              "Data": {
                "Id": "step-1",
                "StatusText": "Prepare",
                "CompletionState": "InProgress"
              }
            }
            """;
 
        var activity = JsonSerializer.Deserialize<PublishingActivity>(json, options);
 
        Assert.NotNull(activity);
        Assert.Equal(PublishingActivityTypes.Step, activity.Type);
        Assert.Equal("step-1", activity.Data.Id);
        Assert.Equal("Prepare", activity.Data.StatusText);
        Assert.Null(activity.Data.ParentStepId);
        Assert.Null(activity.Data.HierarchyLevel);
        Assert.Null(activity.Data.CompletionMessage);
        Assert.Equal(CompletionStates.InProgress, activity.Data.CompletionState);
    }
 
    [Fact]
    public void TerminalReplicaInfo_OldPayloadWithoutNewFields_DeserializesWithNulls()
    {
        // Back-compat: an older AppHost (pre-terminals.v1) that only knows about the original
        // TerminalReplicaInfo shape will not emit CurrentColumns/CurrentRows/AttachedPeerCount/Peers.
        // The CLI must accept that payload and treat the new fields as null. See
        // docs/specs/cli-backchannel.md §3 for the per-feature capability strategy.
        var options = BackchannelJsonSerializerContext.CreateJsonSerializerOptions();
        var json =
            """
            {
              "ReplicaIndex": 0,
              "Label": "myresource-0",
              "ConsumerUdsPath": "/tmp/r0.sock",
              "IsAlive": true,
              "ProducerConnected": true,
              "RestartCount": 0
            }
            """;
 
        var replica = JsonSerializer.Deserialize<TerminalReplicaInfo>(json, options);
 
        Assert.NotNull(replica);
        Assert.Equal(0, replica.ReplicaIndex);
        Assert.Equal("myresource-0", replica.Label);
        Assert.True(replica.IsAlive);
        Assert.Null(replica.CurrentColumns);
        Assert.Null(replica.CurrentRows);
        Assert.Null(replica.AttachedPeerCount);
        Assert.Null(replica.Peers);
    }
 
    [Fact]
    public void ListTerminalsResponse_RoundTripsThroughSerializer()
    {
        var options = BackchannelJsonSerializerContext.CreateJsonSerializerOptions();
        var response = new ListTerminalsResponse
        {
            Terminals =
            [
                new TerminalSummary
                {
                    ResourceName = "myresource",
                    DisplayName = "myresource",
                    ConfiguredColumns = 120,
                    ConfiguredRows = 30,
                    IsHostReachable = true,
                    Replicas =
                    [
                        new TerminalReplicaInfo
                        {
                            ReplicaIndex = 0,
                            Label = "myresource-0",
                            ConsumerUdsPath = "/tmp/r0.sock",
                            IsAlive = true,
                            CurrentColumns = 130,
                            CurrentRows = 32,
                            AttachedPeerCount = 1,
                            Peers =
                            [
                                new TerminalPeerInfo { PeerId = "peer-1", DisplayName = "viewer-1" }
                            ]
                        }
                    ]
                }
            ]
        };
 
        var json = JsonSerializer.Serialize(response, options);
        var roundTripped = JsonSerializer.Deserialize<ListTerminalsResponse>(json, options);
 
        Assert.NotNull(roundTripped);
        Assert.Single(roundTripped.Terminals);
 
        var terminal = roundTripped.Terminals[0];
        Assert.Equal("myresource", terminal.ResourceName);
        Assert.True(terminal.IsHostReachable);
        Assert.NotNull(terminal.Replicas);
        Assert.Single(terminal.Replicas);
 
        var replica = terminal.Replicas[0];
        Assert.Equal(130, replica.CurrentColumns);
        Assert.Equal(32, replica.CurrentRows);
        Assert.Equal(1, replica.AttachedPeerCount);
        Assert.NotNull(replica.Peers);
        Assert.Single(replica.Peers);
        Assert.Equal("peer-1", replica.Peers[0].PeerId);
        Assert.Equal("viewer-1", replica.Peers[0].DisplayName);
    }
}