| File: SelfUpdateChannelPersistenceTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.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.EndToEnd.Tests.Helpers; using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; public sealed class SelfUpdateChannelPersistenceTests(ITestOutputHelper output) { [Fact] [CaptureWorkspaceOnFailure] public async Task SelfUpdateToStaging_RelaunchedCliUsesStagingForImplicitProjectUpdate() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); if (strategy.Mode is not (CliInstallMode.LocalHive or CliInstallMode.PullRequest or CliInstallMode.LocalArchive)) { Assert.Skip( "This test must start from a current source build so its first self-update exercises " + "the channel-persistence implementation under test. Run with ASPIRE_E2E_ARCHIVE or in pull request CI."); } var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); const string projectName = "SelfUpdateChannelApp"; var projectPath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName); var configPath = Path.Combine(projectPath, "aspire.config.json"); // Model the reported scenario where the AppHost predates the self-update. Normalize any // channel assigned by the current-source install so the later update must use CLI identity. await auto.AspireNewCSharpEmptyAppHostAsync( projectName, counter, timeout: TimeSpan.FromMinutes(5)); var createdConfig = ReadConfig(configPath); createdConfig.Remove("channel"); File.WriteAllText(configPath, createdConfig.ToJsonString()); Assert.False(ReadConfig(configPath).ContainsKey("channel")); if (strategy.Mode is CliInstallMode.LocalHive) { // LocalHive setup pins its original Aspire home to local. Remove that competing // global setting before switching to the isolated script-style install. await auto.RunCommandAsync("aspire config delete channel -g", counter); } // Copy the current build into a dedicated get-aspire-cli.sh-style prefix. This gives the // self-update a realistic writable route without replacing the harness's original install. await auto.RunCommandAsync( "install_root=$HOME/.aspire-self-update-e2e; " + "mkdir -p \"$install_root/bin\"; " + "cp \"$(command -v aspire)\" \"$install_root/bin/aspire\"; " + "chmod +x \"$install_root/bin/aspire\"; " + "printf '%s\\n' '{\"source\":\"script\",\"channel\":\"stable\"}' > \"$install_root/bin/.aspire-install.json\"; " + "export PATH=\"$install_root/bin:$PATH\" ASPIRE_CLI_TELEMETRY_OPTOUT=true; hash -r; " + "test \"$(command -v aspire)\" = \"$install_root/bin/aspire\"", counter); await auto.RunCommandAsync( "aspire update --self --channel staging --non-interactive --yes", counter, timeout: TimeSpan.FromMinutes(10)); // This is a new process launched from the path that was replaced above. await auto.RunCommandAsync("hash -r; aspire --version", counter); await auto.RunCommandAsync("aspire config set features.updateNotificationsEnabled false -g", counter); await auto.RunCommandAsync($"cd {projectName}", counter); await auto.RunCommandAsync( "aspire update --non-interactive --yes", counter, timeout: TimeSpan.FromMinutes(10)); var updatedConfig = ReadConfig(configPath); Assert.Equal("staging", updatedConfig["channel"]?.GetValue<string>()); } private static JsonObject ReadConfig(string path) { return JsonNode.Parse(File.ReadAllText(path))?.AsObject() ?? throw new InvalidOperationException($"Unable to read {path}."); } }