File: Commands\AgentInitCommandTests.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.Globalization;
using System.Text.Json.Nodes;
using Aspire.Cli.Agents;
using Aspire.Cli.Agents.AspireSkills;
using Aspire.Cli.Commands;
using Aspire.Cli.Configuration;
using Aspire.Cli.DotNet;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.Npm;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.Agents;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
 
namespace Aspire.Cli.Tests.Commands;
 
public class AgentInitCommandTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData("agent init")]
    [InlineData("mcp init")]
    [InlineData("new aspire-empty")]
    [InlineData("init")]
    public void AgentInitOptions_SelectSkillsWithoutLegacySourceOption(string command)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var provider = CliTestHelper.CreateServiceCollection(workspace, outputHelper).BuildServiceProvider();
        var root = provider.GetRequiredService<RootCommand>();
        Assert.Empty(root.Parse(command).Errors);
 
        foreach (var skills in new[] { "aspire", "playwright-cli", "PLAYWRIGHT-CLI", "dotnet-inspect", "ASPIRE,aspire,Playwright-CLI", "all", "none" })
        {
            Assert.Empty(root.Parse($"{command} --skills {skills}").Errors);
        }
 
        foreach (var options in new[]
        {
            "--aspire-source", "--aspire-source remote", "--aspire-source embedded",
            "--skills", "--skills unsupported", "--skills aspire,none", "--skills mcp", "--skills playwright", "--skills \"\"",
            "--aspire-skills remote", "--playwright", "--dotnet-inspect"
        })
        {
            Assert.NotEmpty(root.Parse($"{command} {options}").Errors);
        }
 
        Assert.Equal(["all", "aspire", "dotnet-inspect", "none", "playwright-cli"],
            root.Parse($"{command} --skills ").GetCompletions().Select(item => item.Label).Order(StringComparer.Ordinal));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task AgentInit_UsesConfiguredOrSelectedAppHostSettings(bool configured)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var first = context.Project.CreateSubdirectory("first");
        var selected = context.Project.CreateSubdirectory("selected");
        var firstProject = new FileInfo(Path.Combine(first.FullName, "First.csproj"));
        var selectedProject = new FileInfo(Path.Combine(selected.FullName, "Selected.csproj"));
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(selected.FullName, AspireConfigFile.FileName), """
            {"skillSources":{"default":{"enabled":false},"team":{"repository":"example/selected","plugins":["deployment"]}}}
            """);
        var interaction = new TestInteractionService
        {
            PromptForSelectionCallback = (prompt, choices, _, _) =>
            {
                Assert.False(configured);
                Assert.Equal(InteractionServiceStrings.SelectAppHostToUse, prompt);
                Assert.Equal([firstProject, selectedProject], choices.Cast<FileInfo>());
                return selectedProject;
            }
        };
        var locator = new TestProjectLocator
        {
            GetAppHostFromSettingsAsyncCallback = _ => Task.FromResult<FileInfo?>(configured ? selectedProject : null),
            FindAppHostProjectFilesAsyncCallback = (directory, scope, _, skipValidation) =>
            {
                Assert.False(configured);
                Assert.True(skipValidation);
                Assert.Equal(context.Project.FullName, directory.FullName);
                Assert.Equal(AppHostDiscoveryScope.DefaultFiltered, scope);
                return Task.FromResult<List<FileInfo>>([firstProject, selectedProject]);
            }
        };
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
            options.ProjectLocatorFactory = _ => locator;
        }).BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot --scope project --skills aspire --non-interactive");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        await Verify(await File.ReadAllTextAsync(
            context.Copilot.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project)), "json")
            .UseParameters(configured);
    }
 
    [Fact]
    public async Task AgentInit_WithoutAspireSkills_DoesNotDiscoverAppHost()
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator
            {
                GetAppHostFromSettingsAsyncCallback = _ => throw new InvalidOperationException("AppHost lookup is unnecessary without Aspire skills."),
                FindAppHostProjectFilesAsyncCallback = (_, _, _, _) => throw new InvalidOperationException("AppHost discovery is unnecessary without Aspire skills.")
            };
        }).BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot --skills none --mcp=false --non-interactive");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("9.0.302")]
    public async Task AgentInit_UsesAppHostSettingsWithoutUsableSdk(string? highestDetectedVersion)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var appHostDirectory = context.Project.CreateSubdirectory("app");
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(appHostDirectory.FullName, "AppHost.csproj"), """
            <Project Sdk="Microsoft.NET.Sdk">
              <PropertyGroup>
                <IsAspireHost>true</IsAspireHost>
              </PropertyGroup>
            </Project>
            """);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, "web", "Web.csproj"), """
            <Project Sdk="Microsoft.NET.Sdk.Web" />
            """);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(appHostDirectory.FullName, AspireConfigFile.FileName), """
            {"skillSources":{"default":{"enabled":false},"team":{"repository":"example/selected","plugins":["deployment"]}}}
            """);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, AspireConfigFile.FileName), """
            {"appHost":{"path":"app/AppHost.csproj"}}
            """);
        var sdkChecks = 0;
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.ProjectLocatorFactory = options.CreateDefaultProjectLocatorFactory;
            options.DotNetSdkInstallerFactory = _ => new TestDotNetSdkInstaller
            {
                CheckAsyncCallback = _ =>
                {
                    sdkChecks++;
                    return (false, highestDetectedVersion, DotNetSdkInstaller.MinimumSdkVersion);
                }
            };
        }).BuildServiceProvider();
        var locator = Assert.IsType<ProjectLocator>(provider.GetRequiredService<IProjectLocator>());
        Assert.IsType<DotNetAppHostProject>(provider.GetRequiredService<IAppHostProjectFactory>().GetProject(
            new FileInfo(Path.Combine(appHostDirectory.FullName, "AppHost.csproj"))));
        Assert.Empty(await locator.FindAppHostProjectFilesAsync(
            context.Project, AppHostDiscoveryScope.DefaultFiltered, CancellationToken.None).DefaultTimeout());
        Assert.Equal(1, sdkChecks);
        Assert.Empty(await locator.FindAppHostProjectFilesAsync(
            context.Project, AppHostDiscoveryScope.ExplicitDirectory, maxDepth: 0,
            CancellationToken.None, skipValidation: true).DefaultTimeout());
        var candidates = await locator.FindAppHostProjectFilesAsync(
            context.Project, AppHostDiscoveryScope.ExplicitDirectory, maxDepth: 1,
            CancellationToken.None, skipValidation: true).DefaultTimeout();
        Assert.Equal([Path.Combine(appHostDirectory.FullName, "AppHost.csproj")],
            candidates.Select(candidate => candidate.FullName));
        Assert.Equal(1, sdkChecks);
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot --scope project --skills aspire --non-interactive");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal(1, sdkChecks);
        await Verify(await File.ReadAllTextAsync(
            context.Copilot.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project)), "json")
            .UseParameters(highestDetectedVersion);
    }
 
    [Theory]
    [InlineData("", true)]
    [InlineData(" true", true)]
    [InlineData("=TRUE", true)]
    [InlineData(" false", false)]
    [InlineData("=false", false)]
    public void McpOption_RemainsAnIndependentConfirmation(string suffix, bool expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var provider = CliTestHelper.CreateServiceCollection(workspace, outputHelper).BuildServiceProvider();
        var root = provider.GetRequiredService<RootCommand>();
        var result = root.Parse($"agent init --mcp{suffix}");
 
        Assert.Empty(result.Errors);
        Assert.Equal(expected, result.GetValue<bool?>("--mcp"));
        Assert.NotEmpty(root.Parse("agent init --mcp invalid").Errors);
        Assert.NotEmpty(root.Parse($"new aspire-empty --mcp{suffix}").Errors);
        Assert.NotEmpty(root.Parse($"init --mcp{suffix}").Errors);
    }
 
    [Theory]
    [InlineData("copilot")]
    [InlineData("claude")]
    [InlineData("opencode")]
    [InlineData("copilot,claude,opencode")]
    public async Task AgentInit_WarnsAboutSelectedClientSourcesBeforeRegistration(string agents)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, AspireConfigFile.FileName), """
            {
              "skillSources": {
                "default": { "openCodeV1Catalog": "https://third-party.example/skills/" },
                "team": {
                  "repository": "example/team",
                  "marketplace": "TEAM",
                  "openCodeV1Catalog": "https://team.example/skills/",
                  "openCodeV2Catalog": "https://team.example/skills/"
                },
                "pluginsOnly": { "repository": "example/plugins" },
                "teamAlias": {
                  "repository": "example/team",
                  "marketplace": "team-marketplace",
                  "ref": "source-demo",
                  "openCodeV1Catalog": "https://team.example/skills/"
                },
                "disabled": {
                  "enabled": false,
                  "repository": "example/disabled",
                  "openCodeV1Catalog": "https://disabled.example/skills/"
                }
              }
            }
            """);
        var messagesBeforeRegistration = new List<string>();
        var interaction = new TestInteractionService
        {
            DisplayMessageCallback = (emoji, message, _) =>
            {
                if (emoji.Equals(KnownEmojis.Information) || emoji.Equals(KnownEmojis.PoliceCarLight))
                {
                    Assert.All(context.Clients, client => Assert.False(File.Exists(
                        client.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project))));
                    messagesBeforeRegistration.Add(message);
                }
            },
            PromptForSelectionCallback = (_, _, _, _) => throw new InvalidOperationException("No source or scope prompt is expected.")
        };
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
            options.InteractionServiceFactory = _ => interaction;
        }).BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse(
            $"agent init --agents {agents} --scope project --skills aspire");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal([AgentCommandStrings.InitCommand_SkillSourcesExplanation, $"[yellow]{SharedCommandStrings.SourceTrustWarning}[/]"],
            messagesBeforeRegistration);
        Assert.NotEmpty(interaction.DisplayedRenderables);
        var warning = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.PoliceCarLight));
        Assert.Equal($"[yellow]{SharedCommandStrings.SourceTrustWarning}[/]", warning.Message);
        Assert.Equal(ConsoleOutput.Error, warning.ConsoleOverride);
        Assert.Equal(messagesBeforeRegistration, interaction.DisplayedMessages
            .Where(message => message.Emoji.Equals(KnownEmojis.Information) || message.Emoji.Equals(KnownEmojis.PoliceCarLight))
            .Select(message => message.Message));
        using var renderedSources = new StringWriter();
        var console = AnsiConsole.Create(new AnsiConsoleSettings
        {
            Ansi = AnsiSupport.No,
            ColorSystem = ColorSystemSupport.NoColors,
            Out = new AnsiConsoleOutput(renderedSources),
            Enrichment = new ProfileEnrichment { UseDefaultEnrichers = false }
        });
        console.Profile.Width = 160;
        foreach (var source in interaction.DisplayedRenderables)
        {
            console.Write(source);
            console.WriteLine();
        }
        await Verify(new { Sources = renderedSources.ToString(), Warnings = messagesBeforeRegistration })
            .UseParameters(agents).ScrubLinesWithReplace(line => line.TrimEnd());
    }
 
    [Fact]
    public async Task AgentInit_KeepsExistingCliSkillLabels()
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        await File.WriteAllTextAsync(Path.Combine(context.Project.FullName, "apphost.cs"), string.Empty);
        var labels = new List<string>();
        var interaction = new TestInteractionService
        {
            PromptForSelectionsCallback = (prompt, choices, formatter, _) =>
            {
                Assert.Equal(AgentCommandStrings.InitCommand_SelectSkills, prompt);
                labels.AddRange(choices.Cast<object>().Select(formatter));
                return [];
            },
            PromptForSelectionCallback = (_, _, _, _) => throw new InvalidOperationException("No source is needed without Aspire.")
        };
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
            options.InteractionServiceFactory = _ => interaction;
        });
        using var provider = services.BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse("agent init --agents copilot --mcp=false");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal(
            [$"{CommonAgentApplicators.AspireSkillName} — {AgentCommandStrings.InitCommand_AspireSkillChoice}",
                $"{SkillDefinition.PlaywrightCli.Name} — {AgentCommandStrings.SkillDescription_PlaywrightCli}",
                $"{SkillDefinition.DotnetInspect.Name} — {AgentCommandStrings.SkillDescription_DotnetInspect}"],
            labels);
        Assert.Empty(context.Home.EnumerateFileSystemInfos());
    }
 
    [Theory]
    [InlineData("", "")]
    [InlineData("   ", "   ")]
    [InlineData("Short description", "Short description")]
    [InlineData("Short description.", "Short description.")]
    [InlineData("First sentence. Second sentence.", "First sentence.")]
    [InlineData("**WORKFLOW SKILL** - Top-level router for Aspire 13.4 distributed apps. Detects the AppHost.", "Top-level router for Aspire 13.4 distributed apps.")]
    [InlineData("**ANALYSIS SKILL** — Observe Aspire apps. USE FOR: aspire logs.", "Observe Aspire apps.")]
    [InlineData("**SETUP SKILL**: One-time setup of resources. INVOKES: aspire add.", "One-time setup of resources.")]
    [InlineData("Visit github.com for docs. Then run the tool.", "Visit github.com for docs.")]
    [InlineData("**TYPE** -", "")]
    // Fix 1 regression: a leading separator that does NOT follow a "**TYPE**" prefix must be preserved.
    // The earlier implementation unconditionally trimmed leading separators after the bold-prefix
    // branch, which silently mutated skill descriptions that happened to start with '-' or ':'.
    [InlineData("-Quickly do X.", "-Quickly do X.")]
    [InlineData(":memo notes", ":memo notes")]
    public void SimplifyDescription_ProducesExpectedSummary(string input, string expected)
    {
        Assert.Equal(expected, AgentInitCommand.SimplifyDescription(input));
    }
 
    [Fact]
    public async Task AgentInitCommand_DoesNotCheckWorkspaceSource()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await TestConfigSourceServices.SaveBindingAsync(
            new ConfigSource { Repository = "https://git.example/configuration", Path = "aspire.config.json", AppliedVersion = "version-1" },
            workspace.WorkspaceRoot, CancellationToken.None);
        using var provider = CliTestHelper.CreateServiceCollection(workspace, outputHelper).BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>().Parse(
            $"agent init --workspace-root \"{workspace.Path}\" --agents copilot --skills aspire --scope project --mcp false --non-interactive");
 
        Assert.Equal(CliExitCodes.Success, await command.InvokeAsync().DefaultTimeout());
 
        var git = Assert.IsType<TestConfigSourceRepository>(provider.GetRequiredService<IGitClient>());
        Assert.Equal(0, git.Calls);
        Assert.Equal(0, git.CheckCalls);
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithNoClients_SucceedsWithNoSkillsInstalled()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents none --skills all");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
 
        // No clients selected, so no skill directories should be created.
        var agentsDir = Path.Combine(workspace.WorkspaceRoot.FullName, ".agents");
        Assert.False(Directory.Exists(agentsDir), $"Expected no .agents directory but found {agentsDir}");
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithoutClientSelection_UsesDefaultClients()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --skills none");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Theory]
    [InlineData("project")]
    [InlineData("global")]
    public async Task AgentInitCommand_NonInteractive_WithoutSkills_RegistersAspireRemotely(string scope)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var configurationScope = Enum.Parse<AgentConfigurationScope>(scope, ignoreCase: true);
 
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
        });
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --agents all --scope {scope}");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
 
        foreach (var client in provider.GetServices<IAgentClient>())
        {
            Assert.True(File.Exists(client.GetIntegrationConfigPath(configurationScope, context.Project)));
            Assert.True(client.HasIntegrationRegistration(
                context.Project, configurationScope, context.Sources));
        }
        var skillsRoot = configurationScope is AgentConfigurationScope.Project ? context.Project : context.Home;
        Assert.False(Directory.Exists(Path.Combine(skillsRoot.FullName, ".agents", "skills")));
        Assert.False(Directory.Exists(Path.Combine(skillsRoot.FullName, ".claude", "skills")));
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithInvalidClient_FailsValidation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents invalid --skills all");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.NotEqual(CliExitCodes.Success, exitCode);
        Assert.Single(result.Errors);
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithoutWorkspaceRoot_UsesWorkingDirectory()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("agent init --agents copilot");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        var client = provider.GetServices<IAgentClient>().Single(client =>
            string.Equals(client.Id, "copilot", StringComparisons.AgentClientId));
        Assert.True(client.HasIntegrationRegistration(
            workspace.WorkspaceRoot, AgentConfigurationScope.Project, SkillSource.Resolve(provider.GetRequiredService<IConfiguration>())));
    }
 
    [Fact]
    public async Task PromptAndChainAsync_RegistersSourcesFromSelectedAppConfiguration()
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var app = context.Project.CreateSubdirectory("app");
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(app.FullName, "aspire.config.json"), """
            {"skillSources":{"default":{"enabled":false},"team":{"repository":"example/team","plugins":["team"]}}}
            """);
        var interaction = new TestInteractionService();
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
        });
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<AgentInitCommand>();
 
        var result = await command.PromptAndChainAsync(
            interaction, CliExitCodes.Success, context.Project, app,
            PromptBinding.CreateDefault(true),
            PromptBinding.CreateDefault<string?>("copilot"),
            PromptBinding.CreateDefault<string?>(CommonAgentApplicators.AspireSkillName),
            PromptBinding.CreateDefault(AgentConfigurationScope.Project),
            CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result.ExitCode);
        var config = await File.ReadAllTextAsync(context.Copilot.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project));
        await Verify(config, "json");
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task PromptAndChainAsync_UsesFreshSelectedAppAndGlobalSources(bool sameDirectory)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, "aspire.config.json"), """
            {"skillSources":{"default":{"enabled":false},"old":{"repository":"example/old"}}}
            """);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, ".aspire", "settings.global.json"), """
            {"skillSources":{"shared":{"repository":"example/shared"},"team":{"repository":"example/global","plugins":["team"],"ref":"main"}}}
            """);
        var interaction = new TestInteractionService();
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.WorkingDirectory = context.Project;
            options.InteractionServiceFactory = _ => interaction;
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
        }).BuildServiceProvider();
        Assert.Equal("example/old", provider.GetRequiredService<IConfiguration>()["skillSources:old:repository"]);
        var app = sameDirectory ? context.Project : context.Project.CreateSubdirectory("app");
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(app.FullName, "aspire.config.json"), """
            {"skillSources":{"default":{},"team":{"repository":"example/team"}}}
            """);
 
        var result = await provider.GetRequiredService<AgentInitCommand>().PromptAndChainAsync(
            interaction, CliExitCodes.Success, context.Project, app,
            PromptBinding.CreateDefault(true),
            PromptBinding.CreateDefault<string?>("copilot"),
            PromptBinding.CreateDefault<string?>(CommonAgentApplicators.AspireSkillName),
            PromptBinding.CreateDefault(AgentConfigurationScope.Project),
            CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result.ExitCode);
        await Verify(await File.ReadAllTextAsync(
            context.Copilot.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project)), "json")
            .UseParameters(sameDirectory);
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithNoneSkills_SucceedsWithNoSkillsInstalled()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents all --skills none");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
 
        // No skills selected, so no skill files should be created
        var aspireSkillPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".agents", "skills", CommonAgentApplicators.AspireSkillName);
        Assert.False(Directory.Exists(aspireSkillPath), $"Expected no aspire skill directory but found {aspireSkillPath}");
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_DoesNotConfigureMcpByDefault()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents all");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.False(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json")));
        var openCodeConfig = JsonNode.Parse(await File.ReadAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc")));
        Assert.NotNull(openCodeConfig);
        Assert.Null(openCodeConfig["mcp"]);
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithMcpTrue_ConfiguresMcp()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents all --mcp");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json")));
        Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc")));
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithMcpFalse_DoesNotConfigureMcp()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents all --mcp=false");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.False(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json")));
        var openCodeConfig = JsonNode.Parse(await File.ReadAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc")));
        Assert.NotNull(openCodeConfig);
        Assert.Null(openCodeConfig["mcp"]);
    }
 
    [Fact]
    public async Task AgentInitCommand_NonInteractive_WithAgentNone_DoesNotInstallAnySkills()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents none --skills all");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
 
        // With --agents none, no skill files should be installed even when --skills all is passed.
        var agentsDir = Path.Combine(workspace.WorkspaceRoot.FullName, ".agents", "skills");
        Assert.False(Directory.Exists(agentsDir), $"Expected no agents/skills directory but found {agentsDir}");
    }
 
    [Theory]
    [InlineData("apphost.cs", true)]
    [InlineData("apphost.mts", false)]
    [InlineData(null, false)]
    public async Task AgentInitCommand_DotnetInspect_RespectsWorkspaceLanguage(string? appHostFileName, bool expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        if (appHostFileName is not null)
        {
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, appHostFileName), string.Empty);
        }
        string[] offeredSkills = [];
        var interactionService = new TestInteractionService
        {
            PromptForSelectionsCallback = (prompt, choices, _, _) =>
            {
                Assert.Equal(AgentCommandStrings.InitCommand_SelectSkills, prompt);
                offeredSkills = choices.Cast<string>().ToArray();
                return [];
            }
        };
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
            options.InteractionServiceFactory = _ => interactionService;
        });
        services.AddSingleton<INpmRunner>(new FakeNpmRunner { IsAvailable = false });
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        Assert.Equal(CliExitCodes.Success, await command.Parse(
            "agent init --agents copilot --scope project --mcp=false").InvokeAsync().DefaultTimeout());
        Assert.Equal(expected ? ["aspire", "playwright-cli", "dotnet-inspect"] : ["aspire", "playwright-cli"], offeredSkills);
 
        Assert.Equal(CliExitCodes.Success, await command.Parse(
            "agent init --non-interactive --agents copilot --scope project --skills all").InvokeAsync().DefaultTimeout());
        Assert.Equal(expected, File.Exists(Path.Combine(workspace.Path, ".agents", "skills", SkillDefinition.DotnetInspect.Name, "SKILL.md")));
    }
 
    [Theory]
    [InlineData(nameof(AgentEnvironment.CopilotCli), "GitHub Copilot CLI")]
    [InlineData(nameof(AgentEnvironment.CopilotApp), "GitHub Copilot App")]
    public async Task AgentInitCommand_GlobalScope_InstallsTelemetryHook_ForSelectedDetectedEnvironment(string environmentName, string displayName)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var homeDirectory = workspace.CreateDirectory("fake-home");
        var interactionService = new TestInteractionService();
        var environment = Enum.Parse<AgentEnvironment>(environmentName);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory);
            options.AgentEnvironmentDetectorFactory = _ => new TestAgentEnvironmentDetector { DetectedEnvironments = environment };
            options.InteractionServiceFactory = _ => interactionService;
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents copilot --scope global --skills aspire");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        var hookFile = Path.Combine(homeDirectory.FullName, ".copilot", "hooks", "aspire-telemetry.json");
        Assert.True(File.Exists(hookFile), $"Expected telemetry hook at {hookFile}");
        Assert.Contains(
            interactionService.DisplayedMessages,
            message => message.Message.Contains(displayName, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task AgentInitCommand_DoesNotFail_WhenTelemetryHookConfigurationThrows()
    {
        // Hook installation is best-effort transparency tooling. A non-IO failure such as a missing
        // embedded hook script (InvalidOperationException from the installer) must not abort `agent init`.
        const string failureMessage = "simulated hook configuration failure";
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var homeDirectory = workspace.CreateDirectory("fake-home");
        var interactionService = new TestInteractionService();
        var subtleMessages = new List<string>();
        interactionService.DisplaySubtleMessageCallback = subtleMessages.Add;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory);
            options.AgentEnvironmentDetectorFactory = _ => new TestAgentEnvironmentDetector { DetectedEnvironments = AgentEnvironment.CopilotCli };
            options.InteractionServiceFactory = _ => interactionService;
            options.TelemetryHookConfiguratorFactory = _ => new TestTelemetryHookConfigurator { Exception = new InvalidOperationException(failureMessage) };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"agent init --workspace-root {workspace.WorkspaceRoot.FullName} --agents copilot --scope global --skills aspire");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Contains(failureMessage, subtleMessages);
    }
 
    private static CliExecutionContext CreateExecutionContext(DirectoryInfo workingDirectory, DirectoryInfo homeDirectory)
    {
        return TestExecutionContextHelper.CreateExecutionContext(
            workingDirectory,
            homeDirectory: homeDirectory);
    }
 
    [Theory]
    [InlineData("copilot", "project", ".github/copilot/settings.json")]
    [InlineData("copilot", "global", ".copilot/settings.json")]
    [InlineData("claude", "project", ".claude/settings.json")]
    [InlineData("claude", "global", ".claude/settings.json")]
    [InlineData("opencode", "project", "opencode.jsonc")]
    [InlineData("opencode", "global", ".config/opencode/opencode.jsonc")]
    public async Task Remote_RegistersSelectedUndetectedClientsWithoutMcp(string client, string scope, string relativePath)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var messages = new List<string>();
        var interaction = new TestInteractionService
        {
            DisplaySubtleMessageCallback = messages.Add,
            PromptForSelectionCallback = (_, _, _, _) => throw new InvalidOperationException("Explicit skills and scope must not prompt.")
        };
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
            options.InteractionServiceFactory = _ => interaction;
        });
        using var provider = services.BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            $"agent init --agents {client} --scope {scope} --skills aspire");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        var root = scope == "project" ? context.Project : context.Home;
        var otherRoot = scope == "project" ? context.Home : context.Project;
        var expectedPath = Path.Combine(root.FullName, relativePath.Replace('/', Path.DirectorySeparatorChar));
        Assert.Equal([expectedPath], Directory.EnumerateFiles(root.FullName, "*", SearchOption.AllDirectories));
        Assert.Empty(otherRoot.EnumerateFileSystemInfos());
        var config = JsonNode.Parse(await File.ReadAllTextAsync(expectedPath))!.AsObject();
        Assert.Equal(client == "opencode" ? ["skills"] : new[] { "enabledPlugins", "extraKnownMarketplaces" }, config.Select(property => property.Key).Order());
        Assert.Empty(messages);
        Assert.Empty(interaction.DisplayedRenderables);
        Assert.Collection(interaction.DisplayedMessages,
            message =>
            {
                Assert.Equal(KnownEmojis.Robot, message.Emoji);
                Assert.Equal(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredTargets,
                    string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_LocationWithAgents,
                        scope == "project" ? relativePath : $"~/{relativePath}",
                        context.Clients.Single(value => value.Id == client).DisplayName)), message.Message);
            });
        Assert.Empty(interaction.DisplayedErrors);
    }
 
    [Theory]
    [InlineData(nameof(AgentConfigurationScope.Project))]
    [InlineData(nameof(AgentConfigurationScope.Global))]
    public async Task Remote_CombinesNewAndExistingRegistrationsWithOneRowPerAgent(string scopeName)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var scope = Enum.Parse<AgentConfigurationScope>(scopeName);
        Assert.All(await context.ConfigureIntegrationAsync(scope, [context.Copilot]).DefaultTimeout(),
            result => Assert.Equal(AgentConfigurationStatus.Configured, result.Status));
        var interaction = new TestInteractionService();
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
            options.AgentEnvironmentDetectorFactory = _ => new TestAgentEnvironmentDetector();
            options.TelemetryHookConfiguratorFactory = _ => new TestTelemetryHookConfigurator();
        });
        using var provider = services.BuildServiceProvider();
        var scopeRoot = scope is AgentConfigurationScope.Project ? context.Project : context.Home;
        var prefix = scope is AgentConfigurationScope.Global ? "~/" : string.Empty;
        var targetEntries = context.Clients.Select(client =>
            $"{prefix}{Path.GetRelativePath(scopeRoot.FullName, client.GetIntegrationConfigPath(scope, context.Project)).Replace(Path.DirectorySeparatorChar, '/')} ({client.DisplayName})").ToArray();
 
        for (var invocation = 0; invocation < 2; invocation++)
        {
            var previousMessageCount = interaction.DisplayedMessages.Count;
            var result = provider.GetRequiredService<RootCommand>().Parse(
                $"agent init --agents copilot,claude,opencode --scope {scope} --skills aspire --mcp=false");
 
            Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
            var messages = interaction.DisplayedMessages.Skip(previousMessageCount).ToArray();
            Assert.Collection(messages,
                message =>
                {
                    Assert.Equal(KnownEmojis.Robot, message.Emoji);
                    Assert.Equal(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredTargets,
                        string.Join(", ", targetEntries)), message.Message);
                });
            Assert.Empty(interaction.DisplayedErrors);
        }
    }
 
    [Fact]
    public async Task Remote_SummaryListsAgentConfigurationFiles()
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, "aspire.config.json"), """
            {"skillSources":{"team":{"repository":"example/team","plugins":["deployment"]}}}
            """);
        var interaction = new TestInteractionService();
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
        }).BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot,claude --scope project --skills aspire --mcp=false");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        var summary = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.Robot));
        await Verify(summary.Message);
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task Remote_SummaryListsAgentConfigurationFilesWithExistingOpenCodeCatalogs(bool array, bool pinned)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var catalogs = new JsonArray("https://example.test/unrelated-catalog/");
        if (pinned)
        {
            catalogs.Add("https://raw.githubusercontent.com/microsoft/aspire-skills/release/opencode/v2/");
        }
        var config = new JsonObject
        {
            ["skills"] = array ? catalogs : new JsonObject { ["urls"] = catalogs }
        };
        await AgentConfigurationTestContext.WriteAsync(
            context.OpenCode.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project), config.ToJsonString());
        var interaction = new TestInteractionService();
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
        }).BuildServiceProvider();
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot,claude,opencode --scope project --skills aspire --mcp=false");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
        outputHelper.WriteLine(string.Join(System.Environment.NewLine, interaction.DisplayedErrors));
        Assert.Empty(interaction.DisplayedErrors);
        Assert.Equal(CliExitCodes.Success, exitCode);
        var summary = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.Robot));
        await Verify(summary.Message).UseParameters(array, pinned);
    }
 
    [Fact]
    public async Task Remote_FailedRegistrationsKeepEachTargetWithItsReason()
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        const string original = """{"enabledPlugins":{"aspire@aspire-skills":false}}""";
        IAgentClient[] clients = [context.Copilot, context.ClaudeCode];
        foreach (var client in clients)
        {
            await AgentConfigurationTestContext.WriteAsync(
                client.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project), original);
        }
        var interaction = new TestInteractionService();
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
        }).BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents copilot,claude --scope project --skills aspire --mcp=false");
 
        Assert.Equal(CliExitCodes.InvalidCommand, await result.InvokeAsync().DefaultTimeout());
        var reason = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ExistingConfigurationConflict,
            "enabledPlugins.aspire@aspire-skills");
        Assert.Equal(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_FailedTarget,
            $"GitHub Copilot: .github/copilot/settings.json{System.Environment.NewLine}  {reason}{System.Environment.NewLine}" +
            $"  Claude Code: .claude/settings.json{System.Environment.NewLine}  {reason}"),
            Assert.Single(interaction.DisplayedErrors));
        foreach (var client in clients)
        {
            Assert.Equal(original, await File.ReadAllTextAsync(
                client.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project)));
        }
        Assert.Empty(context.Home.EnumerateFileSystemInfos());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task Remote_OpenCodeSkillsAndMcpUseIndependentRepresentations(bool array)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var path = Path.Combine(context.Project.FullName, "opencode.json");
        var original = JsonNode.Parse(array
            ? """{"skills":["./team-skills"],"mcp":{"servers":{"aspire":{"type":"local","command":["pinned-aspire"],"disabled":true}}}}"""
            : """{"skills":{"paths":["./team-skills"]},"mcp":{"servers":{"aspire":{"type":"local","command":["pinned-aspire"],"disabled":true}}}}""")!;
        await File.WriteAllTextAsync(path, original.ToJsonString());
        using var provider = context.CreateServiceCollection(outputHelper).BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents opencode --scope project --skills aspire --mcp");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        var content = await File.ReadAllTextAsync(path);
        var configured = JsonNode.Parse(content)!;
        Assert.True(JsonNode.DeepEquals(original["mcp"], configured["mcp"]));
        if (!array)
        {
            Assert.Equal(["./team-skills"], configured["skills"]!["paths"]!.AsArray().Select(value => (string)value!));
        }
        var sources = array ? configured["skills"]!.AsArray() : configured["skills"]!["urls"]!.AsArray();
        Assert.Equal(array
            ? ["./team-skills", "https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v2/"]
            : new[] { "https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v1/" },
            sources.Select(value => (string)value!));
        var repeated = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents opencode --scope project --skills aspire --mcp");
        Assert.Equal(CliExitCodes.Success, await repeated.InvokeAsync().DefaultTimeout());
        Assert.Equal(content, await File.ReadAllTextAsync(path));
        Assert.Equal([path], context.Project.EnumerateFiles().Select(file => file.FullName));
        Assert.Empty(context.Home.EnumerateFileSystemInfos());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task Remote_WarnsForConfiguredOpenCodeSkillRootsAndPreservesTheCopy(bool array)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var configPath = Path.Combine(context.Project.FullName, ".opencode", "opencode.jsonc");
        var skillPath = Path.Combine(context.Project.FullName, "team-skills", "aspire", "SKILL.md");
        await AgentConfigurationTestContext.WriteAsync(configPath, array
            ? """{"skills":["./team-skills"]}"""
            : """{"skills":{"paths":["./team-skills"]}}""");
        const string skill = "My customized Aspire instructions.";
        await AgentConfigurationTestContext.WriteAsync(skillPath, skill);
        var interaction = new TestInteractionService();
        using var provider = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
        }).BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "agent init --agents opencode --scope project --skills aspire --mcp=false");
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Empty(interaction.DisplayedErrors);
        Assert.Contains(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.PoliceCarLight) &&
            message.Message == string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_LocalSkillsConflict,
                "  team-skills/aspire/SKILL.md"));
        Assert.Equal(skill, await File.ReadAllTextAsync(skillPath));
        var config = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!;
        var sources = array ? config["skills"]!.AsArray() : config["skills"]!["urls"]!.AsArray();
        Assert.Equal(array
            ? ["./team-skills", "https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v2/"]
            : new[] { "https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v1/" },
            sources.Select(value => (string)value!));
        Assert.Null(config["mcp"]);
        Assert.Empty(context.Home.EnumerateFileSystemInfos());
    }
 
    [Theory]
    [InlineData(nameof(AgentConfigurationScope.Project))]
    [InlineData(nameof(AgentConfigurationScope.Global))]
    public async Task Mcp_InvalidOpenCodeNativeContainer_DoesNotStopIndependentClients(string scopeName)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var scope = Enum.Parse<AgentConfigurationScope>(scopeName);
        var path = context.OpenCode.GetMcpConfigPath(scope, context.Project);
        const string original = """{"mcp":{"servers":null}}""";
        await AgentConfigurationTestContext.WriteAsync(path, original);
        var interaction = new TestInteractionService();
        var hooks = new TestTelemetryHookConfigurator();
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
            options.AgentEnvironmentDetectorFactory = _ => new TestAgentEnvironmentDetector { DetectedEnvironments = AgentEnvironment.CopilotCli };
            options.TelemetryHookConfiguratorFactory = _ => hooks;
        });
        using var provider = services.BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(
            $"agent init --agents opencode,copilot --scope {scope} --skills none --mcp");
 
        Assert.Equal(CliExitCodes.InvalidCommand, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.True(File.Exists(context.Copilot.GetMcpConfigPath(scope, context.Project)));
        Assert.Equal(scope is AgentConfigurationScope.Global ? AgentEnvironment.CopilotCli : AgentEnvironment.None, hooks.ConfiguredEnvironments);
        Assert.Equal(string.Format(CultureInfo.CurrentCulture,
            AgentCommandStrings.McpServerContainerMustBeJsonObject, "mcp.servers", path), Assert.Single(interaction.DisplayedErrors));
    }
 
    [Theory]
    [InlineData("copilot", "{broken")]
    [InlineData("claude", "{broken")]
    [InlineData("opencode", """{"skills":[true],"mcp":{"servers":{}}}""")]
    public async Task Remote_FailedRegistration_StillConfiguresIndependentlyRequestedMcp(string clientId, string malformed)
    {
        using var context = new AgentConfigurationTestContext(outputHelper);
        var client = context.Clients.Single(client => string.Equals(client.Id, clientId, StringComparisons.AgentClientId));
        var integrationPath = client.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project);
        await AgentConfigurationTestContext.WriteAsync(integrationPath, malformed);
        var interaction = new TestInteractionService();
        var services = context.CreateServiceCollection(outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interaction;
        });
        using var provider = services.BuildServiceProvider();
 
        var clients = clientId == "opencode" ? "opencode,copilot" : clientId;
        var result = provider.GetRequiredService<RootCommand>().Parse(
            $"agent init --agents {clients} --scope project --skills aspire --mcp");
 
        Assert.Equal(CliExitCodes.InvalidCommand, await result.InvokeAsync().DefaultTimeout());
        Assert.Single(interaction.DisplayedErrors);
        var config = JsonNode.Parse(await File.ReadAllTextAsync(
            client.GetMcpConfigPath(AgentConfigurationScope.Project, context.Project)))!;
        if (clientId == "opencode")
        {
            Assert.True(File.Exists(context.Copilot.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project)));
            Assert.True(File.Exists(context.Copilot.GetMcpConfigPath(AgentConfigurationScope.Project, context.Project)));
            Assert.Equal([true], config["skills"]!.AsArray().Select(value => (bool)value!));
            var server = config["mcp"]!["servers"]!["aspire"]!;
            Assert.Equal(["aspire", "agent", "mcp"], server["command"]!.AsArray().Select(value => (string)value!));
            Assert.False((bool)server["disabled"]!);
            Assert.Equal(["servers"], config["mcp"]!.AsObject().Select(property => property.Key));
        }
        else
        {
            Assert.Equal(malformed, await File.ReadAllTextAsync(integrationPath));
            var server = config["mcpServers"]!["aspire"]!;
            Assert.Equal("aspire", (string?)server["command"]);
            Assert.Equal(["agent", "mcp"], server["args"]!.AsArray().Select(value => (string?)value));
        }
        Assert.Empty(context.Home.EnumerateFileSystemInfos());
    }
 
}