From 2fabe65fe9535caa6162efdcdcdc9c673422b76c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:12:55 +0000 Subject: [PATCH 1/2] feat: add fail-fast error detection to WaitUntilAsync lambdas in CLI E2E tests Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/b30cde2e-8639-49e0-9687-9db0f1466b2a Co-authored-by: radical <1472+radical@users.noreply.github.com> --- .../AgentCommandTests.cs | 36 ++++++++++++++----- .../Aspire.Cli.EndToEnd.Tests/BannerTests.cs | 29 +++++++++++---- .../CentralPackageManagementTests.cs | 5 +++ .../DoctorCommandTests.cs | 17 +++++++-- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs index 09a2155a8f0..fb14fc60a90 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs @@ -43,9 +43,15 @@ public async Task AgentCommands_AllHelpOutputs_AreCorrect() // Test 1: aspire agent --help await auto.TypeAsync("aspire agent --help"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText("mcp") && s.ContainsText("init"), - timeout: TimeSpan.FromSeconds(30), description: "agent help showing mcp and init subcommands"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire agent --help failed with an error"); + } + + return s.ContainsText("mcp") && s.ContainsText("init"); + }, timeout: TimeSpan.FromSeconds(30), description: "agent help showing mcp and init subcommands"); await auto.WaitForSuccessPromptAsync(counter); // Test 2: aspire agent mcp --help @@ -63,9 +69,15 @@ await auto.WaitUntilAsync( // Test 4: aspire mcp --help (now shows tools and call subcommands) await auto.TypeAsync("aspire mcp --help"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText("tools") && s.ContainsText("call"), - timeout: TimeSpan.FromSeconds(30), description: "mcp help showing tools and call subcommands"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire mcp --help failed with an error"); + } + + return s.ContainsText("tools") && s.ContainsText("call"); + }, timeout: TimeSpan.FromSeconds(30), description: "mcp help showing tools and call subcommands"); await auto.WaitForSuccessPromptAsync(counter); // Test 5: aspire mcp tools --help @@ -183,9 +195,15 @@ public async Task DoctorCommand_DetectsDeprecatedAgentConfig() File.WriteAllText(configPath, """{"mcpServers":{"aspire":{"command":"aspire","args":["mcp","start"]}}}"""); await auto.TypeAsync("aspire doctor"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText("dev-certs") && s.ContainsText("deprecated") && s.ContainsText("aspire agent init"), - timeout: TimeSpan.FromSeconds(60), description: "doctor output with deprecated warning and fix suggestion"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire doctor failed with an error"); + } + + return s.ContainsText("dev-certs") && s.ContainsText("deprecated") && s.ContainsText("aspire agent init"); + }, timeout: TimeSpan.FromSeconds(60), description: "doctor output with deprecated warning and fix suggestion"); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("exit"); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs index d8f3128413d..aa7c835f2a7 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs @@ -46,9 +46,15 @@ public async Task Banner_DisplayedOnFirstRun() await auto.ClearScreenAsync(counter); await auto.TypeAsync("aspire cache clear"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("Telemetry"), - timeout: TimeSpan.FromSeconds(30), description: "waiting for banner and telemetry notice on first run"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire cache clear failed with an error"); + } + + return s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("Telemetry"); + }, timeout: TimeSpan.FromSeconds(30), description: "waiting for banner and telemetry notice on first run"); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -78,9 +84,15 @@ public async Task Banner_DisplayedWithExplicitFlag() await auto.ClearScreenAsync(counter); await auto.TypeAsync("aspire --banner"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("CLI"), - timeout: TimeSpan.FromSeconds(30), description: "waiting for banner with version info"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire --banner failed with an error"); + } + + return s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("CLI"); + }, timeout: TimeSpan.FromSeconds(30), description: "waiting for banner with version info"); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -119,6 +131,11 @@ public async Task Banner_NotDisplayedWithNoLogoFlag() // before we check for the absence of the banner. await auto.WaitUntilAsync(s => { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire --nologo --help failed with an error"); + } + // Verify the banner does NOT appear if (s.ContainsText(RootCommandStrings.BannerWelcomeText)) { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs index 099501c4689..0c6f2ce7f98 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs @@ -184,6 +184,11 @@ await auto.WaitUntilAsync(s => return true; } + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire add failed with an error"); + } + var successPromptSearcher = new CellPatternSearcher() .FindPattern(counter.Value.ToString()) .RightText(" OK] $ "); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs index de658b1dd95..2eb768216b1 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs @@ -43,9 +43,15 @@ public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire doctor"); await auto.EnterAsync(); - await auto.WaitUntilAsync( - s => s.ContainsText("dev-certs") && s.ContainsText("partially trusted"), - timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with partial trust warning"); + await auto.WaitUntilAsync(s => + { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire doctor failed with an error"); + } + + return s.ContainsText("dev-certs") && s.ContainsText("partially trusted"); + }, timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with partial trust warning"); await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -84,6 +90,11 @@ public async Task DoctorCommand_WithSslCertDir_ShowsTrusted() await auto.EnterAsync(); await auto.WaitUntilAsync(s => { + if (s.ContainsText($"[{counter.Value} ERR:")) + { + throw new InvalidOperationException("aspire doctor failed with an error"); + } + // Wait for doctor to complete if (!s.ContainsText("dev-certs")) { From 4c1b5550f0bbcdfcd803269083213842280772bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:03:36 +0000 Subject: [PATCH 2/2] refactor: use WaitForSuccessPromptFailFastAsync instead of inline ERR throw in WaitUntilAsync lambdas Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/454b53f5-fae3-4211-84ae-fa23f729622a Co-authored-by: radical <1472+radical@users.noreply.github.com> --- .../AgentCommandTests.cs | 42 ++++++------------- .../Aspire.Cli.EndToEnd.Tests/BannerTests.cs | 35 ++++------------ .../CentralPackageManagementTests.cs | 7 +--- .../DoctorCommandTests.cs | 21 +++------- 4 files changed, 27 insertions(+), 78 deletions(-) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs index fb14fc60a90..182b42d3230 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs @@ -43,16 +43,10 @@ public async Task AgentCommands_AllHelpOutputs_AreCorrect() // Test 1: aspire agent --help await auto.TypeAsync("aspire agent --help"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire agent --help failed with an error"); - } - - return s.ContainsText("mcp") && s.ContainsText("init"); - }, timeout: TimeSpan.FromSeconds(30), description: "agent help showing mcp and init subcommands"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText("mcp") && s.ContainsText("init"), + timeout: TimeSpan.FromSeconds(30), description: "agent help showing mcp and init subcommands"); + await auto.WaitForSuccessPromptFailFastAsync(counter); // Test 2: aspire agent mcp --help await auto.TypeAsync("aspire agent mcp --help"); @@ -69,16 +63,10 @@ await auto.WaitUntilAsync(s => // Test 4: aspire mcp --help (now shows tools and call subcommands) await auto.TypeAsync("aspire mcp --help"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire mcp --help failed with an error"); - } - - return s.ContainsText("tools") && s.ContainsText("call"); - }, timeout: TimeSpan.FromSeconds(30), description: "mcp help showing tools and call subcommands"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText("tools") && s.ContainsText("call"), + timeout: TimeSpan.FromSeconds(30), description: "mcp help showing tools and call subcommands"); + await auto.WaitForSuccessPromptFailFastAsync(counter); // Test 5: aspire mcp tools --help await auto.TypeAsync("aspire mcp tools --help"); @@ -195,16 +183,10 @@ public async Task DoctorCommand_DetectsDeprecatedAgentConfig() File.WriteAllText(configPath, """{"mcpServers":{"aspire":{"command":"aspire","args":["mcp","start"]}}}"""); await auto.TypeAsync("aspire doctor"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire doctor failed with an error"); - } - - return s.ContainsText("dev-certs") && s.ContainsText("deprecated") && s.ContainsText("aspire agent init"); - }, timeout: TimeSpan.FromSeconds(60), description: "doctor output with deprecated warning and fix suggestion"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText("dev-certs") && s.ContainsText("deprecated") && s.ContainsText("aspire agent init"), + timeout: TimeSpan.FromSeconds(60), description: "doctor output with deprecated warning and fix suggestion"); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs index aa7c835f2a7..a5f459418f6 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs @@ -46,16 +46,10 @@ public async Task Banner_DisplayedOnFirstRun() await auto.ClearScreenAsync(counter); await auto.TypeAsync("aspire cache clear"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire cache clear failed with an error"); - } - - return s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("Telemetry"); - }, timeout: TimeSpan.FromSeconds(30), description: "waiting for banner and telemetry notice on first run"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("Telemetry"), + timeout: TimeSpan.FromSeconds(30), description: "waiting for banner and telemetry notice on first run"); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -84,16 +78,10 @@ public async Task Banner_DisplayedWithExplicitFlag() await auto.ClearScreenAsync(counter); await auto.TypeAsync("aspire --banner"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire --banner failed with an error"); - } - - return s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("CLI"); - }, timeout: TimeSpan.FromSeconds(30), description: "waiting for banner with version info"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("CLI"), + timeout: TimeSpan.FromSeconds(30), description: "waiting for banner with version info"); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -131,11 +119,6 @@ public async Task Banner_NotDisplayedWithNoLogoFlag() // before we check for the absence of the banner. await auto.WaitUntilAsync(s => { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire --nologo --help failed with an error"); - } - // Verify the banner does NOT appear if (s.ContainsText(RootCommandStrings.BannerWelcomeText)) { @@ -146,7 +129,7 @@ await auto.WaitUntilAsync(s => // Only return true once the help hint is visible at the end of the output return s.ContainsText(HelpGroupStrings.HelpHint); }, timeout: TimeSpan.FromSeconds(30), description: "waiting for help output to complete"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs index 0c6f2ce7f98..c656afaeeb4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs @@ -184,11 +184,6 @@ await auto.WaitUntilAsync(s => return true; } - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire add failed with an error"); - } - var successPromptSearcher = new CellPatternSearcher() .FindPattern(counter.Value.ToString()) .RightText(" OK] $ "); @@ -203,7 +198,7 @@ await auto.WaitUntilAsync(s => await auto.EnterAsync(); } - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitForSuccessPromptFailFastAsync(counter); // Verify the AppHost project does not end up with a version-pinned Redis PackageReference. { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs index 2eb768216b1..2450f6ccb88 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs @@ -43,16 +43,10 @@ public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire doctor"); await auto.EnterAsync(); - await auto.WaitUntilAsync(s => - { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire doctor failed with an error"); - } - - return s.ContainsText("dev-certs") && s.ContainsText("partially trusted"); - }, timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with partial trust warning"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitUntilAsync( + s => s.ContainsText("dev-certs") && s.ContainsText("partially trusted"), + timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with partial trust warning"); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync(); @@ -90,11 +84,6 @@ public async Task DoctorCommand_WithSslCertDir_ShowsTrusted() await auto.EnterAsync(); await auto.WaitUntilAsync(s => { - if (s.ContainsText($"[{counter.Value} ERR:")) - { - throw new InvalidOperationException("aspire doctor failed with an error"); - } - // Wait for doctor to complete if (!s.ContainsText("dev-certs")) { @@ -110,7 +99,7 @@ await auto.WaitUntilAsync(s => return s.ContainsText("certificate is trusted"); }, timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with trusted certificate"); - await auto.WaitForSuccessPromptAsync(counter); + await auto.WaitForSuccessPromptFailFastAsync(counter); await auto.TypeAsync("exit"); await auto.EnterAsync();