Skip to content

Fix Y/n input race in ChannelUpdateWorkflowTests + add CLI E2E troubleshooting doc - #17533

Merged
mitchdenny merged 2 commits into
mainfrom
mitchdenny/cli-e2e-yn-race-fixes
May 27, 2026
Merged

Fix Y/n input race in ChannelUpdateWorkflowTests + add CLI E2E troubleshooting doc#17533
mitchdenny merged 2 commits into
mainfrom
mitchdenny/cli-e2e-yn-race-fixes

Conversation

@mitchdenny

Copy link
Copy Markdown
Member

Description

ChannelUpdateWorkflowTests.UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel has been flaking with an assertion that reads aspire.config.json before aspire add has actually written it. The root cause is a TTY input race against Spectre.Console's [Y/n] prompt.

The Spectre [Y/n] confirmation is a single-character reader, not a line reader. When the test answered with TypeAsync("n") + EnterAsync(), both bytes went into the TTY input queue; the CLI returned on the n keystroke and tore down its stdin handler before \n was consumed. Bash then received the stray \n as an empty command line and bumped CMDCOUNT via PROMPT_COMMAND. The test's SequenceCounter didn't know about that extra cycle, so the next WaitForAspireAddSuccessAsync false-positively matched the [N OK] $ aspire add ... command-echo prompt header line that bash printed when accepting the command, returning success before aspire add actually wrote aspire.config.json via GuestAppHostProject.SaveConfiguration (which only runs after a full BuildAndGenerateSdkAsync round-trip).

The recording signature is unambiguous: a passing run goes straight from [Y/n]: n to [21 OK] $ aspire add ...; the failing run shows an extra empty [21 OK] $ cycle, then [22 OK] $ aspire add ....

The fix

Drop the trailing EnterAsync(). This matches the existing convention in Hex1bAutomatorTestHelpers.DeclineAgentInitPromptAsync, whose comment documents the same race. The fix is only needed when both a character and Enter are sent for a single-character prompt; EnterAsync()-alone (accept-default) and Type(...) + Enter against text-input or selection prompts are not affected because \n is the commit byte for those readers.

Troubleshooting doc

I also added .github/skills/cli-e2e-testing/troubleshooting.md with the diagnostic recipe so future agent sessions can recognize the pattern without re-deriving it:

  • How to download the failing attempt's artifact (CI reruns produce same-named artifacts and gh run download returns the latest, often a passing rerun; only gh api .../artifacts/<id>/zip filtered by created_at works).
  • A small Python snippet for reconstructing the terminal stream from .cast files.
  • The prompt-counter convention (PROMPT_COMMAND / CMDCOUNT / SequenceCounter lockstep).
  • The Y/n race symptom, recording signature, mechanism, and fix.
  • Why a tempting "wait for the prompt text to disappear" helper does not work (Spectre leaves answered prompts in scrollback, so the helper would time out).
  • Placeholder sections for other flake classes and CI artifact-infrastructure gotchas to flesh out as new flakes surface.

SKILL.md gets a one-line pointer to it.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No (the modified test is the regression target; the fix changes how it drives the CLI prompt, not the assertion)
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

…g.md

The Spectre.Console [Y/n] confirmation prompt is a single-character reader,
not a line reader. Sending TypeAsync("n") + EnterAsync() queues both bytes
in the TTY input buffer; when the CLI returns on "n" and tears down its
stdin handler before \n is consumed, bash receives the stray \n as an
empty command and bumps CMDCOUNT via PROMPT_COMMAND.

The test's SequenceCounter then ends up coincidentally agreeing with bash's
drifted CMDCOUNT. The next WaitForAspireAddSuccessAsync false-positives on
the typed-command prompt header line ([N OK] $ aspire add ...) that bash
printed when accepting the command, returning success before aspire add has
actually written aspire.config.json. The follow-on Assert reads the file
too early and fails.

This matches the existing convention in DeclineAgentInitPromptAsync which
documents the same race and avoids it by not sending Enter after the
single-character answer.

Also adds .github/skills/cli-e2e-testing/troubleshooting.md describing the
diagnostic recipe (right-artifact selection, cast reconstruction, prompt-
counter desync, Y/n race signature) so future agent sessions can recognize
the pattern, and references it from SKILL.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17533

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17533"

@mitchdenny
mitchdenny marked this pull request as ready for review May 27, 2026 09:42
Copilot AI review requested due to automatic review settings May 27, 2026 09:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a flaky CLI E2E test by removing a stray EnterAsync() after a single-character [Y/n] prompt response, which was causing a TTY input race that desynced the shell's CMDCOUNT from the test's SequenceCounter. Also adds a troubleshooting guide documenting the diagnostic recipe.

Changes:

  • Drop trailing EnterAsync() after TypeAsync("n") in PreviewStableUpdateAndDeclineAsync, matching the established convention in DeclineAgentInitPromptAsync.
  • Add a new .github/skills/cli-e2e-testing/troubleshooting.md cataloging the Y/n race and related flake patterns.
  • Link the troubleshooting guide from SKILL.md.
Show a summary per file
File Description
tests/Aspire.Cli.EndToEnd.Tests/ChannelUpdateWorkflowTests.cs Removes the racy EnterAsync() and adds an explanatory comment.
.github/skills/cli-e2e-testing/troubleshooting.md New diagnostic guide for CLI E2E flakes.
.github/skills/cli-e2e-testing/SKILL.md Adds pointer to the new troubleshooting guide.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@mitchdenny mitchdenny self-assigned this May 27, 2026
@mitchdenny mitchdenny added this to the 13.4 milestone May 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 107 passed, 0 failed, 2 unknown (commit 35613a0)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated ▶️ View recording
AllPublishMethodsBuildDockerImages ▶️ View recording
AspireAddAndStartWorkAgainstLegacyAppHostTs ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireStartUpdatesStaleTypeScriptAppHostPath ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManagerToDiffer ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoListStepsShowsPipelineSteps ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
IngressWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
JavaScriptHostingApisRunFromTypeScriptAppHost ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopJavaPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopTypeScriptPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScriptSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel ▶️ View recording

📹 Recordings uploaded automatically from CI run #26507778122

@mitchdenny
mitchdenny requested a review from davidfowl May 27, 2026 13:45
@davidfowl

Copy link
Copy Markdown
Contributor

This problem only exists for this one test?

@mitchdenny
mitchdenny merged commit b156c8b into main May 27, 2026
311 checks passed
@mitchdenny
mitchdenny deleted the mitchdenny/cli-e2e-yn-race-fixes branch May 27, 2026 14:17
@mitchdenny

Copy link
Copy Markdown
Member Author

/backport to release/13.4

@microsoft-github-policy-service microsoft-github-policy-service Bot modified the milestones: 13.4, 13.5 May 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/13.4 (link to workflow run)

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

docs_optional → build_or_ci_only

No triggered signals (signal_count == 0); only_test_or_build_changes advisory is true.

All 3 changed files fall under the build_or_ci_only allowlist (.github/ and tests/ paths):

  • tests/.../ChannelUpdateWorkflowTests.cs — test-only flake fix
  • .github/skills/cli-e2e-testing/troubleshooting.md — internal agent skill content
  • .github/skills/cli-e2e-testing/SKILL.md — internal agent skill pointer

None of these paths ship to microsoft/aspire.dev. No user-facing documentation update is required.

@github-actions github-actions Bot locked and limited conversation to collaborators Jun 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants