|
// 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.EndToEnd.Tests.Helpers;
using Aspire.Cli.Tests.Utils;
using Hex1b.Automation;
using Xunit;
namespace Aspire.Cli.EndToEnd.Tests;
/// <summary>
/// End-to-end tests for Aspire CLI agent commands, testing the new `aspire agent`
/// command structure and backward compatibility with `aspire mcp` commands.
/// </summary>
public sealed class AgentCommandTests(ITestOutputHelper output)
{
/// <summary>
/// Tests that all agent command help outputs are correct, including:
/// - aspire agent --help (shows subcommands: mcp, init)
/// - aspire agent mcp --help (shows MCP server description)
/// - aspire agent init --help (shows init description)
/// - aspire mcp --help (legacy, still works)
/// - aspire mcp start --help (legacy, still works)
/// </summary>
[Fact]
public async Task AgentCommands_AllHelpOutputs_AreCorrect()
{
var workspace = TemporaryWorkspace.Create(output);
var prNumber = CliE2ETestHelpers.GetRequiredPrNumber();
var commitSha = CliE2ETestHelpers.GetRequiredCommitSha();
var isCI = CliE2ETestHelpers.IsRunningInCI;
using var terminal = CliE2ETestHelpers.CreateTestTerminal();
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
// Patterns for aspire agent --help
var agentMcpSubcommand = new CellPatternSearcher().Find("mcp");
var agentInitSubcommand = new CellPatternSearcher().Find("init");
// Pattern for legacy aspire mcp --help (should still work)
var legacyMcpStart = new CellPatternSearcher().Find("start");
var legacyMcpInit = new CellPatternSearcher().Find("init");
var counter = new SequenceCounter();
var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();
sequenceBuilder.PrepareEnvironment(workspace, counter);
if (isCI)
{
sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter);
sequenceBuilder.SourceAspireCliEnvironment(counter);
sequenceBuilder.VerifyAspireCliVersion(commitSha, counter);
}
// Test 1: aspire agent --help
sequenceBuilder
.Type("aspire agent --help")
.Enter()
.WaitUntil(s =>
{
var hasMcp = agentMcpSubcommand.Search(s).Count > 0;
var hasInit = agentInitSubcommand.Search(s).Count > 0;
return hasMcp && hasInit;
}, TimeSpan.FromSeconds(30))
.WaitForSuccessPrompt(counter);
// Test 2: aspire agent mcp --help
// Using a more specific pattern that won't match later outputs
var mcpHelpPattern = new CellPatternSearcher().Find("aspire agent mcp [options]");
sequenceBuilder
.Type("aspire agent mcp --help")
.Enter()
.WaitUntil(s => mcpHelpPattern.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.WaitForSuccessPrompt(counter);
// Test 3: aspire agent init --help
var initHelpPattern = new CellPatternSearcher().Find("aspire agent init [options]");
sequenceBuilder
.Type("aspire agent init --help")
.Enter()
.WaitUntil(s => initHelpPattern.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.WaitForSuccessPrompt(counter);
// Test 4: aspire mcp --help (legacy, should still work)
sequenceBuilder
.Type("aspire mcp --help")
.Enter()
.WaitUntil(s =>
{
var hasStart = legacyMcpStart.Search(s).Count > 0;
var hasInit = legacyMcpInit.Search(s).Count > 0;
return hasStart && hasInit;
}, TimeSpan.FromSeconds(30))
.WaitForSuccessPrompt(counter);
// Test 5: aspire mcp start --help (legacy, should still work)
var legacyMcpStartPattern = new CellPatternSearcher().Find("aspire mcp start [options]");
sequenceBuilder
.Type("aspire mcp start --help")
.Enter()
.WaitUntil(s => legacyMcpStartPattern.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.WaitForSuccessPrompt(counter);
sequenceBuilder
.Type("exit")
.Enter();
var sequence = sequenceBuilder.Build();
await sequence.ApplyAsync(terminal, TestContext.Current.CancellationToken);
await pendingRun;
}
/// <summary>
/// Tests that deprecated MCP configs are detected and can be migrated
/// to the new agent mcp format during aspire agent init.
/// </summary>
[Fact]
public async Task AgentInitCommand_MigratesDeprecatedConfig()
{
var workspace = TemporaryWorkspace.Create(output);
var prNumber = CliE2ETestHelpers.GetRequiredPrNumber();
var commitSha = CliE2ETestHelpers.GetRequiredCommitSha();
var isCI = CliE2ETestHelpers.IsRunningInCI;
using var terminal = CliE2ETestHelpers.CreateTestTerminal();
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
// Use .mcp.json (Claude Code format) for simpler testing
// This is the same format used by the doctor test that passes
var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
// Patterns for agent init prompts - look for the colon at the end which indicates
// the prompt is ready for input
var workspacePathPrompt = new CellPatternSearcher().Find("workspace:");
// Patterns for deprecated config detection in agent init
var deprecatedPrompt = new CellPatternSearcher().Find("Update");
// Pattern to detect if no environments are found
var noEnvironmentsMessage = new CellPatternSearcher().Find("No agent environments");
var counter = new SequenceCounter();
var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();
sequenceBuilder.PrepareEnvironment(workspace, counter);
if (isCI)
{
sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter);
sequenceBuilder.SourceAspireCliEnvironment(counter);
sequenceBuilder.VerifyAspireCliVersion(commitSha, counter);
}
// Step 1: Create deprecated config file using Claude Code format (.mcp.json)
// This simulates a config that was created by an older version of the CLI
// Using single-line JSON to avoid any whitespace parsing issues
sequenceBuilder
.CreateDeprecatedMcpConfig(configPath);
// Verify the deprecated config was created
sequenceBuilder
.VerifyFileContains(configPath, "\"mcp\"")
.VerifyFileContains(configPath, "\"start\"");
// Debug: Show that the file exists and where we are
var fileExistsPattern = new CellPatternSearcher().Find(".mcp.json");
sequenceBuilder
.Type($"ls -la {configPath} && pwd")
.Enter()
.WaitUntil(s => fileExistsPattern.Search(s).Count > 0, TimeSpan.FromSeconds(10))
.WaitForSuccessPrompt(counter);
// Step 2: Run aspire agent init - should detect deprecated config
sequenceBuilder
.Type("aspire agent init")
.Enter()
.WaitUntil(s => workspacePathPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.Wait(500) // Small delay to ensure prompt is ready
.Enter() // Accept default workspace path
.WaitUntil(s =>
{
// Either we should see the deprecated config prompt, OR the "no environments" message
// This helps us diagnose whether the scanner is finding anything
var hasDeprecated = deprecatedPrompt.Search(s).Count > 0;
var hasNoEnv = noEnvironmentsMessage.Search(s).Count > 0;
return hasDeprecated || hasNoEnv;
}, TimeSpan.FromSeconds(60));
// Verify we got the deprecated prompt (not "no environments")
// This will show in the terminal capture if the test fails
sequenceBuilder
.Type(" ") // Space to select update
.Enter()
.WaitForSuccessPrompt(counter);
// Debug: Show the scanner log file to diagnose what the scanner found
var debugLogPath = Path.Combine(Path.GetTempPath(), "aspire-deprecated-scan.log");
var debugLogPattern = new CellPatternSearcher().Find("Scanning context");
sequenceBuilder
.Type($"cat {debugLogPath} 2>/dev/null || echo 'No debug log found'")
.Enter()
.WaitUntil(s => debugLogPattern.Search(s).Count > 0, TimeSpan.FromSeconds(10))
.WaitForSuccessPrompt(counter);
// Step 3: Verify config was updated to new format
// The updated config should contain "agent" and "mcp" but not "start"
sequenceBuilder
.VerifyFileContains(configPath, "\"agent\"")
.VerifyFileContains(configPath, "\"mcp\"")
.VerifyFileDoesNotContain(configPath, "\"start\"");
sequenceBuilder
.Type("exit")
.Enter();
var sequence = sequenceBuilder.Build();
await sequence.ApplyAsync(terminal, TestContext.Current.CancellationToken);
await pendingRun;
}
/// <summary>
/// Tests that aspire doctor warns about deprecated agent configs.
/// </summary>
[Fact]
public async Task DoctorCommand_DetectsDeprecatedAgentConfig()
{
var workspace = TemporaryWorkspace.Create(output);
var prNumber = CliE2ETestHelpers.GetRequiredPrNumber();
var commitSha = CliE2ETestHelpers.GetRequiredCommitSha();
var isCI = CliE2ETestHelpers.IsRunningInCI;
using var terminal = CliE2ETestHelpers.CreateTestTerminal();
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json");
// Pattern to detect deprecated config warning in doctor output
var deprecatedWarning = new CellPatternSearcher().Find("deprecated");
// Pattern to detect fix suggestion
var fixSuggestion = new CellPatternSearcher().Find("aspire agent init");
// Pattern to detect doctor completion
var doctorComplete = new CellPatternSearcher().Find("dev-certs");
var counter = new SequenceCounter();
var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();
sequenceBuilder.PrepareEnvironment(workspace, counter);
if (isCI)
{
sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter);
sequenceBuilder.SourceAspireCliEnvironment(counter);
sequenceBuilder.VerifyAspireCliVersion(commitSha, counter);
}
// Create deprecated config file
sequenceBuilder
.CreateDeprecatedMcpConfig(configPath)
.Type("aspire doctor")
.Enter()
.WaitUntil(s =>
{
var hasComplete = doctorComplete.Search(s).Count > 0;
var hasDeprecated = deprecatedWarning.Search(s).Count > 0;
var hasFix = fixSuggestion.Search(s).Count > 0;
return hasComplete && hasDeprecated && hasFix;
}, TimeSpan.FromSeconds(60))
.WaitForSuccessPrompt(counter);
sequenceBuilder
.Type("exit")
.Enter();
var sequence = sequenceBuilder.Build();
await sequence.ApplyAsync(terminal, TestContext.Current.CancellationToken);
await pendingRun;
}
/// <summary>
/// Tests that aspire agent init gracefully handles malformed JSON in MCP config files.
/// When a .vscode/mcp.json file contains invalid JSON, the command should:
/// - Display an error message identifying the malformed file
/// - Display a "Skipping" message
/// - NOT overwrite the malformed file
/// - Exit with a non-zero exit code
/// </summary>
[Fact]
public async Task AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero()
{
var workspace = TemporaryWorkspace.Create(output);
var prNumber = CliE2ETestHelpers.GetRequiredPrNumber();
var commitSha = CliE2ETestHelpers.GetRequiredCommitSha();
var isCI = CliE2ETestHelpers.IsRunningInCI;
using var terminal = CliE2ETestHelpers.CreateTestTerminal();
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
// Set up paths
var vscodePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".vscode");
var mcpConfigPath = Path.Combine(vscodePath, "mcp.json");
var malformedContent = "{ invalid json content";
// Patterns for agent init prompts
var workspacePathPrompt = new CellPatternSearcher().Find("workspace:");
// Pattern for the malformed JSON error message
var malformedError = new CellPatternSearcher().Find("malformed JSON");
// Pattern for the skip message
var skippingMessage = new CellPatternSearcher().Find("Skipping");
// Pattern for the partial success warning message
var completedWithErrors = new CellPatternSearcher().Find("completed with errors");
// Pattern for the agent environment selection prompt
var agentSelectPrompt = new CellPatternSearcher().Find("agent environments");
// Pattern for the additional options prompt that appears after agent environment selection
var additionalOptionsPrompt = new CellPatternSearcher().Find("additional options");
var counter = new SequenceCounter();
var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder();
sequenceBuilder.PrepareEnvironment(workspace, counter);
if (isCI)
{
sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter);
sequenceBuilder.SourceAspireCliEnvironment(counter);
sequenceBuilder.VerifyAspireCliVersion(commitSha, counter);
}
// Step 1: Create .vscode folder with malformed mcp.json
sequenceBuilder
.CreateVsCodeFolder(vscodePath)
.CreateMalformedMcpConfig(mcpConfigPath, malformedContent);
// Verify the malformed config was created
sequenceBuilder
.VerifyFileContains(mcpConfigPath, "invalid json");
// Step 2: Run aspire agent init
sequenceBuilder
.Type("aspire agent init")
.Enter()
.WaitUntil(s => workspacePathPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.Wait(500)
.Enter() // Accept default workspace path
.WaitUntil(s => agentSelectPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(60))
.Type(" ") // Select first option (VS Code)
.Enter()
// Handle the additional options prompt - must select at least one item
// (Spectre.Console MultiSelectionPrompt requires at least one selection)
// Select the first skill file option which is harmless (doesn't touch mcp.json)
.WaitUntil(s => additionalOptionsPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(30))
.Type(" ") // Select first additional option (skill file)
.Enter()
// After all prompts, wait for the error about malformed JSON and non-zero exit
.WaitUntil(s =>
{
var hasError = malformedError.Search(s).Count > 0;
var hasSkip = skippingMessage.Search(s).Count > 0;
var hasCompletedWithErrors = completedWithErrors.Search(s).Count > 0;
return hasError && hasSkip && hasCompletedWithErrors;
}, TimeSpan.FromSeconds(30))
.WaitForErrorPrompt(counter);
// Step 3: Verify the malformed file was NOT overwritten
sequenceBuilder
.VerifyFileContains(mcpConfigPath, "invalid json");
sequenceBuilder
.Type("exit")
.Enter();
var sequence = sequenceBuilder.Build();
await sequence.ApplyAsync(terminal, TestContext.Current.CancellationToken);
await pendingRun;
}
}
|