|
// 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;
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;
var recordingPath = CliE2ETestHelpers.GetTestResultsRecordingPath(
nameof(AgentCommands_AllHelpOutputs_AreCorrect));
var builder = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithAsciinemaRecording(recordingPath)
.WithPtyProcess("/bin/bash", ["--norc"]);
using var terminal = builder.Build();
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;
var recordingPath = CliE2ETestHelpers.GetTestResultsRecordingPath(
nameof(AgentInitCommand_MigratesDeprecatedConfig));
var builder = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithAsciinemaRecording(recordingPath)
.WithPtyProcess("/bin/bash", ["--norc"]);
using var terminal = builder.Build();
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;
var recordingPath = CliE2ETestHelpers.GetTestResultsRecordingPath(
nameof(DoctorCommand_DetectsDeprecatedAgentConfig));
var builder = Hex1bTerminal.CreateBuilder()
.WithHeadless()
.WithAsciinemaRecording(recordingPath)
.WithPtyProcess("/bin/bash", ["--norc"]);
using var terminal = builder.Build();
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;
}
}
|