File: Agents\CommonAgentApplicatorsTests.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.Tests.Utils;
 
namespace Aspire.Cli.Tests.Agents;
 
public class CommonAgentApplicatorsTests(ITestOutputHelper outputHelper)
{
    private const string TestSkillRelativePath = ".github/skills/aspire/SKILL.md";
    private const string TestDescription = "Create Aspire skill file";
 
    [Fact]
    public void TryAddSkillFileApplicator_WhenNotYetAdded_AddsApplicatorAndReturnsTrue()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
 
        // Act
        var result = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
 
        // Assert
        Assert.True(result);
        Assert.True(context.HasSkillFileApplicator(TestSkillRelativePath));
        Assert.Single(context.Applicators);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_WhenAlreadyAdded_DoesNotAddAgainAndReturnsFalse()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        context.MarkSkillFileApplicatorAdded(TestSkillRelativePath);
 
        // Act
        var result = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
 
        // Assert
        Assert.False(result);
        Assert.Empty(context.Applicators);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_WhenSkillFileExistsWithSameContent_DoesNotAddApplicator()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        
        // Create the skill file with the SAME content as SkillFileContent
        var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, TestSkillRelativePath);
        var skillDirectory = Path.GetDirectoryName(skillFilePath)!;
        Directory.CreateDirectory(skillDirectory);
        File.WriteAllText(skillFilePath, CommonAgentApplicators.SkillFileContent);
 
        // Act
        var result = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
 
        // Assert - should not add applicator since skill file already exists with same content
        Assert.False(result);
        Assert.True(context.HasSkillFileApplicator(TestSkillRelativePath));
        Assert.Empty(context.Applicators);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_WhenSkillFileExistsWithDifferentContent_AddsUpdateApplicator()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        
        // Create the skill file with DIFFERENT content
        var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, TestSkillRelativePath);
        var skillDirectory = Path.GetDirectoryName(skillFilePath)!;
        Directory.CreateDirectory(skillDirectory);
        File.WriteAllText(skillFilePath, "# Old Aspire Skill\n\nThis is outdated content.");
 
        // Act
        var result = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
 
        // Assert - should add an update applicator since content differs
        Assert.True(result);
        Assert.True(context.HasSkillFileApplicator(TestSkillRelativePath));
        Assert.Single(context.Applicators);
        Assert.Contains("update", context.Applicators[0].Description);
    }
 
    [Fact]
    public async Task TryAddSkillFileApplicator_UpdateApplicator_ReplacesContent()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        
        // Create the skill file with old content
        var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, TestSkillRelativePath);
        var skillDirectory = Path.GetDirectoryName(skillFilePath)!;
        Directory.CreateDirectory(skillDirectory);
        var oldContent = "# Old Aspire Skill\n\nThis is outdated content.";
        File.WriteAllText(skillFilePath, oldContent);
 
        // Act
        CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
        await context.Applicators[0].ApplyAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert - content should be replaced with new content
        var newContent = await File.ReadAllTextAsync(skillFilePath);
        Assert.NotEqual(oldContent, newContent);
        Assert.Equal(CommonAgentApplicators.SkillFileContent, newContent);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_WhenSkillFileExistsWithDifferentLineEndings_DoesNotAddApplicator()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        
        // Create the skill file with CRLF line endings (Windows style)
        var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, TestSkillRelativePath);
        var skillDirectory = Path.GetDirectoryName(skillFilePath)!;
        Directory.CreateDirectory(skillDirectory);
        var contentWithCrlf = CommonAgentApplicators.SkillFileContent.ReplaceLineEndings("\r\n");
        File.WriteAllText(skillFilePath, contentWithCrlf);
 
        // Act
        var result = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
 
        // Assert - should not add applicator since content is the same (just different line endings)
        Assert.False(result);
        Assert.True(context.HasSkillFileApplicator(TestSkillRelativePath));
        Assert.Empty(context.Applicators);
    }
 
    [Fact]
    public async Task TryAddSkillFileApplicator_CreatesSkillFileWhenItDoesNotExist()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
 
        // Act
        CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            TestSkillRelativePath,
            TestDescription);
        await context.Applicators[0].ApplyAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert
        var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, TestSkillRelativePath);
        Assert.True(File.Exists(skillFilePath));
        var content = await File.ReadAllTextAsync(skillFilePath);
        Assert.Contains("# Aspire Skill", content);
        Assert.Contains("Running Aspire in agent environments", content);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_DifferentPaths_AddsBothApplicators()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        var path1 = ".github/skills/aspire/SKILL.md";
        var path2 = ".claude/skills/aspire/SKILL.md";
 
        // Act
        var result1 = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            path1,
            "Description 1");
        var result2 = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            path2,
            "Description 2");
 
        // Assert - both should be added since they are different paths
        Assert.True(result1);
        Assert.True(result2);
        Assert.Equal(2, context.Applicators.Count);
    }
 
    [Fact]
    public void TryAddSkillFileApplicator_SamePathTwice_OnlyAddsOnce()
    {
        // Arrange
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var context = CreateScanContext(workspace.WorkspaceRoot);
        var path = ".github/skills/aspire/SKILL.md";
 
        // Act - try to add the same path twice (simulating VS Code and Copilot CLI both trying to add)
        var result1 = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            path,
            "Description 1");
        var result2 = CommonAgentApplicators.TryAddSkillFileApplicator(
            context,
            workspace.WorkspaceRoot,
            path,
            "Description 2");
 
        // Assert - only the first should be added
        Assert.True(result1);
        Assert.False(result2);
        Assert.Single(context.Applicators);
    }
 
    private static AgentEnvironmentScanContext CreateScanContext(DirectoryInfo workingDirectory)
    {
        return new AgentEnvironmentScanContext
        {
            WorkingDirectory = workingDirectory,
            RepositoryRoot = workingDirectory
        };
    }
}