File: Dcp\DcpHostNotificationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Diagnostics;
using Aspire.Hosting.Resources;
using Aspire.Hosting.Tests.Utils;
using Aspire.Tests;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
 
namespace Aspire.Hosting.Tests.Dcp;
 
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
[Trait("Partition", "4")]
public sealed class DcpHostNotificationTests
{
    private static Locations CreateTestLocations()
    {
        var directoryService = new FileSystemService(new ConfigurationBuilder().Build());
        return new Locations(directoryService);
    }
 
    [Fact]
    public void DcpHost_WithIInteractionService_CanBeConstructed()
    {
        // Arrange
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService();
        var locations = CreateTestLocations();
        var applicationModel = new DistributedApplicationModel(new ResourceCollection());
        var timeProvider = new FakeTimeProvider();
 
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        // Act & Assert - should not throw
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        Assert.NotNull(dcpHost);
    }
 
    [Fact]
    public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.Contains("docker", interaction.Message);
        Assert.Contains(string.Format(CultureInfo.InvariantCulture, InteractionStrings.ContainerRuntimeUnhealthyMessage, "docker"), interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
    }
 
    [Fact]
    public async Task DcpHost_WithUntrustedDeveloperCertificate_ShowsNotificationAndLogsWarning()
    {
        // Arrange
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = CreateApplicationModelWithHttpsEndpoint();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedMessage, interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning && w.Message is not null && w.Message.Contains("aka.ms/aspire/devcerts", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed and running - healthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = true,
                    Error = null
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
    }
 
    [Fact]
    public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = false }; // Dashboard disabled
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
    }
 
    [Fact]
    public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "podman", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Podman is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.Contains("podman", interaction.Message);
        Assert.Contains(InteractionStrings.ContainerRuntimePodmanAdvice, interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Null(notificationOptions.LinkUrl); // No specific link for Podman
    }
 
    [Fact]
    public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhenRuntimeBecomesHealthy()
    {
        // Arrange - this test verifies that the notification is cancelled when runtime becomes healthy
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Initially unhealthy
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new SignalingFakeTimeProvider(TimeSpan.FromSeconds(5));
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        // Use ReadAsync with timeout to wait for the notification
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
        await timeProvider.TimerCreated.DefaultTimeout();
        
        // Assert - Verify notification was shown initially
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.False(interaction.CancellationToken.IsCancellationRequested); // Should not be cancelled yet
 
        // Simulate container runtime becoming healthy
        dependencyCheckService.DcpInfoResult = new DcpInfo
        {
            Containers = new DcpContainersInfo
            {
                Installed = true,
                Running = true,
                Error = null
            }
        };
 
        // Advance time by 5 seconds to trigger the next polling cycle
        timeProvider.Advance(TimeSpan.FromSeconds(5));
 
        await interaction.CancellationToken.WaitUntilCancelledAsync().DefaultTimeout();
    }
 
    [Fact]
    public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWithoutPolling()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container not installed
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = false,
                    Running = false,
                    Error = "No container runtime found"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeNotInstalledTitle, interaction.Title);
        Assert.Contains(InteractionStrings.ContainerRuntimeNotInstalledMessage, interaction.Message);
        Assert.Contains("https://aka.ms/aspire/containers", interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Equal(InteractionStrings.ContainerRuntimeLinkText, notificationOptions.LinkText);
        Assert.Equal("https://aka.ms/aspire/containers", notificationOptions.LinkUrl);
 
        timeProvider.Advance(TimeSpan.FromSeconds(5));
        Assert.False(interaction.CancellationToken.IsCancellationRequested);
    }
 
    [Fact]
    public void CreateDcpProcessSpec_WithoutTlsCertThumbprint_DoesNotIncludeThumbprintArgument()
    {
        // Arrange
        var dcpHost = CreateDcpHostForProcessSpecTests();
        var locations = CreateTestLocations();
 
        // Act
        var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
        // Assert
        Assert.DoesNotContain("--tls-cert-thumbprint", processSpec.Arguments);
        Assert.DoesNotContain("--tls-cert-file", processSpec.Arguments);
        Assert.DoesNotContain("--tls-key-file", processSpec.Arguments);
    }
 
    [Fact]
    public async Task CreateDcpProcessSpec_WithDcpDeveloperCertificateDefault_IncludesDeveloperCertificateArguments()
    {
        var activities = new ConcurrentBag<Activity>();
        using var listener = ActivityListenerHelper.Create(ProfilingTelemetry.ActivitySource, onActivityStopped: activities.Add);
        using var certificate = CreateExportableCertificate();
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [KnownConfigNames.ProfilingEnabled] = "true"
            })
            .Build();
        var dcpHost = CreateDcpHostForProcessSpecTests(
            developerCertificateService: new TestDeveloperCertificateService([certificate], false, false, false),
            configuration: configuration);
        var locations = CreateTestLocations();
 
        await dcpHost.PrepareDcpTlsCertificateAsync(CancellationToken.None);
 
        // Act
        var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
        // Assert
        Assert.Contains($"--tls-cert-thumbprint \"{certificate.Thumbprint}\"", processSpec.Arguments);
        var certificateActivity = Assert.Single(activities, activity => activity.OperationName == ProfilingTelemetry.Activities.DcpPrepareTlsCertificate);
        Assert.Equal(true, certificateActivity.GetTagItem(ProfilingTelemetry.Tags.DcpTlsDeveloperCertificateEnabled));
        Assert.Equal(ProfilingTelemetry.Values.DcpTlsCertificateResultPrepared, certificateActivity.GetTagItem(ProfilingTelemetry.Tags.DcpTlsCertificateResult));
        Assert.Equal(true, certificateActivity.GetTagItem(ProfilingTelemetry.Tags.DcpTlsCertificatePrepared));
 
        if (OperatingSystem.IsWindows())
        {
            Assert.DoesNotContain("--tls-cert-file", processSpec.Arguments);
            Assert.DoesNotContain("--tls-key-file", processSpec.Arguments);
            Assert.Equal(ProfilingTelemetry.Values.DcpTlsCertificateModeThumbprint, certificateActivity.GetTagItem(ProfilingTelemetry.Tags.DcpTlsCertificateMode));
        }
        else
        {
            var certificatePath = GetQuotedArgumentValue(processSpec.Arguments, "--tls-cert-file");
            var keyPath = GetQuotedArgumentValue(processSpec.Arguments, "--tls-key-file");
 
            Assert.Equal(certificate.ExportCertificatePem(), File.ReadAllText(certificatePath));
            Assert.Contains("PRIVATE KEY", File.ReadAllText(keyPath));
            Assert.Equal(ProfilingTelemetry.Values.DcpTlsCertificateModeFiles, certificateActivity.GetTagItem(ProfilingTelemetry.Tags.DcpTlsCertificateMode));
        }
    }
 
    [Fact]
    public async Task CreateDcpProcessSpec_WithDcpDeveloperCertificateDisabled_DoesNotIncludeThumbprintArgument()
    {
        using var certificate = CreateUntrustedCertificate();
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [KnownConfigNames.DcpDeveloperCertificate] = "false"
            })
            .Build();
        var dcpHost = CreateDcpHostForProcessSpecTests(
            developerCertificateService: new TestDeveloperCertificateService([certificate], false, false, false),
            configuration: configuration);
        var locations = CreateTestLocations();
 
        await dcpHost.PrepareDcpTlsCertificateAsync(CancellationToken.None);
 
        // Act
        var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
        // Assert - thumbprint should not appear because config is explicitly disabled
        Assert.DoesNotContain("--tls-cert-thumbprint", processSpec.Arguments);
        Assert.DoesNotContain("--tls-cert-file", processSpec.Arguments);
        Assert.DoesNotContain("--tls-key-file", processSpec.Arguments);
    }
 
    [Fact]
    public void CreateDcpProcessSpec_WithContainerRuntime_IncludesContainerRuntimeArgument()
    {
        // Arrange
        var resolvedOptions = GetDcpOptions();
        resolvedOptions.ContainerRuntime = "podman";
        var dcpHost = CreateDcpHostForProcessSpecTests(dcpOptions: resolvedOptions);
        var locations = CreateTestLocations();
 
        // Act
        var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
        // Assert
        Assert.Contains("--container-runtime \"podman\"", processSpec.Arguments);
    }
 
    [Theory]
    [InlineData(Architecture.X64, "x64")]
    [InlineData(Architecture.Arm64, "arm64")]
    public void TryGetBundledConPtyPath_WithCompleteBundlePayload_ReturnsTerminalHostDirectory(Architecture architecture, string architectureDirectory)
    {
        var directory = Directory.CreateTempSubdirectory();
        try
        {
            var terminalHostPath = Path.Combine(directory.FullName, "aspire-managed.exe");
            File.WriteAllText(terminalHostPath, "");
            File.WriteAllText(Path.Combine(directory.FullName, "conpty.dll"), "");
            Directory.CreateDirectory(Path.Combine(directory.FullName, architectureDirectory));
            File.WriteAllText(Path.Combine(directory.FullName, architectureDirectory, "OpenConsole.exe"), "");
 
            var found = DcpHost.TryGetBundledConPtyPath(terminalHostPath, architecture, architecture, out var conPtyPath);
 
            Assert.True(found);
            Assert.Equal(directory.FullName, conPtyPath);
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(Architecture.X64, Architecture.X64, "win-x64", "x64")]
    [InlineData(Architecture.X64, Architecture.Arm64, "win-x64", "arm64")]
    [InlineData(Architecture.Arm64, Architecture.Arm64, "win-arm64", "arm64")]
    public void TryGetBundledConPtyPath_WithCompleteRepoPayload_ReturnsRuntimeNativeDirectory(
        Architecture processArchitecture,
        Architecture osArchitecture,
        string runtimeIdentifier,
        string nativeHostDirectory)
    {
        var directory = Directory.CreateTempSubdirectory();
        try
        {
            var terminalHostPath = Path.Combine(directory.FullName, "aspire-managed.exe");
            File.WriteAllText(terminalHostPath, "");
            var nativeDirectory = Path.Combine(directory.FullName, "runtimes", runtimeIdentifier, "native");
            Directory.CreateDirectory(Path.Combine(nativeDirectory, nativeHostDirectory));
            File.WriteAllText(Path.Combine(nativeDirectory, "conpty.dll"), "");
            File.WriteAllText(Path.Combine(nativeDirectory, nativeHostDirectory, "OpenConsole.exe"), "");
 
            var found = DcpHost.TryGetBundledConPtyPath(
                terminalHostPath,
                processArchitecture,
                osArchitecture,
                out var conPtyPath);
 
            Assert.True(found);
            Assert.Equal(nativeDirectory, conPtyPath);
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Theory]
    [InlineData(false, true)]
    [InlineData(true, false)]
    public void TryGetBundledConPtyPath_WithIncompletePayload_ReturnsFalse(bool includeConPty, bool includeOpenConsole)
    {
        var directory = Directory.CreateTempSubdirectory();
        try
        {
            var terminalHostPath = Path.Combine(directory.FullName, "aspire-managed.exe");
            File.WriteAllText(terminalHostPath, "");
            if (includeConPty)
            {
                File.WriteAllText(Path.Combine(directory.FullName, "conpty.dll"), "");
            }
 
            if (includeOpenConsole)
            {
                var architectureDirectory = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
                Directory.CreateDirectory(Path.Combine(directory.FullName, architectureDirectory));
                File.WriteAllText(Path.Combine(directory.FullName, architectureDirectory, "OpenConsole.exe"), "");
            }
 
            var found = DcpHost.TryGetBundledConPtyPath(
                terminalHostPath,
                RuntimeInformation.ProcessArchitecture,
                RuntimeInformation.OSArchitecture,
                out var conPtyPath);
 
            Assert.False(found);
            Assert.Null(conPtyPath);
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void CreateDcpProcessSpec_DoesNotInheritExcludedEnvironmentVariables()
    {
        var excludedVars = new Dictionary<string, string>
        {
            ["aspnetcore_urls"] = "http://localhost:5000",
            ["DOTNET_LAUNCH_PROFILE"] = "MyProfile",
            [KnownAspNetCoreConfigNames.Environment] = "Development",
            [KnownAspNetCoreConfigNames.DotNetEnvironment] = "Development",
            ["aspire_loglevel"] = "Debug",
        };
 
        var options = new RemoteInvokeOptions();
        foreach (var (key, value) in excludedVars)
        {
            options.StartInfo.Environment[key] = value;
        }
 
        options.StartInfo.Environment["MY_CUSTOM_SETTING"] = "keep-me";
        options.StartInfo.Environment["ASPNETCORE_URLS_FOO"] = "keep-me-too";
        options.StartInfo.Environment["ASPIRE_LOGLEVEL_EXTRA"] = "keep-me-three";
 
        RemoteExecutor.Invoke(static () =>
        {
            // Use the test assembly as a dummy CliPath since CreateDcpProcessSpec
            // validates the file exists.
            var dummyPath = typeof(DcpHostNotificationTests).Assembly.Location;
            var dcpOptions = new DcpOptions { CliPath = dummyPath, DashboardPath = "/dummy/dashboard" };
            var dcpHost = CreateDcpHostForProcessSpecTests(dcpOptions: dcpOptions);
            var locations = CreateTestLocations();
 
            var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
            string[] expectedExcluded =
            [
                "aspnetcore_urls",
                "DOTNET_LAUNCH_PROFILE",
                KnownAspNetCoreConfigNames.Environment,
                KnownAspNetCoreConfigNames.DotNetEnvironment,
                "aspire_loglevel",
            ];
 
            foreach (var key in expectedExcluded)
            {
                Assert.False(processSpec.EnvironmentVariables.ContainsKey(key),
                    $"DCP process should not inherit '{key}' from the app host.");
            }
 
            Assert.True(processSpec.EnvironmentVariables.ContainsKey("MY_CUSTOM_SETTING"),
                "DCP process should inherit environment variables that are not in the exclusion list.");
            Assert.Equal("keep-me", processSpec.EnvironmentVariables["MY_CUSTOM_SETTING"]);
            Assert.Equal("keep-me-too", processSpec.EnvironmentVariables["ASPNETCORE_URLS_FOO"]);
            Assert.Equal("keep-me-three", processSpec.EnvironmentVariables["ASPIRE_LOGLEVEL_EXTRA"]);
        }, options).Dispose();
    }
 
    [Fact]
    public void CreateDcpProcessSpec_MapsAspireProfilingConfigurationToDcpOtelEnvironmentVariables()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [KnownConfigNames.ProfilingEnabled] = "true",
                [KnownConfigNames.ProfilingSessionId] = "profile-session",
                [KnownConfigNames.Legacy.StartupTraceParent] = "00-11111111111111111111111111111111-2222222222222222-01",
                [KnownConfigNames.Legacy.StartupTraceState] = "vendor=value"
            })
            .Build();
 
        var dcpHost = CreateDcpHostForProcessSpecTests(configuration: configuration);
        var locations = CreateTestLocations();
 
        var processSpec = dcpHost.CreateDcpProcessSpec(locations);
 
        Assert.Equal("true", processSpec.EnvironmentVariables[KnownConfigNames.DcpOtelStartupProfilingEnabled]);
        Assert.Equal("profile-session", processSpec.EnvironmentVariables[KnownConfigNames.DcpOtelProfilingSessionId]);
        Assert.Equal("00-11111111111111111111111111111111-2222222222222222-01", processSpec.EnvironmentVariables[KnownConfigNames.DcpOtelStartupTraceParent]);
        Assert.Equal("vendor=value", processSpec.EnvironmentVariables[KnownConfigNames.DcpOtelStartupTraceState]);
    }
 
    [Theory]
    [InlineData(0, true)]
    [InlineData(1, false)]
    public async Task EnsureDcpHostRunning_TransfersSessionFolderOwnershipToDcpOnlyWhenDcpStarts(int launcherExitCode, bool sessionFolderOwnedByDcp)
    {
        // DCP is started with --detach and DCP_SESSION_FOLDER, so it outlives the AppHost and still reads
        // the kubeconfig in the session folder during its own cleanup before deleting the folder itself.
        // Disposing the managed file system service must therefore not delete the folder once DCP has been launched,
        // but the folder must still be deleted when the DCP launcher reports that startup failed.
        var launcherDir = Directory.CreateTempSubdirectory("aspire-dcp-launcher");
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var locations = new Locations(fileSystemService);
        var dcpOptions = new DcpOptions { CliPath = CreateFakeDcpLauncher(launcherDir.FullName, launcherExitCode) };
        var dcpHost = new DcpHost(
            new NullLoggerFactory(),
            Options.Create(dcpOptions),
            new TestDcpDependencyCheckService(),
            new TestInteractionService(),
            locations,
            new DistributedApplicationModel(new ResourceCollection()),
            new FakeTimeProvider(),
            new TestDeveloperCertificateService([], false, false, false),
            new ConfigurationBuilder().Build());
 
        var sessionDir = locations.DcpSessionDir;
        try
        {
            await File.WriteAllTextAsync(locations.DcpKubeconfigPath, "kubeconfig written by DCP");
 
            dcpHost.EnsureDcpHostRunning();
 
            // Shut down without waiting for the launcher result, as an AppHost that stops right after starting DCP would.
            fileSystemService.Dispose();
 
            await dcpHost.DcpLaunchResultObserved.WaitAsync(TimeSpan.FromSeconds(60));
 
            Assert.Equal(sessionFolderOwnedByDcp, File.Exists(locations.DcpKubeconfigPath));
        }
        finally
        {
            await dcpHost.StopAsync();
            if (Directory.Exists(sessionDir))
            {
                Directory.Delete(sessionDir, recursive: true);
            }
            launcherDir.Delete(recursive: true);
        }
    }
 
    private static string CreateFakeDcpLauncher(string directory, int exitCode)
    {
        if (OperatingSystem.IsWindows())
        {
            var scriptPath = Path.Combine(directory, "dcp.cmd");
            File.WriteAllText(scriptPath, $"@exit /b {exitCode}{Environment.NewLine}");
            return scriptPath;
        }
 
        var shellScriptPath = Path.Combine(directory, "dcp");
        File.WriteAllText(shellScriptPath, $"#!/bin/sh{Environment.NewLine}exit {exitCode}{Environment.NewLine}");
        File.SetUnixFileMode(shellScriptPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        return shellScriptPath;
    }
 
    [Fact]
    public void DcpSessionFolder_IsDeletedOnDisposeWhenDcpWasNotStarted()
    {
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var locations = new Locations(fileSystemService);
        var sessionDir = locations.DcpSessionDir;
        File.WriteAllText(locations.DcpKubeconfigPath, "kubeconfig");
 
        fileSystemService.Dispose();
 
        Assert.False(Directory.Exists(sessionDir));
    }
 
    private static DcpHost CreateDcpHostForProcessSpecTests(
        IDeveloperCertificateService? developerCertificateService = null,
        IConfiguration? configuration = null,
        DcpOptions? dcpOptions = null)
    {
        return new DcpHost(
            new NullLoggerFactory(),
            Options.Create(dcpOptions ?? GetDcpOptions()),
            new TestDcpDependencyCheckService(),
            new TestInteractionService(),
            CreateTestLocations(),
            new DistributedApplicationModel(new ResourceCollection()),
            new FakeTimeProvider(),
            developerCertificateService ?? new TestDeveloperCertificateService([], false, false, false),
            configuration ?? new ConfigurationBuilder().Build());
    }
 
    private static DcpOptions GetDcpOptions()
    {
        var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
        {
            DisableDashboard = true
        });
        using var host = builder.Build();
        return host.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
    }
 
    private static DistributedApplication CreateAppWithContainers()
    {
        var builder = DistributedApplication.CreateBuilder();
        builder.AddContainer("test-container", "nginx:latest");
        return builder.Build();
    }
 
    private static DistributedApplicationModel CreateApplicationModelWithHttpsEndpoint()
    {
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https"));
        return new DistributedApplicationModel(new ResourceCollection([resource]));
    }
 
    private static DistributedApplicationModel CreateApplicationModelWithHttpEndpoint()
    {
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http"));
        return new DistributedApplicationModel(new ResourceCollection([resource]));
    }
 
    private static X509Certificate2 CreateUntrustedCertificate()
    {
        var searchPaths = new[]
        {
            Path.Combine(Directory.GetCurrentDirectory(), "tests", "Shared", "TestCertificates", "testCert.pfx"),
            Path.Combine(AppContext.BaseDirectory, "shared", "TestCertificates", "testCert.pfx"),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "tests", "Shared", "TestCertificates", "testCert.pfx"))
        };
 
        foreach (var path in searchPaths)
        {
            if (File.Exists(path))
            {
                return X509CertificateLoader.LoadPkcs12FromFile(path, "testPassword");
            }
        }
 
        throw new FileNotFoundException("Could not locate test certificate file 'testCert.pfx' in expected locations.");
    }
 
    private static X509Certificate2 CreateExportableCertificate()
    {
        var subject = new X500DistinguishedName($"CN=aspire-test-{Guid.NewGuid():N}");
 
        using var rsa = RSA.Create(2048);
        var request = new CertificateRequest(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        request.CertificateExtensions.Add(new X509Extension("1.3.6.1.4.1.311.84.1.1", [0], critical: false));
        using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30));
 
        return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), password: null, X509KeyStorageFlags.Exportable);
    }
 
    private static string GetQuotedArgumentValue(string? arguments, string option)
    {
        if (arguments is null)
        {
            throw new InvalidOperationException("Expected process arguments to be set.");
        }
 
        var prefix = $"{option} \"";
        var start = arguments.IndexOf(prefix, StringComparison.Ordinal);
        Assert.NotEqual(-1, start);
        start += prefix.Length;
 
        var end = arguments.IndexOf('"', start);
        Assert.NotEqual(-1, end);
 
        return arguments[start..end];
    }
 
    [Fact]
    public async Task DcpHost_WithNoHttpsResources_DoesNotShowCertificateWarning()
    {
        // Arrange - only HTTP endpoints, no HTTPS
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = CreateApplicationModelWithHttpEndpoint();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithNoResources_DoesNotShowCertificateWarning()
    {
        // Arrange - empty resource model
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = new DistributedApplicationModel(new ResourceCollection());
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallback_ShowsCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but no HTTPS endpoint
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test");
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AppHost:Directory"] = appHostDirectory
            })
            .Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert - warning should be shown because HttpsCertificateConfigurationCallbackAnnotation indicates TLS
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallbackDisabledByWithoutHttpsCertificate_DoesNotShowCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but disabled by WithoutHttpsCertificate
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        // This is the state set by WithoutHttpsCertificate()
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            Certificate = null,
            UseDeveloperCertificate = false,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateAnnotationOnly_DoesNotShowCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateAnnotation but NO HttpsCertificateConfigurationCallbackAnnotation
        // HttpsCertificateAnnotation alone has no effect without the callback annotation
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            UseDeveloperCertificate = true,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallbackAndDevCert_ShowsCertificateWarning()
    {
        // Arrange - resource has both HttpsCertificateConfigurationCallbackAnnotation and HttpsCertificateAnnotation
        // with UseDeveloperCertificate = true (not disabled)
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            UseDeveloperCertificate = true,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test");
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AppHost:Directory"] = appHostDirectory
            })
            .Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token).DefaultTimeout();
 
        // Assert - warning should be shown because TLS is active
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    private sealed class TestDcpDependencyCheckService : IDcpDependencyCheckService
    {
        public DcpInfo? DcpInfoResult { get; set; } = new DcpInfo
        {
            VersionString = DcpVersion.Dev.ToString(),
            Version = DcpVersion.Dev,
            Containers = new DcpContainersInfo
            {
                Runtime = "docker",
                Installed = true,
                Running = true
            }
        };
 
        public Task<DcpInfo?> GetDcpInfoAsync(bool force = false, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(DcpInfoResult);
        }
    }
}
 
#pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.