|
// 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);
}
}
}
|