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 Microsoft.AspNetCore.InternalTesting;
using Aspire.Cli.Agents;
using Aspire.Cli.Agents.ClaudeCode;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
using Semver;
 
namespace Aspire.Cli.Tests.Agents;
 
public class ClaudeCodeAgentEnvironmentScannerTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task ApplyAsync_WithMalformedMcpJson_ThrowsInvalidOperationException()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        workspace.CreateDirectory(".claude");
 
        // Create a malformed .mcp.json at the workspace root
        var mcpJsonPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
        await File.WriteAllTextAsync(mcpJsonPath, "{ invalid json content");
 
        var claudeCodeCliRunner = new FakeClaudeCodeCliRunner(new SemVersion(1, 0, 0));
        var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(claudeCodeCliRunner, executionContext, NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = CreateScanContext(workspace.WorkspaceRoot);
 
        await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        // The scan should succeed (HasServerConfigured catches JsonException)
        Assert.NotEmpty(context.Applicators);
        var aspireApplicator = context.Applicators.First(a => a.Description.Contains("Aspire MCP"));
 
        // Applying should throw with a descriptive message
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => aspireApplicator.ApplyAsync(CancellationToken.None)).DefaultTimeout();
        Assert.Contains(mcpJsonPath, ex.Message);
        Assert.Contains("malformed JSON", ex.Message);
    }
 
    [Fact]
    public async Task ApplyAsync_WithEmptyMcpJson_ThrowsInvalidOperationException()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        workspace.CreateDirectory(".claude");
 
        // Create an empty .mcp.json
        var mcpJsonPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
        await File.WriteAllTextAsync(mcpJsonPath, "");
 
        var claudeCodeCliRunner = new FakeClaudeCodeCliRunner(new SemVersion(1, 0, 0));
        var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(claudeCodeCliRunner, executionContext, NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = CreateScanContext(workspace.WorkspaceRoot);
 
        await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.NotEmpty(context.Applicators);
        var aspireApplicator = context.Applicators.First(a => a.Description.Contains("Aspire MCP"));
 
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => aspireApplicator.ApplyAsync(CancellationToken.None)).DefaultTimeout();
        Assert.Contains(mcpJsonPath, ex.Message);
    }
 
    [Fact]
    public async Task ApplyAsync_WithMalformedMcpJson_DoesNotOverwriteFile()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        workspace.CreateDirectory(".claude");
 
        // Create a malformed .mcp.json with content the user may want to preserve
        var mcpJsonPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
        var originalContent = "{ \"mcpServers\": { \"my-server\": { \"command\": \"test\" } }";
        await File.WriteAllTextAsync(mcpJsonPath, originalContent);
 
        var claudeCodeCliRunner = new FakeClaudeCodeCliRunner(new SemVersion(1, 0, 0));
        var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
        var scanner = new ClaudeCodeAgentEnvironmentScanner(claudeCodeCliRunner, executionContext, NullLogger<ClaudeCodeAgentEnvironmentScanner>.Instance);
        var context = CreateScanContext(workspace.WorkspaceRoot);
 
        await scanner.ScanAsync(context, CancellationToken.None).DefaultTimeout();
 
        Assert.NotEmpty(context.Applicators);
        var aspireApplicator = context.Applicators.First(a => a.Description.Contains("Aspire MCP"));
 
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => aspireApplicator.ApplyAsync(CancellationToken.None)).DefaultTimeout();
 
        // The original file content should be preserved
        var currentContent = await File.ReadAllTextAsync(mcpJsonPath);
        Assert.Equal(originalContent, currentContent);
    }
 
    private static AgentEnvironmentScanContext CreateScanContext(
        DirectoryInfo workingDirectory)
    {
        return new AgentEnvironmentScanContext
        {
            WorkingDirectory = workingDirectory,
            RepositoryRoot = workingDirectory
        };
    }
 
    private static CliExecutionContext CreateExecutionContext(DirectoryInfo workingDirectory)
    {
        return new CliExecutionContext(
            workingDirectory: workingDirectory,
            hivesDirectory: workingDirectory,
            cacheDirectory: workingDirectory,
            sdksDirectory: workingDirectory,
            logsDirectory: workingDirectory,
            logFilePath: "test.log",
            debugMode: false,
            environmentVariables: new Dictionary<string, string?>(),
            homeDirectory: workingDirectory);
    }
 
    private sealed class FakeClaudeCodeCliRunner(SemVersion? version) : IClaudeCodeCliRunner
    {
        public Task<SemVersion?> GetVersionAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(version);
        }
    }
}