File: ResourceCommandServiceTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Threading.Channels;
using Aspire.Hosting.Testing;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Tests;
 
#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
 
[Trait("Partition", "2")]
public class ResourceCommandServiceTests(ITestOutputHelper testOutputHelper)
{
    /// <summary>
    /// Creates a test builder with version checking disabled to prevent the VersionCheckService
    /// from racing with tests that use IInteractionService.
    /// </summary>
    private IDistributedApplicationTestingBuilder CreateBuilder()
    {
        var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        return builder;
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NoMatchingResource_Failure()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync("NotFoundResourceId", "NotFound");
 
        // Assert
        Assert.False(result.Success);
        Assert.Equal("Resource 'NotFoundResourceId' not found.", result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_ResourceNameMultipleMatches_Failure()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "NotFound");
 
        // Assert
        Assert.False(result.Success);
        Assert.Equal("Resource 'myResource' not found.", result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NoMatchingCommand_Failure()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "NotFound");
 
        // Assert
        Assert.False(result.Success);
        Assert.Equal("Command 'NotFound' not available for resource 'myResource'.", result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NoMatchingCommand_SingleInstance_MessageUsesDisplayName()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0)
            ]));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "NotFound");
 
        Assert.False(result.Success);
        Assert.Equal("Command 'NotFound' not available for resource 'myResource'.", result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NoMatchingCommand_HasReplicas_MessageUsesResourceId()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
        custom.WithAnnotation(new ReplicaAnnotation(2));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "NotFound");
 
        Assert.False(result.Success);
        Assert.Contains("'myResource-abcdwxyz'", result.Message);
        Assert.Contains("'myResource-efghwxyz'", result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_ResourceNameMultipleMatches_Success()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var commandResourcesChannel = Channel.CreateUnbounded<string>();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0)
            ]));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: async e =>
                {
                    await commandResourcesChannel.Writer.WriteAsync(e.ResourceName);
                    return new ExecuteCommandResult { Success = true };
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand");
        commandResourcesChannel.Writer.Complete();
 
        // Assert
        Assert.True(result.Success);
 
        var resolvedResourceNames = custom.Resource.GetResolvedResourceNames().ToList();
        await foreach (var resourceName in commandResourcesChannel.Reader.ReadAllAsync().DefaultTimeout())
        {
            Assert.True(resolvedResourceNames.Remove(resourceName));
        }
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_HasReplicas_Success_CalledPerReplica()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var commandResourcesChannel = Channel.CreateUnbounded<string>();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: async e =>
                {
                    await commandResourcesChannel.Writer.WriteAsync(e.ResourceName);
                    return new ExecuteCommandResult { Success = true };
                });
 
        // Act
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
        commandResourcesChannel.Writer.Complete();
 
        // Assert
        Assert.True(result.Success);
 
        var resolvedResourceNames = custom.Resource.GetResolvedResourceNames().ToList();
        Assert.Equal(2, resolvedResourceNames.Count);
        Assert.Contains("myResource-abcdwxyz", resolvedResourceNames);
        Assert.Contains("myResource-efghwxyz", resolvedResourceNames);
 
        await foreach (var resourceName in commandResourcesChannel.Reader.ReadAllAsync().DefaultTimeout())
        {
            Assert.True(resolvedResourceNames.Remove(resourceName));
        }
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_HasReplicas_Failure_CalledPerReplica()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    return Task.FromResult(new ExecuteCommandResult { Success = false, Message = "Failure!" });
                });
 
        // Act
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        // Assert
        Assert.False(result.Success);
 
        var resourceNames = custom.Resource.GetResolvedResourceNames();
        Assert.Equal(2, resourceNames.Length);
        Assert.Equal("myResource-abcdwxyz", resourceNames[0]);
        Assert.Equal("myResource-efghwxyz", resourceNames[1]);
 
        Assert.Equal($"""
            2 command executions failed.
            Resource '{resourceNames[0]}' failed with error message: Failure!
            Resource '{resourceNames[1]}' failed with error message: Failure!
            """, result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_Canceled_Success()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    return Task.FromResult(CommandResults.Canceled());
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        // Assert
        Assert.False(result.Success);
        Assert.True(result.Canceled);
        Assert.Null(result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_HasReplicas_Canceled_CalledPerReplica()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    return Task.FromResult(CommandResults.Canceled());
                });
 
        // Act
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        // Assert
        Assert.False(result.Success);
        Assert.True(result.Canceled);
        Assert.Null(result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_HasReplicas_MixedFailureAndCanceled_OnlyFailuresInErrorMessage()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var callCount = 0;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1),
            new DcpInstance("myResource-ijklwxyz", "ijklwxyz", 2)
            ]));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    var count = Interlocked.Increment(ref callCount);
                    return Task.FromResult(count switch
                    {
                        1 => CommandResults.Failure("Failure!"),
                        2 => CommandResults.Canceled(),
                        _ => CommandResults.Success()
                    });
                });
 
        // Act
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        // Assert
        Assert.False(result.Success);
        Assert.False(result.Canceled); // Should not be canceled since there was at least one failure
 
        var resourceNames = custom.Resource.GetResolvedResourceNames();
        Assert.Equal($"""
            1 command executions failed.
            Resource '{resourceNames[0]}' failed with error message: Failure!
            """, result.Message);
    }
 
    [Fact] 
    public void CommandResults_Canceled_ProducesCorrectResult()
    {
        // Act
        var result = CommandResults.Canceled();
 
        // Assert
        Assert.False(result.Success);
        Assert.True(result.Canceled);
        Assert.Null(result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_OperationCanceledException_Canceled()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    throw new OperationCanceledException("Command was canceled");
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        // Assert
        Assert.False(result.Success);
        Assert.True(result.Canceled);
        Assert.Null(result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_CommandException_Failure()
    {
        using var builder = CreateBuilder();
        const string diagnostic = "Failed to apply launch configuration. Process fallback is unavailable.";
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(
            name: "mycommand",
            displayName: "My command",
            executeCommand: _ => throw new FailedToApplyEnvironmentException(diagnostic));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Equal(diagnostic, result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_LegacyCommandName_FallsBackToCurrentName()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: KnownResourceCommands.StartCommand,
                displayName: "Start",
                executeCommand: _ => Task.FromResult(new ExecuteCommandResult { Success = true }));
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act - use the legacy "resource-start" name
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "resource-start");
 
        // Assert - should succeed via fallback
        Assert.True(result.Success);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_LegacyCommandName_ById_FallsBackToCurrentName()
    {
        // Arrange
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: KnownResourceCommands.StopCommand,
                displayName: "Stop",
                executeCommand: _ => Task.FromResult(new ExecuteCommandResult { Success = true }));
 
        var app = builder.Build();
        await app.StartAsync();
 
        // Act - use the legacy "resource-stop" name via resource ID
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "resource-stop");
 
        // Assert - should succeed via fallback
        Assert.True(result.Success);
    }
 
    [Theory]
    [InlineData("set-parameter", "parameter-set")]
    [InlineData("delete-parameter", "parameter-delete")]
    public async Task ExecuteCommandAsync_LegacyParameterCommandName_FallsBackToCurrentName(string currentCommandName, string legacyCommandName)
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: currentCommandName,
                displayName: "Parameter command",
                executeCommand: _ => Task.FromResult(new ExecuteCommandResult { Success = true }));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, legacyCommandName);
 
        Assert.True(result.Success);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_SuccessWithResult_ReturnsResultData()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "generate-token",
                displayName: "Generate Token",
                executeCommand: _ => Task.FromResult(CommandResults.Success("Generated token.", "{\"token\": \"abc123\"}", CommandResultFormat.Json)));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "generate-token");
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.Equal("{\"token\": \"abc123\"}", result.Data.Value);
        Assert.Equal(CommandResultFormat.Json, result.Data.Format);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_SuccessWithoutResult_ReturnsNoResultData()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()));
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand");
 
        Assert.True(result.Success);
        Assert.Null(result.Data);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithArgumentCollection_PassesArgumentsToCommand()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text
                        },
                        new InteractionInput
                        {
                            Name = "clickCount",
                            InputType = InputType.Number
                        }
                    ]
                });
 
        var arguments = new InteractionInputCollection(
        [
            new InteractionInput
            {
                Name = "selector",
                InputType = InputType.Text,
                Value = "#submit"
            },
            new InteractionInput
            {
                Name = "clickCount",
                InputType = InputType.Number,
                Value = "2"
            }
        ]);
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand", arguments);
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("#submit", capturedArguments.GetString("selector"));
        Assert.Equal(2, capturedArguments.GetInt32("clickCount"));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithArgumentValuesAndResource_PassesArgumentsToCommand()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "message",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            custom.Resource,
            "mycommand",
            new Dictionary<string, string?> { ["message"] = "hello" },
            CancellationToken.None);
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("hello", capturedArguments.GetString("message"));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithArgumentValuesAndResource_DoesNotRequirePublishedResourceState()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "message",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            custom.Resource,
            "mycommand",
            new Dictionary<string, string?> { ["message"] = "hello" },
            CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("hello", capturedArguments.GetString("message"));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_SecretTextArgument_PreservesWhitespace()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "password",
                            InputType = InputType.SecretText,
                            Required = true
                        }
                    ]
                });
 
        var arguments = new InteractionInputCollection(
        [
            new InteractionInput
            {
                Name = "password",
                InputType = InputType.SecretText,
                Value = "  secret  "
            }
        ]);
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand", arguments);
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("  secret  ", capturedArguments.GetString("password"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_WithOrderedArgumentValues_MapsArgumentsByOrder()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text
                        },
                        new InteractionInput
                        {
                            Name = "clickCount",
                            InputType = InputType.Number,
                            Value = "1"
                        },
                        new InteractionInput
                        {
                            Name = "snapshotAfter",
                            InputType = InputType.Boolean,
                            Value = "true"
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        IReadOnlyList<string?> argumentValues = ["#submit", "2"];
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Null(errorMessage);
        Assert.Equal("#submit", arguments.GetString("selector"));
        Assert.Equal(2, arguments.GetInt32("clickCount"));
        Assert.True(arguments.GetBoolean("snapshotAfter"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_TooManyOrderedArgumentValues_ReturnsError()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        IReadOnlyList<string?> argumentValues = ["#submit", "extra"];
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Equal("Command 'mycommand' accepts 1 argument(s), but 2 were provided.", errorMessage);
        Assert.Null(arguments.GetString("selector"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_UnknownNamedArgumentValues_ReturnsError()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var argumentValues = new Dictionary<string, string?> { ["selecter"] = "#submit" };
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Equal("Unknown argument 'selecter' for command 'mycommand'.", errorMessage);
        Assert.Null(arguments.GetString("selector"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_DisabledNamedArgumentValues_ReturnsError()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "saveToUserSecrets",
                            InputType = InputType.Boolean,
                            Disabled = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var argumentValues = new Dictionary<string, string?> { ["saveToUserSecrets"] = "true" };
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Equal("Argument 'saveToUserSecrets' for command 'mycommand' is disabled.", errorMessage);
        Assert.Equal("true", arguments.GetString("saveToUserSecrets"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_DynamicDisabledNamedArgumentValues_DoesNotReturnError()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "item",
                            InputType = InputType.Choice,
                            Disabled = true,
                            DynamicLoading = new InputLoadOptions
                            {
                                DependsOnInputs = ["category"],
                                LoadCallback = context =>
                                {
                                    context.Input.Disabled = false;
                                    return Task.CompletedTask;
                                }
                            }
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var argumentValues = new Dictionary<string, string?> { ["item"] = "banana" };
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Null(errorMessage);
        Assert.Equal("banana", arguments.GetString("item"));
    }
 
    [Fact]
    public async Task CreateCommandArguments_DisabledOrderedArgumentValues_ReturnsError()
    {
        using var builder = CreateBuilder();
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ => Task.FromResult(CommandResults.Success()),
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "saveToUserSecrets",
                            InputType = InputType.Boolean,
                            Disabled = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        IReadOnlyList<string?> argumentValues = ["true"];
        var (arguments, errorMessage) = app.ResourceCommands.CreateCommandArguments("myResource", "mycommand", argumentValues);
 
        Assert.Equal("Argument 'saveToUserSecrets' for command 'mycommand' is disabled.", errorMessage);
        Assert.Equal("true", arguments.GetString("saveToUserSecrets"));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NoArguments_PassesEmptyArgumentsToCommand()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand");
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Empty(capturedArguments);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_InvalidBuiltInArgumentValidation_DoesNotExecuteCommand()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            Label = "Selector",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand");
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.Equal("Command argument validation failed.", result.Message);
        Assert.NotNull(result.InvalidArguments);
        var invalidArgument = Assert.Single(result.InvalidArguments);
        Assert.Equal("selector", invalidArgument.Name);
        Assert.Equal("Value is required.", Assert.Single(invalidArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_LoadsDependentChoiceOptionsBeforeBuiltInValidation()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var loadCount = 0;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "subscription",
                            InputType = InputType.Choice,
                            Required = true,
                            Options = [KeyValuePair.Create("sub-a", "Subscription A")]
                        },
                        new InteractionInput
                        {
                            Name = "location",
                            InputType = InputType.Choice,
                            Required = true,
                            DynamicLoading = new InputLoadOptions
                            {
                                DependsOnInputs = ["subscription"],
                                LoadCallback = context =>
                                {
                                    loadCount++;
                                    Assert.Equal("sub-a", context.AllInputs.GetString("subscription"));
                                    context.Input.Options = [KeyValuePair.Create("westus", "West US")];
 
                                    return Task.CompletedTask;
                                }
                            }
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions
            {
                ArgumentValues = new Dictionary<string, string?>
                {
                    ["subscription"] = "sub-a",
                    ["location"] = "centralus"
                },
                ArgumentsProvided = true,
                NonInteractive = true
            },
            CancellationToken.None).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.Equal(1, loadCount);
        Assert.NotNull(result.InvalidArguments);
        var invalidArgument = Assert.Single(result.InvalidArguments, argument => argument.ValidationErrors.Count > 0);
        Assert.Equal("location", invalidArgument.Name);
        Assert.Equal("Value must be one of the provided options.", Assert.Single(invalidArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_SubmittedDynamicArgumentStillDisabledAfterLoading_ReturnsDisabledValidationError()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "category",
                            InputType = InputType.Choice,
                            Required = true,
                            Options = [KeyValuePair.Create("fruit", "Fruit")]
                        },
                        new InteractionInput
                        {
                            Name = "item",
                            InputType = InputType.Choice,
                            Required = true,
                            Disabled = true,
                            DynamicLoading = new InputLoadOptions
                            {
                                DependsOnInputs = ["category"],
                                LoadCallback = context =>
                                {
                                    context.Input.Disabled = false;
                                    context.Input.Options = [KeyValuePair.Create("banana", "Banana")];
 
                                    return Task.CompletedTask;
                                }
                            }
                        },
                        new InteractionInput
                        {
                            Name = "priority",
                            InputType = InputType.Choice,
                            Disabled = true,
                            DynamicLoading = new InputLoadOptions
                            {
                                DependsOnInputs = ["item"],
                                LoadCallback = context =>
                                {
                                    context.Input.Disabled = false;
                                    context.Input.Options = [KeyValuePair.Create("express", "Express")];
 
                                    return Task.CompletedTask;
                                }
                            }
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions
            {
                ArgumentValues = new Dictionary<string, string?>
                {
                    ["category"] = "fruit",
                    ["priority"] = "express"
                },
                ArgumentsProvided = true,
                NonInteractive = true
            },
            CancellationToken.None).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.NotNull(result.InvalidArguments);
 
        var itemArgument = Assert.Single(result.InvalidArguments, argument => argument.Name == "item");
        Assert.Equal("Value is required.", Assert.Single(itemArgument.ValidationErrors));
 
        var priorityArgument = Assert.Single(result.InvalidArguments, argument => argument.Name == "priority");
        Assert.Equal("Argument is disabled.", Assert.Single(priorityArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_LoadedDynamicArgumentStillDisabledWithDefaultValue_DoesNotReturnDisabledValidationError()
    {
        using var builder = CreateBuilder();
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: context =>
                {
                    capturedArguments = context.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "mode",
                            InputType = InputType.Choice,
                            Required = true,
                            Options = [KeyValuePair.Create("isolated", "Isolated")]
                        },
                        new InteractionInput
                        {
                            Name = "profile",
                            InputType = InputType.Choice,
                            Disabled = true,
                            DynamicLoading = new InputLoadOptions
                            {
                                DependsOnInputs = ["mode"],
                                LoadCallback = context =>
                                {
                                    context.Input.Disabled = true;
                                    context.Input.Value = "default";
                                    context.Input.Options = [KeyValuePair.Create("default", "Default")];
 
                                    return Task.CompletedTask;
                                }
                            }
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions
            {
                ArgumentValues = new Dictionary<string, string?>
                {
                    ["mode"] = "isolated",
                    ["profile"] = "default"
                },
                ArgumentsProvided = true,
                NonInteractive = true
            },
            CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("default", capturedArguments.GetString("profile"));
        Assert.Empty(capturedArguments["profile"].ValidationErrors);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_UnknownNamedArgumentValues_DoesNotExecuteCommand()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: _ =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions
            {
                ArgumentValues = new Dictionary<string, string?> { ["selecter"] = "#submit" },
                ArgumentsProvided = true,
                NonInteractive = true
            },
            CancellationToken.None).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.Equal("Unknown argument 'selecter' for command 'mycommand'.", result.Message);
        Assert.Null(result.InvalidArguments);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_InteractiveWithoutArguments_PromptsForArguments()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    capturedArguments = e.Arguments;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Description = "Command description",
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            Label = "Selector",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var resultTask = app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions { NonInteractive = false },
            CancellationToken.None).DefaultTimeout();
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal("My command", interaction.Title);
        Assert.Equal("Command description", interaction.Message);
        var input = Assert.Single(interaction.Inputs);
        Assert.Equal("selector", input.Name);
        input.Value = "#submit";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await resultTask;
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.Equal("#submit", capturedArguments.GetString("selector"));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_InteractiveDisabledDynamicArgumentWithDefaultValue_Succeeds()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        InteractionInputCollection? capturedArguments = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(
            name: "mycommand",
            displayName: "My command",
            executeCommand: context =>
            {
                capturedArguments = context.Arguments;
                return Task.FromResult(CommandResults.Success());
            },
            commandOptions: new CommandOptions
            {
                Arguments =
                [
                    new InteractionInput
                    {
                        Name = "generateTraces",
                        InputType = InputType.Boolean,
                        Required = true,
                        Value = "true"
                    },
                    new InteractionInput
                    {
                        Name = "traceCount",
                        InputType = InputType.Number,
                        Required = true,
                        Value = "50000",
                        DynamicLoading = new InputLoadOptions
                        {
                            DependsOnInputs = ["generateTraces"],
                            LoadCallback = context =>
                            {
                                context.Input.Disabled = !context.AllInputs.GetBoolean("generateTraces");
                                return Task.CompletedTask;
                            }
                        }
                    }
                ]
            });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var resultTask = app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions { NonInteractive = false },
            CancellationToken.None).DefaultTimeout();
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        interaction.Inputs["generateTraces"].Value = "false";
        interaction.Inputs["traceCount"].Disabled = true;
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await resultTask;
 
        Assert.True(result.Success);
        Assert.NotNull(capturedArguments);
        Assert.False(capturedArguments.GetBoolean("generateTraces"));
        Assert.Equal(50000, capturedArguments.GetInt32("traceCount"));
        Assert.True(capturedArguments["traceCount"].Disabled);
        Assert.Empty(capturedArguments["traceCount"].ValidationErrors);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NonInteractiveWithoutArguments_DoesNotPrompt()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            Label = "Selector",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions { NonInteractive = true },
            CancellationToken.None).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
        Assert.NotNull(result.InvalidArguments);
        var invalidArgument = Assert.Single(result.InvalidArguments);
        Assert.Equal("selector", invalidArgument.Name);
        Assert.Equal("Value is required.", Assert.Single(invalidArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_NonInteractive_IsAvailableReturnsFalse()
    {
        using var builder = CreateBuilder();
 
        bool? isAvailableDuringExecution = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    var interactionService = e.Services.GetRequiredService<IInteractionService>();
                    isAvailableDuringExecution = interactionService.IsAvailable;
                    return Task.FromResult(CommandResults.Success());
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions { NonInteractive = true },
            CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.NotNull(isAvailableDuringExecution);
        Assert.False(isAvailableDuringExecution.Value);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_Interactive_IsAvailableNotAffectedByScope()
    {
        using var builder = CreateBuilder();
 
        bool? isAvailableDuringExecution = null;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    var interactionService = e.Services.GetRequiredService<IInteractionService>();
                    isAvailableDuringExecution = interactionService.IsAvailable;
                    return Task.FromResult(CommandResults.Success());
                });
 
        var app = builder.Build();
 
        // Get the baseline IsAvailable value (may be false in test environments where dashboard is disabled)
        var baselineIsAvailable = app.Services.GetRequiredService<IInteractionService>().IsAvailable;
 
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(
            "myResource",
            "mycommand",
            new ResourceCommandExecutionOptions { NonInteractive = false },
            CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.NotNull(isAvailableDuringExecution);
 
        // Interactive mode should not change the baseline IsAvailable value
        Assert.Equal(baselineIsAvailable, isAvailableDuringExecution.Value);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_PartialArgumentCollection_ValidatesMissingDeclaredArguments()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "selector",
                            InputType = InputType.Text,
                            Required = true
                        },
                        new InteractionInput
                        {
                            Name = "target",
                            InputType = InputType.Text,
                            Required = true
                        }
                    ]
                });
 
        var arguments = new InteractionInputCollection(
        [
            new InteractionInput
            {
                Name = "selector",
                InputType = InputType.Text,
                Value = "#submit"
            }
        ]);
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand", arguments);
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.NotNull(result.InvalidArguments);
        var invalidArgument = Assert.Single(result.InvalidArguments, argument => argument.ValidationErrors.Count > 0);
        Assert.Equal("target", invalidArgument.Name);
        Assert.Equal("Value is required.", Assert.Single(invalidArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_InvalidCustomArgumentValidation_DoesNotExecuteCommand()
    {
        using var builder = CreateBuilder();
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "mycommand",
                displayName: "My command",
                executeCommand: e =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success());
                },
                commandOptions: new CommandOptions
                {
                    Arguments =
                    [
                        new InteractionInput
                        {
                            Name = "target",
                            Label = "Target",
                            InputType = InputType.Text,
                            Value = "prod"
                        }
                    ],
                    ValidateArguments = context =>
                    {
                        var target = context.Inputs.Single(argument => argument.Name == "target");
                        context.AddValidationError(target, "Target must not be prod.");
 
                        return Task.CompletedTask;
                    }
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand");
 
        Assert.False(result.Success);
        Assert.False(executed);
        Assert.NotNull(result.InvalidArguments);
        var invalidArgument = Assert.Single(result.InvalidArguments);
        Assert.Equal("target", invalidArgument.Name);
        Assert.Equal("Target must not be prod.", Assert.Single(invalidArgument.ValidationErrors));
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_HasReplicas_SuccessWithResult_ReturnsFirstResultData()
    {
        using var builder = CreateBuilder();
 
        var callCount = 0;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithAnnotation(new DcpInstancesAnnotation([
            new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
            new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
            ]));
        custom.WithCommand(name: "generate-token",
                displayName: "Generate Token",
                executeCommand: e =>
                {
                    var count = Interlocked.Increment(ref callCount);
                    return Task.FromResult(CommandResults.Success("Generated token.", $"token-{count}", CommandResultFormat.Text));
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "generate-token");
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.StartsWith("token-", result.Data.Value);
        Assert.Equal(CommandResultFormat.Text, result.Data.Format);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_RebuildCommand_ReturnsBuildOutput()
    {
        const string rebuildOutputMarker = "ASPIRE_REBUILD_OUTPUT_MARKER";
 
        using var workspace = TemporaryWorkspace.Create(testOutputHelper);
        var projectPath = CreateBuildOutputTestProject(workspace.WorkspaceRoot.FullName, rebuildOutputMarker);
 
        using var builder = CreateBuilder();
        var project = builder.AddProject("myProject", projectPath, options => options.ExcludeLaunchProfile = true);
        using var app = builder.Build();
 
        await app.StartAsync().DefaultTimeout(TimeSpan.FromMinutes(2));
 
        var resourceNotificationService = app.Services.GetRequiredService<ResourceNotificationService>();
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await resourceNotificationService.WaitForResourceAsync(project.Resource.Name, e => KnownResourceStates.BuildableStates.Contains(e.Snapshot.State?.Text), cts.Token).DefaultTimeout(TimeSpan.FromMinutes(2));
 
        var result = await app.ResourceCommands.ExecuteCommandAsync(project.Resource, KnownResourceCommands.RebuildCommand).DefaultTimeout(TimeSpan.FromMinutes(2));
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.Equal(CommandResultFormat.Text, result.Data.Format);
        Assert.Contains("[build] Building project...", result.Data.Value);
        Assert.Contains(rebuildOutputMarker, result.Data.Value);
    }
 
    [Fact]
    public void CommandResults_SuccessWithResult_ProducesCorrectResult()
    {
        var result = CommandResults.Success("Success.", "{\"key\": \"value\"}", CommandResultFormat.Json);
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.Equal("{\"key\": \"value\"}", result.Data.Value);
        Assert.Equal(CommandResultFormat.Json, result.Data.Format);
    }
 
    [Fact]
    public void CommandResults_SuccessWithResultAndDisplayImmediately_ProducesCorrectResult()
    {
        var result = CommandResults.Success("Success.", "{\"key\": \"value\"}", CommandResultFormat.Json, displayImmediately: true);
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.Equal("{\"key\": \"value\"}", result.Data.Value);
        Assert.Equal(CommandResultFormat.Json, result.Data.Format);
        Assert.True(result.Data.DisplayImmediately);
    }
 
    [Fact]
    public void CommandResults_SuccessWithTextResult_DefaultsToText()
    {
        var result = CommandResults.Success("Success.", "hello world");
 
        Assert.True(result.Success);
        Assert.NotNull(result.Data);
        Assert.Equal("hello world", result.Data.Value);
        Assert.Equal(CommandResultFormat.Text, result.Data.Format);
    }
 
    private static string CreateBuildOutputTestProject(string directoryPath, string buildOutputMarker)
    {
        var projectPath = Path.Combine(directoryPath, "BuildOutputProject.csproj");
        var programPath = Path.Combine(directoryPath, "Program.cs");
 
        File.WriteAllText(projectPath,
            $$"""
            <Project Sdk="Microsoft.NET.Sdk">
              <PropertyGroup>
                <OutputType>Exe</OutputType>
                <TargetFramework>net10.0</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
              </PropertyGroup>
 
              <Target Name="EmitAspireRebuildOutputMarker" AfterTargets="Build">
                <Message Importance="High" Text="{{buildOutputMarker}}" />
              </Target>
            </Project>
            """);
 
        File.WriteAllText(programPath,
            """
            Console.WriteLine("Hello from rebuild test project.");
            """);
 
        return projectPath;
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithProgressOptions_CancelsCommandWhenProgressCanceled()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        var commandStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "cancelable-command",
                displayName: "Cancelable Command",
                executeCommand: async e =>
                {
                    commandStarted.SetResult();
                    await Task.Delay(Timeout.Infinite, e.CancellationToken);
                    return CommandResults.Success();
                },
                commandOptions: new CommandOptions
                {
                    Progress = new CommandProgressOptions { Message = "Processing...", Title = "Cancelable Command" }
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var resultTask = app.ResourceCommands.ExecuteCommandAsync("myResource", "cancelable-command");
 
        // Wait for the Work callback to write the interaction and for the command to start.
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionType.Progress, interaction.Type);
        Assert.Equal("Cancelable Command", interaction.Title);
        Assert.Equal("Processing...", interaction.Message);
        await commandStarted.Task.DefaultTimeout();
 
        // Simulate the user clicking the Cancel button in the progress dialog.
        interaction.CompletionTcs.SetResult(InteractionResult.Cancel<bool>());
 
        var result = await resultTask.DefaultTimeout();
 
        Assert.True(testInteractionService.PromptProgressCalled);
        Assert.False(result.Success);
        Assert.True(result.Canceled);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithProgressOptions_ReturnsCanceledWhenCommandHandlesProgressCancellation()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        var commandStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "cancelable-command",
                displayName: "Cancelable Command",
                executeCommand: async e =>
                {
                    commandStarted.SetResult();
 
                    try
                    {
                        await Task.Delay(Timeout.Infinite, e.CancellationToken);
                    }
                    catch (OperationCanceledException) when (e.CancellationToken.IsCancellationRequested)
                    {
                        return CommandResults.Success("Cleanup completed.");
                    }
 
                    return CommandResults.Success();
                },
                commandOptions: new CommandOptions
                {
                    Progress = new CommandProgressOptions { Message = "Processing..." }
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var resultTask = app.ResourceCommands.ExecuteCommandAsync("myResource", "cancelable-command");
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionType.Progress, interaction.Type);
        await commandStarted.Task.DefaultTimeout();
 
        interaction.CompletionTcs.SetResult(InteractionResult.Cancel<bool>());
 
        var result = await resultTask.DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.True(result.Canceled);
        Assert.Null(result.Message);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithProgressOptions_SkipsProgressWhenNotAvailable()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        testInteractionService.IsAvailable = false;
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        var executed = false;
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "progress-command",
                displayName: "Progress Command",
                executeCommand: e =>
                {
                    executed = true;
                    return Task.FromResult(CommandResults.Success("Done"));
                },
                commandOptions: new CommandOptions
                {
                    Progress = new CommandProgressOptions { Message = "Working..." }
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "progress-command");
 
        Assert.True(result.Success);
        Assert.Equal("Done", result.Message);
        Assert.True(executed);
        Assert.False(testInteractionService.PromptProgressCalled);
    }
 
    [Fact]
    public async Task ExecuteCommandAsync_WithProgressOptions_SuccessPathPropagatesResult()
    {
        using var builder = CreateBuilder();
 
        var testInteractionService = new TestInteractionService();
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
 
        var custom = builder.AddResource(new CustomResource("myResource"));
        custom.WithCommand(name: "success-command",
                displayName: "Success Command",
                executeCommand: e =>
                {
                    return Task.FromResult(CommandResults.Success("Operation completed successfully."));
                },
                commandOptions: new CommandOptions
                {
                    Progress = new CommandProgressOptions { Message = "Processing..." }
                });
 
        var app = builder.Build();
        await app.StartAsync();
 
        var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "success-command");
 
        Assert.True(result.Success);
        Assert.Equal("Operation completed successfully.", result.Message);
        Assert.True(testInteractionService.PromptProgressCalled);
    }
 
    private sealed class CustomResource(string name) : Resource(name), IResourceWithEndpoints, IResourceWithWaitSupport
    {
 
    }
}