Skip to content

Keep launch-configuration AppHost targets out of the workspace default - #19126

Merged
Adam Ratzman (adamint) merged 31 commits into
microsoft:mainfrom
adamint:adamint/fix-19080-launch-config-persistence
Aug 12, 2026
Merged

Adam Ratzman (adamint) merged 31 commits into
microsoft:mainfrom
adamint:adamint/fix-19080-launch-config-persistence

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 7, 2026

Copy link
Copy Markdown
Member

Fixes #19080.

A launch.json configuration that points at a specific AppHost is choosing what to run for that session. It should not silently replace the workspace default in aspire.config.json.

This change carries that distinction from the VS Code debug configuration into the CLI's existing ProjectLocator persistence path:

  • explicit launch.json targets preserve a valid existing default
  • they can still establish a default when the workspace has none, or replace one whose target was deleted
  • normal discovery and direct user selections keep the existing persistence behavior
  • detached CLI launches retain the marker, while AppHost/build child processes do not inherit it
  • generated launch.json entries do not persist the extension's internal marker

The persistence decision stays in ProjectLocator, so it applies consistently to every command that resolves an AppHost. The cross-process lock, filesystem-identity probes, E2E control plumbing, and unrelated configuration-service changes are intentionally not part of this fix.

Validation

VS Code extension focused tests: 65 passing
Aspire CLI focused tests: 130 passing, 1 skipped on non-Windows

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
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

Adam Ratzman and others added 4 commits August 6, 2026 18:01
Keep explicit --apphost selection one-shot while preserving implicit and prompted selection persistence. Add focused CLI contract coverage and a real Extension Host launch-configuration regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ffeff87e-f284-434d-87d3-843e21a7aebb
Keep explicit launch-configuration targets session-scoped while preserving persistence for directory discovery, direct CLI selection, and extension user selection.

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

Copilot-Session: ffeff87e-f284-434d-87d3-843e21a7aebb
Move the explicit-launch-configuration check out of RunCommand and into
ProjectLocator.CreateSettingsFileAsync, the single point that writes
aspire.config.json. A VS Code launch configuration can name run, publish,
deploy, or do, and every one of those commands resolved its AppHost with
createSettingsFile: true, so the workspace default was still clobbered for
all commands other than run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da1d8af7-a583-4a0c-b434-3dd5d6feb7ff
Copilot AI balanced review requested due to automatic review settings August 7, 2026 13:39
@github-actions

github-actions Bot commented Aug 7, 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 -- 19126

Or

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

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

Prevents explicit VS Code launch configurations from changing the workspace’s persisted default AppHost.

Changes:

  • Adds AppHost-selection provenance across the extension-to-CLI boundary.
  • Suppresses configuration persistence for explicit launch targets.
  • Adds unit and extension E2E regression coverage.
Show a summary per file
File Description
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Supplies CLI configuration to ProjectLocator.
tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs Tests origin-based persistence behavior.
tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs Verifies commands request centralized persistence.
src/Shared/KnownConfigNames.cs Defines the provenance environment variable.
src/Aspire.Cli/Projects/ProjectLocator.cs Suppresses persistence for explicit launch configurations.
extension/src/utils/environment.ts Adds the environment-variable identifier.
extension/src/types/extensionApi.ts Adds E2E named-configuration launching.
extension/src/testing/e2eStateFileBridge.ts Implements the new E2E control command.
extension/src/test/aspireDebugSession.test.ts Tests provenance forwarding to the CLI.
extension/src/test/aspireDebugConfigurationProvider.test.ts Tests provenance classification and preservation.
extension/src/test/appHostLaunchService.test.ts Verifies user-selection classification.
extension/src/test-e2e/debugDashboard.e2e.test.ts Covers alternating named launch configurations.
extension/src/services/AppHostLaunchService.ts Marks UI-selected AppHosts.
extension/src/server/interactionService.ts Classifies RPC-started debug sessions.
extension/src/debugger/AspireDebugSession.ts Passes provenance through the CLI environment.
extension/src/debugger/AspireDebugConfigurationProvider.ts Classifies launch targets by origin.
extension/src/debugger/AspireDebugConfigurationMetadata.ts Defines internal provenance metadata.
extension/src/dcp/types.ts Types the provenance field.

Review details

  • Files reviewed: 18/18 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread extension/src/debugger/AspireDebugSession.ts
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Address code review of the AppHost selection-origin work.

The debug configuration provider was registered once for both the Dynamic
and Initial trigger kinds. VS Code serializes an Initial provider's result
verbatim into a newly created launch.json, so the internal selection-origin
marker was written into a user-owned file alongside a concrete AppHost
program. Because resolve-time classification short-circuits when the marker
is already present, such an entry stayed pinned to 'default-discovery' and
kept rewriting the workspace default AppHost -- reproducing issue microsoft#19080 for
launch configurations the extension itself generated.

Register separate provider instances per trigger kind and only stamp the
marker on ephemeral Dynamic configurations. An entry that lands in
launch.json is now classified when it is resolved, so a generated
configuration naming a specific AppHost is correctly treated as explicit.

Also narrow the directory downgrade. Any directory-valued program used to be
demoted to 'default-discovery', so a hand-written per-AppHost directory
configuration still clobbered the workspace default. Only a program pointing
at the workspace folder root delegates back to discovery now.

Test fixes: create the secondary AppHost candidate inside the try block and
remove it in suite teardown so a throw cannot leak it into later tests, give
the multi-configuration test its own timeout, and have the E2E control
command wait for VS Code to see a newly written launch configuration before
starting it by name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da1d8af7-a583-4a0c-b434-3dd5d6feb7ff
Copilot AI review requested due to automatic review settings August 7, 2026 14:54

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.

Review details

Suppressed comments (1)

extension/src/debugger/AspireDebugConfigurationProvider.ts:90

  • The documented contract says any program that resolves to a directory is downgraded to default-discovery and therefore persists the selected AppHost. This condition only does that for the workspace root; AppHost subdirectories remain explicit-launch-configuration (as the new test also codifies). Please align the implementation and tests with the stated directory behavior, or update the PR description if subdirectories are intentionally session-scoped.
            if (aspireConfig[appHostSelectionOriginConfigKey] === 'explicit-launch-configuration' && this.isWorkspaceFolderRoot(program, folder)) {
                // Only a program pointing at the workspace folder root delegates the choice back to
                // normal discovery, which is what the extension's own default configuration does. A
                // configuration naming a specific AppHost file *or* subdirectory is scoped to that
                // target and must not become the workspace default.
                aspireConfig[appHostSelectionOriginConfigKey] = 'default-discovery';
  • Files reviewed: 20/20 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Second round of code review fixes.

Suppressing the write outright meant a repo whose only AppHost was launched
from a generated launch.json never got an aspire.config.json at all. The
startup prompt from checkForExistingAppHostPathInWorkspace is not gated on
ambiguity, so those users would be asked to create the file on every window
open instead of having the first launch settle it.

Narrow the rule to what issue microsoft#19080 actually reports: an explicit launch
configuration must not *replace* a workspace default the user already has.
It may still establish one when there is nothing to preserve, and it still
heals a default whose AppHost no longer exists, since there is no choice
left to protect in either case. Alternating between per-AppHost launch
configurations remains stable, which is the behavior being fixed.

Also lower the new waitForLaunchConfiguration budget to 5s. At 15s it sat
above the 10s test-side control-command timeout, so its diagnostic was
unreachable and a timed-out test would leave the handler running long enough
to start a debug session behind the test's own teardown.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da1d8af7-a583-4a0c-b434-3dd5d6feb7ff
Copilot AI review requested due to automatic review settings August 7, 2026 15:18

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.

Review details

Suppressed comments (1)

src/Aspire.Cli/Projects/ProjectLocator.cs:155

  • This summary contradicts the persistence rule below: an explicit launch target can become the workspace default when no valid default exists; it only must not replace an existing valid default. Describing it as “never” becoming the default could lead future changes to break the intentional first-launch behavior.
    /// <summary>
    /// Identifies a CLI invocation whose AppHost target came from an editor launch configuration
    /// (for example a VS Code <c>launch.json</c> entry with an explicit <c>program</c>). Such a target
    /// is owned by the individual debug session, so it must never become the workspace default.
    /// </summary>
  • Files reviewed: 20/20 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

The explicit-launch-configuration check asked whether a recorded default
existed, but it did not read that default the way the rest of the CLI does.

Two gaps:

- It loaded the config with a plain Load, which does not see a legacy
  .aspire/settings.json workspace. A workspace that had never been migrated
  therefore looked like it had no recorded default, so a launch configuration
  was free to write one -- the exact overwrite microsoft#19080 reports. LoadOrCreate
  cannot be called from a read-only predicate because it persists the
  migration as a side effect, so the config the upward search already loaded
  is threaded out and reused instead.

- It resolved the recorded path with Path.GetFullPath alone, unlike the
  canonical readers, which normalize separators first. A config committed from
  Windows carries backslashes, so on a Unix checkout the recorded path was
  read as a literal filename.

The predicate also no longer requires the recorded file to exist. A missing
file is indistinguishable from a branch switch or a sparse checkout, and
treating it as "nothing to preserve" would let the next launch permanently
re-point a default the user still wants. Stale entries are still healed by
every other selection origin, which is where a deliberate choice comes from.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da1d8af7-a583-4a0c-b434-3dd5d6feb7ff
Copilot AI review requested due to automatic review settings August 7, 2026 15:34
ASPIRE_CLI_APPHOST_SELECTION_ORIGIN describes one CLI invocation: it tells
ProjectLocator that this run's AppHost was named by a VS Code launch
configuration and therefore must not rewrite the workspace default. The
extension sets it on the CLI process, but ProcessExecutionFactory only stripped
the ASPIRE_CLI_* identity overrides, so every child inherited the marker --
`dotnet build`, the AppHost itself, and the env block the CLI hands to the
extension for an IDE-launched AppHost. Any nested `aspire` invocation inside
that tree would then believe its own target came from the outer launch
configuration and silently skip recording its workspace default.

Strip the marker alongside the identity overrides in both factory overloads.
The (fileName, args, env, ...) overload applies caller env after the strip, so
AppHostLauncher re-adds the marker for its detached child CLI, which continues
the same logical invocation and re-resolves the AppHost. The ProcessStartInfo
overload serves only AppHost/guest spawn paths, so it always strips.

Tests cover both directions: the marker is removed from a parent-inherited
block and from an explicitly populated ProcessStartInfo, and it survives on the
detached-child-CLI path spawned with AppHostLauncher's own env and options.
Both strip tests fail against the previous behavior.

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

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Projects/ProjectLocator.cs:1081

  • This process-scoped handoff is being read through the merged IConfiguration, but workspace/global settings are registered after environment variables (Program.cs:299-312, ConfigurationHelper.cs:73-83) and therefore take precedence. A same-named top-level setting can make an explicit launch rewrite the default, or make ordinary CLI invocations suppress persistence. Read this internal signal from the existing IEnvironment.GetEnvironmentVariable(...) abstraction instead, and remove the new IConfiguration constructor plumbing.
        var isExplicitLaunchConfiguration = string.Equals(
            configuration[KnownConfigNames.CliAppHostSelectionOrigin],
            ExplicitLaunchConfigurationSelectionOrigin,
            StringComparison.OrdinalIgnoreCase);

src/Aspire.Cli/Projects/ProjectLocator.cs:156

  • This documentation contradicts the implemented “establish, but never replace” rule: CreateSettingsFileAsync intentionally allows this origin to become the default when none exists. Describe that exception so future callers do not incorrectly suppress all persistence.
    /// Identifies a CLI invocation whose AppHost target came from an editor launch configuration
    /// (for example a VS Code <c>launch.json</c> entry with an explicit <c>program</c>). Such a target
    /// is owned by the individual debug session, so it must never become the workspace default.
    /// </summary>
  • Files reviewed: 25/25 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 7, 2026 15:41

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.

Review details

Suppressed comments (1)

tests/Aspire.Cli.Tests/DotNet/ProcessExecutionFactoryEnvironmentTests.cs:21

  • These tests mutate process-wide environment variables, but this class is not serialized with the repository's existing EnvVarMutatingTestCollection. While the overrides on lines 42–43 or 78 are active, an unrelated test can snapshot ASPIRE_CLI_APPHOST_SELECTION_ORIGIN into IConfiguration or a child process and unexpectedly suppress its config write. Put this class in the same non-parallel collection used by the other EnvVarOverride suites.
public sealed class ProcessExecutionFactoryEnvironmentTests
  • Files reviewed: 25/25 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs Outdated
TryGetRecordedAppHostDefault resolved appHost.path only to hand the result to a
LogDebug argument: once the File.Exists check was dropped, every caller consumed
it through an `is { }` null test. The resolution could not change an outcome, but
it could still throw.

GetAppHostProjectFileFromSettingsAsync guards its resolution with
IsValidConfiguredAppHostPath, which rejects NUL bytes and other invalid
characters that survive JSON parsing precisely because Path.GetFullPath throws
ArgumentException on them (microsoft#17624). This helper had no such guard. The upward
search resolves the config it finds and would throw first, but the
`recordedConfig ??= AspireConfigFile.Load(...)` fallback reaches the
working-directory config in the case where the AppHost sits outside that config's
tree -- a config nothing else in this method had ever resolved. An explicit
--apphost never runs the settings reader, so no earlier validation applies.

Return the recorded path as written instead. Presence is the whole decision, the
raw string is the more useful log value because it is what appears in the file,
and the throw path disappears with the dead code. The config directory is now
logged separately so the message still says which file was consulted.

The remarks no longer claim stale entries are healed by every other origin. The
extension's startup prompt only checks that appHost.path is present, never that
it resolves, so a default left stale by a rename is repaired only by a selection
the user actually makes. That is the real cost of preferring preservation, and it
belongs in the remarks rather than an overstated reassurance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da1d8af7-a583-4a0c-b434-3dd5d6feb7ff
Copilot AI review requested due to automatic review settings August 7, 2026 15:51
…robe

Joining only the segments before the differing one drops the root separator on a
rooted Windows path: C:\Foo and C:\foo rebuild the parent as "C:", which names
the process's current directory on drive C rather than the drive root. The probe
then enumerated the wrong directory, missed both spellings, and reported the two
AppHosts as one file. Rebuilding through the differing segment and taking its
parent keeps rooted-path semantics on every platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 487b899d-d79d-4349-a84b-a026861bde7f
Copilot AI review requested due to automatic review settings August 10, 2026 01:30

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.

Review details

Suppressed comments (1)

src/Aspire.Cli/Projects/ProjectLocator.cs:1334

  • This platform shortcut contradicts the filesystem probe below: Linux can use case-insensitive filesystems (for example, an SMB or exFAT workspace). There, two casing variants can open the same AppHost, but this branch always reports different files and unnecessarily rewrites the recorded default. Run the existing PathExists/directory-entry probe on every platform; it already distinguishes a case-sensitive directory containing both variants.
        // A casing-only difference means two files wherever the volume is case-sensitive, which is
        // the default on Linux.
        if (!environment.IsWindows() && !environment.IsMacOS())
        {
            return false;
        }
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

The sibling case-insensitive test already skips where its premise fails, but
this one still asserts unconditionally that a second project differing only in
casing can be promoted to the workspace default. Where the volume folds the two
spellings there is no second project: CreateSubdirectory("foo") hands back the
"Foo" created a line earlier, both writes land in one file, and the explicit
"foo" path is normalized to the on-disk "Foo" before the first assertion runs.
That is the windows-latest failure, which compared

  ...\Workspace\3jhpl2ni.4fb\foo\AppHost.csproj
  ...\Workspace\3jhpl2ni.4fb\Foo\AppHost.csproj

The guard probes the volume the workspace actually sits on rather than switching
on OperatingSystem, matching the sibling tests, because NTFS and APFS can each
be formatted either way. Verified both ways locally: the test runs and passes on
a case-sensitive APFS volume and skips on a case-insensitive one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 10, 2026 01:40

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.

Review details

  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Carry the resolved settings file, scoped base directory, and recorded config together through ProjectLocator config preservation and writing.

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

Copilot-Session: 0d1cf760-b1bc-46ba-a6d4-628354b00f2c
Copilot AI review requested due to automatic review settings August 10, 2026 06:04

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Configuration/ConfigurationService.cs:411

  • includeGlobalSettings has no production caller that passes false, so this new branch is dead and does not implement ProjectLocator's local-only check (that code reads RecordedConfig directly). It expands IConfigurationService, changes every test double, and adds untested behavior without affecting the fix. Please remove the parameter/branch, or wire the intended local-only production call and add direct ConfigurationServiceTests coverage.
        if (includeGlobalSettings && File.Exists(globalSettingsFile.FullName))

tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs:2058

  • This comment no longer matches the implementation: ProjectLocator decides whether a workspace default exists from WorkspaceConfigTarget.RecordedAppHostPath; directory-scoped reads are only used here to inherit SDK settings for polyglot AppHosts. Please describe that current purpose so future changes do not preserve this test wiring for the wrong reason.
        // Directory-scoped reads are answered by the real implementation rather than stubbed out.
        // ProjectLocator uses them to decide whether the workspace already records an AppHost
        // default, and that is a question about config files these tests actually write to disk, so
        // a stub returning null would quietly make every preservation assertion vacuous. The global
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI review requested due to automatic review settings August 10, 2026 07:09

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Projects/ProjectLocator.cs:1331

  • This hard-codes Linux as effectively case-sensitive, but the surrounding remarks correctly note that case sensitivity is a volume/directory property (e.g., SMB/CIFS mounts, FUSE, WSL/DrvFS scenarios). With the current early-return, two paths that differ only by casing will never be treated as the same entry on Linux even when the underlying filesystem folds them, which can cause incorrect ‘heal/replace’ decisions. Consider removing the OS gate and using the existing BothCaseVariantsExist probe (with its exception-tolerant behavior) on all platforms for the casing-only path case.
        // A casing-only difference means two files wherever the volume is case-sensitive, which is
        // the default on Linux.
        if (!environment.IsWindows() && !environment.IsMacOS())
        {
            return false;
        }

extension/src/utils/appHostDiscovery.ts:1223

  • Using fs.statSync in extension-host code can block the event loop (notably on networked/slow filesystems) and this helper is called from debug-configuration resolution paths. Since the surrounding APIs are already async, consider switching this helper to use fs.promises.stat (or vscode.workspace.fs.stat) and making callers await it, to avoid potential UI/extension-host stalls.
export function isSameFileSystemEntry(left: string, right: string): boolean {
    const resolvedLeft = path.resolve(left);
    const resolvedRight = path.resolve(right);
    if (resolvedLeft === resolvedRight) {
        return true;
    }

    try {
        const leftStat = fs.statSync(resolvedLeft, { bigint: true });
        const rightStat = fs.statSync(resolvedRight, { bigint: true });

        // Windows reports ino as 0 on filesystems with no stable file index, which would make every
        // unrelated pair look like one entry.
        if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
            return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
        }
    }
    catch {
        // A path that cannot be stat'ed, because it is missing or unreadable, carries no identity
        // to compare. Fall back to the textual comparison below.
    }

    return isSamePath(resolvedLeft, resolvedRight);
}
  • Files reviewed: 30/30 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

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

Copilot-Session: 0d1cf760-b1bc-46ba-a6d4-628354b00f2c

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.

Review details

Suppressed comments (3)

src/Aspire.Cli/Projects/ProjectLocator.cs:1141

  • This read/decision is not serialized with the later whole-file update. Compound launch configurations can start multiple explicit AppHosts concurrently while no default exists; each process can observe no path and then write its own target, so the last writer silently becomes the workspace default (and concurrent JSON rewrites can collide). The complete resolve/read/decision/write operation needs a cross-process lock or atomic compare-and-set scoped to the resolved config file.
            if (isExplicitLaunchConfiguration && File.Exists(resolvedPath))
            {
                logger.LogDebug(
                    "Not replacing recorded AppHost default {RecordedAppHost} with {AppHost} because the latter was selected by {SelectionOrigin}.",
                    resolvedPath,
                    projectFile.FullName,
                    selectionOrigin);
                return;

extension/src/debugger/AspireDebugConfigurationProvider.ts:159

  • isSamePath folds case on all macOS systems, so this can misclassify an explicit target as the workspace root on a case-sensitive APFS volume. For example, separate /repo and /Repo directories compare equal, changing the origin to default-discovery and allowing that launch target to overwrite the workspace default. Use filesystem-canonical identity (which also handles case-insensitive volumes) for this classification.
    private isWorkspaceFolderRoot(program: string, folder: vscode.WorkspaceFolder | undefined): boolean {
        const owningFolder = folder ?? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(program));

        return owningFolder !== undefined && isSamePath(program, owningFolder.uri.fsPath);

src/Aspire.Cli/Projects/ProjectLocator.cs:1121

  • This assumes every macOS volume is case-insensitive. On a case-sensitive APFS volume, Foo/AppHost.csproj and foo/AppHost.csproj can both exist; this comparison treats them as the same target and returns without recording the project that was actually selected. Determine path identity from the filesystem/volume (or canonicalize both existing paths) rather than choosing comparison rules from the OS.
            var pathComparison = environment.IsWindows() || environment.IsMacOS()
                ? StringComparison.OrdinalIgnoreCase
                : StringComparison.Ordinal;
  • Files reviewed: 21/21 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

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.

I reviewed the final, reduced diff again, including the full extension-to-CLI AppHost-selection flow: Initial/Dynamic debug configuration behavior, hidden metadata classification, environment merging and process-boundary propagation, ProjectLocator persistence semantics, and the updated focused tests. I also rechecked the earlier feedback against the current head and completed another Aspire-specific architecture pass.

The structure makes sense. The extension owns selection provenance, the CLI owns workspace-default policy, ProjectLocator remains the common persistence point, and ProcessExecutionFactory is the right boundary for keeping the marker invocation-scoped. I did not find any new correctness or architectural issues in the final diff.

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

Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4

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.

Review details

Suppressed comments (4)

extension/src/utils/appHostDiscovery.ts:1229

  • Making this shared comparator case-sensitive on macOS regresses existing callers that were not migrated to filesystem identity. For example, findCandidateForEditorFile uses it at line 801; on default case-insensitive APFS, a .csproj path emitted by aspire ls with different casing from the VS Code path no longer matches, and .csproj has no source-file fallback. Either retain the prior macOS fallback or migrate all existing-file comparisons to the identity-aware helper.
    const comparison = process.platform === 'win32'
        ? 'case-insensitive'
        : 'case-sensitive';

extension/src/utils/appHostDiscovery.ts:1198

  • The PR description says filesystem-identity probes are intentionally excluded, but this adds synchronous stat-based identity probing and corresponding tests. Either remove this unrelated behavior from the change or update the PR description and validation scope to reflect it.
export function isSameFileSystemEntry(
    left: string,
    right: string,
    getIdentity: FileSystemEntryIdentityProvider = tryGetFileSystemEntryIdentity): boolean {

extension/src/debugger/AspireDebugConfigurationProvider.ts:90

  • This regression crosses VS Code configuration resolution, debug-session environment forwarding, CLI configuration binding, and on-disk persistence, but the added tests exercise those layers only in isolation. Add an extension E2E scenario with two AppHosts and an existing default that starts the other through an explicit launch configuration and verifies aspire.config.json remains unchanged; otherwise a broken handoff between these unit-tested layers can reintroduce #19080.
            if (aspireConfig[appHostSelectionOriginConfigKey] === 'explicit-launch-configuration' && this.isWorkspaceFolderRoot(program, folder)) {
                // Only a program pointing at the workspace folder root delegates the choice back to
                // normal discovery, which is what the extension's own default configuration does. A
                // configuration naming a specific AppHost file *or* subdirectory is scoped to that
                // target and must not become the workspace default.
                aspireConfig[appHostSelectionOriginConfigKey] = 'default-discovery';

extension/src/debugger/AspireDebugConfigurationMetadata.ts:13

  • This documentation says an explicit launch target can never replace a default, but the CLI intentionally replaces a missing or deleted target. Qualify this as a valid existing default so the documented contract matches ProjectLocator.
 * one invocation and must never replace a default the user already has.
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs
Resolve the target config before reading it, then hold the CLI cross-process file lock through migration, the preserve decision, and every config write. This prevents compound launch configurations from each observing an empty default and replacing one another.

Add concurrent ProjectLocator coverage that verifies only one launch writes while unrelated config survives.

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

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.

Review details

Suppressed comments (6)

src/Aspire.Cli/Projects/ProjectLocator.cs:1090

  • The lock key is derived after ResolveWorkspaceConfigTarget has already fallen back to the current working directory. Concurrent explicit launches for AppHosts in different subdirectories can therefore choose different config files and different locks when no config exists, allowing both to establish defaults (and potentially creating multiple nested aspire.config.json files). Resolve and revalidate the target under a stable lock, reacquiring the lock if the resolved config root changes before the read/write section.
        using var configLock = await TryAcquireWorkspaceConfigLockAsync(settingsFile, cancellationToken);

extension/src/utils/appHostDiscovery.ts:1229

  • isSamePath is shared by editor-file matching, selected-candidate parsing, and workspace selection, so removing macOS from the case-insensitive branch changes all of those callers—not just launch-origin classification. On the default case-insensitive APFS volume, differently-cased paths still identify the same entry; these callers can now fail to find/select that AppHost. Keep the existing macOS behavior for textual path matching and use the identity-aware comparison only where case-sensitive volumes must distinguish real entries, or migrate every affected caller to identity-aware matching.
    const comparison = process.platform === 'win32'
        ? 'case-insensitive'
        : 'case-sensitive';

extension/src/debugger/AspireDebugSession.ts:311

  • The new unit tests verify the provider, spawn arguments, and CLI persistence separately, but none exercises the regression across the actual VS Code-to-CLI boundary. A dropped or overwritten marker between debug configuration resolution and the spawned CLI would leave every focused test passing while launch.json still replaces aspire.config.json. Add an extension E2E scenario that records AppHost A as the default, launches AppHost B from launch.json, and asserts the config remains unchanged.
      void this.spawnAspireCommand(args, appHostPath, noDebug, commandLabel, this.getAppHostSelectionOriginEnvironment(appHostSelectionOrigin));

src/Aspire.Cli/Projects/ProjectLocator.cs:1216

  • The PR description says the cross-process lock is intentionally not part of this fix, but this hunk adds and uses that lock. Please either remove this out-of-scope behavior or update the PR description and validation notes so reviewers can evaluate the actual concurrency and operational scope.
            return await FileLock.AcquireAsync(lockPath, cancellationToken, s_workspaceConfigLockTimeout);

extension/src/utils/appHostDiscovery.ts:1198

  • The PR description explicitly excludes filesystem-identity probes, but this function introduces synchronous stat-based identity probing and changes configured-candidate matching to use it. Please remove this unrelated change or update the stated scope and validation to cover the additional cross-platform filesystem behavior.

This issue also appears on line 1227 of the same file.

export function isSameFileSystemEntry(
    left: string,
    right: string,
    getIdentity: FileSystemEntryIdentityProvider = tryGetFileSystemEntryIdentity): boolean {

src/Aspire.Cli/Projects/ProjectLocator.cs:1075

  • This makes persisted-path equality case-sensitive on every macOS volume. Default APFS is case-insensitive, so two spellings of the same AppHost now fall through to SetConfigurationInFileAsync, rewriting the config and reporting an update during normal discovery/direct selection even though the target did not change. The existing discovery path explicitly accounts for default macOS case-insensitivity at ProjectLocator.cs:462-467; use filesystem identity (or volume-aware comparison) here so case-sensitive APFS can still distinguish genuinely different entries.

This issue also appears on line 1090 of the same file.

        var pathComparison = environment.IsWindows()
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Launching aspire from different vscode launch configurations (using the extension) overrides the aspire.config.json file

3 participants