File: Agents\OpenCodeAgentEnvironmentScannerTests.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.OpenCode;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
using Semver;
 
namespace Aspire.Cli.Tests.Agents;
 
public class OpenCodeAgentEnvironmentScannerTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task ApplyAsync_WithMalformedOpenCodeJsonc_ThrowsInvalidOperationException()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
 
        // Create a malformed opencode.jsonc at the workspace root
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc");
        await File.WriteAllTextAsync(configPath, "{ invalid json content");
 
        var openCodeCliRunner = new FakeOpenCodeCliRunner(new SemVersion(1, 0, 0));
        var scanner = new OpenCodeAgentEnvironmentScanner(openCodeCliRunner, NullLogger<OpenCodeAgentEnvironmentScanner>.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(configPath, ex.Message);
        Assert.Contains("malformed JSON", ex.Message);
    }
 
    [Fact]
    public async Task ApplyAsync_WithEmptyOpenCodeJsonc_ThrowsInvalidOperationException()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
 
        // Create an empty opencode.jsonc
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc");
        await File.WriteAllTextAsync(configPath, "");
 
        var openCodeCliRunner = new FakeOpenCodeCliRunner(new SemVersion(1, 0, 0));
        var scanner = new OpenCodeAgentEnvironmentScanner(openCodeCliRunner, NullLogger<OpenCodeAgentEnvironmentScanner>.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(configPath, ex.Message);
    }
 
    [Fact]
    public async Task ApplyAsync_WithMalformedOpenCodeJsonc_DoesNotOverwriteFile()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
 
        // Create a malformed opencode.jsonc with content the user may want to preserve
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "opencode.jsonc");
        var originalContent = "{ \"mcp\": { \"my-server\": { \"command\": [\"test\"] } }";
        await File.WriteAllTextAsync(configPath, originalContent);
 
        var openCodeCliRunner = new FakeOpenCodeCliRunner(new SemVersion(1, 0, 0));
        var scanner = new OpenCodeAgentEnvironmentScanner(openCodeCliRunner, NullLogger<OpenCodeAgentEnvironmentScanner>.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(configPath);
        Assert.Equal(originalContent, currentContent);
    }
 
    private static AgentEnvironmentScanContext CreateScanContext(
        DirectoryInfo workingDirectory)
    {
        return new AgentEnvironmentScanContext
        {
            WorkingDirectory = workingDirectory,
            RepositoryRoot = workingDirectory
        };
    }
 
    private sealed class FakeOpenCodeCliRunner(SemVersion? version) : IOpenCodeCliRunner
    {
        public Task<SemVersion?> GetVersionAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(version);
        }
    }
}