Skip to content

Eliminate Unknown CLI E2E test results in PR comment#17484

Merged
mitchdenny merged 1 commit into
mainfrom
mitchdenny/investigate-unknown-cli-e2e-results
May 26, 2026
Merged

Eliminate Unknown CLI E2E test results in PR comment#17484
mitchdenny merged 1 commit into
mainfrom
mitchdenny/investigate-unknown-cli-e2e-results

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

@mitchdenny mitchdenny commented May 26, 2026

Eliminate Unknown CLI E2E test results

Across recent PRs (#17474, #17463, #17249, …) the recording-comment bot consistently tagged the same five tests as Unknown (❔):

❓ CLI E2E Tests unknown — 96 passed, 0 failed, 5 unknown
Test (from cast filename) Shape
AgentMcpListStructuredLogsFromStarterAppCore private *Core helper called by 3 public [Fact]s
DashboardRunWithAgentMcpCore private *Core helper called by 2 public [Fact]s
DashboardRunWithOtelTracesReturnsNoTracesCore private *Core helper called by 2 public [Fact]s
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive [Theory] with InlineData("Test.sln") / ("Test.slnx")
(5th, same pattern)

Root causes (two independent bugs)

Bug 1 — [CallerMemberName] captures the helper, not the public test

CliE2ETestHelpers.CreateTestTerminal / CreateDockerTestTerminal / CreatePodmanDockerTestTerminal (and the shared Hex1bTestHelpers.CreateTestTerminal) name the .cast file using [CallerMemberName]. When a [Fact] is a thin wrapper delegating into a private helper:

[Fact]
public Task DashboardRunWithAgentMcpListTracesReturnsNoTraces()
    => DashboardRunWithAgentMcpCore(...);

private async Task DashboardRunWithAgentMcpCore(...) {
    using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(...); // CallerMemberName = "DashboardRunWithAgentMcpCore"
}

… the cast file ends up named after the helper. The TRX has no entry for DashboardRunWithAgentMcpCore, so the workflow's jq -r '.[$name] // "Unknown"' falls through to Unknown.

Bug 2 — jq bare_method splits on . inside theory parameters

The workflow's bare_method runs in the wrong order:

def bare_method(name):
  (name | split(".") | last) | sub("\\(.*$"; "");

For ...Hive(solutionFileName: "Test.sln"), split(".") chops inside the quoted param and last returns sln"), not the method name. Verified locally:

$ echo '...Test.sln")"... ' | jq '<old bare_method>'
{ "sln\")": "Passed" }

Hits every theory whose parameters contain . (URLs, file extensions, version strings).

Fixes in this PR

# Fix Pushed?
1 CreateTestTerminal family + shared helper: prefer TestContext.Current?.TestCase?.TestMethodName over [CallerMemberName], fall back when no test context is active. ✅ commit 9dfee9a2c
2 cli-e2e-recording-comment.yml: swap order in bare_method (strip params first, then split on .) ⏳ patch below — needs workflow scope
3 cli-e2e-recording-comment.yml: emit a ::warning:: annotation listing any unmatched recordings so future regressions show up in the workflow log itself ⏳ same patch

Both Bug 1 and Bug 2 are needed to clear all five Unknowns: Bug 1 fixes the *Core helpers (4 tests), Bug 2 fixes the dotted-theory case (AspireInit…ChannelHive).

Workflow fix patch (apply with workflow scope)

Copilot's OAuth token doesn't have workflow scope, so the workflow change has to be applied by a maintainer. Save the block below to workflow-fix.patch and run git am workflow-fix.patch, or just edit the YAML by hand:

Patch for .github/workflows/cli-e2e-recording-comment.yml
From 8ed659e806273845ddee8d3aec6849cdf77d0695 Mon Sep 17 00:00:00 2001
From: Mitch Denny <midenn@microsoft.com>
Date: Tue, 26 May 2026 13:03:02 +1000
Subject: [PATCH] Fix Unknown CLI E2E results from jq splitting theory params
 on '.'
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolves bug #2 of the two root causes that left CLI E2E recordings
tagged as Unknown in the PR recording-comment.

The 'bare_method' jq function used to match TRX testNames against .cast
recording filenames split on '.' before stripping the theory parameter
suffix:

    def bare_method(name):
      (name | split(".") | last) | sub("\\(.*$"; "");

For a TRX testName like
    Aspire.Cli.EndToEnd.Tests.CSharpProjectModeInitTests.AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive(solutionFileName: "Test.sln")

split(".") chops inside the quoted parameter and 'last' returns
'sln")', not the method name. So the bare-name key in the lookup map
becomes 'sln")' / 'slnx")' instead of the actual test method name,
and the .cast file matches neither — Unknown. This is the same pattern
for any [Theory] parameter that contains a '.' (URLs, file extensions,
version strings).

Fix: strip the '(<params>)' suffix first, then split on '.'.

Also adds a defensive ::warning:: annotation when any .cast file fails
to match a TRX outcome, so a future regression of the matching logic is
visible in the recording-comment workflow's own log instead of only
showing up as a degraded count in the PR comment that few people read
the workflow logs for.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../workflows/cli-e2e-recording-comment.yml   | 25 ++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/cli-e2e-recording-comment.yml b/.github/workflows/cli-e2e-recording-comment.yml
index 5c76728b0..9abd81e2c 100644
--- a/.github/workflows/cli-e2e-recording-comment.yml
+++ b/.github/workflows/cli-e2e-recording-comment.yml
@@ -195,8 +195,15 @@ jobs:
                   # parameter data stripped) so a .cast file named after the
                   # CallerMemberName matches a TRX entry like
                   # "Namespace.Class.Method(toolchain: "pnpm")".
+                  #
+                  # IMPORTANT: strip the "(<params>)" suffix BEFORE splitting on
+                  # ".". Theory parameters frequently contain "." (URLs, file
+                  # extensions like "Test.sln", version strings). If split runs
+                  # first, `last` returns garbage like `sln")` instead of the
+                  # actual method name and the test reports as "Unknown" on every
+                  # PR. See https://github.com/microsoft/aspire/pull/17484.
                   def bare_method(name):
-                    (name | split(".") | last) | sub("\\(.*$"; "");
+                    (name | sub("\\(.*$"; "")) | split(".") | last;
                   def fqn_no_params(name):
                     name | sub("\\(.*$"; "");
                   def merge(map; key; outcome):
@@ -276,6 +283,7 @@ jobs:
             # Arrays to track failed test recordings separately
             FAILED_TESTS_BODY=""
             TABLE_BODY=""
+            UNKNOWN_FILENAMES=""
 
             for castfile in "$RECORDINGS_DIR"/*.cast; do
               if [ -f "$castfile" ]; then
@@ -307,6 +315,12 @@ jobs:
                   STATUS_EMOJI="❔"
                   LINK_LABEL="❔ ▶️ View recording"
                   TEST_UNKNOWN_COUNT=$((TEST_UNKNOWN_COUNT + 1))
+                  # Track the filename so we can emit a workflow warning below.
+                  # "Unknown" almost always means the recording name didn't match any TRX
+                  # entry (e.g. recording named after a private helper rather than the
+                  # public [Fact]). Surfacing it here makes the regression obvious in the
+                  # workflow logs instead of only in the PR comment.
+                  UNKNOWN_FILENAMES="${UNKNOWN_FILENAMES} ${safe_filename}"
                 fi
 
                 # Upload to asciinema with retry logic for transient failures
@@ -351,6 +365,15 @@ jobs:
 
             echo "Uploaded $UPLOAD_COUNT recordings, $FAIL_COUNT upload failures, $TEST_PASS_COUNT passed, $TEST_FAIL_COUNT failed, $TEST_UNKNOWN_COUNT unknown"
 
+            # Emit a workflow annotation for unmatched recordings so the regression
+            # surfaces in the GitHub Actions UI of this workflow run, not only in the
+            # eventual PR comment. The PR comment summarises by count, which is easy
+            # to overlook; a `::warning::` annotation is hard to miss when triaging
+            # the recording-comment workflow itself.
+            if [ "$TEST_UNKNOWN_COUNT" -gt 0 ]; then
+              echo "::warning title=CLI E2E recordings without a matching TRX outcome::${TEST_UNKNOWN_COUNT} recording(s) could not be matched to a test result and will be tagged 'Unknown' in the PR comment.${UNKNOWN_FILENAMES} See https://github.com/microsoft/aspire/blob/main/.github/workflows/cli-e2e-recording-comment.yml for the matching logic."
+            fi
+
             # Build the summary line in the same style as the deployment E2E comment:
             # "<emoji> **CLI E2E Tests <status>** — X passed, Y failed[, Z unknown]"
             # Status reflects test outcomes; recording-upload failures are a secondary concern
-- 
2.50.1 (Apple Git-155)

Verification

  • Both Aspire.Cli.EndToEnd.Tests and Aspire.Deployment.EndToEnd.Tests build clean against the C# changes.

  • The new jq bare_method was tested locally against a synthetic TRX containing the failing theory shape and a normal test:

    {
      "AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive": "Failed",
      "SimpleSmokeTest": "Passed"
    }
    

    (Verified that "Failed" wins over "Passed" across theory rows via the existing merge rule.)

Out of scope

One .cast file per theory case: today [Theory] invocations share a single recording path and the last case wins. Worth doing eventually but orthogonal to making the matching correct.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

🚀 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 -- 17484

Or

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

Resolves bug #1 of the two root causes that left CLI E2E recordings
tagged as Unknown in the PR recording-comment.

CliE2ETestHelpers.CreateTestTerminal / CreateDockerTestTerminal /
CreatePodmanDockerTestTerminal (and the shared
Hex1bTestHelpers.CreateTestTerminal) use [CallerMemberName] to pick the
.cast filename. When a public [Fact] is a thin wrapper that delegates
into a private helper — e.g. DashboardRunWithAgentMcpListTracesReturnsNoTraces
=> DashboardRunWithAgentMcpCore in DashboardRunTests, or the *Core
helper in AgentMcpLogsTests — [CallerMemberName] captures the helper.
The .cast file ends up named after the helper, the TRX has no entry
for that name, and the recording-comment workflow's lookup falls
through to Unknown on every PR.

Fix: prefer TestContext.Current?.TestCase?.TestMethodName when running
inside a live xUnit test context, fall back to the [CallerMemberName]
default otherwise. The public API surface is unchanged (no caller
passes testName explicitly), so the recording filenames quietly flip
from 'DashboardRunWithAgentMcpCore' to the public test name with no
test-side edits required.

The companion workflow-side fix for the second root cause (jq splitting
on '.' inside theory parameters before stripping the param suffix) ships
in a follow-up commit on the same PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny force-pushed the mitchdenny/investigate-unknown-cli-e2e-results branch from 9b3f6af to 9dfee9a Compare May 26, 2026 03:03
@mitchdenny mitchdenny changed the title [Investigation] Eliminate Unknown CLI E2E test results in PR comment Eliminate Unknown CLI E2E test results in PR comment May 26, 2026
@mitchdenny mitchdenny marked this pull request as ready for review May 26, 2026 04:34
Copilot AI review requested due to automatic review settings May 26, 2026 04:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes the C# side of two bugs that caused 5 CLI E2E tests to be tagged "Unknown" in the recording-comment PR bot. The recording-comment workflow joins .cast filenames to TRX test outcomes by method name, but when a [Fact] delegates into a private helper, [CallerMemberName] captures the helper's name and no TRX entry matches. This PR makes the recording helpers prefer xUnit's reported TestContext.Current.TestCase.TestMethodName over the caller-member-name fallback so recordings are always named after the public test.

Changes:

  • Add Hex1bTestHelpers.ResolveTestMethodName that prefers Xunit.TestContext.Current?.TestCase?.TestMethodName and falls back to the [CallerMemberName] value.
  • Apply the resolved name in Hex1bTestHelpers.CreateTestTerminal and in all three CLI E2E factories (CreateTestTerminal, CreateDockerTestTerminal, CreatePodmanDockerTestTerminal).
  • Bug #2 (jq bare_method in cli-e2e-recording-comment.yml) is described in the PR body as a patch to be applied by a maintainer and is not part of the pushed diff.
Show a summary per file
File Description
tests/Shared/Hex1bTestHelpers.cs Adds ResolveTestMethodName and uses it in the shared CreateTestTerminal so deployment E2E gets the fix too.
tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs Resolves the xUnit test method name in all three CLI terminal factories so .cast files are named after the public [Fact]/[Theory] rather than a *Core helper.

Copilot's findings

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

@mitchdenny mitchdenny enabled auto-merge (squash) May 26, 2026 04:46
@mitchdenny mitchdenny merged commit 38d3b34 into main May 26, 2026
617 of 620 checks passed
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.4 milestone May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 105 passed, 0 failed, 1 unknown (commit 9dfee9a)

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
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_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_AugmentsExistingViteRepoAtRoot ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
JavaScriptHostingApisRunFromTypeScriptAppHost ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogLevelTrace_ProducesTraceEntriesInCliLogFile ▶️ 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_FailsWhenInteractionServiceIsRequired ▶️ 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_TypeScript_PicksUpStablePackages ▶️ View recording

📹 Recordings uploaded automatically from CI run #26429822821

@aspire-repo-bot
Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

docs_required → already documented by name (false-positive signal)

Triggered signals (1): pr_body_has_cli_flag_mention.

The signal fired because the PR body contains diff --git a/.github/workflows/cli-e2e-recording-comment.yml (a git diff patch header). The --git substring matched the --something long-form CLI flag pattern. No new Aspire user-facing CLI flag was introduced by this PR.

All changed files are internal test helpers (only_test_or_build_changes: true):

  • tests/Shared/Hex1bTestHelpers.cs — adds ResolveTestMethodName preferring TestContext.Current?.TestCase?.TestMethodName over [CallerMemberName]
  • tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs — applies the resolved name in CLI E2E terminal factories

This PR fixes how .cast recording files are named in CI E2E tests so the recording-comment bot correctly matches recordings to TRX test outcomes. It has no user-facing impact and no microsoft/aspire.dev documentation is warranted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants