File: DotNet\DotNetAppHostNuGetConfigMergerTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.InternalTesting;
using System.Xml.Linq;
using System.Xml;
using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Aspire.Cli.DotNet;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.DotNet;
 
public class DotNetAppHostNuGetConfigMergerTests
{
    private readonly ITestOutputHelper _outputHelper;
 
    public DotNetAppHostNuGetConfigMergerTests(ITestOutputHelper outputHelper)
    {
        _outputHelper = outputHelper;
    }
 
    private static async Task<FileInfo> WriteConfigAsync(DirectoryInfo dir, string content)
    {
        var path = Path.Combine(dir.FullName, "nuget.config");
        await File.WriteAllTextAsync(path, content);
        return new FileInfo(path);
    }
 
    private static PackageChannel CreateChannel(PackageMapping[] mappings) => PackageChannel.CreateExplicitChannel("test", PackageChannelQuality.Both, mappings, new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CreateOrUpdateAsync_SwitchesNamedIntegrationFeedsWithoutLosingOtherMappings(bool legacyAlias)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var team = workspace.CreateDirectory("team-feed");
        var other = workspace.CreateDirectory("other-feed");
        var sdk = workspace.CreateDirectory("sdk-feed");
        var publicFeed = workspace.CreateDirectory("public-feed");
        const string packageId = "Contoso.Hosting.One";
        foreach (var (feed, name) in new[] { (team, "team"), (other, "other") })
        {
            NuGetTestHelper.CreatePackage(feed, packageId, "1.0.0",
                new Dictionary<string, string> { ["content/source.txt"] = name });
        }
        var teamKey = legacyAlias ? "team" : "aspire-integration-team";
        var config = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="sdk" value="{{sdk.FullName}}" />
                <add key="public" value="{{publicFeed.FullName}}" />
                <add key="{{teamKey}}" value="{{team.FullName}}" />
              </packageSources>
              <packageSourceCredentials>
                <{{teamKey}}><add key="Username" value="user" /><add key="ClearTextPassword" value="test-password" /></{{teamKey}}>
              </packageSourceCredentials>
              <packageSourceMapping>
                <clear />
                <packageSource key="sdk"><package pattern="Aspire*" /></packageSource>
                <packageSource key="public"><package pattern="*" /></packageSource>
                <packageSource key="{{teamKey}}">
                  <package pattern="{{packageId}}" />
                  <package pattern="Contoso.Hosting.Keep" />
                  <package pattern="Contoso.Internal.*" />
                </packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        File.WriteAllText(Path.Combine(workspace.Path, AspireConfigFile.FileName), $$"""
            {"integrationSources":{
              "team":{"source":{{System.Text.Json.JsonSerializer.Serialize(team.FullName)}},"patterns":["Contoso.*"]},
              "other":{"source":{{System.Text.Json.JsonSerializer.Serialize(other.FullName)}},"patterns":["Contoso.*"]}
            }
            }
            """);
        var teamSource = Assert.Single(
            NuGetTestHelper.CreateService().GetNuGetSettings(workspace.Path, TestContext.Current.CancellationToken).Sources,
            source => source.Name == teamKey);
        Assert.True(teamSource.IsIntegrationSource);
        Assert.False(teamSource.IsCliManaged);
        var client = NuGetTestHelper.CreateClient();
        var configurations = new XElement("configurations");
        var step = 0;
        foreach (var (feed, name) in new[] { (team, "team"), (other, "other"), (team, "team") })
        {
            await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(
                workspace.WorkspaceRoot, [new(packageId, feed.FullName) { SourceKey = name }],
                cancellationToken: TestContext.Current.CancellationToken);
 
            Assert.Equal([feed.FullName], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, packageId));
            Assert.Equal([team.FullName], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Contoso.Hosting.Keep"));
            Assert.Equal([team.FullName], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Contoso.Internal.Dependency"));
            Assert.Equal([sdk.FullName], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Aspire.Hosting.AppHost"));
            Assert.Equal([publicFeed.FullName], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Unrelated.Package"));
            var packages = workspace.CreateDirectory($"packages-{step}");
            var restore = workspace.CreateDirectory($"restore-{step++}");
            await client.RestoreAsync(
                [(packageId, "1.0.0")], "net10.0", runtimeIdentifier: null, restore.FullName,
                sources: [], nugetConfigPaths: [config.FullName], workingDirectory: workspace.Path,
                globalPackagesFolderOverride: packages.FullName, sensitiveSources: [],
                TestContext.Current.CancellationToken);
            Assert.Equal(name, await File.ReadAllTextAsync(
                Path.Combine(packages.FullName, packageId.ToLowerInvariant(), "1.0.0", "content", "source.txt"),
                TestContext.Current.CancellationToken));
            configurations.Add(new XElement("selection", new XAttribute("feed", name), XDocument.Load(config.FullName).Root));
        }
 
        await Verify(configurations.ToString(), "xml")
            .AddScrubber(builder => builder.Replace(workspace.Path, "<workspace>").Replace('\\', '/'))
            .UseParameters(legacyAlias);
    }
 
    [Theory]
    [InlineData(false, "Contoso.Hosting.One", false)]
    [InlineData(true, "Contoso.Hosting.One", false)]
    [InlineData(true, "Inherited.*", false)]
    [InlineData(false, "Contoso.Hosting.One", true)]
    public async Task PrepareAsync_SwitchingNamedFeedsDoesNotModifyOrExposeInheritedMappings(
        bool localMappingExists, string inheritedPattern, bool selectedMappingExists)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://company.example/v3/index.json";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="team" value="{{source}}" /></packageSources>
              <packageSourceMapping><packageSource key="team"><package pattern="{{inheritedPattern}}" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var childDirectory = workspace.CreateDirectory("apphost");
        var child = await WriteConfigAsync(childDirectory, $$"""
            <configuration>
              <packageSources>
                <add key="team" value="{{source}}" />
                {{(selectedMappingExists ? """<add key="other" value="https://other.example/v3/index.json" />""" : "")}}
              </packageSources>
              <packageSourceMapping>
                {{(localMappingExists ? """<packageSource key="team"><package pattern="Contoso.Hosting.One" /></packageSource>""" : "")}}
                {{(selectedMappingExists ? """<packageSource key="other"><package pattern="Contoso.Hosting.One" /></packageSource>""" : "")}}
              </packageSourceMapping>
            </configuration>
            """);
        File.WriteAllText(Path.Combine(childDirectory.FullName, AspireConfigFile.FileName), $$"""
            {"integrationSources":{
              "team":{"source":"{{source}}","patterns":["Contoso.*"]},
              "other":{"source":"https://other.example/v3/index.json","patterns":["Contoso.*"]}
            }
            }
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var originalChild = await File.ReadAllBytesAsync(child.FullName);
 
        await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                childDirectory,
                [new("Contoso.Hosting.One", "https://other.example/v3/index.json") { SourceKey = "other" }],
                createIfMissing: true, configureGlobalPackagesFolder: false, TestContext.Current.CancellationToken));
 
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
        Assert.Equal(originalChild, await File.ReadAllBytesAsync(child.FullName));
    }
 
    [Theory]
    [InlineData("Contoso.*", true)]
    [InlineData("Contoso.Hosting.One", false)]
    public async Task PrepareAsync_IntegrationAliasReuseRetainsChannelAndDisabledSourceGuards(string pattern, bool enabled)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://company.example/v3/index.json";
        var config = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="company-feed" value="{{source}}" /></packageSources>
              <disabledPackageSources><add key="company-feed" value="{{(!enabled).ToString().ToLowerInvariant()}}" /></disabledPackageSources>
              <packageSourceMapping><clear /><packageSource key="company-feed"><package pattern="Existing.*" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(config.FullName);
        File.WriteAllText(Path.Combine(workspace.Path, AspireConfigFile.FileName), $$"""
            {
              "integrationSources": {
                "team": {"source": "{{source}}", "patterns": ["Contoso.*"]}
              }
            }
            """);
 
        await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                workspace.WorkspaceRoot, [new(pattern, source) { SourceKey = "team" }],
                createIfMissing: true, configureGlobalPackagesFolder: false, TestContext.Current.CancellationToken));
 
        Assert.Equal(original, await File.ReadAllBytesAsync(config.FullName));
    }
 
    [Theory]
    [InlineData(false, "Contoso.*", "company-feed")]
    [InlineData(true, "Contoso.*", "company-feed")]
    [InlineData(false, "*", "company-feed")]
    [InlineData(true, "*", "company-feed")]
    [InlineData(false, "Existing.*", "company-feed")]
    [InlineData(true, "Existing.*", "company-feed")]
    [InlineData(false, "Contoso.*", "aspire-integration-team")]
    [InlineData(true, "Contoso.*", "aspire-integration-team")]
    [InlineData(false, "*", "aspire-integration-team")]
    [InlineData(true, "*", "aspire-integration-team")]
    [InlineData(false, "Existing.*", "aspire-integration-team")]
    [InlineData(true, "Existing.*", "aspire-integration-team")]
    public async Task PrepareAsync_ReusesInheritedIntegrationMappingWithoutShadowingIt(
        bool childConfigExists, string existingPattern, string sourceKey)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://company.example/v3/index.json";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="{{sourceKey}}" value="{{source}}" /></packageSources>
              <packageSourceCredentials>
                <{{sourceKey}}><add key="Username" value="user" /><add key="ClearTextPassword" value="test-password" /></{{sourceKey}}>
              </packageSourceCredentials>
              <packageSourceMapping><clear /><packageSource key="{{sourceKey}}"><package pattern="{{existingPattern}}" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        File.WriteAllText(Path.Combine(directory.FullName, AspireConfigFile.FileName), $$"""
            {
              "integrationSources": {
                "team": {"source": "{{source}}", "patterns": ["Contoso.*"]}
              }
            }
            """);
        var childPath = Path.Combine(directory.FullName, "nuget.config");
        byte[]? originalChild = null;
        if (childConfigExists)
        {
            await WriteConfigAsync(directory, $$"""
                <configuration><packageSources><add key="{{sourceKey}}" value="{{source}}" /></packageSources></configuration>
                """);
            originalChild = await File.ReadAllBytesAsync(childPath);
        }
        PackageMapping[] mappings = [new("Contoso.Hosting.One", source) { SourceKey = "team" }];
 
        if (existingPattern == "Existing.*")
        {
            await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
                DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                    directory, mappings, createIfMissing: true, configureGlobalPackagesFolder: false,
                    TestContext.Current.CancellationToken));
        }
        else
        {
            var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                directory, mappings, createIfMissing: true, configureGlobalPackagesFolder: false,
                TestContext.Current.CancellationToken);
            Assert.Null(candidate);
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Contoso.Hosting.One"));
        }
 
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
        if (originalChild is not null)
        {
            Assert.Equal(originalChild, await File.ReadAllBytesAsync(childPath));
        }
        else
        {
            Assert.False(File.Exists(childPath));
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void GetRecommendedNuGetConfigDirectory_PrefersNearestExistingConfiguration(bool appHostConfigExists)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var appHostDirectory = workspace.CreateDirectory("apphost");
        var expectedDirectory = appHostConfigExists ? appHostDirectory : workspace.WorkspaceRoot;
        var configPaths = appHostConfigExists
            ? new[] { Path.Combine(appHostDirectory.FullName, "NuGet.Config"), Path.Combine(workspace.Path, "nuget.config") }
            : [Path.Combine(workspace.Path, "nuget.config")];
 
        var directory = DotNetAppHostNuGetConfigMerger.GetRecommendedNuGetConfigDirectory(configPaths, appHostDirectory);
 
        Assert.Equal(expectedDirectory.FullName, directory.FullName);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void GetRecommendedNuGetConfigDirectory_FallsBackToAppHostWithoutLocalConfiguration(bool globalConfigExists)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var appHostDirectory = workspace.CreateDirectory("apphost");
        var globalDirectory = Environment.OSVersion.Platform == PlatformID.Win32NT
            ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "NuGet")
            : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "NuGet");
        string[] configPaths = globalConfigExists ? [Path.Combine(globalDirectory, "NuGet.Config")] : [];
 
        var directory = DotNetAppHostNuGetConfigMerger.GetRecommendedNuGetConfigDirectory(configPaths, appHostDirectory);
 
        Assert.Equal(appHostDirectory.FullName, directory.FullName);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task PrepareAsync_UnmappedAmbientSourceDoesNotRequireAliasOwnership(bool enabled)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://company.example/v3/index.json";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="company" value="{{source}}" /></packageSources>
              <disabledPackageSources>{{(enabled ? "" : """<add key="company" value="true" />""")}}</disabledPackageSources>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var channel = CreateChannel([new("Aspire*", source)]);
 
        if (enabled)
        {
            var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                directory, channel, createIfMissing: true, TestContext.Current.CancellationToken);
            Assert.NotNull(candidate);
            await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Other.Dependency"));
            Assert.Empty(XDocument.Load(candidate.TargetFile.FullName).Descendants("packageSource"));
        }
        else
        {
            await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
                DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                    directory, channel, createIfMissing: true, TestContext.Current.CancellationToken));
            Assert.Empty(directory.EnumerateFiles());
        }
 
        Assert.Equal(original, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Theory]
    [InlineData(false, "company", "https://feed.example/v3/index.json")]
    [InlineData(true, "company", "https://feed.example/v3/index.json")]
    [InlineData(true, "aspire-staging", "https://feed.example/v3/index.json")]
    [InlineData(false, "company", "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-existing/nuget/v3/index.json")]
    public async Task PrepareAsync_RefusesBorrowingExistingChannelFeed(
        bool inherited, string sourceKey, string source)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var target = workspace.CreateDirectory("apphost");
        var existingDirectory = inherited ? workspace.WorkspaceRoot : target;
        var existingFile = await WriteConfigAsync(existingDirectory, $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="{{sourceKey}}" value="{{source}}" />
              </packageSources>
              <packageSourceCredentials>
                <{{sourceKey}}>
                  <add key="Username" value="test-user" />
                  <add key="ClearTextPassword" value="test-password" />
                </{{sourceKey}}>
              </packageSourceCredentials>
              <packageSourceMapping>
                <clear />
                <packageSource key="{{sourceKey}}"><package pattern="Private.*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(existingFile.FullName);
 
        var error = await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                target, CreateChannel([new("Aspire*", source)]), createIfMissing: true,
                TestContext.Current.CancellationToken));
 
        Assert.Equal(string.Format(
            System.Globalization.CultureInfo.CurrentCulture,
            UpdateCommandStrings.ChannelFeedAlreadyConfiguredFormat,
            Path.Combine(target.FullName, "nuget.config"), sourceKey), error.Message);
        Assert.Equal(original, await File.ReadAllBytesAsync(existingFile.FullName));
        if (inherited)
        {
            Assert.Empty(target.EnumerateFiles());
        }
    }
 
    [Fact]
    public async Task PrepareAsync_MaintainsExistingLocalCliOwnedChannelFeed()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://feed.example/v3/index.json";
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="aspire-test" value="{{source}}" /></packageSources>
              <packageSourceMapping>
                <clear />
                <packageSource key="aspire-test"><package pattern="Private.*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, CreateChannel([new("Aspire*", source)]), createIfMissing: true,
            TestContext.Current.CancellationToken);
 
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
        Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Private.Library"));
        Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Aspire.Hosting"));
        Assert.Single(XDocument.Load(target.FullName).Root!.Element("packageSources")!.Elements("add"));
        Assert.Single(XDocument.Load(target.FullName).Root!.Element("packageSourceMapping")!.Elements("clear"));
    }
 
    [Theory]
    [InlineData(true, true, false)]
    [InlineData(false, true, false)]
    [InlineData(true, false, false)]
    [InlineData(false, false, false)]
    [InlineData(true, true, true)]
    public async Task PrepareAsync_RefusesEditsRequiringDisabledChannelSource(
        bool inheritedDisabledSetting, bool requiresEdit, bool clearsDisabledLocally)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string disabledSetting = """<add key="aspire-test" value="true" />""";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="company" value="https://company.example/v3/index.json" /></packageSources>
              <disabledPackageSources>{{(inheritedDisabledSetting ? disabledSetting : "")}}</disabledPackageSources>
              <packageSourceMapping><packageSource key="company"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        const string source = "https://selected.example/v3/index.json";
        var target = await WriteConfigAsync(directory, $$"""
            <configuration>
              <packageSources><add key="aspire-test" value="{{source}}" /></packageSources>
              <disabledPackageSources>
                {{(clearsDisabledLocally ? "<clear />" : "")}}
                {{(!inheritedDisabledSetting ? disabledSetting : "")}}
              </disabledPackageSources>
              <packageSourceMapping>
                <packageSource key="aspire-test"><package pattern="{{(requiresEdit ? "Private.*" : "Aspire*")}}" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
        var originalDisabled = XDocument.Load(target.FullName).Root!.Element("disabledPackageSources")!.ToString();
        var service = NuGetTestHelper.CreateService();
        var configuration = service.BuildAppHostConfiguration(
            directory, "test", [new("Aspire*", source)], overrideIntegrationSources: false, TestContext.Current.CancellationToken);
        Assert.Equal(clearsDisabledLocally, Assert.Single(configuration.ConfigSources).IsEnabled);
        var merger = new DotNetAppHostNuGetConfigMerger(service);
 
        if (requiresEdit && !clearsDisabledLocally)
        {
            var error = await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
                merger.PrepareAsync(directory, configuration, createIfMissing: true, globalPackagesFolder: null,
                    TestContext.Current.CancellationToken));
            Assert.Equal(string.Format(System.Globalization.CultureInfo.CurrentCulture,
                UpdateCommandStrings.ChannelFeedAlreadyConfiguredFormat, target.FullName, "aspire-test"), error.Message);
        }
        else
        {
            var candidate = await merger.PrepareAsync(
                directory, configuration, createIfMissing: true, globalPackagesFolder: null,
                TestContext.Current.CancellationToken);
            Assert.Equal(requiresEdit, candidate is not null);
            if (candidate is not null)
            {
                Assert.Equal(
                    originalDisabled,
                    XDocument.Parse(candidate.GetProposedDocument().OuterXml).Root!.Element("disabledPackageSources")!.ToString());
            }
        }
 
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    public async Task PrepareAsync_LocalChannelSourceDoesNotTakeOwnershipOfInheritedMappings(
        bool hasLocalMapping, bool clearsMappings)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://feed.example/v3/index.json";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources><clear /></packageSources>
              <packageSourceMapping>
                <packageSource key="aspire-test"><package pattern="Private.*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var localMapping = hasLocalMapping
            ? """<packageSource key="aspire-test"><package pattern="Local.*" /></packageSource>"""
            : string.Empty;
        var target = await WriteConfigAsync(directory, $$"""
            <configuration>
              <packageSources><add key="aspire-test" value="{{source}}" /></packageSources>
              <packageSourceMapping>
                {{(clearsMappings ? "<clear />" : "")}}
                {{localMapping}}
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
        var channel = CreateChannel([new("Aspire*", source)]);
 
        if (!hasLocalMapping && !clearsMappings)
        {
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Private.Library"));
            await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
                DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                    directory, channel, createIfMissing: true, TestContext.Current.CancellationToken));
            Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Private.Library"));
        }
        else
        {
            var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                directory, channel, createIfMissing: true, TestContext.Current.CancellationToken);
            Assert.Equal(hasLocalMapping, candidate is not null);
            if (candidate is not null)
            {
                await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
            }
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting"));
            Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Local.Library"));
        }
 
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_CreatesConfigFromMappings_WhenNoExistingConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.CreateDirectory("apphost");
        await WriteConfigAsync(workspace.WorkspaceRoot, "<configuration><packageSources><clear /></packageSources></configuration>");
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example"),
            new PackageMapping("CommunityToolkit*", "https://feed2.example")
        };
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, mappings).DefaultTimeout();
 
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.True(File.Exists(targetConfigPath));
 
        await Verify(XDocument.Load(targetConfigPath).ToString(), "xml");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_GeneratesConfigFromMappings_WhenChannelProvided()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.CreateDirectory("apphost");
        await WriteConfigAsync(workspace.WorkspaceRoot, "<configuration><packageSources><clear /></packageSources></configuration>");
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example"),
            new PackageMapping("CommunityToolkit*", "https://feed2.example")
        };
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, mappings).DefaultTimeout();
 
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.True(File.Exists(targetConfigPath));
 
        var xml = XDocument.Load(targetConfigPath);
        var packageSources = xml.Root!.Element("packageSources")!;
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://feed1.example");
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://feed2.example");
 
        var psm = xml.Root!.Element("packageSourceMapping");
        Assert.Null(psm);
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_AddsMissingSources_WhenUpdatingExistingConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Existing config with one source only
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="aspire-test" value="https://feed1.example" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="aspire-test">
                        <package pattern="Aspire.*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example"),
            new PackageMapping("Microsoft.*", "https://feed2.example") // feed2 missing
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://feed2.example");
 
        // Ensure existing mapping retained
        var psm = xml.Root!.Element("packageSourceMapping")!;
        Assert.NotNull(psm.Elements("packageSource").First().Elements("package").FirstOrDefault(p => (string?)p.Attribute("pattern") == "Aspire.*"));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_PreservesUserMappingsForSelectedPattern()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Existing config: pattern Lib.* mapped to old source only
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="https://old.example" value="https://old.example" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="https://old.example">
                        <package pattern="Lib.*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Lib.*", "https://new.example")
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
        Assert.Equal(["https://old.example", "https://new.example"],
            packageSources.Elements("add").Select(e => (string?)e.Attribute("value")));
        Assert.Equal(["https://new.example", "https://old.example"],
            NuGetTestHelper.GetEligiblePackageSources(root.FullName, "Lib.Example"));
        await Verify(xml.ToString(), "xml");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_RetiresStagingPolicyWithoutAddingStableMappings()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
        const string stagingSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/aspire-staging/nuget/v3/index.json";
        const string stableSource = "https://api.nuget.org/v3/index.json";
 
        await WriteConfigAsync(root,
            $$"""
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="aspire-staging" value="{{stagingSource}}" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="aspire-staging">
                        <package pattern="Aspire.*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("*", stableSource)
        };
 
        var channel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Stable, PackageChannelQuality.Both, mappings, new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var sources = NuGetTestHelper.CreateClient().GetSettings(root.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]).Sources;
        Assert.Equal(["nuget.org"], sources.Where(static source => source.IsEnabled).Select(static source => source.Name));
 
        Assert.Null(xml.Root!.Element("packageSourceMapping"));
        Assert.Equal([stableSource], NuGetTestHelper.GetEligiblePackageSources(root.FullName, "Aspire.Hosting"));
        Assert.Equal([stableSource], NuGetTestHelper.GetEligiblePackageSources(root.FullName, "Example.Dependency"));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_PreservesUnmappedSources_WhenMappingAbsent()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Existing config without packageSourceMapping
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <clear />
                    <add key="aspire-test" value="https://feed1.example" />
                    <add key="aspire-test-0" value="https://feed2.example" />
                </packageSources>
            </configuration>
            """);
 
        var target = Path.Combine(root.FullName, "nuget.config");
        var original = await File.ReadAllBytesAsync(target);
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example"),
            new PackageMapping("Microsoft.*", "https://feed2.example")
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        Assert.Equal(original, await File.ReadAllBytesAsync(target));
        var xml = XDocument.Load(target);
        var psm = xml.Root!.Element("packageSourceMapping");
        Assert.Null(psm);
        Assert.Equal(["https://feed1.example", "https://feed2.example"],
            NuGetTestHelper.GetEligiblePackageSources(root.FullName, "Other.Dependency"));
    }
 
    [Theory]
    [InlineData(false, false, false)]
    [InlineData(false, true, false)]
    [InlineData(false, true, true)]
    [InlineData(true, false, false)]
    [InlineData(true, true, false)]
    [InlineData(true, true, true)]
    public async Task CreateOrUpdateAsync_PreservesEffectiveMappingMode(
        bool parentHasMapping, bool hasLocalMappingSection, bool clearsMappings)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string companySource = "https://company.example/v3/index.json";
        const string channelSource = "https://selected.example/v3/index.json";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear /><add key="company" value="{{companySource}}" /></packageSources>
              {{(parentHasMapping ? """<packageSourceMapping><packageSource key="company"><package pattern="*" /></packageSource></packageSourceMapping>""" : "")}}
            </configuration>
            """);
        var parentBytes = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var mappingSection = hasLocalMappingSection
            ? $"<packageSourceMapping>{(clearsMappings ? "<clear />" : "")}</packageSourceMapping>"
            : "";
        var target = await WriteConfigAsync(directory, $"<configuration>{mappingSection}</configuration>");
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(
            directory, CreateChannel([new("Aspire*", channelSource)])).DefaultTimeout();
 
        var mappingEnabled = parentHasMapping && !clearsMappings;
        var document = XDocument.Load(target.FullName);
        Assert.Equal([channelSource], document.Root!.Element("packageSources")!.Elements("add")
            .Select(static source => (string?)source.Attribute("value")));
        Assert.Equal(mappingEnabled ? 1 : 0,
            document.Root.Element("packageSourceMapping")?.Elements("packageSource").Count() ?? 0);
        Assert.Equal(clearsMappings ? 1 : 0,
            document.Root.Element("packageSourceMapping")?.Elements("clear").Count() ?? 0);
        Assert.Equal(mappingEnabled ? [channelSource] : [companySource, channelSource],
            NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting.Redis"));
        Assert.Equal(mappingEnabled ? [companySource] : [companySource, channelSource],
            NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Other.Dependency"));
        Assert.Equal(parentBytes, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Fact]
    public void HasMissingSources_ReturnsTrue_WhenConfigAbsent()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
        var mappings = new[] { new PackageMapping("Aspire.*", "https://feed.example") };
        var channel = CreateChannel(mappings);
        Assert.True(DotNetAppHostNuGetConfigTestHelper.HasMissingSources(root, channel));
    }
 
    [Fact]
    public async Task HasMissingSources_ReturnsTrue_WhenPatternMappedToWrongSource()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="https://feed1.example" value="https://feed1.example" />
                    <add key="https://feed2.example" value="https://feed2.example" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="https://feed1.example">
                        <package pattern="Aspire.*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed2.example") // should be feed2, but config has feed1
        };
 
        var channel = CreateChannel(mappings);
        Assert.True(DotNetAppHostNuGetConfigTestHelper.HasMissingSources(root, channel));
    }
 
    [Fact]
    public async Task HasMissingSources_ReturnsFalse_WhenAllSourcesAndMappingsPresent()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="https://feed1.example" value="https://feed1.example" />
                    <add key="https://feed2.example" value="https://feed2.example" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="https://feed1.example">
                        <package pattern="Aspire.*" />
                    </packageSource>
                    <packageSource key="https://feed2.example">
                        <package pattern="Microsoft.*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example"),
            new PackageMapping("Microsoft.*", "https://feed2.example")
        };
 
        var channel = CreateChannel(mappings);
        Assert.False(DotNetAppHostNuGetConfigTestHelper.HasMissingSources(root, channel));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_DoesNotPersistDefaultSourceMappings()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="feed1" value="https://feed1.example" />
                    <add key="feed2" value="https://feed2.example" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="feed1">
                        <package pattern="*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("*", "https://feed1.example"),
            new PackageMapping("*", "https://feed2.example")
        };
        var configPath = Path.Combine(root.FullName, "nuget.config");
        var original = await File.ReadAllBytesAsync(configPath);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, mappings).DefaultTimeout();
 
        var document = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var mappedSources = document.Descendants("packageSourceMapping")
            .Elements("packageSource")
            .Where(element => element.Elements("package").Any(package => package.Attribute("pattern")?.Value == "*"))
            .Select(element => element.Attribute("key")!.Value)
            .ToArray();
 
        Assert.Equal(["feed1"], mappedSources);
        Assert.Equal(original, await File.ReadAllBytesAsync(configPath));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_IntegrationConfigurationDoesNotCaptureUserDefaultAliases()
    {
        using var generator = TemporaryWorkspace.CreateForCli(_outputHelper);
        using var consumer = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string generatorConfig = """
            <configuration>
              <packageSources><clear /><add key="company-public" value="https://api.nuget.org/v3/index.json" /></packageSources>
              <packageSourceMapping><packageSource key="company-public"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """;
        const string consumerConfig = """
            <configuration>
              <packageSources><clear /><add key="nuget.org" value="https://api.nuget.org/v3/index.json" /></packageSources>
              <packageSourceMapping><packageSource key="nuget.org"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """;
        var generatorParent = await WriteConfigAsync(generator.WorkspaceRoot, generatorConfig);
        var consumerParent = await WriteConfigAsync(consumer.WorkspaceRoot, consumerConfig);
        var output = generator.CreateDirectory("apphost");
        var destination = consumer.CreateDirectory("apphost");
        const string channelSource = "https://channel.example/v3/index.json";
        var channel = CreateChannel([new("Aspire*", channelSource), new("*", PackageSources.NuGetOrg)]);
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(output, channel);
        var generated = await File.ReadAllBytesAsync(Path.Combine(output.FullName, "nuget.config"));
        await File.WriteAllBytesAsync(Path.Combine(destination.FullName, "nuget.config"), generated);
 
        Assert.Equal([channelSource], NuGetTestHelper.GetEligiblePackageSources(destination.FullName, "Aspire.Hosting"));
        Assert.Equal([PackageSources.NuGetOrg], NuGetTestHelper.GetEligiblePackageSources(destination.FullName, "Microsoft.Extensions.Options"));
        Assert.Equal(generatorConfig, await File.ReadAllTextAsync(generatorParent.FullName));
        Assert.Equal(consumerConfig, await File.ReadAllTextAsync(consumerParent.FullName));
        await Verify(XDocument.Load(Path.Combine(output.FullName, "nuget.config")).ToString(), "xml");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_RefusesMultipleUserOwnedAliasesForChannelFeed()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
        const string sourceUrl = "https://private.example/v3/index.json";
 
        await WriteConfigAsync(root,
            $$"""
            <configuration>
              <packageSources>
                <add key="authenticated" value="{{sourceUrl}}" />
                <add key="anonymousAlias" value="{{sourceUrl}}" />
              </packageSources>
              <packageSourceCredentials>
                <authenticated>
                  <add key="Username" value="user" />
                  <add key="ClearTextPassword" value="secret" />
                </authenticated>
              </packageSourceCredentials>
              <packageSourceMapping>
                <packageSource key="authenticated"><package pattern="Private.*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var mappings = new[]
        {
            new PackageMapping("Aspire*", sourceUrl)
        };
        var channel = CreateChannel(mappings);
        var configPath = Path.Combine(root.FullName, "nuget.config");
        var original = await File.ReadAllBytesAsync(configPath);
 
        await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel));
 
        Assert.Equal(original, await File.ReadAllBytesAsync(configPath));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_PreservesAmbientSourceKeysWithoutPersistingDefaultMappings()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Existing config with custom key names (like "nuget" instead of URL)
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <clear />
                    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
                    <add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
                </packageSources>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://example.com/aspire-feed"),
            new PackageMapping("*", "https://api.nuget.org/v3/index.json")
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        // Existing sources should still be present with their original keys
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget" && (string?)e.Attribute("value") == "https://api.nuget.org/v3/index.json");
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "dotnet9" && (string?)e.Attribute("value") == "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json");
 
        // New source should be added
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://example.com/aspire-feed");
 
        Assert.Null(xml.Root.Element("packageSourceMapping"));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_DoesNotSynthesizeMappingsForExistingSources()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Scenario from @mitchdenny: config has multiple sources but NO packageSourceMapping
        // This means all sources can serve all packages (implicit behavior)
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <clear />
                    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
                    <add key="custom" value="https://example.com/custom/nuget/v3/index.json" />
                </packageSources>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire*", "https://example.com/aspire-daily")
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        // All original sources should still be present
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget.org");
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "custom");
 
        // New aspire source should be added
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://example.com/aspire-daily");
 
        Assert.Null(xml.Root.Element("packageSourceMapping"));
        Assert.Equal(
            ["https://api.nuget.org/v3/index.json", "https://example.com/aspire-daily", "https://example.com/custom/nuget/v3/index.json"],
            NuGetTestHelper.GetEligiblePackageSources(root.FullName, "Other.Dependency"));
        await Verify(xml.ToString(), "xml");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_AddsSpecificMappings_WhenExistingWildcardMappingPresent()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Scenario: existing config already has a wildcard mapping on nuget.org
        // When we add explicit mappings for Aspire packages to a new source,
        // the code should add the new mappings without interfering with the existing wildcard
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <clear />
                    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="nuget.org">
                        <package pattern="*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        // aspire update adds specific mappings for Aspire packages to a new channel
        var mappings = new[]
        {
            new PackageMapping("Aspire*", "https://example.com/aspire-daily"),
            new PackageMapping("Microsoft.Extensions.ServiceDiscovery*", "https://example.com/aspire-daily")
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        // Original source should still be present
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("key") == "nuget.org");
 
        // New aspire source should be added
        Assert.Contains(packageSources.Elements("add"), e => (string?)e.Attribute("value") == "https://example.com/aspire-daily");
 
        // Debug: Print the XML to understand what's happening
        _outputHelper.WriteLine("Generated XML:");
        _outputHelper.WriteLine(xml.ToString());
 
        // Package source mapping should have both the original wildcard and the new specific mappings
        var psm = xml.Root!.Element("packageSourceMapping")!;
 
        // Original nuget.org should still have the wildcard pattern
        var nugetMapping = psm.Elements("packageSource").FirstOrDefault(ps => (string?)ps.Attribute("key") == "nuget.org");
        Assert.NotNull(nugetMapping);
        Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*");
 
        // The aspire source should have its specific patterns
        var aspireMapping = psm.Elements("packageSource").FirstOrDefault(ps => (string?)ps.Attribute("key") == "aspire-test");
        Assert.NotNull(aspireMapping);
        Assert.Contains(aspireMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Aspire*");
        Assert.Contains(aspireMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Microsoft.Extensions.ServiceDiscovery*");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_DoesNotAddWildcardToPrivateSourceWithExistingPatterns()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // User has nuget.org with wildcard and a private source with specific patterns
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
                    <add key="github" value="https://nuget.pkg.github.com/myorg/index.json" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="nuget.org">
                        <package pattern="*" />
                    </packageSource>
                    <packageSource key="github">
                        <package pattern="myorg.*" />
                        <package pattern="other" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        // Stable channel: maps everything to nuget.org
        var mappings = new[]
        {
            new PackageMapping("*", "https://api.nuget.org/v3/index.json")
        };
 
        var channel = PackageChannel.CreateExplicitChannel(
            PackageChannelNames.Stable, PackageChannelQuality.Stable, mappings,
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var psm = xml.Root!.Element("packageSourceMapping")!;
 
        // nuget.org should retain the wildcard
        var nugetMapping = psm.Elements("packageSource")
            .FirstOrDefault(ps => (string?)ps.Attribute("key") == "nuget.org");
        Assert.NotNull(nugetMapping);
        Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*");
 
        // The private source should keep its original patterns without a wildcard being added
        var githubMapping = psm.Elements("packageSource")
            .FirstOrDefault(ps => (string?)ps.Attribute("key") == "github");
        Assert.NotNull(githubMapping);
        Assert.Contains(githubMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "myorg.*");
        Assert.Contains(githubMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "other");
        Assert.DoesNotContain(githubMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*");
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_RemovesUnrequiredSources_InsteadOfAddingWildcardPattern()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Existing config with a PR hive source that should be removed and a user-defined source that should be preserved
        await WriteConfigAsync(root,
            """
            <?xml version="1.0"?>
            <configuration>
                <packageSources>
                    <add key="https://valid.example" value="https://valid.example" />
                    <add key="C:\Users\user\.aspire\hives\invalid-pr" value="C:\Users\user\.aspire\hives\invalid-pr" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="https://valid.example">
                        <package pattern="ValidPkg*" />
                    </packageSource>
                    <packageSource key="C:\Users\user\.aspire\hives\invalid-pr">
                        <package pattern="Aspire*" />
                        <package pattern="Microsoft.Extensions.ServiceDiscovery*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        // Remap the CLI-owned hive's patterns to a new channel feed.
        const string selectedSource = "https://new.example/v3/index.json";
        var mappings = new[]
        {
            new PackageMapping("Aspire*", selectedSource),
            new PackageMapping("Microsoft.Extensions.ServiceDiscovery*", selectedSource),
            new PackageMapping("*", selectedSource)
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        // The PR hive source should be removed because it's safe to remove and no longer needed
        Assert.DoesNotContain(packageSources.Elements("add"),
            e => (string?)e.Attribute("value") == "C:\\Users\\user\\.aspire\\hives\\invalid-pr");
 
        // The user-defined source should be preserved even though its patterns were remapped
        Assert.Contains(packageSources.Elements("add"),
            e => (string?)e.Attribute("value") == "https://valid.example");
 
        var sources = NuGetTestHelper.CreateClient().GetSettings(root.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]).Sources;
        Assert.Contains(sources, static source => source.Name == "aspire-test" && source.IsEnabled);
 
        var psm = xml.Root!.Element("packageSourceMapping")!;
 
        // The PR hive source should not have any mapping entries (removed entirely)
        Assert.DoesNotContain(psm.Elements("packageSource"),
            ps => (string?)ps.Attribute("key") == "C:\\Users\\user\\.aspire\\hives\\invalid-pr");
 
        // The user-defined source should keep its original patterns without a wildcard being added
        var validExampleMapping = psm.Elements("packageSource")
            .FirstOrDefault(ps => (string?)ps.Attribute("key") == "https://valid.example");
        Assert.NotNull(validExampleMapping);
        Assert.Contains(validExampleMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "ValidPkg*");
        Assert.DoesNotContain(validExampleMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "*");
 
        // The new channel feed gets only the integration-specific patterns.
        var nugetMapping = psm.Elements("packageSource")
            .FirstOrDefault(ps => (string?)ps.Attribute("key") == "aspire-test");
        Assert.NotNull(nugetMapping);
        Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Aspire*");
        Assert.Contains(nugetMapping.Elements("package"), p => (string?)p.Attribute("pattern") == "Microsoft.Extensions.ServiceDiscovery*");
        Assert.Equal(
            ["Aspire*", "Microsoft.Extensions.ServiceDiscovery*"],
            nugetMapping.Elements("package").Select(p => (string)p.Attribute("pattern")!));
 
        // There should be two packageSource elements (the channel feed and valid.example).
        Assert.Equal(2, psm.Elements("packageSource").Count());
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_RemovesOldPrHive_WhenSwitchingBetweenPrHives()
    {
        // Reproduces the scenario reported on `aspire update --channel pr-<new>` when the
        // previous channel was also a PR hive: switching between two `~/.aspire/hives/pr-*/packages`
        // sources must remove the old source from <packageSources>, not just from
        // <packageSourceMapping>. If the stale path lingers, subsequent `dotnet restore`
        // fails with NU1301 when that hive directory has since been deleted/cleaned.
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        var oldHive = workspace.CreateDirectory(".aspire/hives/pr-17182/packages").FullName;
        var newHive = workspace.CreateDirectory(".aspire/hives/pr-17192/packages").FullName;
 
        await WriteConfigAsync(root,
            $"""
            <?xml version="1.0" encoding="utf-8"?>
            <configuration>
                <packageSources>
                    <add key="{oldHive}" value="{oldHive}" />
                    <add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
                </packageSources>
                <packageSourceMapping>
                    <packageSource key="{oldHive}">
                        <package pattern="Aspire*" />
                    </packageSource>
                    <packageSource key="https://api.nuget.org/v3/index.json">
                        <package pattern="*" />
                    </packageSource>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire*", newHive),
            new PackageMapping("*", "https://api.nuget.org/v3/index.json"),
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        Assert.DoesNotContain(packageSources.Elements("add"),
            e => string.Equals((string?)e.Attribute("value"), oldHive, StringComparison.Ordinal));
        Assert.Contains(packageSources.Elements("add"),
            e => string.Equals((string?)e.Attribute("value"), newHive, StringComparison.Ordinal));
 
        var psm = xml.Root!.Element("packageSourceMapping")!;
        Assert.DoesNotContain(psm.Elements("packageSource"),
            ps => string.Equals((string?)ps.Attribute("key"), oldHive, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_RemovesOldPrHive_WhenItHasNoMappingElement()
    {
        // This is the *real* shape of the regression reported on pr-17192 follow-up:
        // the AppHost-level nuget.config had pr-17182 listed in <packageSources> but the
        // <packageSourceMapping> contained no entry for that source at all. Because
        // RemoveEmptyPackageSourceElements only cleans up entries whose mapping element
        // became empty *during the merge*, a source that never had a mapping element
        // survives the merge and breaks subsequent `dotnet restore` once the hive
        // directory is deleted.
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        const string oldHive = "/Users/midenn/.aspire/hives/pr-17182/packages";
        const string newHive = "/Users/midenn/.aspire/hives/pr-17192/packages";
 
        await WriteConfigAsync(root,
            $"""
            <?xml version="1.0" encoding="utf-8"?>
            <configuration>
                <packageSources>
                    <add key="{oldHive}" value="{oldHive}" />
                </packageSources>
                <packageSourceMapping>
                </packageSourceMapping>
            </configuration>
            """);
 
        var mappings = new[]
        {
            new PackageMapping("Aspire*", newHive),
            new PackageMapping("*", "https://api.nuget.org/v3/index.json"),
        };
 
        var channel = CreateChannel(mappings);
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        var xml = XDocument.Load(Path.Combine(root.FullName, "nuget.config"));
        var packageSources = xml.Root!.Element("packageSources")!;
 
        Assert.DoesNotContain(packageSources.Elements("add"),
            e => string.Equals((string?)e.Attribute("value"), oldHive, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_CallbackInvokedForNewConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example")
        };
 
        var channel = CreateChannel(mappings);
 
        bool callbackInvoked = false;
        FileInfo? callbackTargetFile = null;
        XmlDocument? callbackOriginalContent = null;
        XmlDocument? callbackProposedContent = null;
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) =>
        {
            callbackInvoked = true;
            callbackTargetFile = targetFile;
            callbackOriginalContent = originalContent;
            callbackProposedContent = proposedContent;
            return Task.FromResult(true); // Proceed with the update
        });
 
        // Verify callback was invoked
        Assert.True(callbackInvoked);
        Assert.NotNull(callbackTargetFile);
        Assert.Null(callbackOriginalContent); // Should be null for new files
        Assert.NotNull(callbackProposedContent);
 
        // Verify file was created
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.True(File.Exists(targetConfigPath));
        Assert.Equal(targetConfigPath, callbackTargetFile.FullName);
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_CallbackCanPreventNewConfigCreation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example")
        };
 
        var channel = CreateChannel(mappings);
 
        bool callbackInvoked = false;
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) =>
        {
            callbackInvoked = true;
            return Task.FromResult(false); // Prevent the update
        });
 
        // Verify callback was invoked
        Assert.True(callbackInvoked);
 
        // Verify file was NOT created
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.False(File.Exists(targetConfigPath));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_CallbackInvokedForExistingConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Create an existing config
        var existingConfig = """
            <?xml version="1.0" encoding="utf-8"?>
            <configuration>
              <packageSources>
                <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
              </packageSources>
            </configuration>
            """;
 
        await WriteConfigAsync(root, existingConfig).DefaultTimeout();
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example")
        };
 
        var channel = CreateChannel(mappings);
 
        bool callbackInvoked = false;
        FileInfo? callbackTargetFile = null;
        XmlDocument? callbackOriginalContent = null;
        XmlDocument? callbackProposedContent = null;
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) =>
        {
            callbackInvoked = true;
            callbackTargetFile = targetFile;
            callbackOriginalContent = originalContent;
            callbackProposedContent = proposedContent;
            return Task.FromResult(true); // Proceed with the update
        });
 
        // Verify callback was invoked
        Assert.True(callbackInvoked);
        Assert.NotNull(callbackTargetFile);
        Assert.NotNull(callbackOriginalContent); // Should have original content for existing files
        Assert.NotNull(callbackProposedContent);
 
        // Verify file exists and was updated
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.True(File.Exists(targetConfigPath));
        Assert.Equal(targetConfigPath, callbackTargetFile.FullName);
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_CallbackCanPreventExistingConfigUpdate()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        // Create an existing config
        var existingConfig = """
            <?xml version="1.0" encoding="utf-8"?>
            <configuration>
              <packageSources>
                <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
              </packageSources>
            </configuration>
            """;
 
        await WriteConfigAsync(root, existingConfig).DefaultTimeout();
        var originalContent = await File.ReadAllTextAsync(Path.Combine(root.FullName, "nuget.config")).DefaultTimeout();
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example")
        };
 
        var channel = CreateChannel(mappings);
 
        bool callbackInvoked = false;
 
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel, (targetFile, originalContent, proposedContent, cancellationToken) =>
        {
            callbackInvoked = true;
            return Task.FromResult(false); // Prevent the update
        });
 
        // Verify callback was invoked
        Assert.True(callbackInvoked);
 
        // Verify file content was NOT changed
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        var currentContent = await File.ReadAllTextAsync(targetConfigPath);
        Assert.Equal(NormalizeLineEndings(originalContent), NormalizeLineEndings(currentContent));
    }
 
    [Fact]
    public async Task CreateOrUpdateAsync_WorksWithoutCallback()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var root = workspace.WorkspaceRoot;
 
        var mappings = new[]
        {
            new PackageMapping("Aspire.*", "https://feed1.example")
        };
 
        var channel = CreateChannel(mappings);
 
        // Call without callback - should work as before
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(root, channel).DefaultTimeout();
 
        // Verify file was created
        var targetConfigPath = Path.Combine(root.FullName, "nuget.config");
        Assert.True(File.Exists(targetConfigPath));
    }
 
    [Fact]
    public async Task PrepareAsync_DefersExistingConfigChanges()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var original = """
            <configuration>
              <packageSources>
                <add key="private" value="https://private.example/v3/index.json" protocolVersion="3" />
              </packageSources>
              <packageSourceCredentials>
                <private>
                  <add key="Username" value="test-user" />
                  <add key="ClearTextPassword" value="test-password" />
                </private>
              </packageSourceCredentials>
            </configuration>
            """;
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, original);
        var channel = CreateChannel([new PackageMapping("Aspire*", "https://feed.example/v3/index.json")]);
 
        var update = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, channel, createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.NotNull(update);
        Assert.Equal(target.FullName, update.TargetFile.FullName);
        Assert.Equal(original, await File.ReadAllTextAsync(target.FullName));
        Assert.Equal(XDocument.Parse(original).ToString(), XDocument.Parse(update.GetOriginalDocument()!.OuterXml).ToString());
        await Verify(XDocument.Parse(update.GetProposedDocument().OuterXml).ToString(), "xml");
    }
 
    [Fact]
    public async Task PrepareAsync_DoesNotCreateTargetDirectory()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var directory = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "new-config"));
        var channel = CreateChannel([new PackageMapping("Aspire*", "https://feed.example/v3/index.json")]);
 
        var update = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, channel, createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.NotNull(update);
        Assert.Null(update.OriginalContent);
        Assert.False(Directory.Exists(directory.FullName));
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(update, TestContext.Current.CancellationToken);
        Assert.Equal(XDocument.Parse(update.GetProposedDocument().OuterXml).ToString(), XDocument.Load(update.TargetFile.FullName).ToString());
    }
 
    [Fact]
    public async Task PrepareAsync_NoCreatePolicyLeavesMissingConfigAbsent()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var directory = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "new-config"));
        var channel = CreateChannel([new PackageMapping("*", "https://feed.example/v3/index.json")]);
 
        var update = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, channel, createIfMissing: false, TestContext.Current.CancellationToken);
 
        Assert.Null(update);
        Assert.False(Directory.Exists(directory.FullName));
    }
 
    [Fact]
    public async Task PrepareAsync_EquivalentPolicyPreservesExistingBytes()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <!-- Preserve formatting and unrelated mappings on a no-op update. -->
              <packageSources>
                <clear />
                <add key="company" value="https://feed.example/v3/index.json" />
              </packageSources>
              <packageSourceMapping>
                <clear />
                <packageSource key="company">
                  <package pattern="Company.*" />
                  <package pattern="ASPIRE*" />
                </packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, CreateChannel([new("Aspire*", "https://feed.example/v3/index.json")]),
            createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.Null(candidate);
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
    }
 
    [Fact]
    public async Task PrepareAsync_EquivalentInheritedPolicyDoesNotCreateChildConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources>
                <clear />
                <add key="company" value="https://feed.example/v3/index.json" />
              </packageSources>
              <packageSourceMapping>
                <clear />
                <packageSource key="company">
                  <package pattern="Aspire*" />
                </packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var appHostDirectory = workspace.CreateDirectory("apphost");
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            appHostDirectory, CreateChannel([new("Aspire*", "https://feed.example/v3/index.json")]),
            createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.Null(candidate);
        Assert.Empty(appHostDirectory.EnumerateFiles());
    }
 
    [Theory]
    [InlineData(false, false, false, true)]
    [InlineData(false, false, false, false)]
    [InlineData(true, false, false, false)]
    [InlineData(true, true, false, false)]
    [InlineData(true, false, true, false)]
    [InlineData(true, true, true, false)]
    [InlineData(true, false, false, true)]
    [InlineData(true, true, false, true)]
    public async Task PrepareAsync_ChannelCleanupOnlyChangesLocalEntries(
        bool locallyDefined, bool clearSources, bool clearMappings, bool hasUnrelatedPattern)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string oldSource = "https://old.example/v3/index.json";
        const string newSource = "https://new.example/v3/index.json";
        var oldMapping = $$"""
            <packageSource key="aspire-staging">
              <package pattern="Aspire.Hosting.Redis" />
              {{(hasUnrelatedPattern ? """<package pattern="Private.*" />""" : "")}}
            </packageSource>
            """;
        const string oldSourceEntry = """<add key="aspire-staging" value="https://old.example/v3/index.json" />""";
        const string companySource = """<add key="company" value="https://company.example/v3/index.json" />""";
        const string companyMapping = """<packageSource key="company"><package pattern="*" /></packageSource>""";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear />{{oldSourceEntry}}{{companySource}}</packageSources>
              <packageSourceMapping>{{oldMapping}}{{companyMapping}}</packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var target = await WriteConfigAsync(directory, $$"""
            <configuration>
              <packageSources>
                {{(clearSources ? "<clear />" + companySource : "")}}
                {{(locallyDefined ? oldSourceEntry : "")}}
              </packageSources>
              <packageSourceMapping>
                {{(clearMappings ? "<clear />" + companyMapping : "")}}
                {{(locallyDefined ? oldMapping : "")}}
              </packageSourceMapping>
            </configuration>
            """);
        var channel = CreateChannel([new("Aspire*", newSource)]);
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, channel, createIfMissing: true, TestContext.Current.CancellationToken);
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
        var document = XDocument.Load(target.FullName);
        var expectedLocalSourceKeys = new List<string> { "aspire-test" };
        if (clearSources)
        {
            expectedLocalSourceKeys.Add("company");
        }
        if (locallyDefined && hasUnrelatedPattern)
        {
            expectedLocalSourceKeys.Add("aspire-staging");
        }
        Assert.Equal(expectedLocalSourceKeys.Order(StringComparer.Ordinal),
            document.Root!.Element("packageSources")!.Elements("add")
                .Select(static source => source.Attribute("key")!.Value).Order(StringComparer.Ordinal));
        var localOldMappings = document.Root.Element("packageSourceMapping")!.Elements("packageSource")
            .Where(static mapping => mapping.Attribute("key")?.Value == "aspire-staging")
            .SelectMany(static mapping => mapping.Elements("package"))
            .Select(static pattern => pattern.Attribute("pattern")!.Value);
        Assert.Equal(locallyDefined && hasUnrelatedPattern ? ["Private.*"] : [], localOldMappings);
        var inheritsOldMapping = !clearMappings && !(locallyDefined && hasUnrelatedPattern);
        string[] expectedAspireSources = !inheritsOldMapping ? [newSource] : clearSources ? [] : [oldSource];
        Assert.Equal(expectedAspireSources,
            NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting.Redis"));
 
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Theory]
    [InlineData(false, null, null)]
    [InlineData(true, null, null)]
    [InlineData(false, "aspire-staging", "true")]
    [InlineData(true, "aspire-staging", "true")]
    [InlineData(false, "aspire-staging", "false")]
    [InlineData(true, "aspire-staging", "false")]
    [InlineData(false, "ASPIRE-STAGING", "true")]
    [InlineData(true, "ASPIRE-STAGING", "true")]
    public async Task CreatePersistentNuGetConfigContentAsync_StableCleanupRemovesLocalRecordsRegardlessOfParent(
        bool hasMappings, string? disabledKey, string? disabledValue)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var disabled = disabledKey is null ? "" : $"""<disabledPackageSources><add key="{disabledKey}" value="{disabledValue}" /></disabledPackageSources>""";
        const string oldSource = """<add key="aspire-staging" value="https://old.example/v3/index.json" />""";
        const string oldMapping = """<packageSource key="aspire-staging"><package pattern="Aspire.Hosting.Redis" /></packageSource>""";
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear />{{oldSource}}</packageSources>
              {{disabled}}
              {{(hasMappings ? "<packageSourceMapping>" + oldMapping + "</packageSourceMapping>" : "")}}
            </configuration>
            """);
        var parentBytes = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var target = await WriteConfigAsync(directory, $$"""
            <configuration>
              <packageSources>
                {{oldSource}}
                <add key="user" value="https://private.example/v3/index.json" />
              </packageSources>
              <disabledPackageSources><add key="future-private" value="true" /></disabledPackageSources>
              {{(hasMappings ? "<packageSourceMapping>" + oldMapping + """<packageSource key="user"><package pattern="User.*" /></packageSource></packageSourceMapping>""" : "")}}
            </configuration>
            """);
        var originalDisabled = XDocument.Load(target.FullName).Root!.Element("disabledPackageSources")!.ToString();
        var channel = PackageChannel.CreateExplicitChannel(
            PackageChannelNames.Stable, PackageChannelQuality.Stable, null,
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
        var original = await File.ReadAllBytesAsync(target.FullName);
        var service = NuGetTestHelper.CreateService();
        var configuration = service.BuildChannelConfiguration(
            directory, "test", channel, null, null, TestContext.Current.CancellationToken);
 
        var content = await new DotNetAppHostNuGetConfigMerger(service).CreatePersistentNuGetConfigContentAsync(
            configuration, target, original, globalPackagesFolder: null, TestContext.Current.CancellationToken);
 
        using var stream = new MemoryStream(content);
        var document = XDocument.Load(stream);
        Assert.Equal(["user"], document.Root!.Element("packageSources")!.Elements("add")
            .Select(static source => source.Attribute("key")!.Value));
        Assert.Equal(hasMappings ? ["user"] : [],
            document.Root.Element("packageSourceMapping")?.Elements("packageSource")
                .Select(static mapping => mapping.Attribute("key")!.Value) ?? []);
        Assert.Equal(originalDisabled, document.Root.Element("disabledPackageSources")!.ToString());
        Assert.Equal(parentBytes, await File.ReadAllBytesAsync(parent.FullName));
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
    }
 
    [Fact]
    public async Task PrepareAsync_InheritedOldMappingDoesNotBlockByteIdenticalNoOp()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources><clear /><add key="aspire-staging" value="https://old.example/v3/index.json" /></packageSources>
              <packageSourceMapping>
                <packageSource key="aspire-staging"><package pattern="Aspire.Hosting.Redis" /><package pattern="Private.*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var directory = workspace.CreateDirectory("apphost");
        var target = await WriteConfigAsync(directory, """
            <configuration>
              <packageSources><add key="aspire-test" value="https://selected.example/v3/index.json" /></packageSources>
              <packageSourceMapping><packageSource key="aspire-test"><package pattern="Aspire*" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var original = await File.ReadAllBytesAsync(target.FullName);
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, CreateChannel([new("Aspire*", "https://selected.example/v3/index.json")]),
            createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.Null(candidate);
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Fact]
    public async Task PrepareAsync_AlreadyDisabledUnmappedInheritedSourceDoesNotConflictWithNewChannel()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources>
                <clear />
                <add key="aspire-staging" value="https://old.example/v3/index.json" />
                <add key="company" value="https://company.example/v3/index.json" />
              </packageSources>
              <disabledPackageSources><add key="aspire-staging" value="true" /></disabledPackageSources>
              <packageSourceMapping><packageSource key="company"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        await WriteConfigAsync(directory, "<configuration />");
        const string source = "https://new.example/v3/index.json";
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, CreateChannel([new("Aspire*", source)]), createIfMissing: true,
            TestContext.Current.CancellationToken);
 
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
        Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting.Redis"));
        Assert.Equal(["aspire-staging"], NuGetTestHelper.CreateClient().GetSettings(
            directory.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]).DisabledPackageSourceKeys);
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task PrepareAsync_DiscoveryOnlyChangesDoNotRequireOwnershipOfUnchangedUserMapping(bool requiresEdit)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string source = "https://selected.example/v3/index.json";
        const string privateSource = "https://private.example/v3/index.json";
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="company" value="{{source}}" />
                <add key="private" value="{{privateSource}}" />
              </packageSources>
              <packageSourceMapping>
                <packageSource key="company"><package pattern="{{(requiresEdit ? "Company.*" : "Aspire*")}}" /></packageSource>
                <packageSource key="private"><package pattern="Aspire.Hosting.Redis" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
        var service = NuGetTestHelper.CreateService();
        var configuration = service.BuildAppHostConfiguration(
            workspace.WorkspaceRoot, "test", [new("Aspire*", source)], overrideIntegrationSources: false, TestContext.Current.CancellationToken);
        Assert.True(configuration.HasSourcePolicyChanges);
        var merger = new DotNetAppHostNuGetConfigMerger(service);
 
        if (requiresEdit)
        {
            await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
                merger.PrepareAsync(workspace.WorkspaceRoot, configuration, createIfMissing: true,
                    globalPackagesFolder: null, TestContext.Current.CancellationToken));
        }
        else
        {
            var candidate = await merger.PrepareAsync(
                workspace.WorkspaceRoot, configuration, createIfMissing: true, globalPackagesFolder: null,
                TestContext.Current.CancellationToken);
            Assert.Null(candidate);
        }
 
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
        Assert.Equal([privateSource], NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Aspire.Hosting.Redis"));
    }
 
    [Theory]
    [InlineData("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-old/nuget/v3/index.json")]
    [InlineData("./.aspire/hives/old/packages")]
    public async Task PrepareAsync_AlreadyDisabledInheritedFeedDoesNotTriggerRepeatedChanges(string retiredSource)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="retired" value="{{retiredSource}}" />
                <add key="company" value="https://feed.example/v3/index.json" />
              </packageSources>
              <disabledPackageSources>
                <add key="retired" value="true" />
              </disabledPackageSources>
            </configuration>
            """);
        var appHostDirectory = workspace.CreateDirectory("apphost");
        var target = await WriteConfigAsync(appHostDirectory, """
            <configuration>
              <packageSourceMapping>
                <clear />
                <packageSource key="company"><package pattern="Aspire*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            appHostDirectory, CreateChannel([new("Aspire*", "https://feed.example/v3/index.json")]),
            createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.Null(candidate);
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task PrepareAsync_PreservesDisabledSourcesWithoutNormalization(bool hasLocalSection, bool clearDisabled)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources>
                <clear />
                <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
                <add key="company" value="https://company.example/v3/index.json" />
              </packageSources>
              <disabledPackageSources>
                <add key="nuget.org" value="true" />
                <add key="parent-only" value="true" />
              </disabledPackageSources>
              <packageSourceMapping><packageSource key="company"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """);
        var originalParent = await File.ReadAllBytesAsync(parent.FullName);
        var directory = workspace.CreateDirectory("apphost");
        var disabledSection = hasLocalSection
            ? $$"""
              <disabledPackageSources>
                <!-- Keep explicit values and aliases even after their source is removed. -->
                {{(clearDisabled ? "<clear />" : "")}}
                <add key="nuget.org" value="true" />
                <add key="aspire-staging" value="false" />
                <add key="aspire-apphost-0123456789abcdef" value="true" />
              </disabledPackageSources>
              """
            : string.Empty;
        var target = await WriteConfigAsync(directory, $$"""
            <configuration>
              <packageSources><add key="aspire-staging" value="https://old.example/v3/index.json" /></packageSources>
              {{disabledSection}}
              <packageSourceMapping>
                <clear />
                <packageSource key="aspire-staging"><package pattern="Aspire*" /></packageSource>
                <packageSource key="company"><package pattern="*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var originalDisabled = XDocument.Load(target.FullName).Root!.Element("disabledPackageSources")?.ToString();
        const string source = "https://new.example/v3/index.json";
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            directory, CreateChannel([new("Aspire*", source), new("*", PackageSources.NuGetOrg)]),
            createIfMissing: true, TestContext.Current.CancellationToken);
 
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
        var proposed = XDocument.Load(target.FullName);
        Assert.Equal(originalDisabled, proposed.Root!.Element("disabledPackageSources")?.ToString());
        var settings = NuGetTestHelper.CreateClient().GetSettings(
            directory.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]);
        Assert.False(Assert.Single(settings.Sources, static item => item.Name == "nuget.org").IsEnabled);
        Assert.Equal([source], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting.Redis"));
        Assert.Equal(originalParent, await File.ReadAllBytesAsync(parent.FullName));
        await Verify(proposed.ToString(), "xml").UseParameters(hasLocalSection, clearDisabled);
    }
 
    [Theory]
    [InlineData(true, false, false)]
    [InlineData(false, true, false)]
    [InlineData(true, true, false)]
    [InlineData(true, true, true)]
    public async Task PrepareAsync_PersistencePreservesDisabledSourcesWhenRetiringLocalSources(
        bool locallyDefined, bool inheritedDefined, bool clearSources)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        const string retired = """<add key="retired" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-old/nuget/v3/index.json" />""";
        const string userSources = """
            <add key="company" value="https://company.example/v3/index.json" />
            <add key="user-disabled" value="https://private.example/v3/index.json" />
            """;
        var parent = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources><clear />{{userSources}}{{(inheritedDefined ? retired : "")}}</packageSources>
              <disabledPackageSources><add key="user-disabled" value="true" /></disabledPackageSources>
            </configuration>
            """);
        var parentBytes = await File.ReadAllBytesAsync(parent.FullName);
        var appHostDirectory = workspace.CreateDirectory("apphost");
        var target = await WriteConfigAsync(appHostDirectory, $$"""
            <configuration>
              <packageSources>{{(clearSources ? "<clear />" + userSources : "")}}{{(locallyDefined ? retired : "")}}</packageSources>
              <disabledPackageSources>
                <clear />
                <add key="user-disabled" value="true" />
                <add key="future-private" value="true" />
                <add key="aspire-apphost-0e77e1652908bf0e" value="true" />
                <add key="aspire-apphost-efc407738a41fb53-0" value="true" />
              </disabledPackageSources>
              <packageSourceMapping>
                <clear />
                <packageSource key="retired"><package pattern="Aspire*" /></packageSource>
                <packageSource key="company"><package pattern="*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            appHostDirectory, CreateChannel([new("Aspire*", "https://new.example/v3/index.json")]),
            createIfMissing: true, TestContext.Current.CancellationToken);
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
 
        var snapshot = NuGetTestHelper.CreateClient().GetSettings(
            appHostDirectory.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]);
        Assert.Equal(
            ["aspire-apphost-0e77e1652908bf0e", "aspire-apphost-efc407738a41fb53-0", "future-private", "user-disabled"],
            snapshot.DisabledPackageSourceKeys.Order(StringComparer.Ordinal));
        Assert.Equal(["https://new.example/v3/index.json"],
            NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Aspire.Hosting.Redis"));
        Assert.Equal(parentBytes, await File.ReadAllBytesAsync(parent.FullName));
        await Verify(XDocument.Load(target.FullName).ToString(), "xml")
            .UseParameters(locallyDefined, inheritedDefined, clearSources);
    }
 
    [Fact]
    public async Task PrepareAsync_ChannelRoundTripDoesNotAccumulateProjectionMasks()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources><clear /><add key="company" value="https://company.example/v3/index.json" /></packageSources>
            </configuration>
            """);
        var service = NuGetTestHelper.CreateService();
        const string prSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-pr-19763/nuget/v3/index.json";
        const string dailySource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json";
        foreach (var source in new[] { prSource, dailySource, prSource, dailySource, prSource })
        {
            var configuration = service.BuildAppHostConfiguration(
                workspace.WorkspaceRoot, "apphost-0123456789abcdef", [new("Aspire*", source)],
                overrideIntegrationSources: false,
                cancellationToken: TestContext.Current.CancellationToken);
            using var projection = await service.CreateConfigurationPreviewAsync(
                workspace.WorkspaceRoot, configuration, globalPackagesFolder: null, TestContext.Current.CancellationToken);
            Assert.Equal([source],
                NuGetTestHelper.GetEligiblePackageSources(projection.EffectiveWorkingDirectory.FullName, "Aspire.Hosting.Redis"));
            var candidate = await new DotNetAppHostNuGetConfigMerger(service).PrepareAsync(
                workspace.WorkspaceRoot, configuration, createIfMissing: true,
                globalPackagesFolder: null, TestContext.Current.CancellationToken);
            Assert.NotNull(candidate);
            await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
            Assert.Empty(service.GetNuGetSettings(workspace.Path, TestContext.Current.CancellationToken).DisabledPackageSourceKeys);
            Assert.Equal(new[] { source, "https://company.example/v3/index.json" }.Order(StringComparer.Ordinal),
                NuGetTestHelper.GetEligiblePackageSources(workspace.Path, "Aspire.Hosting.Redis"));
        }
 
        await Verify(XDocument.Load(target.FullName).ToString(), "xml");
    }
 
    [Fact]
    public async Task PrepareAsync_RefusesReenablingInheritedChannelSource()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var parentFile = await WriteConfigAsync(workspace.WorkspaceRoot, """
            <configuration>
              <packageSources>
                <clear />
                <add key="selected" value="https://selected.example/v3/index.json" />
                <add key="user-disabled" value="https://private.example/v3/index.json" />
              </packageSources>
              <disabledPackageSources>
                <add key="selected" value="true" />
                <add key="user-disabled" value="true" />
                <add key="future-private" value="true" />
              </disabledPackageSources>
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(parentFile.FullName);
        var appHostDirectory = workspace.CreateDirectory("apphost");
        await Assert.ThrowsAsync<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
                appHostDirectory, CreateChannel([new("Aspire*", "https://selected.example/v3/index.json")]),
                createIfMissing: true, TestContext.Current.CancellationToken));
 
        var snapshot = NuGetTestHelper.CreateClient().GetSettings(
            appHostDirectory.FullName, new byte[NuGetSourceIdentity.KeySizeInBytes]);
        Assert.Equal(["future-private", "selected", "user-disabled"], snapshot.DisabledPackageSourceKeys.Order(StringComparer.Ordinal));
        Assert.Empty(NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Aspire.Hosting.Redis"));
        Assert.Empty(appHostDirectory.EnumerateFiles());
        Assert.Equal(original, await File.ReadAllBytesAsync(parentFile.FullName));
    }
 
    [Theory]
    [InlineData(null, true)]
    [InlineData(".nugetpackages", false)]
    [InlineData("company-cache", false)]
    public async Task PrepareAsync_EquivalentSourcePolicyStillEvaluatesCacheFolder(
        string? existingFolder, bool expectedCandidate)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var cacheSetting = existingFolder is null
            ? string.Empty
            : $"""<config><add key="globalPackagesFolder" value="{existingFolder}" /></config>""";
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="aspire-test" value="https://feed.example/v3/index.json" />
              </packageSources>
              <packageSourceMapping>
                <clear />
                <packageSource key="aspire-test">
                  <package pattern="Aspire*" />
                </packageSource>
              </packageSourceMapping>
              {{cacheSetting}}
            </configuration>
            """);
        var original = await File.ReadAllBytesAsync(target.FullName);
 
        var candidate = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, [new("Aspire*", "https://feed.example/v3/index.json")],
            createIfMissing: true, configureGlobalPackagesFolder: true, TestContext.Current.CancellationToken);
 
        Assert.Equal(expectedCandidate, candidate is not null);
        if (candidate is not null)
        {
            Assert.Equal(CliPathHelper.StagingNuGetPackagesFolderName,
                candidate.GetProposedDocument().SelectSingleNode("/configuration/config/add[@key='globalPackagesFolder']/@value")!.Value);
        }
        Assert.Equal(original, await File.ReadAllBytesAsync(target.FullName));
    }
 
    [Fact]
    public async Task ApplyAsync_RefusesChangedConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var target = await WriteConfigAsync(workspace.WorkspaceRoot, "<configuration />");
        var channel = CreateChannel([new PackageMapping("Aspire*", "https://feed.example/v3/index.json")]);
        var update = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, channel, createIfMissing: true, TestContext.Current.CancellationToken);
        Assert.NotNull(update);
        const string changedContent = "<configuration><!-- concurrent edit --></configuration>";
        await File.WriteAllTextAsync(target.FullName, changedContent);
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => DotNetAppHostNuGetConfigMerger.ApplyAsync(update, TestContext.Current.CancellationToken));
 
        Assert.Contains(target.FullName, exception.Message);
        Assert.Equal(changedContent, await File.ReadAllTextAsync(target.FullName));
    }
 
    [Fact]
    public async Task ApplyAsync_RefusesConfigCreatedAfterPreparation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        var channel = CreateChannel([new PackageMapping("Aspire*", "https://feed.example/v3/index.json")]);
        var update = await DotNetAppHostNuGetConfigTestHelper.PrepareAsync(
            workspace.WorkspaceRoot, channel, createIfMissing: true, TestContext.Current.CancellationToken);
        Assert.NotNull(update);
        const string createdContent = "<configuration><!-- concurrent creation --></configuration>";
        await File.WriteAllTextAsync(update.TargetFile.FullName, createdContent);
 
        await Assert.ThrowsAsync<IOException>(
            () => DotNetAppHostNuGetConfigMerger.ApplyAsync(update, TestContext.Current.CancellationToken));
 
        Assert.Equal(createdContent, await File.ReadAllTextAsync(update.TargetFile.FullName));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CreateOrUpdateAsync_ConfirmsCompleteGlobalPackagesFolderChange(bool existingConfig)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(_outputHelper);
        if (existingConfig)
        {
            await WriteConfigAsync(workspace.WorkspaceRoot, "<configuration />");
        }
 
        XmlDocument? confirmedDocument = null;
        await DotNetAppHostNuGetConfigTestHelper.CreateOrUpdateAsync(
            workspace.WorkspaceRoot,
            [new PackageMapping("Aspire*", "https://feed.example/v3/index.json")],
            configureGlobalPackagesFolder: true,
            confirmationCallback: (_, _, proposed, _) =>
            {
                confirmedDocument = proposed;
                Assert.Equal(
                    CliPathHelper.StagingNuGetPackagesFolderName,
                    proposed.SelectSingleNode("/configuration/config/add[@key='globalPackagesFolder']/@value")!.Value);
                return Task.FromResult(true);
            },
            TestContext.Current.CancellationToken);
 
        Assert.NotNull(confirmedDocument);
        Assert.Equal(
            XDocument.Parse(confirmedDocument.OuterXml).ToString(),
            XDocument.Load(Path.Combine(workspace.WorkspaceRoot.FullName, "nuget.config")).ToString());
    }
 
    private static string NormalizeLineEndings(string text) => text.Replace("\r\n", "\n");
}