File: Agents\ClaudeCodeAgentEnvironmentScannerTests.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.ClaudeCode;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
using Semver;
 
namespace Aspire.Cli.Tests.Agents;
 
public class ClaudeCodeAgentEnvironmentScannerTests(ITestOutputHelper outputHelper)
{
    [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 configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
        await File.WriteAllTextAsync(configPath, content);
        var runner = new FakeClaudeCodeCliRunner(new SemVersion(1, 0, 0));
        var scanner = new ClaudeCodeAgentEnvironmentScanner(runner,
            TestExecutionContextHelper.CreateExecutionContext(workspace.WorkspaceRoot),
            NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workspace.WorkspaceRoot,
            RepositoryRoot = workspace.WorkspaceRoot
        };
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(AgentEnvironment.ClaudeCode, environments);
        Assert.Equal(content, await File.ReadAllTextAsync(configPath));
    }
 
    [Theory]
    [InlineData(".", false)]
    [InlineData("subdir", false)]
    [InlineData(".", true)]
    [InlineData("subdir", true)]
    public async Task ScanAsync_WithProjectDirectory_DetectsClaudeWithoutCallingRunner(string relativeWorkingDirectory, bool trailingRootSeparator)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        workspace.CreateDirectory(".claude");
        var workingDirectory = relativeWorkingDirectory == "." ? workspace.WorkspaceRoot : workspace.CreateDirectory(relativeWorkingDirectory);
        var homeDirectory = workspace.CreateDirectory("fake-home");
        var runner = new FakeClaudeCodeCliRunner(null);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(runner,
            TestExecutionContextHelper.CreateExecutionContext(workingDirectory, homeDirectory: homeDirectory),
            NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workingDirectory,
            RepositoryRoot = trailingRootSeparator
                ? new DirectoryInfo(workspace.WorkspaceRoot.FullName + Path.DirectorySeparatorChar)
                : workspace.WorkspaceRoot
        };
        var before = Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories);
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(AgentEnvironment.ClaudeCode, environments);
        Assert.False(runner.WasCalled);
        Assert.Equal(before, Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories));
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ScanAsync_IgnoresHomeAndDirectoriesAboveRepositoryRoot(bool directoryIsHome)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        workspace.CreateDirectory(".claude");
        var childDirectory = workspace.CreateDirectory("subdir");
        var homeDirectory = directoryIsHome ? workspace.WorkspaceRoot : workspace.CreateDirectory("fake-home");
        var runner = new FakeClaudeCodeCliRunner(null);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(runner,
            TestExecutionContextHelper.CreateExecutionContext(childDirectory, homeDirectory: homeDirectory),
            NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = childDirectory,
            RepositoryRoot = directoryIsHome ? workspace.WorkspaceRoot : childDirectory
        };
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(AgentEnvironment.None, environments);
        Assert.True(runner.WasCalled);
    }
 
    [Theory]
    [InlineData(".", false)]
    [InlineData(".", true)]
    [InlineData("sibling", false)]
    [InlineData("sibling", true)]
    [InlineData("selected-sibling", false)]
    [InlineData("selected-sibling", true)]
    public async Task ScanAsync_WithWorkingDirectoryOutsideRoot_UsesSelectedWorkspaceRoot(string relativeWorkingDirectory, bool rootHasProjectDirectory)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var selectedRoot = workspace.CreateDirectory("selected");
        var workingDirectory = relativeWorkingDirectory == "." ? workspace.WorkspaceRoot : workspace.CreateDirectory(relativeWorkingDirectory);
        Directory.CreateDirectory(Path.Combine(workingDirectory.FullName, ".claude"));
        if (rootHasProjectDirectory)
        {
            Directory.CreateDirectory(Path.Combine(selectedRoot.FullName, ".claude"));
        }
        var homeDirectory = workspace.CreateDirectory("fake-home");
        var runner = new FakeClaudeCodeCliRunner(null);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(runner,
            TestExecutionContextHelper.CreateExecutionContext(workingDirectory, homeDirectory: homeDirectory),
            NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = new AgentEnvironmentScanContext
        {
            WorkingDirectory = workingDirectory,
            RepositoryRoot = selectedRoot
        };
        var before = Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories);
 
        var environments = await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(rootHasProjectDirectory ? AgentEnvironment.ClaudeCode : AgentEnvironment.None, environments);
        Assert.Equal(!rootHasProjectDirectory, runner.WasCalled);
        Assert.Equal(before, Directory.GetFileSystemEntries(workspace.WorkspaceRoot.FullName, "*", SearchOption.AllDirectories));
    }
 
    private sealed class FakeClaudeCodeCliRunner(SemVersion? version) : IClaudeCodeCliRunner
    {
        public bool WasCalled { get; private set; }
 
        public Task<SemVersion?> GetVersionAsync(CancellationToken cancellationToken)
        {
            WasCalled = true;
 
            return Task.FromResult(version);
        }
    }
}