File: BrowserLogsBuilderExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Browsers.Tests\Aspire.Hosting.Browsers.Tests.csproj (Aspire.Hosting.Browsers.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREBROWSERLOGS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREUSERSECRETS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Browsers.Resources;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Aspire.Hosting.Eventing;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus;
 
namespace Aspire.Hosting.Browsers.Tests;
 
[Trait("Partition", "2")]
public class BrowserLogsBuilderExtensionsTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public void WithBrowserLogs_CreatesChildResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var browserLogsResource = Assert.Single(appModel.Resources.OfType<BrowserLogsResource>());
        Assert.Equal("web-browser-logs", browserLogsResource.Name);
        Assert.Equal(web.Resource.Name, browserLogsResource.ParentResource.Name);
        Assert.Equal("chrome", browserLogsResource.InitialConfiguration.Browser);
        Assert.Null(browserLogsResource.InitialConfiguration.Profile);
        Assert.Contains(browserLogsResource.Annotations.OfType<NameValidationPolicyAnnotation>(), static annotation => annotation == NameValidationPolicyAnnotation.None);
 
        Assert.True(browserLogsResource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
        var parentRelationship = Assert.Single(relationships, relationship => relationship.Type == "Parent");
        Assert.Equal(web.Resource.Name, parentRelationship.Resource.Name);
 
        var command = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName);
        Assert.Equal(BrowserCommandStrings.OpenTrackedBrowserName, command.DisplayName);
        Assert.Equal(BrowserCommandStrings.OpenTrackedBrowserDescription, command.DisplayDescription);
        var configureCommand = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName);
        Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserName, configureCommand.DisplayName);
        Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserDescription, configureCommand.DisplayDescription);
        var screenshotCommand = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.CaptureScreenshotCommandName);
        Assert.Equal(BrowserCommandStrings.CaptureScreenshotName, screenshotCommand.DisplayName);
        Assert.Equal(BrowserCommandStrings.CaptureScreenshotDescription, screenshotCommand.DisplayDescription);
 
        var snapshot = browserLogsResource.Annotations.OfType<ResourceSnapshotAnnotation>().Single().InitialSnapshot;
        Assert.Equal(BrowserLogsBuilderExtensions.BrowserResourceType, snapshot.ResourceType);
        Assert.NotNull(snapshot.CreationTimeStamp);
        Assert.Contains(snapshot.Properties, property => property.Name == CustomResourceKnownProperties.Source && Equals(property.Value, "web"));
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.BrowserPropertyName && Equals(property.Value, "chrome"));
        Assert.DoesNotContain(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.ProfilePropertyName);
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName && Equals(property.Value, 0));
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.ActiveSessionsPropertyName && Equals(property.Value, "None"));
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.BrowserSessionsPropertyName && Equals(property.Value, "[]"));
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.TotalSessionsLaunchedPropertyName && Equals(property.Value, 0));
        Assert.Empty(snapshot.HealthReports);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandInputsDefaultToCurrentConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "custom-browser", profile: "Profile 1", userDataMode: BrowserUserDataMode.Shared);
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
        var configureCommand = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName);
        var arguments = configureCommand.Arguments;
        var browserInput = arguments.Single(input => input.Name == BrowserLogsConfigurationManager.BrowserInputName);
        var userDataModeInput = arguments.Single(input => input.Name == BrowserLogsConfigurationManager.UserDataModeInputName);
        var profileInput = arguments.Single(input => input.Name == BrowserLogsConfigurationManager.ProfileInputName);
 
        await browserInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = browserInput,
            AllInputs = new InteractionInputCollection(arguments),
            Services = app.Services,
            CancellationToken = CancellationToken.None
        });
        await userDataModeInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = userDataModeInput,
            AllInputs = new InteractionInputCollection(arguments),
            Services = app.Services,
            CancellationToken = CancellationToken.None
        });
        await profileInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = profileInput,
            AllInputs = new InteractionInputCollection(arguments),
            Services = app.Services,
            CancellationToken = CancellationToken.None
        });
 
        Assert.Equal("custom-browser", browserInput.Value);
        Assert.Equal(nameof(BrowserUserDataMode.Shared), userDataModeInput.Value);
        Assert.Equal("Profile 1", profileInput.Value);
        var knownBrowserOptionKeys = new HashSet<string>(["msedge", "chrome", "chromium"], StringComparer.OrdinalIgnoreCase);
        Assert.All(browserInput.Options!, option => Assert.Contains(option.Key, knownBrowserOptionKeys));
        Assert.Contains(profileInput.Options!, option => option.Key == "Profile 1" && option.Value == "Profile 1");
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandProfileInputDefaultsToCurrentConfigurationBeforeDependenciesLoad()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "custom-browser", profile: "Profile 1", userDataMode: BrowserUserDataMode.Shared);
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
        var configureCommand = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName);
        var arguments = configureCommand.Arguments;
        var profileInput = arguments.Single(input => input.Name == BrowserLogsConfigurationManager.ProfileInputName);
 
        await profileInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = profileInput,
            AllInputs = new InteractionInputCollection(arguments),
            Services = app.Services,
            CancellationToken = CancellationToken.None
        });
 
        Assert.Equal("Profile 1", profileInput.Value);
        Assert.False(profileInput.Disabled);
        Assert.Contains(profileInput.Options!, option => option.Key == "Profile 1" && option.Value == "Profile 1");
    }
 
    [Fact]
    public void WithBrowserLogs_ConfigureCommandSaveInputDescribesUnavailableUserSecrets()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
        var configureCommand = Assert.Single(browserLogsResource.Annotations.OfType<ResourceCommandAnnotation>(), annotation => annotation.Name == BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName);
        var arguments = configureCommand.Arguments;
        var saveInput = arguments.Single(input => input.Name == BrowserLogsConfigurationManager.SaveToUserSecretsInputName);
 
        Assert.True(saveInput.Disabled);
        Assert.Null(saveInput.Value);
        Assert.Equal(BrowserCommandStrings.ConfigureTrackedBrowserSaveToUserSecretsDescriptionNotConfigured, saveInput.Description);
    }
 
    [Fact]
    public void WithBrowserLogs_UsesResourceSpecificConfigurationWhenArgumentsAreOmitted()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "msedge";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = "Default";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Shared);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = "Profile 1";
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
 
        Assert.Equal("chrome", browserLogsResource.InitialConfiguration.Browser);
        Assert.Equal("Profile 1", browserLogsResource.InitialConfiguration.Profile);
 
        var snapshot = browserLogsResource.Annotations.OfType<ResourceSnapshotAnnotation>().Single().InitialSnapshot;
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.BrowserPropertyName && Equals(property.Value, "chrome"));
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.ProfilePropertyName && Equals(property.Value, "Profile 1"));
    }
 
    [Fact]
    public void GetDefaultBrowser_PrefersEdgeWhenSharedModeAndEdgeIsInstalled()
    {
        var browser = BrowserConfiguration.GetDefaultBrowser(BrowserUserDataMode.Shared, browser =>
            browser switch
            {
                "chrome" => "/resolved/chrome",
                "msedge" => "/resolved/edge",
                _ => null
            });
 
        Assert.Equal("msedge", browser);
    }
 
    [Fact]
    public void GetDefaultBrowser_PrefersChromeWhenIsolatedModeAndChromeIsInstalled()
    {
        var browser = BrowserConfiguration.GetDefaultBrowser(BrowserUserDataMode.Isolated, browser =>
            browser switch
            {
                "chrome" => "/resolved/chrome",
                "msedge" => "/resolved/edge",
                _ => null
            });
 
        Assert.Equal("chrome", browser);
    }
 
    [Fact]
    public void GetDefaultBrowser_FallsBackToEdgeWhenChromeIsMissing()
    {
        var browser = BrowserConfiguration.GetDefaultBrowser(browser =>
            browser switch
            {
                "msedge" => "/resolved/edge",
                _ => null
            });
 
        Assert.Equal("msedge", browser);
    }
 
    [Fact]
    public void GetDefaultBrowser_FallsBackToChromeWhenKnownBrowsersAreMissing()
    {
        var browser = BrowserConfiguration.GetDefaultBrowser(static _ => null);
 
        Assert.Equal("chrome", browser);
    }
 
    [Fact]
    public void WithBrowserLogs_UsesDetectedDefaultBrowserWhenConfigurationIsMissing()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
 
        Assert.Equal(BrowserConfiguration.GetDefaultBrowser(ChromiumBrowserResolver.TryResolveExecutable), browserLogsResource.InitialConfiguration.Browser);
        Assert.Null(browserLogsResource.InitialConfiguration.Profile);
    }
 
    [Fact]
    public void WithBrowserLogs_ExplicitArgumentsOverrideConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = "Profile 1";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Shared);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "msedge", profile: "Default", userDataMode: BrowserUserDataMode.Shared);
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
 
        Assert.Equal("msedge", browserLogsResource.InitialConfiguration.Browser);
        Assert.Equal("Default", browserLogsResource.InitialConfiguration.Profile);
        Assert.Equal(BrowserUserDataMode.Shared, browserLogsResource.InitialConfiguration.UserDataMode);
    }
 
    [Fact]
    public void WithBrowserLogs_DefaultsToSharedUserDataMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
 
        Assert.Equal(BrowserUserDataMode.Shared, browserLogsResource.InitialConfiguration.UserDataMode);
        var snapshot = browserLogsResource.Annotations.OfType<ResourceSnapshotAnnotation>().Single().InitialSnapshot;
        Assert.Contains(snapshot.Properties, property => property.Name == BrowserLogsBuilderExtensions.UserDataModePropertyName && Equals(property.Value, nameof(BrowserUserDataMode.Shared)));
    }
 
    [Fact]
    public void WithBrowserLogs_ReadsUserDataModeFromConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Shared);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
 
        Assert.Equal(BrowserUserDataMode.Shared, browserLogsResource.InitialConfiguration.UserDataMode);
    }
 
    [Fact]
    public void WithBrowserLogs_RejectsProfileWhenUserDataModeIsIsolated()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        var ex = Assert.Throws<InvalidOperationException>(
            () => web.WithBrowserLogs(profile: "Default", userDataMode: BrowserUserDataMode.Isolated));
        Assert.Contains(BrowserLogsBuilderExtensions.UserDataModeConfigurationKey, ex.Message);
    }
 
    [Fact]
    public void WithBrowserLogs_ExplicitUserDataModeOverridesConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Isolated);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(userDataMode: BrowserUserDataMode.Shared);
 
        using var app = builder.Build();
        var browserLogsResource = Assert.Single(app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>());
        Assert.Equal(BrowserUserDataMode.Shared, browserLogsResource.InitialConfiguration.UserDataMode);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandStartsTrackedSession()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionManager = new FakeBrowserLogsSessionManager();
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sessionManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
        Assert.True(result.Success);
 
        var call = Assert.Single(sessionManager.Calls);
        Assert.Same(browserLogsResource, call.Resource);
        Assert.Equal(browserLogsResource.Name, call.ResourceName);
        Assert.Equal("chrome", call.Configuration.Browser);
        Assert.Null(call.Configuration.Profile);
        Assert.Equal(new Uri("http://localhost:8080", UriKind.Absolute), call.Url);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandSavesResourceScopedBrowserSettingsAndAppliesImmediately()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("resource", "msedge", nameof(BrowserUserDataMode.Shared), "Default", saveToUserSecrets: "true");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.True(result.Success);
        Assert.Equal("msedge", userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"]);
        Assert.Equal(nameof(BrowserUserDataMode.Shared), userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"]);
        Assert.Equal("Default", userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"]);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal("msedge", effectiveConfiguration.Browser);
        Assert.Equal(BrowserUserDataMode.Shared, effectiveConfiguration.UserDataMode);
        Assert.Equal("Default", effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandAppliesRuntimeSettingsWhenUserSecretsAreUnavailable()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager
        {
            IsAvailable = false
        };
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("resource", "msedge", nameof(BrowserUserDataMode.Shared), "Default");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.True(result.Success);
        Assert.Equal(string.Format(CultureInfo.CurrentCulture, BrowserCommandStrings.ConfigureTrackedBrowserApplied, "web"), result.Message);
        Assert.Empty(userSecretsManager.Secrets);
        Assert.Empty(userSecretsManager.DeletedSecrets);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal("msedge", effectiveConfiguration.Browser);
        Assert.Equal(BrowserUserDataMode.Shared, effectiveConfiguration.UserDataMode);
        Assert.Equal("Default", effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandDoesNotOverrideExplicitBuilderSettings()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("resource", "msedge", nameof(BrowserUserDataMode.Shared), "__aspire_browser_default__", saveToUserSecrets: "true");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.True(result.Success);
        Assert.Equal("msedge", userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"]);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal("chrome", effectiveConfiguration.Browser);
        Assert.Equal(BrowserUserDataMode.Shared, effectiveConfiguration.UserDataMode);
        Assert.Null(effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandSavesGlobalSettingsAndClearsProfile()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = "Profile 1";
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("global", "chrome", nameof(BrowserUserDataMode.Isolated), "__aspire_browser_default__", saveToUserSecrets: "true");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.True(result.Success);
        Assert.Equal("chrome", userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"]);
        Assert.Equal(nameof(BrowserUserDataMode.Isolated), userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"]);
        Assert.Contains($"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}", userSecretsManager.DeletedSecrets);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal("chrome", effectiveConfiguration.Browser);
        Assert.Equal(BrowserUserDataMode.Isolated, effectiveConfiguration.UserDataMode);
        Assert.Null(effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandDoesNotApplyRuntimeSettingsWhenUserSecretSaveFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Shared);
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var userDataModeKey = $"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}";
        userSecretsManager.FailingSetSecretNames.Add(userDataModeKey);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("resource", "msedge", nameof(BrowserUserDataMode.Isolated), "__aspire_browser_default__", saveToUserSecrets: "true");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.False(result.Success);
        Assert.Contains(userDataModeKey, result.Message, StringComparison.Ordinal);
        Assert.Equal("msedge", userSecretsManager.Secrets[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:web:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"]);
        Assert.DoesNotContain(userDataModeKey, userSecretsManager.Secrets.Keys);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal("chrome", effectiveConfiguration.Browser);
        Assert.Equal(BrowserUserDataMode.Shared, effectiveConfiguration.UserDataMode);
        Assert.Null(effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandRefreshesAllBrowserLogsResourcesForGlobalSettings()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        var admin = builder.AddResource(new TestHttpResource("admin"))
            .WithHttpEndpoint(targetPort: 8081)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8081))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
        admin.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResources = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().ToArray();
        var webBrowserLogsResource = browserLogsResources.Single(resource => resource.ParentResource.Name == "web");
        var adminBrowserLogsResource = browserLogsResources.Single(resource => resource.ParentResource.Name == "admin");
        var arguments = CreateConfigureArguments("global", "chrome", nameof(BrowserUserDataMode.Isolated), "__aspire_browser_default__");
        var result = await app.ResourceCommands.ExecuteCommandAsync(webBrowserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.True(result.Success);
 
        await app.ResourceNotifications.WaitForResourceAsync(
            adminBrowserLogsResource.Name,
            resourceEvent =>
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserPropertyName, "chrome") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.UserDataModePropertyName, nameof(BrowserUserDataMode.Isolated)) &&
                DoesNotHaveProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ProfilePropertyName)).DefaultTimeout();
    }
 
    [Fact]
    public async Task WithBrowserLogs_ConfigureCommandValidatesEffectiveConfigurationBeforeSaving()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userSecretsManager = new RecordingUserSecretsManager();
        builder.Configuration[KnownConfigNames.VersionCheckDisabled] = "true";
        builder.Services.AddSingleton<IUserSecretsManager>(userSecretsManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(profile: "Default");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var arguments = CreateConfigureArguments("resource", "chrome", nameof(BrowserUserDataMode.Isolated), "__aspire_browser_default__");
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.ConfigureTrackedBrowserCommandName, arguments);
 
        Assert.False(result.Success);
        Assert.Empty(userSecretsManager.Secrets);
        Assert.Empty(userSecretsManager.DeletedSecrets);
 
        var effectiveConfiguration = browserLogsResource.ResolveCurrentConfiguration(
            app.Services.GetRequiredService<IConfiguration>(),
            app.Services.GetRequiredService<BrowserLogsConfigurationStore>());
        Assert.Equal(BrowserUserDataMode.Shared, effectiveConfiguration.UserDataMode);
        Assert.Equal("Default", effectiveConfiguration.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CaptureScreenshotCommandReturnsArtifactResult()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionManager = new FakeBrowserLogsSessionManager
        {
            ScreenshotResult = new BrowserLogsScreenshotCaptureResult(
                "session-0002",
                "msedge",
                @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
                BrowserHostOwnership.Adopted.ToString(),
                4242,
                "target-0002",
                new Uri("https://localhost:8443/"),
                new BrowserLogsArtifact(
                    "web-browser-logs",
                    "screenshot",
                    Path.Combine(AppContext.BaseDirectory, "artifacts", "screenshot.png"),
                    "image/png",
                    1234,
                    new DateTimeOffset(2026, 4, 27, 12, 0, 0, TimeSpan.Zero)))
        };
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sessionManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.CaptureScreenshotCommandName).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.Equal("web-browser-logs", Assert.Single(sessionManager.CaptureScreenshotCalls));
        Assert.Contains("screenshot.png", result.Message);
        Assert.NotNull(result.Data);
        Assert.Equal(CommandResultFormat.Json, result.Data.Format);
        Assert.True(result.Data.DisplayImmediately);
 
        using var document = JsonDocument.Parse(result.Data.Value);
        Assert.Equal("web-browser-logs", document.RootElement.GetProperty("resourceName").GetString());
        Assert.Equal("session-0002", document.RootElement.GetProperty("sessionId").GetString());
        Assert.Equal("msedge", document.RootElement.GetProperty("browser").GetString());
        Assert.Equal(@"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", document.RootElement.GetProperty("browserExecutable").GetString());
        Assert.Equal("Adopted", document.RootElement.GetProperty("browserHostOwnership").GetString());
        Assert.Equal(4242, document.RootElement.GetProperty("processId").GetInt32());
        Assert.Equal("target-0002", document.RootElement.GetProperty("targetId").GetString());
        Assert.Equal("https://localhost:8443/", document.RootElement.GetProperty("targetUrl").GetString());
        Assert.EndsWith("screenshot.png", document.RootElement.GetProperty("path").GetString(), StringComparison.Ordinal);
        Assert.Equal("image/png", document.RootElement.GetProperty("mimeType").GetString());
        Assert.Equal(1234, document.RootElement.GetProperty("sizeBytes").GetInt32());
    }
 
    [Fact]
    public async Task WithBrowserLogs_CaptureScreenshotCommandReturnsClearFailureWhenNoSessionIsActive()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.CaptureScreenshotCommandName).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.Equal("No active tracked browser session is available to capture.", result.Message);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CaptureScreenshotCommandWritesPngArtifact()
    {
        var artifactDirectory = Directory.CreateTempSubdirectory();
        try
        {
            using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
            var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
            builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
                new BrowserLogsSessionManager(
                    sp.GetRequiredService<ResourceLoggerService>(),
                    sp.GetRequiredService<ResourceNotificationService>(),
                    sp.GetRequiredService<TimeProvider>(),
                    sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                    new BrowserLogsArtifactWriter(sp.GetRequiredService<TimeProvider>(), () => artifactDirectory.FullName),
                    sessionFactory));
 
            var web = builder.AddResource(new TestHttpResource("web"))
                .WithHttpEndpoint(targetPort: 8080)
                .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
                .WithInitialState(new CustomResourceSnapshot
                {
                    ResourceType = "TestHttp",
                    State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                    Properties = []
                });
 
            web.WithBrowserLogs(browser: "chrome");
 
            using var app = builder.Build();
            await app.StartAsync();
 
            var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
            var openResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
            Assert.True(openResult.Success);
 
            var session = Assert.Single(sessionFactory.Sessions);
            session.ScreenshotBytes = [1, 2, 3, 4];
 
            var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.CaptureScreenshotCommandName).DefaultTimeout();
            var logs = await ConsoleLoggingTestHelpers.WatchForLogsAsync(
                app.Services.GetRequiredService<ResourceLoggerService>().WatchAsync(browserLogsResource.Name),
                targetLogCount: 6).DefaultTimeout();
 
            Assert.True(result.Success);
            Assert.NotNull(result.Data);
 
            using var document = JsonDocument.Parse(result.Data.Value);
            var path = document.RootElement.GetProperty("path").GetString();
 
            Assert.NotNull(path);
            Assert.StartsWith(artifactDirectory.FullName, path, StringComparison.OrdinalIgnoreCase);
            Assert.Equal(session.ScreenshotBytes, await File.ReadAllBytesAsync(path));
            Assert.Equal("session-0001", document.RootElement.GetProperty("sessionId").GetString());
            Assert.Equal("chrome", document.RootElement.GetProperty("browser").GetString());
            Assert.Equal("/fake/browser-1", document.RootElement.GetProperty("browserExecutable").GetString());
            Assert.Equal("Owned", document.RootElement.GetProperty("browserHostOwnership").GetString());
            Assert.Equal(1001, document.RootElement.GetProperty("processId").GetInt32());
            Assert.Equal("target-1", document.RootElement.GetProperty("targetId").GetString());
            Assert.Equal("http://localhost:8080/", document.RootElement.GetProperty("targetUrl").GetString());
            Assert.Equal(session.ScreenshotBytes.Length, document.RootElement.GetProperty("sizeBytes").GetInt32());
            Assert.Contains(logs, log => log.Content.Contains(path, StringComparison.Ordinal) &&
                log.Content.Contains("4 bytes", StringComparison.Ordinal) &&
                log.Content.Contains("target-1", StringComparison.Ordinal));
        }
        finally
        {
            artifactDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandUsesLatestConfiguredSettingsAndRefreshesProperties()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = "Default";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}"] = nameof(BrowserUserDataMode.Shared);
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "msedge";
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.ProfileConfigurationKey}"] = null;
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
        Assert.True(result.Success);
 
        var launchConfiguration = Assert.Single(sessionFactory.Configurations);
        Assert.Equal("msedge", launchConfiguration.Browser);
        Assert.Null(launchConfiguration.Profile);
 
        var runningEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserPropertyName, "msedge") &&
                !resourceEvent.Snapshot.Properties.Any(property => property.Name == BrowserLogsBuilderExtensions.ProfilePropertyName)).DefaultTimeout();
 
        var session = Assert.Single(GetBrowserSessions(runningEvent.Snapshot));
        Assert.Equal("msedge", session.Browser);
        Assert.Null(session.Profile);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandRefreshesBrowserExecutablePropertyWhenRelaunchFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
 
        var firstResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(firstResult.Success);
 
        await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserExecutablePropertyName, "/fake/browser-1")).DefaultTimeout();
 
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            var tempBrowserPath = Path.Combine(tempDirectory.FullName, OperatingSystem.IsWindows() ? "tracked-browser.exe" : "tracked-browser");
            await File.WriteAllTextAsync(tempBrowserPath, string.Empty);
 
            builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = tempBrowserPath;
            sessionFactory.NextStartException = new InvalidOperationException("Launch failed.");
 
            var secondResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
            Assert.False(secondResult.Success);
            Assert.Equal("Launch failed.", secondResult.Message);
 
            var failedEvent = await app.ResourceNotifications.WaitForResourceAsync(
                browserLogsResource.Name,
                resourceEvent =>
                    resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                    HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserPropertyName, tempBrowserPath) &&
                    HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserExecutablePropertyName, tempBrowserPath) &&
                    HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserHostOwnershipPropertyName, nameof(BrowserHostOwnership.Owned)) &&
                    HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, "InvalidOperationException: Launch failed.") &&
                    HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                    resourceEvent.Snapshot.HealthReports.Any(report =>
                        report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName &&
                        report.Status == HealthStatus.Unhealthy)).DefaultTimeout();
 
            Assert.Collection(
                GetBrowserSessions(failedEvent.Snapshot),
                session =>
                {
                    Assert.Equal("session-0001", session.SessionId);
                    Assert.Equal("/fake/browser-1", session.BrowserExecutable);
                });
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandRemovesStaleBrowserExecutablePropertyWhenBrowserCannotBeResolved()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "chrome";
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
 
        var firstResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(firstResult.Success);
 
        await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserExecutablePropertyName, "/fake/browser-1")).DefaultTimeout();
 
        builder.Configuration[$"{BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName}:{BrowserLogsBuilderExtensions.BrowserConfigurationKey}"] = "missing-browser";
        sessionFactory.NextStartException = new InvalidOperationException("Launch failed.");
 
        var secondResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
        Assert.False(secondResult.Success);
 
        var failedEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserPropertyName, "missing-browser") &&
                !resourceEvent.Snapshot.Properties.Any(property => property.Name == BrowserLogsBuilderExtensions.BrowserExecutablePropertyName) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserHostOwnershipPropertyName, nameof(BrowserHostOwnership.Owned)) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, "InvalidOperationException: Launch failed.") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                resourceEvent.Snapshot.HealthReports.Any(report =>
                    report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName &&
                    report.Status == HealthStatus.Unhealthy)).DefaultTimeout();
 
        Assert.Collection(
            GetBrowserSessions(failedEvent.Snapshot),
            session =>
            {
                Assert.Equal("session-0001", session.SessionId);
                Assert.Equal("/fake/browser-1", session.BrowserExecutable);
            });
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandPublishesFailureDiagnosticsWhenLaunchFailsBeforeAnySession()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory
        {
            NextStartException = new InvalidOperationException("Launch failed.", new TimeoutException("CDP timed out."))
        };
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.Equal("Launch failed.", result.Message);
 
        var errorText = "InvalidOperationException: Launch failed. --> TimeoutException: CDP timed out.";
        var failedEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.FailedToStart &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 0) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "None") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, errorText) &&
                resourceEvent.Snapshot.HealthReports.Any(report =>
                    report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName &&
                    report.Status == HealthStatus.Unhealthy &&
                    report.Description == errorText)).DefaultTimeout();
 
        Assert.Single(failedEvent.Snapshot.HealthReports);
        Assert.Empty(GetBrowserSessions(failedEvent.Snapshot));
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandClearsLastErrorAfterSuccessfulLaunch()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory
        {
            NextStartException = new InvalidOperationException("Launch failed.")
        };
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var failedResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.False(failedResult.Success);
 
        await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.FailedToStart &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, "InvalidOperationException: Launch failed.")).DefaultTimeout();
 
        var successfulResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(successfulResult.Success);
 
        var runningEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                DoesNotHaveProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName) &&
                !resourceEvent.Snapshot.HealthReports.Any(report => report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName)).DefaultTimeout();
 
        Assert.Collection(
            GetBrowserSessions(runningEvent.Snapshot),
            session => Assert.Equal("session-0002", session.SessionId));
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandSurfacesAdoptedBrowserDiagnostics()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory
        {
            NextBrowserHostOwnership = BrowserHostOwnership.Adopted,
            NextProcessIdIsNull = true
        };
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "msedge");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(result.Success);
 
        var runningEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserHostOwnershipPropertyName, nameof(BrowserHostOwnership.Adopted)) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "session-0001 (adopted browser)")).DefaultTimeout();
 
        var session = Assert.Single(GetBrowserSessions(runningEvent.Snapshot));
        Assert.Equal(nameof(BrowserHostOwnership.Adopted), session.BrowserHostOwnership);
        Assert.Null(session.ProcessId);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandFailsWhenEndpointIsMissing()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionManager = new FakeBrowserLogsSessionManager();
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sessionManager);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs();
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.Equal("Resource 'web' does not have an HTTP or HTTPS endpoint. Browser logs require an endpoint to navigate to.", result.Message);
        Assert.Empty(sessionManager.Calls);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandBecomesEnabledWhenParentReady()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = KnownResourceStates.NotStarted,
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
        var initialEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent => resourceEvent.Snapshot.Commands.Any(command =>
                command.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName &&
                command.State == ResourceCommandState.Disabled)).DefaultTimeout();
 
        Assert.Equal(ResourceCommandState.Disabled, initialEvent.Snapshot.Commands.Single(command => command.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).State);
 
        await app.ResourceNotifications.PublishUpdateAsync(web.Resource, snapshot => snapshot with
        {
            State = KnownResourceStates.Running
        }).DefaultTimeout();
 
        var eventing = app.Services.GetRequiredService<IDistributedApplicationEventing>();
        await eventing.PublishAsync(new ResourceReadyEvent(web.Resource, app.Services)).DefaultTimeout();
 
        var enabledEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent => resourceEvent.Snapshot.Commands.Any(command =>
                command.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName &&
                command.State == ResourceCommandState.Enabled)).DefaultTimeout();
 
        Assert.Equal(ResourceCommandState.Enabled, enabledEvent.Snapshot.Commands.Single(command => command.Name == BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).State);
    }
 
    [Fact]
    public async Task WithBrowserLogs_CommandTracksMultipleSessionsWithUniqueIds()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome", profile: "Default", userDataMode: BrowserUserDataMode.Shared);
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
 
        var firstResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(firstResult.Success);
 
        var firstSession = Assert.Single(sessionFactory.Sessions);
        Assert.Equal("session-0001", firstSession.SessionId);
 
        var firstRunningEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "session-0001 (PID 1001)") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.TotalSessionsLaunchedPropertyName, 1) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastSessionPropertyName, "session-0001") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserExecutablePropertyName, "/fake/browser-1") &&
                resourceEvent.Snapshot.HealthReports.Any(report => report.Name == "session-0001" && report.Status == HealthStatus.Healthy)).DefaultTimeout();
 
        Assert.Single(firstRunningEvent.Snapshot.HealthReports);
        Assert.Collection(
            GetBrowserSessions(firstRunningEvent.Snapshot),
            session =>
            {
                Assert.Equal("session-0001", session.SessionId);
                Assert.Equal("chrome", session.Browser);
                Assert.Equal("/fake/browser-1", session.BrowserExecutable);
                Assert.Equal(nameof(BrowserHostOwnership.Owned), session.BrowserHostOwnership);
                Assert.Equal(1001, session.ProcessId);
                Assert.Equal("Default", session.Profile);
                Assert.Equal("http://localhost:8080/", session.TargetUrl);
                Assert.Equal("ws://127.0.0.1:9001/devtools/browser/browser-1", session.CdpEndpoint);
                Assert.Equal("ws://127.0.0.1:9001/devtools/page/target-1", session.PageCdpEndpoint);
                Assert.Equal("target-1", session.TargetId);
            });
        Assert.Equal(0, firstSession.StopCallCount);
 
        var secondResult = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
        Assert.True(secondResult.Success);
 
        Assert.Equal(2, sessionFactory.Sessions.Count);
        var secondSession = sessionFactory.Sessions[1];
        Assert.Equal("session-0002", secondSession.SessionId);
 
        var secondRunningEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 2) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "session-0001 (PID 1001), session-0002 (PID 1002)") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.TotalSessionsLaunchedPropertyName, 2) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastSessionPropertyName, "session-0002") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.BrowserExecutablePropertyName, "/fake/browser-2") &&
                resourceEvent.Snapshot.HealthReports.Any(report => report.Name == "session-0001" && report.Status == HealthStatus.Healthy) &&
                resourceEvent.Snapshot.HealthReports.Any(report => report.Name == "session-0002" && report.Status == HealthStatus.Healthy)).DefaultTimeout();
 
        Assert.Equal(2, secondRunningEvent.Snapshot.HealthReports.Length);
        Assert.Collection(
            GetBrowserSessions(secondRunningEvent.Snapshot),
            session =>
            {
                Assert.Equal("session-0001", session.SessionId);
                Assert.Equal("ws://127.0.0.1:9001/devtools/browser/browser-1", session.CdpEndpoint);
                Assert.Equal("ws://127.0.0.1:9001/devtools/page/target-1", session.PageCdpEndpoint);
            },
            session =>
            {
                Assert.Equal("session-0002", session.SessionId);
                Assert.Equal("ws://127.0.0.1:9002/devtools/browser/browser-2", session.CdpEndpoint);
                Assert.Equal("ws://127.0.0.1:9002/devtools/page/target-2", session.PageCdpEndpoint);
                Assert.Equal("target-2", session.TargetId);
            });
        Assert.Equal(0, firstSession.StopCallCount);
 
        await firstSession.CompleteAsync(exitCode: 0);
 
        var firstCompletedEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "session-0002 (PID 1002)") &&
                resourceEvent.Snapshot.HealthReports.Length == 1 &&
                resourceEvent.Snapshot.HealthReports[0].Name == "session-0002").DefaultTimeout();
 
        Assert.Equal("session-0002", firstCompletedEvent.Snapshot.HealthReports[0].Name);
        Assert.Collection(
            GetBrowserSessions(firstCompletedEvent.Snapshot),
            session => Assert.Equal("session-0002", session.SessionId));
 
        await secondSession.CompleteAsync(exitCode: 0);
 
        var allCompletedEvent = await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Finished &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 0) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionsPropertyName, "None") &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.TotalSessionsLaunchedPropertyName, 2) &&
                resourceEvent.Snapshot.HealthReports.IsEmpty).DefaultTimeout();
 
        Assert.Equal(KnownResourceStates.Finished, allCompletedEvent.Snapshot.State?.Text);
        Assert.Empty(GetBrowserSessions(allCompletedEvent.Snapshot));
    }
 
    [Fact]
    public async Task WithBrowserLogs_PreservesLastErrorWhenOneOfMultipleSessionsFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
 
        Assert.True((await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout()).Success);
        Assert.True((await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout()).Success);
 
        var firstSession = sessionFactory.Sessions[0];
        var secondSession = sessionFactory.Sessions[1];
        await firstSession.CompleteAsync(exitCode: 0, error: new InvalidOperationException("Target crashed."));
 
        var errorText = "InvalidOperationException: Target crashed.";
        await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 1) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, errorText) &&
                resourceEvent.Snapshot.HealthReports.Any(report =>
                    report.Name == "session-0002" &&
                    report.Status == HealthStatus.Healthy) &&
                resourceEvent.Snapshot.HealthReports.Any(report =>
                    report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName &&
                    report.Status == HealthStatus.Unhealthy &&
                    report.Description == errorText)).DefaultTimeout();
 
        await secondSession.CompleteAsync(exitCode: 0);
 
        await app.ResourceNotifications.WaitForResourceAsync(
            browserLogsResource.Name,
            resourceEvent =>
                resourceEvent.Snapshot.State?.Text == KnownResourceStates.Exited &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.ActiveSessionCountPropertyName, 0) &&
                HasProperty(resourceEvent.Snapshot, BrowserLogsBuilderExtensions.LastErrorPropertyName, errorText) &&
                resourceEvent.Snapshot.HealthReports.Any(report =>
                    report.Name == BrowserLogsBuilderExtensions.LastErrorPropertyName &&
                    report.Status == HealthStatus.Unhealthy &&
                    report.Description == errorText)).DefaultTimeout();
    }
 
    [Fact]
    public async Task WithBrowserLogs_DisposeWaitsForCompletionObservers()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var sessionFactory = new FakeBrowserLogsRunningSessionFactory();
 
        builder.Services.AddSingleton<IBrowserLogsSessionManager>(sp =>
            new BrowserLogsSessionManager(
                sp.GetRequiredService<ResourceLoggerService>(),
                sp.GetRequiredService<ResourceNotificationService>(),
                sp.GetRequiredService<TimeProvider>(),
                sp.GetRequiredService<ILogger<BrowserLogsSessionManager>>(),
                artifactWriter: null,
                sessionFactory: sessionFactory));
 
        var web = builder.AddResource(new TestHttpResource("web"))
            .WithHttpEndpoint(targetPort: 8080)
            .WithEndpoint("http", endpoint => endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 8080))
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = "TestHttp",
                State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success),
                Properties = []
            });
 
        web.WithBrowserLogs(browser: "chrome");
 
        var app = builder.Build();
        var disposed = false;
 
        try
        {
            await app.StartAsync();
 
            var browserLogsResource = app.Services.GetRequiredService<DistributedApplicationModel>().Resources.OfType<BrowserLogsResource>().Single();
            var result = await app.ResourceCommands.ExecuteCommandAsync(browserLogsResource, BrowserLogsBuilderExtensions.OpenTrackedBrowserCommandName).DefaultTimeout();
            Assert.True(result.Success);
 
            var session = Assert.Single(sessionFactory.Sessions);
            session.PauseCompletionObserver();
 
            var disposeTask = app.DisposeAsync().AsTask();
 
            await session.CompletionObserverStarted.DefaultTimeout();
            Assert.False(disposeTask.IsCompleted);
 
            session.ResumeCompletionObserver();
            await disposeTask.DefaultTimeout();
            disposed = true;
        }
        finally
        {
            if (!disposed)
            {
                await app.DisposeAsync();
            }
        }
    }
 
    [Fact]
    public async Task BrowserEventLogger_LogsSuccessfulNetworkRequests()
    {
        var resourceLoggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var resourceLogger = resourceLoggerService.GetLogger("web-browser-logs");
        var eventLogger = new BrowserEventLogger("session-0001", resourceLogger);
        var logs = await CaptureLogsAsync(resourceLoggerService, "web-browser-logs", () =>
        {
            eventLogger.HandleEvent(ParseProtocolEvent("""
                {
                  "method": "Network.requestWillBeSent",
                  "sessionId": "target-session-1",
                  "params": {
                    "requestId": "request-1",
                    "timestamp": 1.5,
                    "type": "Fetch",
                    "request": {
                      "url": "https://example.test/api/todos",
                      "method": "GET"
                    }
                  }
                }
                """));
            eventLogger.HandleEvent(ParseProtocolEvent("""
                {
                  "method": "Network.responseReceived",
                  "sessionId": "target-session-1",
                  "params": {
                    "requestId": "request-1",
                    "timestamp": 1.6,
                    "type": "Fetch",
                    "response": {
                      "url": "https://example.test/api/todos",
                      "status": 200,
                      "statusText": "OK",
                      "fromDiskCache": false,
                      "fromServiceWorker": false
                    }
                  }
                }
                """));
            eventLogger.HandleEvent(ParseProtocolEvent("""
                {
                  "method": "Network.loadingFinished",
                  "sessionId": "target-session-1",
                  "params": {
                    "requestId": "request-1",
                    "timestamp": 1.75,
                    "encodedDataLength": 1024
                  }
                }
                """));
        });
        var log = Assert.Single(logs);
 
        Assert.Equal("2000-12-29T20:59:59.0000000Z [session-0001] [network.fetch] GET https://example.test/api/todos -> 200 OK (250 ms, 1024 B)", log.Content);
    }
 
    [Fact]
    public async Task BrowserEventLogger_LogsFailedNetworkRequests()
    {
        var resourceLoggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var resourceLogger = resourceLoggerService.GetLogger("web-browser-logs");
        var eventLogger = new BrowserEventLogger("session-0002", resourceLogger);
        var logs = await CaptureLogsAsync(resourceLoggerService, "web-browser-logs", () =>
        {
            eventLogger.HandleEvent(ParseProtocolEvent("""
                {
                  "method": "Network.requestWillBeSent",
                  "sessionId": "target-session-2",
                  "params": {
                    "requestId": "request-2",
                    "timestamp": 5.0,
                    "type": "Document",
                    "request": {
                      "url": "https://127.0.0.1:1/browser-network-failure",
                      "method": "GET"
                    }
                  }
                }
                """));
            eventLogger.HandleEvent(ParseProtocolEvent("""
                {
                  "method": "Network.loadingFailed",
                  "sessionId": "target-session-2",
                  "params": {
                    "requestId": "request-2",
                    "timestamp": 5.15,
                    "errorText": "net::ERR_CONNECTION_REFUSED",
                    "canceled": false
                  }
                }
                """));
        });
        var log = Assert.Single(logs);
 
        Assert.Equal("2000-12-29T20:59:59.0000000Z [session-0002] [network.document] GET https://127.0.0.1:1/browser-network-failure failed: net::ERR_CONNECTION_REFUSED (150 ms)", log.Content);
    }
 
    private sealed class TestHttpResource(string name) : Resource(name), IResourceWithEndpoints;
 
    private static InteractionInputCollection CreateConfigureArguments(string scope, string browser, string userDataMode, string profile, string? saveToUserSecrets = null)
    {
        return new InteractionInputCollection(
        [
            new InteractionInput { Name = "scope", InputType = InputType.Choice, Value = scope },
            new InteractionInput { Name = "browser", InputType = InputType.Choice, Value = browser },
            new InteractionInput { Name = "userDataMode", InputType = InputType.Choice, Value = userDataMode },
            new InteractionInput { Name = "profile", InputType = InputType.Choice, Value = profile },
            new InteractionInput { Name = "saveToUserSecrets", InputType = InputType.Boolean, Value = saveToUserSecrets },
        ]);
    }
 
    private static bool HasProperty(CustomResourceSnapshot snapshot, string name, object expectedValue) =>
        snapshot.Properties.Any(property => property.Name == name && Equals(property.Value, expectedValue));
 
    private static bool DoesNotHaveProperty(CustomResourceSnapshot snapshot, string name) =>
        !snapshot.Properties.Any(property => property.Name == name);
 
    private static IReadOnlyList<BrowserSessionPropertyValue> GetBrowserSessions(CustomResourceSnapshot snapshot)
    {
        var property = snapshot.Properties.Single(property => property.Name == BrowserLogsBuilderExtensions.BrowserSessionsPropertyName);
        var value = Assert.IsType<string>(property.Value);
        return JsonSerializer.Deserialize<List<BrowserSessionPropertyValue>>(value, BrowserSessionPropertyJsonOptions)
            ?? throw new InvalidOperationException("Expected browser session property JSON.");
    }
 
    private static BrowserLogsCdpProtocolEvent ParseProtocolEvent(string json)
    {
        var payload = Encoding.UTF8.GetBytes(json);
        return BrowserLogsCdpProtocol.ParseEvent(BrowserLogsCdpProtocol.ParseMessageHeader(payload), payload)
            ?? throw new InvalidOperationException("Expected a browser protocol event frame.");
    }
 
    private static Task<IReadOnlyList<LogLine>> CaptureLogsAsync(ResourceLoggerService resourceLoggerService, string resourceName, Action writeLogs) =>
        ConsoleLoggingTestHelpers.CaptureLogsAsync(resourceLoggerService, resourceName, targetLogCount: 1, writeLogs);
 
    private sealed class FakeBrowserLogsSessionManager : IBrowserLogsSessionManager
    {
        public List<SessionStartCall> Calls { get; } = [];
 
        public List<string> CaptureScreenshotCalls { get; } = [];
 
        public BrowserLogsScreenshotCaptureResult ScreenshotResult { get; set; } = new(
            "session-0001",
            "chrome",
            "/fake/browser",
            BrowserHostOwnership.Owned.ToString(),
            1001,
            "target-1",
            new Uri("https://localhost:5001/"),
            new BrowserLogsArtifact(
                "web-browser-logs",
                "screenshot",
                Path.Combine(AppContext.BaseDirectory, "screenshot.png"),
                "image/png",
                0,
                DateTimeOffset.UnixEpoch));
 
        public Task StartSessionAsync(BrowserLogsResource resource, BrowserConfiguration configuration, string resourceName, Uri url, CancellationToken cancellationToken)
        {
            Calls.Add(new SessionStartCall(resource, configuration, resourceName, url));
            return Task.CompletedTask;
        }
 
        public Task<BrowserLogsScreenshotCaptureResult> CaptureScreenshotAsync(string resourceName, CancellationToken cancellationToken)
        {
            CaptureScreenshotCalls.Add(resourceName);
            return Task.FromResult(ScreenshotResult);
        }
    }
 
    private sealed record SessionStartCall(BrowserLogsResource Resource, BrowserConfiguration Configuration, string ResourceName, Uri Url);
 
    private sealed class FakeBrowserLogsRunningSessionFactory : IBrowserLogsRunningSessionFactory
    {
        public List<FakeBrowserLogsRunningSession> Sessions { get; } = [];
        public List<BrowserConfiguration> Configurations { get; } = [];
        public Exception? NextStartException { get; set; }
        public BrowserHostOwnership NextBrowserHostOwnership { get; set; } = BrowserHostOwnership.Owned;
        public int? NextProcessId { get; set; }
        public bool NextProcessIdIsNull { get; set; }
 
        public Task<IBrowserLogsRunningSession> StartSessionAsync(
            BrowserConfiguration configuration,
            string resourceName,
            Uri url,
            string sessionId,
            ILogger resourceLogger,
            CancellationToken cancellationToken)
        {
            Configurations.Add(configuration);
 
            if (NextStartException is { } exception)
            {
                NextStartException = null;
                return Task.FromException<IBrowserLogsRunningSession>(exception);
            }
 
            var sessionNumber = Sessions.Count + 1;
            var processId = NextProcessIdIsNull ? (int?)null : NextProcessId ?? 1000 + sessionNumber;
            var session = new FakeBrowserLogsRunningSession(
                sessionId,
                $"/fake/browser-{sessionNumber}",
                processId,
                sessionNumber,
                NextBrowserHostOwnership,
                startedAt: DateTime.UtcNow);
 
            Sessions.Add(session);
            NextBrowserHostOwnership = BrowserHostOwnership.Owned;
            NextProcessId = null;
            NextProcessIdIsNull = false;
 
            return Task.FromResult<IBrowserLogsRunningSession>(session);
        }
    }
 
    private sealed class FakeBrowserLogsRunningSession(
        string sessionId,
        string browserExecutable,
        int? processId,
        int sessionNumber,
        BrowserHostOwnership browserHostOwnership,
        DateTime startedAt) : IBrowserLogsRunningSession
    {
        private TaskCompletionSource<object?> _completionObserverGate = CreateSignaledTaskCompletionSource();
        private readonly TaskCompletionSource<(int ExitCode, Exception? Error)> _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
        private Task? _completionObserverTask;
 
        public string SessionId { get; } = sessionId;
 
        public string BrowserExecutable { get; } = browserExecutable;
 
        public Uri BrowserDebugEndpoint { get; } = new($"ws://127.0.0.1:{9000 + sessionNumber}/devtools/browser/browser-{sessionNumber}");
 
        public BrowserHostOwnership BrowserHostOwnership { get; } = browserHostOwnership;
 
        public int? ProcessId { get; } = processId;
 
        public DateTime StartedAt { get; } = startedAt;
 
        public string TargetId { get; } = $"target-{sessionNumber}";
 
        public int StopCallCount { get; private set; }
 
        public byte[] ScreenshotBytes { get; set; } = [0x89, 0x50, 0x4e, 0x47];
 
        public Task CompletionObserverStarted => CompletionObserverStartedSource.Task;
 
        private TaskCompletionSource<object?> CompletionObserverStartedSource { get; set; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public Task StartCompletionObserver(Func<int?, Exception?, Task> onCompleted)
        {
            _completionObserverTask = ObserveCompletionAsync(onCompleted);
            return _completionObserverTask;
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            StopCallCount++;
            _completionSource.TrySetResult((0, null));
            return Task.CompletedTask;
        }
 
        public Task<byte[]> CaptureScreenshotAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(ScreenshotBytes);
        }
 
        public async Task CompleteAsync(int exitCode, Exception? error = null)
        {
            _completionSource.TrySetResult((exitCode, error));
            await (_completionObserverTask ?? Task.CompletedTask);
        }
 
        public void PauseCompletionObserver()
        {
            CompletionObserverStartedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
            _completionObserverGate = new(TaskCreationOptions.RunContinuationsAsynchronously);
        }
 
        public void ResumeCompletionObserver()
        {
            _completionObserverGate.TrySetResult(null);
        }
 
        private async Task ObserveCompletionAsync(Func<int?, Exception?, Task> onCompleted)
        {
            var (exitCode, error) = await _completionSource.Task;
            CompletionObserverStartedSource.TrySetResult(null);
            await _completionObserverGate.Task;
            await onCompleted(exitCode, error);
        }
 
        private static TaskCompletionSource<object?> CreateSignaledTaskCompletionSource()
        {
            var source = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
            source.TrySetResult(null);
            return source;
        }
    }
 
    private sealed class RecordingUserSecretsManager : IUserSecretsManager
    {
        public Dictionary<string, string> Secrets { get; } = new(StringComparer.Ordinal);
 
        public HashSet<string> DeletedSecrets { get; } = new(StringComparer.Ordinal);
 
        public HashSet<string> FailingSetSecretNames { get; } = new(StringComparer.Ordinal);
 
        public HashSet<string> FailingDeleteSecretNames { get; } = new(StringComparer.Ordinal);
 
        public bool IsAvailable { get; init; } = true;
 
        public string FilePath => Path.Combine(AppContext.BaseDirectory, "test-secrets.json");
 
        public bool TrySetSecret(string name, string value)
        {
            if (FailingSetSecretNames.Contains(name))
            {
                return false;
            }
 
            Secrets[name] = value;
            DeletedSecrets.Remove(name);
            return true;
        }
 
        public bool TryDeleteSecret(string name)
        {
            if (FailingDeleteSecretNames.Contains(name))
            {
                return false;
            }
 
            Secrets.Remove(name);
            DeletedSecrets.Add(name);
            return true;
        }
 
        public void GetOrSetSecret(IConfigurationManager configuration, string name, Func<string> valueGenerator)
        {
            configuration[name] ??= valueGenerator();
        }
 
        public Task SaveStateAsync(JsonObject state, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private static JsonSerializerOptions BrowserSessionPropertyJsonOptions { get; } = new(JsonSerializerDefaults.Web);
 
    private sealed record BrowserSessionPropertyValue(
        string SessionId,
        string Browser,
        string BrowserExecutable,
        int? ProcessId,
        string? Profile,
        DateTime StartedAt,
        string TargetUrl,
        string BrowserHostOwnership,
        string CdpEndpoint,
        string PageCdpEndpoint,
        string TargetId);
}
 
#pragma warning restore ASPIREUSERSECRETS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning restore ASPIREBROWSERLOGS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.