File: Agents\CopilotAgentEnvironmentScannerTests.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 Aspire.Cli.Agents;
using Aspire.Cli.Agents.Copilot;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
using Semver;
 
namespace Aspire.Cli.Tests.Agents;
 
public class CopilotAgentEnvironmentScannerTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(true, true)]
    [InlineData(false, false)]
    public async Task ScanAsync_ReturnsInstalledCopilotEnvironmentsWithoutCreatingConfiguration(
        bool cliInstalled, bool appInstalled)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var runner = new FakeCopilotCliRunner(cliInstalled ? new SemVersion(1, 0, 0) : null);
        var scanner = new CopilotAgentEnvironmentScanner(runner,
            new FakeCopilotAppInstallationDetector(appInstalled ? "AI_AGENT" : null),
            new TestEnvironment(), NullLogger<CopilotAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workspace.WorkspaceRoot,
            RepositoryRoot = workspace.WorkspaceRoot
        };
        var before = Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories);
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal((cliInstalled ? AgentEnvironment.CopilotCli : AgentEnvironment.None) |
            (appInstalled ? AgentEnvironment.CopilotApp : AgentEnvironment.None), environments);
        Assert.True(runner.WasCalled);
        Assert.Equal(before, Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ScanAsync_WhenInVsCode_SkipsCliProbeWithoutClaimingEditorDetection(bool appInstalled)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var runner = new FakeCopilotCliRunner(new SemVersion(1, 0, 0));
        var environment = new TestEnvironment(new Dictionary<string, string?> { ["TERM_PROGRAM"] = "vscode" });
        var scanner = new CopilotAgentEnvironmentScanner(runner,
            new FakeCopilotAppInstallationDetector(appInstalled ? "AI_AGENT" : null),
            environment, NullLogger<CopilotAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workspace.WorkspaceRoot,
            RepositoryRoot = workspace.WorkspaceRoot
        };
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(appInstalled ? AgentEnvironment.CopilotApp : AgentEnvironment.None, environments);
        Assert.False(runner.WasCalled);
    }
 
    [Theory]
    [InlineData("{ invalid json content")]
    [InlineData("")]
    [InlineData("""{"mcpServers":{"aspire":{"command":"custom-aspire"}}}""")]
    public async Task ScanAsync_WithMcpConfiguration_DetectsInstalledCliWithoutModifyingFile(string content)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var configDirectory = workspace.CreateDirectory(".copilot");
        var configPath = Path.Combine(configDirectory.FullName, "mcp-config.json");
        await File.WriteAllTextAsync(configPath, content);
        var scanner = new CopilotAgentEnvironmentScanner(new FakeCopilotCliRunner(new SemVersion(1, 0, 0)),
            new FakeCopilotAppInstallationDetector(), new TestEnvironment(),
            NullLogger<CopilotAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workspace.WorkspaceRoot,
            RepositoryRoot = workspace.WorkspaceRoot
        };
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(AgentEnvironment.CopilotCli, environments);
        Assert.Equal(content, await File.ReadAllTextAsync(configPath));
    }
 
    private sealed class FakeCopilotAppInstallationDetector(string? marker = null) : ICopilotAppInstallationDetector
    {
        public string? GetInstallationMarker() => marker;
    }
 
    private sealed class FakeCopilotCliRunner(SemVersion? version) : ICopilotCliRunner
    {
        public bool WasCalled { get; private set; }
 
        public Task<SemVersion?> GetVersionAsync(CancellationToken cancellationToken)
        {
            WasCalled = true;
 
            return Task.FromResult(version);
        }
    }
}