File: Commands\NewCommandLocalTemplateTests.NuGetValues.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 System.Text.Json.Nodes;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Resources;
using Aspire.Cli.Templating;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
using RootCommand = Aspire.Cli.Commands.RootCommand;
 
namespace Aspire.Cli.Tests.Commands;
 
public partial class NewCommandLocalTemplateTests
{
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task LocalTemplate_ComputesPackageVersionFromTemplatePolicy(bool prerelease, bool interactive)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var feed = workspace.CreateDirectory("feed");
        foreach (var version in new[] { "13.6.2", "13.6.3-preview.2", "13.6.3-preview.10", "13.7.0" })
        {
            NuGetTestHelper.CreatePackage(feed, "Aspire.Hosting.Dotnet", version, new Dictionary<string, string>());
        }
        await CreateComputedVersionTemplateAsync(workspace, $$"""
            {
              "kind": "nugetPackageVersion",
              "packageId": "aspire.hosting.dotnet",
              "versionRange": "[13.6.0-preview.0,13.7.0)",
              "allowPrerelease": {{prerelease.ToString().ToLowerInvariant()}}
            }
            """);
        await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "NuGet.Config"), """
            <configuration>
              <packageSources><clear /><add key="template-feed" value="../feed" /></packageSources>
              <packageSourceMapping>
                <clear /><packageSource key="template-feed"><package pattern="Aspire.Hosting.Dotnet" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive);
        services.AddSingleton(workspace.CreateExecutionContext(identityChannel: "unavailable", identityVersion: "13.6.3"));
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.True(exitCode == CliExitCodes.Success, string.Join(Environment.NewLine, interaction.DisplayedErrors));
        Assert.Empty(interaction.StringPromptCalls);
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
        await Verify(ReadOutput(workspace)).UseParameters(prerelease, interactive);
    }
 
    [Theory]
    [InlineData("ancestor")]
    [InlineData("nested")]
    [InlineData("channel")]
    [InlineData("embedded")]
    public async Task LocalTemplate_ComputedVersionUsesEffectiveNuGetConfiguration(string policy)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var feed = workspace.CreateDirectory("feed");
        NuGetTestHelper.CreatePackage(feed, "Aspire.Hosting.Dotnet", "13.6.1-preview.3", new Dictionary<string, string>());
        await CreateComputedVersionTemplateAsync(workspace, $$"""
            {
              "kind": "nugetPackageVersion", "packageId": "Aspire.Hosting.Dotnet",
              "versionRange": "[13.6.0-preview.0,13.7.0)", "allowPrerelease": true,
              "workingDirectory": "{{(policy == "nested" ? "AppHost" : ".")}}"
            }
            """);
        var config = """
            <configuration>
              <packageSources><clear /><add key="chosen" value="feed" /></packageSources>
              <packageSourceMapping><clear /><packageSource key="chosen"><package pattern="*" /></packageSource></packageSourceMapping>
            </configuration>
            """;
        if (policy == "ancestor")
        {
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, "NuGet.Config"), config);
        }
        else if (policy == "nested")
        {
            workspace.CreateDirectory("template/AppHost");
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "AppHost", "nuget.config"),
                config.Replace("value=\"feed\"", "value=\"../../feed\"", StringComparison.Ordinal));
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "nuget.config"),
                "<configuration><packageSources><clear /></packageSources><packageSourceMapping><clear /></packageSourceMapping></configuration>");
        }
        else if (policy == "embedded")
        {
            var embedded = workspace.CreateDirectory("template/feed");
            File.Copy(Path.Combine(feed.FullName, "Aspire.Hosting.Dotnet.13.6.1-preview.3.nupkg"),
                Path.Combine(embedded.FullName, "Aspire.Hosting.Dotnet.13.6.1-preview.3.nupkg"));
            await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "nuget.config"), config);
        }
        else
        {
            var host = Path.Combine(workspace.Path, "template", ".template.config", "aspirecli.host.json");
            var metadata = JsonNode.Parse(await File.ReadAllTextAsync(host))!;
            metadata["createNuGetConfig"] = true;
            await File.WriteAllTextAsync(host, metadata.ToJsonString());
        }
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton(workspace.CreateExecutionContext(identityChannel: "daily", identityVersion: "13.6.3"));
        services.AddSingleton<IPackagingService>(new TestPackagingService
        {
            GetChannelsAsyncCallback = _ => Task.FromResult<IEnumerable<PackageChannel>>(
            [
                PackageChannel.CreateExplicitChannel("daily", PackageChannelQuality.Prerelease,
                    [new PackageMapping("Aspire*", feed.FullName)],
                    new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance)
            ])
        });
        using var provider = services.BuildServiceProvider();
        workspace.CreateDirectory("out");
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.True(exitCode == CliExitCodes.Success, string.Join(Environment.NewLine, interaction.DisplayedErrors));
        var output = Path.Combine(workspace.Path, "out");
        Assert.Equal(
            [policy == "embedded" ? Path.Combine(output, "feed") : feed.FullName],
            NuGetTestHelper.GetEligiblePackageSources(policy == "nested" ? Path.Combine(output, "AppHost") : output, "Aspire.Hosting.Dotnet"));
        await Verify(await File.ReadAllTextAsync(Path.Combine(output, "versions.json"))).UseParameters(policy);
    }
 
    [Theory]
    [InlineData("[]")]
    [InlineData("null")]
    [InlineData("{}")]
    [InlineData("""{"kind":"unknown","packageId":"Example","versionRange":"*"}""")]
    [InlineData("""{"kind":"nugetPackageVersion","packageId":"bad id","versionRange":"*"}""")]
    [InlineData("""{"kind":"nugetPackageVersion","packageId":"Example","versionRange":"invalid"}""")]
    [InlineData("""{"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*","allowPrerelease":"true"}""")]
    [InlineData("""{"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*","workingDirectory":null}""")]
    public async Task LocalTemplate_RejectsInvalidComputedValueMetadata(string valueSource)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, valueSource);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
        Assert.NotEmpty(interaction.DisplayedErrors);
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task LocalTemplate_ComputedLookupFailureDoesNotPublishOrUseDefault(bool queryFails, bool existingOutput)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Aspire.Hosting.Dotnet","versionRange":"[13.6.0,13.7.0)"}
            """);
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, _) => queryFails
                ? throw new NuGetOperationException("Lookup failed.")
                : Task.FromResult<IReadOnlyList<NuGetVersion>>([NuGetVersion.Parse("13.6.1-preview.1"), NuGetVersion.Parse("13.7.0")])
        });
        using var provider = services.BuildServiceProvider();
        if (existingOutput)
        {
            workspace.CreateDirectory("out");
        }
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToCreateNewProject, exitCode);
        Assert.Single(interaction.DisplayedErrors);
        Assert.Empty(interaction.DisplayedSuccess);
        Assert.Equal(existingOutput, Directory.Exists(Path.Combine(workspace.Path, "out")));
        if (existingOutput)
        {
            Assert.Empty(Directory.GetFileSystemEntries(Path.Combine(workspace.Path, "out")));
        }
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
    }
 
    [Theory]
    [InlineData("../feed")]
    [InlineData("missing")]
    [InlineData("/")]
    public async Task LocalTemplate_ComputedWorkingDirectoryCannotEscapeOrFallBack(string directory)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        workspace.CreateDirectory("feed");
        await CreateComputedVersionTemplateAsync(workspace, $$"""
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*","workingDirectory":"{{directory}}"}
            """);
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, _) => throw new InvalidOperationException("Invalid directories must not query feeds.")
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
    }
 
    [Fact]
    public async Task LocalTemplate_ComputedValuesCannotChangeTheirNuGetPolicy()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"[1.0.0,2.0.0)"}
            """);
        await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "nuget.config"), """
            <configuration><packageSources><clear /><add key="self" value="../feed/IntegrationVersionValue" /></packageSources></configuration>
            """);
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, _) => Task.FromResult<IReadOnlyList<NuGetVersion>>([NuGetVersion.Parse("1.5.0")])
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToCreateNewProject, exitCode);
        Assert.Equal(TemplatingStrings.TemplateNuGetConfigurationChanged, Assert.Single(interaction.DisplayedErrors));
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task LocalTemplate_ComputedValuesCannotControlPrompting(bool interactive)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var path = Path.Combine(workspace.Path, "template", ".template.config", "template.json");
        var template = JsonNode.Parse(await File.ReadAllTextAsync(path))!;
        template["symbols"]!["Dependent"] = new JsonObject
        {
            ["type"] = "parameter", ["datatype"] = "string", ["isEnabled"] = "(IntegrationVersion == \"1.0.0\")"
        };
        await File.WriteAllTextAsync(path, template.ToJsonString());
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive).BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToCreateNewProject, exitCode);
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
        Assert.Empty(interaction.StringPromptCalls);
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task LocalTemplate_OnlyQueriesEnabledComputedParameters(bool enabled, bool interactive)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var path = Path.Combine(workspace.Path, "template", ".template.config", "template.json");
        var template = JsonNode.Parse(await File.ReadAllTextAsync(path))!;
        template["symbols"]!["IncludeIntegration"] = new JsonObject
        {
            ["type"] = "parameter", ["datatype"] = "bool", ["defaultValue"] = enabled ? "true" : "false"
        };
        template["symbols"]!["IntegrationVersion"]!["isEnabled"] = "(IncludeIntegration)";
        template["sources"] = JsonNode.Parse("""
            [{"modifiers":[{"condition":"(!IncludeIntegration)","exclude":["versions.json"]}]}]
            """);
        await File.WriteAllTextAsync(path, template.ToJsonString());
        var calls = 0;
        var interaction = new TestInteractionService();
        interaction.SetupBooleanResponse(enabled);
        var services = CreateServices(workspace, interaction, interactive);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, _) =>
            {
                calls++;
                return Task.FromResult<IReadOnlyList<NuGetVersion>>([NuGetVersion.Parse("1.2.3")]);
            }
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.True(exitCode == CliExitCodes.Success, string.Join(Environment.NewLine, interaction.DisplayedErrors));
        Assert.Equal(enabled ? 1 : 0, calls);
        Assert.Empty(interaction.StringPromptCalls);
        Assert.Equal(enabled, File.Exists(Path.Combine(workspace.Path, "out", "versions.json")));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task LocalTemplate_ComputedValuesUseOnlySelectedHostMetadata(bool emptyPrimary)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var config = Path.Combine(workspace.Path, "template", ".template.config");
        File.Move(Path.Combine(config, "aspirecli.host.json"), Path.Combine(config, "dotnetcli.host.json"));
        var definitionPath = Path.Combine(config, "template.json");
        var definition = JsonNode.Parse(await File.ReadAllTextAsync(definitionPath))!;
        definition["symbols"]!["IntegrationVersion"]!["isRequired"] = false;
        await File.WriteAllTextAsync(definitionPath, definition.ToJsonString());
        if (emptyPrimary)
        {
            await File.WriteAllTextAsync(Path.Combine(config, "aspirecli.host.json"), "{}");
        }
        var calls = 0;
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton(workspace.CreateExecutionContext(identityVersion: "13.6.3"));
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, _) =>
            {
                calls++;
                return Task.FromResult<IReadOnlyList<NuGetVersion>>([NuGetVersion.Parse("1.2.3")]);
            }
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.True(exitCode == CliExitCodes.Success, string.Join(Environment.NewLine, interaction.DisplayedErrors));
        Assert.Equal(emptyPrimary ? 0 : 1, calls);
        await Verify(ReadOutput(workspace)).UseParameters(emptyPrimary);
    }
 
    [Theory]
    [InlineData("Missing", "string")]
    [InlineData("AspireVersion", "string")]
    [InlineData("IntegrationVersion", "bool")]
    public async Task LocalTemplate_ComputedMetadataRejectsInvalidTargets(string target, string datatype)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var config = Path.Combine(workspace.Path, "template", ".template.config");
        var hostPath = Path.Combine(config, "aspirecli.host.json");
        var host = JsonNode.Parse(await File.ReadAllTextAsync(hostPath))!;
        var source = host["symbolInfo"]!["IntegrationVersion"]!.DeepClone();
        host["symbolInfo"] = new JsonObject { [target] = source };
        await File.WriteAllTextAsync(hostPath, host.ToJsonString());
        var templatePath = Path.Combine(config, "template.json");
        var template = JsonNode.Parse(await File.ReadAllTextAsync(templatePath))!;
        template["symbols"]!["IntegrationVersion"]!["datatype"] = datatype;
        await File.WriteAllTextAsync(templatePath, template.ToJsonString());
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
        Assert.NotEmpty(interaction.DisplayedErrors);
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
    }
 
    [Fact]
    public async Task LocalTemplate_CancelingComputedLookupRemovesStaging()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var cancellation = new CancellationTokenSource();
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var services = CreateServices(workspace, new TestInteractionService(), interactive: false);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (_, _, token) =>
            {
                cancellation.Cancel();
                return Task.FromCanceled<IReadOnlyList<NuGetVersion>>(token);
            }
        });
        using var provider = services.BuildServiceProvider();
        var templates = await provider.GetRequiredService<ITemplateProvider>().GetTemplatesAsync(new TemplateSource("template", null, null));
        var parseResult = provider.GetRequiredService<RootCommand>().Parse("new --template-path template");
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => Assert.Single(templates).ApplyTemplateAsync(
            new TemplateInputs { Name = "Demo", Output = "out" }, parseResult, cancellation.Token));
 
        Assert.False(Directory.Exists(Path.Combine(workspace.Path, "out")));
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
    }
 
    [Fact]
    public async Task LocalTemplate_ComputedGenerationProtectsConcurrentOutputWrites()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Example","versionRange":"*"}
            """);
        var output = workspace.CreateDirectory("out");
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = async (_, _, token) =>
            {
                await File.WriteAllTextAsync(Path.Combine(output.FullName, "keep.txt"), "Concurrent user content", token);
                return [NuGetVersion.Parse("1.2.3")];
            }
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToCreateNewProject, exitCode);
        Assert.Empty(interaction.DisplayedSuccess);
        Assert.Empty(Directory.GetFileSystemEntries(workspace.Path, ".aspire-template-*"));
        await Verify(ReadOutput(workspace));
    }
 
    [Fact]
    public async Task LocalTemplate_ComputesIndependentPackageVersionsAndDiscardsPreliminaryFiles()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await CreateComputedVersionTemplateAsync(workspace, """
            {"kind":"nugetPackageVersion","packageId":"Aspire.Hosting.Dotnet","versionRange":"*","allowPrerelease":true}
            """);
        var config = Path.Combine(workspace.Path, "template", ".template.config");
        var path = Path.Combine(config, "template.json");
        var template = JsonNode.Parse(await File.ReadAllTextAsync(path))!;
        template["symbols"]!["KubernetesVersion"] = new JsonObject
        {
            ["type"] = "parameter", ["datatype"] = "string", ["replaces"] = "KubernetesVersionValue"
        };
        template["sources"] = JsonNode.Parse("""
            [{"modifiers":[{"condition":"(IntegrationVersion != \"0.0.0\")","exclude":["preliminary-only.txt"]}]}]
            """);
        await File.WriteAllTextAsync(path, template.ToJsonString());
        path = Path.Combine(config, "aspirecli.host.json");
        var host = JsonNode.Parse(await File.ReadAllTextAsync(path))!;
        host["symbolInfo"]!["KubernetesVersion"] = JsonNode.Parse("""
            {"valueSource":{"kind":"nugetPackageVersion","packageId":"Aspire.Hosting.Kubernetes","versionRange":"[13.6.0-preview.0,13.7.0)","allowPrerelease":true}}
            """);
        await File.WriteAllTextAsync(path, host.ToJsonString());
        await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "preliminary-only.txt"), "Placeholder render only");
        await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "versions.json"),
            """{"sdk":"AspireVersionValue","dotnet":"IntegrationVersionValue","kubernetes":"KubernetesVersionValue"}""");
        var interaction = new TestInteractionService();
        var services = CreateServices(workspace, interaction, interactive: false);
        services.AddSingleton(workspace.CreateExecutionContext(identityChannel: "stable", identityVersion: "13.6.3"));
        services.AddSingleton<INuGetClient>(new FakeNuGetClient
        {
            GetPackageVersionsCallback = (id, _, _) => Task.FromResult<IReadOnlyList<NuGetVersion>>(
                [NuGetVersion.Parse(id == "Aspire.Hosting.Dotnet" ? "13.6.0" : "13.6.1-preview.2")])
        });
        using var provider = services.BuildServiceProvider();
 
        var exitCode = await provider.GetRequiredService<RootCommand>()
            .Parse("new --template-path template --name Demo --output out --suppress-agent-init")
            .InvokeAsync().DefaultTimeout();
 
        Assert.True(exitCode == CliExitCodes.Success, string.Join(Environment.NewLine, interaction.DisplayedErrors));
        await Verify(ReadOutput(workspace));
    }
 
    private static async Task CreateComputedVersionTemplateAsync(TemporaryWorkspace workspace, string valueSource)
    {
        var config = workspace.CreateDirectory("template/.template.config");
        await File.WriteAllTextAsync(Path.Combine(config.FullName, "template.json"), """
            {
              "identity": "Aspire.Test.ComputedVersion", "name": "Computed version", "shortName": "computed",
              "symbols": {
                "AspireVersion": {"type":"parameter","datatype":"string","replaces":"AspireVersionValue"},
                "IntegrationVersion": {
                  "type":"parameter","datatype":"string","defaultValue":"99.0.0","isRequired":true,
                  "replaces":"IntegrationVersionValue"
                }
              }
            }
            """);
        await File.WriteAllTextAsync(Path.Combine(config.FullName, "aspirecli.host.json"), $$"""
            {
              "symbolInfo": {
                "IntegrationVersion": {
                  "valueSource": {{valueSource}}
                }
              }
            }
            """);
        await File.WriteAllTextAsync(Path.Combine(workspace.Path, "template", "versions.json"),
            """{"sdk":"AspireVersionValue","integration":"IntegrationVersionValue"}""");
    }
}