Skip to content

Add aspire installs command; drop install table from aspire doctor#17461

Open
radical wants to merge 6 commits into
microsoft:mainfrom
radical:radical/installs-discovery
Open

Add aspire installs command; drop install table from aspire doctor#17461
radical wants to merge 6 commits into
microsoft:mainfrom
radical:radical/installs-discovery

Conversation

@radical
Copy link
Copy Markdown
Member

@radical radical commented May 25, 2026

Description

Aspire CLI install enumeration lived inside aspire doctor alongside the
environment checks. That mixed two unrelated concerns into one command, and
the install table couldn't be inspected without running the full
prerequisite check pass each time.

A new aspire installs command surface owns CLI install enumeration.
aspire doctor now reports environment checks only.

User-facing usage

List every discovered CLI install plus any orphan package hives:

aspire installs list

The vertical, color-aware layout shows each install's Status, Channel,
binary Path, Hive path, and the reason if a row is in a non-ok state.

For tooling, --format json emits the contract documented in
docs/specs/cli-output-formats.md:

aspire installs list --format json
[
  {
    "id": "stable",
    "kind": "homebrew",
    "channel": "stable",
    "path": "/opt/homebrew/Caskroom/aspire/13.2.0/aspire",
    "status": "active",
    "managedBy": "homebrew"
  },
  {
    "id": "pr-17400",
    "kind": "orphan-hive",
    "channel": "pr-17400",
    "hive": "/home/user/.aspire/hives/pr-17400",
    "status": "no install found",
    "statusReason": "No discovered install reports this hive's channel."
  }
]

Changes

The diff is large (75 files, ~1700 net) but most of it is mechanical. By area:

  • aspire installs command (new)src/Aspire.Cli/Commands/InstallsCommand.cs is new (~330 lines). The hidden --self --format json peer-probe endpoint is documented as an internal cross-version contract in docs/specs/cli-output-formats.md. Peer-probe wiring moves from aspire doctor --self to aspire installs --self.
  • aspire doctor shrinks to env checks — install-table rendering removed from DoctorCommand.cs; DoctorCommand.cs no longer takes the --self flag. DoctorCommandStrings.{resx,Designer.cs} and all 13 .xlf translations are deleted (~900 lines net deletion).
  • Bulk rename: routesource — ~30 files. Code, tests, installer scripts (get-aspire-cli{,-pr}.{sh,ps1}, eng/homebrew/aspire.rb.template, localhive.{sh,ps1}), and docs (docs/specs/install-routes.mdinstall-sources.md). On-disk wire strings are unchangedscript / pr / winget / brew / dotnet-tool / localhive all stay literal. Only C# identifiers (e.g. InstallSource.BrewInstallSource.Homebrew), filenames, and prose were renamed. Sidecar field name remains source (it always was).
  • Winget probe hoist (internal cleanup)WingetFirstRunProbe.Run invocation moved from BundleService + InstallsCommand.ListCommand into the CliExecutionContext DI factory in Program.cs. Same behavior, single call site; InstallsCommand and BundleService are now installer-agnostic.
  • Tests
    • InstallsCommandTests covers the new command surface.
    • WindowsRegistryReaderTests (Windows-only) round-trips against the real HKCU\…\Uninstall hive with Microsoft.Aspire.Tests.<guid>-prefixed entries that are deleted in a using and swept by a static initializer on each test run.
    • WingetStartupProbeTests pins the new DI-factory invocation contract and the swallow-on-failure behavior.
  • Path lookup fixPathLookupHelper on Windows preserves on-disk executable casing after PATHEXT resolution (so aspire.exe does not render as aspire.EXE just because PATHEXT contains .EXE).

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
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 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 -- 17461

Or

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

@radical

This comment was marked as outdated.

radical and others added 3 commits May 25, 2026 18:27
Move CLI install enumeration and per-install metadata reporting out of
`aspire doctor` into a new `aspire installs` command surface, and align
the install-metadata vocabulary on `source` everywhere so the CLI,
sidecar spec, installer scripts, and documentation use the same term.

`aspire installs list` reports every discovered Aspire CLI install and
every orphan hive in a vertical, color-aware layout. `--format json`
emits the same rows as a stable contract documented in
`docs/specs/cli-output-formats.md`.

`aspire installs --self --format json` replaces the hidden `aspire
doctor --self` endpoint that install discovery uses to cross-check a
peer install's reported metadata. `aspire doctor` no longer ships an
install table or `--self` flag; it now reports environment checks only.
Install discovery still discovers peers, but the peer probe now invokes
`aspire installs --self`.

The sidecar spec moves to `docs/specs/install-sources.md`; the
`InstallSidecarReader`, `InstallationInfo`, scripts, homebrew template,
localhive helpers, and tests all read and write `source` (formerly
`route`). Homebrew installs are tagged with the explicit `homebrew`
source value in the sidecar and user-facing output.

The WinGet first-run install sidecar probe now runs before `aspire
installs list` instead of `aspire doctor`, so PR users still get a fresh
sidecar populated on the first invocation after `winget install
Microsoft.Aspire`.

On Windows, the shared `PathLookupHelper` preserves the executable
casing recorded on disk after PATHEXT resolution, so `aspire.exe` does
not render as `aspire.EXE` just because PATHEXT contains `.EXE`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The install-source sidecar's `source` field is the wire-level schema
value: the release script, PR installer, dotnet-tool packaging, and the
Homebrew cask all write the literal `brew` string (matching the cask's
`postflight` block and the actual Homebrew CLI binary name).
`BundleService.ComputeDefaultExtractDir`, `ParseInstallSource`, and the
installer test theory rows consume the same wire value.

The user-facing surfaces in `aspire installs list` — `GetInstallKind`,
`GetCleanupHint`, `GetManagedBy`, and the matching test expectations —
render the friendlier `homebrew` / `Homebrew` label because that's what
users recognise.

Match on `"brew"` at the wire layer, translate to `"homebrew"` at the
display layer. The schema stays minimal, the cask sidecar stays a
literal one-liner, and users still see the conventional spelling.

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

WingetFirstRunProbe is the writer that lets a winget portable install identify
itself as such on first run — winget has no install hook, so the running CLI
self-stamps `.aspire-install.json` after consulting Windows ARP. It was being
invoked from two unrelated consumer sites:

  - InstallsCommand.ListCommand.ExecuteAsync, before discovery reads the sidecar
  - BundleService.GetBundleExtractDirForCurrentProcess, before the extract dir
    is computed from the sidecar source

Each consumer paid the same try/catch cost and each leaked installer-specific
knowledge into a layer that should be installer-agnostic ("commands shouldn't
depend on any specific installer"). Worse, the factory that builds
CliExecutionContext also reads the sidecar (via GetUsersAspirePath ->
CliPathHelper.GetAspireHomeDirectory) but ran *before* either consumer fired
the probe — so a fresh winget install on its first invocation derived its
Aspire home with the sidecar still unstamped.

Hoist the invocation into the CliExecutionContext DI singleton factory in
Program.cs, immediately before BuildCliExecutionContext. The factory body now
runs the probe once per CLI invocation, before any downstream code reads the
sidecar. BundleService drops its optional WingetFirstRunProbe? parameter and
InstallsCommand drops its ctor parameter, field, and explicit call site.

Tests:

  - WindowsRegistryReaderTests (new, 7 tests, Windows-only) exercises the real
    HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall hive: matching
    entry returns true; wrong identifier / missing target / different target /
    surrounded by noise all return false; path comparison is case-insensitive;
    empty processPath returns false. Each test creates a uniquely-named
    `Microsoft.Aspire.Tests.<guid>` subkey and deletes it in a using; a static
    sweeper removes leftovers from any crashed prior run before tests start.

  - WingetStartupProbeTests (new, 2 tests) pins the new TryRunWingetFirstRunProbe
    contract: it resolves the probe from DI and invokes Run with the binary
    directory of Environment.ProcessPath, and any IWindowsRegistryReader
    exception is swallowed so CLI startup is never broken by a probe failure.
    TryRunWingetFirstRunProbe is internal for testing; the factory's use of it
    remains a one-line check at code review.

Net: -54/+50 production lines; +279 test lines; all existing tests still pass
(1366/1366 in the Acquisition + Commands + Bundles surface on macOS).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@radical radical force-pushed the radical/installs-discovery branch from 53d1e36 to 102af0d Compare May 25, 2026 22:29
@radical radical marked this pull request as ready for review May 25, 2026 22:41
Copilot AI review requested due to automatic review settings May 25, 2026 22:41
@radical radical requested a review from joperezr May 25, 2026 22:42
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

This PR splits Aspire CLI installation enumeration out of aspire doctor into a dedicated aspire installs command, leaving doctor focused on environment checks only. It also completes a broad terminology rename from “install route” to “install source” across CLI code, scripts, tests, and docs.

Changes:

  • Add new aspire installs command (including hidden --self --format json peer-probe surface) and supporting hive enumeration.
  • Remove installation-table output and --self support from aspire doctor, updating JSON contracts and resource strings accordingly.
  • Hoist WinGet first-run sidecar stamping into CLI startup (CliExecutionContext factory) and fix Windows PATH casing preservation in PathLookupHelper.

Reviewed changes

Copilot reviewed 74 out of 75 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/PathLookupHelperTests.cs Adds Windows-only regression test for preserving filesystem casing when resolving PATHEXT hits.
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Registers InstallsCommand and HiveEnumerator in the CLI test DI container.
tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs Renames route→source helpers/tests for Aspire home directory derivation.
tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs Updates cross-reference to renamed macOS firmlink test name.
tests/Aspire.Cli.Tests/Configuration/DotNetBasedAppHostServerChannelResolutionTests.cs Updates terminology in test doc comment (route→source).
tests/Aspire.Cli.Tests/Commands/SetupCommandTests.cs Renames route→source in test names/comments for setup default path behavior.
tests/Aspire.Cli.Tests/Commands/InstallsCommandTests.cs New unit tests covering aspire installs (list output and hidden --self JSON).
tests/Aspire.Cli.Tests/Commands/DoctorCommandTests.cs Updates doctor tests to assert installations are no longer included and --self is rejected.
tests/Aspire.Cli.Tests/BundleServiceComputeDefaultExtractDirTests.cs Updates comments/terminology for bundle extraction layout selection (route→source).
tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossSourceExtractionTests.cs Renames cross-route extraction matrix test to cross-source and updates homebrew naming.
tests/Aspire.Cli.Tests/Acquisition/WingetStartupProbeTests.cs New tests pinning startup-time WinGet sidecar probe behavior and failure swallowing.
tests/Aspire.Cli.Tests/Acquisition/WindowsRegistryReaderTests.cs New Windows-only round-trip tests against HKCU Uninstall for WinGet detection.
tests/Aspire.Cli.Tests/Acquisition/PeerInstallProbeTests.cs Updates peer probe to call installs --self --format json and accept bare-array JSON shape.
tests/Aspire.Cli.Tests/Acquisition/InstallSidecarReaderTests.cs Updates spec reference and enum expectations (brewHomebrew) plus “future-source” wording.
tests/Aspire.Cli.Tests/Acquisition/InstallationDiscoveryDiscoverAllTests.cs Updates expectations/messages and property names (route→source) for discovery behavior.
tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs Updates user-facing verifier assertions to “install-source sidecar” wording.
tests/Aspire.Acquisition.Tests/Scripts/ReleaseScriptShellTests.cs Updates dry-run sidecar messaging assertions (route→source).
tests/Aspire.Acquisition.Tests/Scripts/ReleaseScriptPowerShellTests.cs Updates -WhatIf sidecar messaging assertions (route→source).
tests/Aspire.Acquisition.Tests/Scripts/PRScriptToolModeTests.cs Updates tool-mode assertions to ensure no “source sidecar” messaging appears.
tests/Aspire.Acquisition.Tests/Scripts/PRScriptShellTests.cs Updates PR-script dry-run tests and sidecar messaging (route→source).
tests/Aspire.Acquisition.Tests/Scripts/PRScriptPowerShellTests.cs Updates PR-script WhatIf tests and sidecar messaging (route→source).
tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallerModeTests.cs Updates installer-mode expectations and wording (“Homebrew” command naming).
tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallE2ETests.cs Updates E2E assertion wording for the sidecar presence (route→source).
tests/Aspire.Acquisition.Tests/Scripts/LocalHiveScriptFunctionTests.cs Adds guard test ensuring localhive scripts don’t reference removed aspire info.
tests/Aspire.Acquisition.Tests/Scripts/Common/FakeArchiveHelper.cs Updates spec reference in archive helper docs (install-routes→install-sources).
src/Shared/PathLookupHelper.cs Returns filesystem-cased executable paths on Windows instead of inheriting PATHEXT casing.
src/Aspire.Cli/Utils/EnvironmentChecker/EnvironmentCheckResult.cs Removes installations from doctor JSON response model.
src/Aspire.Cli/Utils/CliPathHelper.cs Renames route→source for Aspire home directory selection helper.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf Removes installation-table-related localized resources from doctor XLF.
src/Aspire.Cli/Resources/DoctorCommandStrings.resx Removes installation-table string resources now that doctor no longer renders installs.
src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs Removes generated resource accessors for deleted installation-table strings.
src/Aspire.Cli/Program.cs Runs WinGet first-run probe during CLI startup and registers InstallsCommand/HiveEnumerator.
src/Aspire.Cli/JsonSourceGenerationContext.cs Adds source-gen support for InstallationInfo[] and List<InstallListItem>.
src/Aspire.Cli/Commands/SetupCommand.cs Updates comments for source-aware vs source-independent extract/install path behavior.
src/Aspire.Cli/Commands/RootCommand.cs Wires InstallsCommand into the CLI root command.
src/Aspire.Cli/Commands/InstallsCommand.cs Implements new aspire installs command and JSON/list outputs including orphan hives.
src/Aspire.Cli/Commands/InstallationInfoOutput.cs Removes doctor-oriented discovery output; keeps safe self-description helper for installs self-probe.
src/Aspire.Cli/Commands/DoctorCommand.cs Removes install enumeration/self-probe and limits output to environment checks.
src/Aspire.Cli/CliExecutionContext.cs Updates docs/comments to reflect source-specific Aspire home selection behavior.
src/Aspire.Cli/Bundles/BundleService.cs Removes WinGet probe call from bundle extraction path and updates spec reference.
src/Aspire.Cli/Acquisition/WingetFirstRunProbe.cs Updates documentation wording to “install-source sidecar”.
src/Aspire.Cli/Acquisition/PeerInstallProbe.cs Switches peer self-describe call to installs --self --format json and supports new JSON shape.
src/Aspire.Cli/Acquisition/IPeerInstallProbe.cs Updates contract docs for the new peer-probe invocation surface.
src/Aspire.Cli/Acquisition/InstallSource.cs Renames enum member to Homebrew while keeping the wire string as brew.
src/Aspire.Cli/Acquisition/InstallSidecarReader.cs Updates comments/spec reference to install-sources.
src/Aspire.Cli/Acquisition/InstallationInfo.cs Renames JSON field routesource and updates contract docs to installs surfaces.
src/Aspire.Cli/Acquisition/InstallationDiscovery.cs Renames/threads Source through discovery and updates messaging to install-source terminology.
src/Aspire.Cli/Acquisition/InstallationCandidateSources.cs Updates comment wording for installs discovery.
src/Aspire.Cli/Acquisition/IInstallSidecarReader.cs Updates sidecar reader docs to install-source terminology and removes obsolete command references.
src/Aspire.Cli/Acquisition/IInstallationDiscovery.cs Updates discovery interface docs for aspire installs and new peer-probe surface.
src/Aspire.Cli/Acquisition/HiveEnumerator.cs Adds enumeration of hives used by aspire installs list to show orphan hives.
localhive.sh Updates sidecar stamping comments/spec reference to install-sources terminology.
localhive.ps1 Updates sidecar stamping and archive validation messaging to install-sources terminology.
eng/scripts/verify-cli-tool-nupkg.ps1 Updates comment wording around sidecar source expectations.
eng/scripts/verify-cli-archive.ps1 Updates archive sidecar validation messaging to install-sources terminology.
eng/scripts/get-aspire-cli.sh Updates installer sidecar messaging to “source sidecar” and spec reference.
eng/scripts/get-aspire-cli.ps1 Updates installer sidecar messaging to “source sidecar” and spec reference.
eng/scripts/get-aspire-cli-pr.sh Renames PR sidecar writer function and updates comments/messages to install-sources terminology.
eng/scripts/get-aspire-cli-pr.ps1 Renames PR sidecar writer function and updates WhatIf messaging to “source sidecar”.
eng/clipack/Common.projitems Updates build-time guard/error text to “install sources” terminology and new spec reference.
docs/specs/install-sources.md Renames/updates the sidecar specification from install-routes to install-sources.
docs/specs/cli-output-formats.md Documents aspire installs list --format json and clarifies hidden installs --self contract.
docs/dogfooding-pull-requests.md Updates wording around Homebrew uninstall behavior and shared prefixes (route→source).
Files not reviewed (1)
  • src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs: Language not supported

Comment thread src/Aspire.Cli/Acquisition/HiveEnumerator.cs Outdated
`HiveEnumerator.HasHive` and `GetHivePath` previously fed the channel
string straight into `Path.Combine(<hivesRoot>, channel)` and
`new DirectoryInfo(...)`. The channel value reaches this surface via
peer `installs --self` JSON, which is read from `InstallationInfo.
FromJsonElement` without any shape validation. A peer reporting `".."`,
an absolute path, or a string containing invalid path characters could
therefore make `aspire installs list` display a path outside the hives
root, or throw `ArgumentException` from the `DirectoryInfo` constructor.

Run the channel through `IdentityChannelReader.IsValidChannel` — the
same allow-list the build pipeline uses on the producing side
(`stable`, `staging`, `daily`, `local`, `pr-<digits>`) — and treat any
non-matching value as "no hive". `GetHivePath` now returns `string?`
to match; the only caller already short-circuits via `HasHive` so the
nullable return is sink-compatible.

`GetHives()` is intentionally left untouched: it enumerates whatever
directories exist under the hives root so an orphan hive from an older
install script or a future channel name still surfaces in `installs
list`.
@radical radical added this to the 13.4 milestone May 26, 2026
Copy link
Copy Markdown
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

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

Reviewed the install-discovery split. Three related findings, all on InstallsCommand.cs — the status field in the new installs list --format json contract is currently overloaded for non-Ok rows, which both complicates programmatic consumption and forces a couple of magic-string duplications.

Comment thread src/Aspire.Cli/Commands/InstallsCommand.cs Outdated
Comment thread src/Aspire.Cli/Commands/InstallsCommand.cs Outdated
Comment thread src/Aspire.Cli/Commands/InstallsCommand.cs
@mitchdenny
Copy link
Copy Markdown
Member

PR Testing Report

PR Information

CLI Version Verification

  • Expected Commit: f863564d6
  • Installed Version: 13.4.0-pr.17461.gf863564d
  • Status: ✅ Verified

Test Scenarios Executed

Scenario 1: aspire installs list (human output)

Objective: Verify the new vertical layout renders all fields for the running install.
Coverage: Happy path — ✅ Passed

Output:

pr-17461  pr
  Status   notOnPath
  Channel  pr-17461
  Path     <testDir>/dogfood/pr-17461/bin/aspire
  Hive     <testDir>/hives/pr-17461

Scenario 2: aspire installs list --format json

Objective: Verify JSON contract matches docs/specs/cli-output-formats.md.
Coverage: Happy path — ✅ Passed

[
  {
    "id": "pr-17461",
    "kind": "pr",
    "channel": "pr-17461",
    "path": ".../dogfood/pr-17461/bin/aspire",
    "hive": ".../hives/pr-17461",
    "status": "notOnPath"
  }
]

All documented fields (id, kind, channel, path, hive, status) present and match the spec. managedBy correctly omitted for PR-source installs.


Scenario 3: aspire installs --self --format json (hidden peer-probe endpoint)

Objective: Exercise the contract PeerInstallProbe now invokes against peer binaries.
Coverage: Happy path — ✅ Passed

[
  {
    "path": ".../dogfood/pr-17461/bin/aspire",
    "canonicalPath": ".../dogfood/pr-17461/bin/aspire",
    "version": "13.4.0-pr.17461.gf863564d",
    "channel": "pr-17461",
    "source": "pr",
    "pathStatus": "notOnPath",
    "status": "ok"
  }
]

Wire field is correctly named source (renamed from route in this PR). Array shape ([ … ]) matches the documented contract and what InstallationInfoParser expects.


Scenario 4: aspire doctor reduced surface

Objective: Verify no install table; env checks only.
Coverage: Happy path — ✅ Passed

Sections rendered: Aspire, .NET SDK, Container Runtime, Environment. No installations section. Exit code 1 reflects the missing .NET SDK on the test host (expected, unrelated to PR).

4b. aspire doctor --format json also verified: response contains only checks and summary keys — the previously-present installations array is gone, matching the EnvironmentCheckResult model change.


Scenario 5 (Unhappy): aspire doctor --self

Objective: --self was removed; verify clean rejection.
Coverage: Negative — ✅ Passed

Unrecognized command or argument '--self'.

Followed by usage. Exit code 1. Behavior is correct: older peers probing this binary with the legacy doctor --self invocation will get a non-zero exit and PeerInstallProbe will correctly fall back to --version.


Scenario 6 (Unhappy): aspire installs (no subcommand, no --self)

Objective: Verify help display.
Coverage: Negative — ✅ Passed (minor nit)

Help text displayed correctly, listing the list subcommand. Exit code 1. Most CLIs return 0 for help-only invocations; if intentional this is fine, otherwise consider returning CommandResult.Success() after DisplayHelp. This is a minor UX nit, not blocking.


Summary

# Scenario Status
1 installs list (human)
2 installs list --format json
3 installs --self --format json (peer probe)
4 doctor (env-only)
4b doctor --format json (no installations key)
5 doctor --self rejected
6 installs help fallback

Overall Result

✅ PR VERIFIED

The new aspire installs command works, the peer-probe contract emits the renamed source field in the expected array shape, and aspire doctor has been correctly stripped of install enumeration.

Observations

  • Could not exercise a non-Ok install row in this happy-install environment, so the status: "failed: <reason>" concatenation flagged in the code review remains untested at runtime.
  • aspire installs (bare) returns exit code 1 when displaying help — minor UX nit (see Scenario 6).

Notes on coverage gaps

  • Windows-only behaviors not exercised: the PathLookupHelper PATHEXT casing fix and the WingetFirstRunProbe startup hoist are Windows-specific. Verified covered by the new unit tests (WindowsRegistryReaderTests, WingetStartupProbeTests, PathLookupHelperTests) but not end-to-end on Windows here.
  • Orphan hive listing not exercised because no extra hives exist in the isolated test $HOME. Covered by InstallsCommandTests.

`GetInstallStatus` was concatenating `$"{install.Status}: {install.StatusReason}"`
for non-Ok rows, so the JSON `status` field became `"failed: <reason>"` and the
human "list" output printed the reason twice — once in the `Status` line, once
in the `Reason` line — because `InstallListItem.StatusReason` is already wired
to `install.StatusReason` separately.

Keep `status` enum-shaped (`active|shadowed|notOnPath|failed|notProbed|no install found`)
so programmatic consumers can `switch` on it without splitting on `": "`, and let
`statusReason` carry the human-readable message alone. The human-format `Reason`
field already renders only when `StatusReason` is non-empty, so the on-screen
duplication disappears for free once `status` stops embedding the reason.

Also extract the literal `"no install found"` into `OrphanHiveStatus` const so
the orphan-row constructor and the `GetSortRank` switch share a single source
of truth — otherwise a rename of one without the other would silently demote
orphan rows out of their last-sorted bucket into the `_ => 3` fallback.

Update `docs/specs/cli-output-formats.md` to drop the `failed: <reason>` /
`notProbed: <reason>` shapes from the documented `status` set, call out that
`statusReason` is where the message lives, and add a failed-row example.
@radical
Copy link
Copy Markdown
Member Author

radical commented May 26, 2026

Thanks for the thorough PR-test pass @mitchdenny. All three inline comments addressed in aa0d308:

  • GetInstallStatus no longer concatenates failed: <reason>status stays enum-shaped, message rides on statusReason alone. Spec updated, failed-row example added, new test guards both human and JSON surfaces against regression.
  • "no install found" extracted to OrphanHiveStatus const, shared between the row constructor and the sort-rank switch.
  • Human duplication auto-resolved once status stops embedding the reason.

On Scenario 6 (bare aspire installs returning exit 1 when displaying help): I checked and that's the codebase convention — CommandResult.DisplayHelp() is hard-wired to CliExitCodes.InvalidCommand (src/Aspire.Cli/Commands/CommandResult.cs:40), and aspire config (also a ParentCommand-style subcommand-with-no-action) does the same. Changing it just for installs would diverge from config. Happy to take it up as a separate, repo-wide change if you think the convention itself should flip.

CI green, ready for re-review.

@radical
Copy link
Copy Markdown
Member Author

radical commented May 26, 2026

PR Testing Report

PR Information

  • PR Number: #17461
  • Title: Add aspire installs command; drop install table from aspire doctor
  • Head Commit: 8bf409353f74e6eb14630e62bcc65fddf9c3a8f4
  • Tested At: 2026-05-26 (local, macOS arm64)

CLI Version Verification

  • Expected commit: 8bf40935
  • Installed binary reports: 13.4.0-pr.17461.g8bf40935
  • Status: ✅ Verified — short SHA matches.

Install path: <testDir>/dogfood/pr-17461/bin/aspire
Hive path: <testDir>/hives/pr-17461/packages

Changes Analyzed (per PR body)

  • New aspire installs command (with hidden --self --format json peer-probe).
  • aspire doctor shrinks to env checks; loses --self; install-table strings/xlf deleted.
  • Bulk rename routesource across acquisition layer, scripts, and docs (on-disk wire strings unchanged).
  • WingetFirstRunProbe hoisted into CliExecutionContext DI factory.
  • Windows casing fix in PathLookupHelper (not exercised on macOS).

Test Scenarios Executed

S1: Install + version verification — ✅ Passed

  • get-aspire-cli-pr.sh ... 17461 --install-path <tmp> --skip-path --skip-extension succeeded.
  • Binary at <testDir>/dogfood/pr-17461/bin/aspire reports 13.4.0-pr.17461.g8bf40935, matches PR head.

S2: aspire installs list (default text) — ✅ Passed

Vertical layout renders exactly as advertised; PR-17461 install appears with Status / Channel / Path / Hive rows.
Example row from <testDir>/evidence/s2-installs-list.txt:

pr-17461  pr
  Status   notOnPath
  Channel  pr-17461
  Path     .../dogfood/pr-17461/bin/aspire
  Hive     .../hives/pr-17461

Note: a first-run interactive welcome animation rendered on the very first invocation. Setting ASPIRE_NON_INTERACTIVE=1 (matching the documented automation flag) suppressed it for all subsequent runs. This is pre-existing first-run behavior, not introduced by the PR.

S3: aspire installs list --format json — ✅ Passed

JSON contract matches docs/specs/cli-output-formats.md. Sample (full file in <testDir>/evidence/s3-installs-list-json.txt):

{
  "id": "pr-17461",
  "kind": "pr",
  "channel": "pr-17461",
  "path": ".../dogfood/pr-17461/bin/aspire",
  "hive": ".../hives/pr-17461",
  "status": "notOnPath"
}

managedBy is omitted for kind: pr (intentional — only present for homebrew / dotnet-tool rows in this run), matching the spec's "optional" convention.

S4: aspire doctor no longer shows install table — ✅ Passed

  • Output is env-only: Aspire / .NET SDK / Container Runtime / Environment sections + summary. No install enumeration.
  • aspire doctor --help confirms --self is gone from the options list.

S5: Hidden peer-probe aspire installs --self --format json — ✅ Passed

Emits the documented per-binary JSON shape with path, canonicalPath, version, channel, source, pathStatus, status:

{
  "path": ".../dogfood/pr-17461/bin/aspire",
  "canonicalPath": ".../dogfood/pr-17461/bin/aspire",
  "version": "13.4.0-pr.17461.g8bf40935",
  "channel": "pr-17461",
  "source": "pr",
  "pathStatus": "notOnPath",
  "status": "ok"
}

Wire field name source (not route) — matches the rename's stated contract.

S6: aspire new aspire-empty smoke test — ✅ Passed

Project created at <testDir>/PrSmoke using --source <pr-hive> and --version 13.4.0-pr.17461.g8bf40935. apphost.cs, aspire.config.json, apphost.run.json, nuget.config, .vscode/extensions.json all written. Renamed InstallSource plumbing did not regress the templating happy path.

U1 (unhappy): aspire doctor --self — ✅ Passed

Clean error: Unrecognized command or argument '--self'. followed by usage. No crash, no stack trace.

Caveat: The process exit code was 0. This is pre-existing System.CommandLine behavior in this repo (also reproduced in U2), not regression introduced by this PR — but worth flagging because scripts that previously branched on aspire doctor --self exit codes will continue to see success even though the flag is gone.

U2 (unhappy): aspire installs list --format bogus — ✅ Passed

Clean validation error with suggestions:

Cannot parse argument 'bogus' for option '--format' as expected type 'Aspire.Cli.Commands.InstallsOutputFormat'. Did you mean one of the following?
Json
List

Same pre-existing exit-0 quirk applies.

U3 (unhappy/boundary): orphan hive surfaces in installs list — ✅ Passed (after correcting hive location)

A first attempt placed the fake hive under ~/.aspire/hives/pr-99999999/ and did not surface. Reading CliPathHelper.GetAspireHomeDirectory revealed that for source: pr installs, the running CLI uses the install prefix (<testDir>) as its AspireHome, so the orphan must live in the install-local <testDir>/hives/. Once moved there, the orphan surfaced exactly as the PR description claims:

{
  "id": "pr-99999999",
  "kind": "orphan-hive",
  "channel": "pr-99999999",
  "hive": ".../hives/pr-99999999",
  "status": "no install found",
  "statusReason": "No discovered install reports this hive's channel."
}

This isn't a bug — it's documented isolation behavior — but it is a usability nuance: orphan-hive detection is per-AspireHome, so a user with both a homebrew install (uses ~/.aspire) and a PR install (uses its own prefix) would see different orphan rows depending on which binary they run. Worth a quick note in docs/specs/install-sources.md if not already covered.

Summary

Scenario Coverage Status
S1: Install + version verification Happy
S2: installs list (text) Happy
S3: installs list --format json Happy
S4: doctor (no install table, no --self) Happy
S5: installs --self --format json Happy
S6: aspire new aspire-empty smoke Happy
U1: doctor --self (flag removed) Unhappy
U2: installs list --format bogus Unhappy
U3: orphan hive surfaces Boundary

Overall Result

✅ PR VERIFIED — all 9 scenarios pass. The new aspire installs surface, the JSON contract, and the aspire doctor slim-down all behave exactly as the PR description states.

Observations / Recommendations

  • Exit codes for unknown args / invalid enum values are 0. Pre-existing System.CommandLine behavior, not introduced by this PR. Scripts that previously branched on aspire doctor --self failing won't observe a non-zero exit when they get migrated.
  • Orphan hive detection is AspireHome-scoped. A PR install can't see orphan hives in ~/.aspire/hives/, and vice-versa. The PR description's example assumes a single AspireHome; consider one sentence in docs/specs/install-sources.md (if not already) calling out that orphan rows are per-AspireHome.
  • First-run welcome animation interleaves with installs list output until ASPIRE_NON_INTERACTIVE=1 is set. Existing first-run UX; not a PR-introduced issue.

Evidence

Logs saved under <testDir>/evidence/:

  • s2-installs-list.txt, s2b-installs-help.txt, s2c-installs-list-help.txt
  • s3-installs-list-json.txt
  • s4-doctor.txt, s4b-doctor-help.txt
  • s5-installs-self-json.txt
  • s6-new.txt
  • u1-doctor-self.txt, u2-installs-bogus-format.txt
  • u3-orphan-hive-json.txt, u3-orphan-hive-text.txt

@radical
Copy link
Copy Markdown
Member Author

radical commented May 26, 2026

PR Testing Report — Windows x64

Platform: Windows 11 / x64 / PowerShell 7. Scenarios were chosen to exercise the Windows-only surfaces this PR adds (winget probe + WindowsRegistryReader + PowerShell dogfood installer + localhive.ps1).

Setup

  • Installed via the PowerShell dogfood installer (get-aspire-cli-pr.ps1 17461) into an isolated %TEMP%\aspire-pr-17461-…\.aspire with -SkipPath -SkipExtension.
  • USERPROFILE was scoped to the temp dir for the duration of the test so discovery only saw installs inside the sandbox.

CLI Version Verification

Expected (PR head, short) 8bf40935
Installed CLI reports 13.4.0-pr.17461.g8bf40935
Status ✅ Verified

Scenario Results

# Scenario Result Notes
1 CLI version check (PowerShell dogfood installer) ✅ Pass Installer stamped sidecar .aspire-install.json = {"source":"pr"}
2 aspire installs list (text) ✅ Pass Single row pr-17461 / pr / active / channel=pr-17461 / hive present
3 aspire installs list --format json ✅ Pass Clean JSON, Windows backslashes properly escaped
4 aspire installs --self (text + JSON) ✅ Pass source=pr flows from sidecar, pathStatus=active, version matches PR head
5 aspire doctor (no install table) ✅ Pass Only Aspire / .NET SDK / Container Runtime / Environment sections render
6 Simulated localhive install ✅ Pass kind=localhive classified from sidecar; orphan local hive correctly listed as kind=orphan-hive
7 Winget probe simulation (HKCU entry + first-run) ✅ Pass Probe wrote {"source":"winget"}; discovery emits kind=winget, managedBy=winget, status=shadowed

Detailed Findings

Scenario 7 — WingetFirstRunProbe end-to-end on real Windows registry

  1. Seeded HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\<unique> with WinGetPackageIdentifier=Microsoft.Aspire + PortableTargetFullPath=<fake binary>.
  2. Ran the fake binary once — startup WingetFirstRunProbe correctly read the registry, matched the Aspire entry, and atomically wrote .aspire-install.json next to the binary.
  3. With the winget bin dir added to PATH, aspire installs list classified the install as kind=winget, surfaced managedBy=winget in JSON, and reported status=shadowed because another aspire (the PR dogfood install) precedes it on PATH.
{
  "id": "pr-17461-2",
  "kind": "winget",
  "channel": "pr-17461",
  "path": "...\\WinGetPackages\\Microsoft.Aspire_Microsoft.Winget.Source_8wekyb3d8bbwe\\x64\\aspire.exe",
  "hive": "...\\.aspire\\hives\\pr-17461",
  "status": "shadowed",
  "managedBy": "winget"
}

This validates the full IWindowsRegistryReaderWingetFirstRunProbeInstallSidecarReaderInstallationDiscovery chain against the real Windows registry. Macs/Linux can't exercise this path.

Scenario 6 nuance

The simulated localhive install reported channel=pr-17461 (not local) because the binary copied into the localhive layout was the PR aspire.exe whose assembly metadata embeds AspireCliChannel=pr-17461. A real localhive.ps1-built AOT binary would bake channel=local in, so this mismatch wouldn't happen in practice. The behavior confirms the design intent: the binary's embedded channel is authoritative; the sidecar tags the install kind, not the channel. The leftover local hive folder then correctly surfaces as kind=orphan-hive with reason "No discovered install reports this hive's channel."

Scenario 5 — aspire doctor regression

Install table is fully gone. Only Aspire / .NET SDK / Container Runtime / Environment sections render now, and the command exits 0 despite environment warnings — matching doctor's existing convention. Discovery of installs now lives entirely under aspire installs.

Overall Result

PR VERIFIED on Windows

All 7 scenarios passed. Windows-unique surfaces (WingetFirstRunProbe, WindowsRegistryReader, the PowerShell installer's sidecar stamping, Windows path discovery + JSON escaping) all behave as designed. aspire installs / aspire installs list produce the documented text + JSON output, and aspire doctor correctly omits the install table.

Cleanup

  • Test directory removed
  • HKCU registry probe entry removed
  • User PATH untouched (installer was run with -SkipPath)

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