File: Orchestrator\ParameterProcessorTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text;
using System.Text.Json.Nodes;
using Aspire.Dashboard.Model;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Pipelines.Internal;
using Aspire.Hosting.Resources;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
 
namespace Aspire.Hosting.Tests.Orchestrator;
 
[Trait("Partition", "3")]
public class ParameterProcessorTests
{
    [Fact]
    public async Task InitializeParametersAsync_WithValidParameters_SetsRunningState()
    {
        // Arrange
        var parameterProcessor = CreateParameterProcessor();
        var parameters = new[]
        {
            CreateParameterResource("param1", "value1"),
            CreateParameterResource("param2", "value2")
        };
 
        // Act
        await parameterProcessor.InitializeParametersAsync(parameters).DefaultTimeout();
 
        // Assert
        foreach (var param in parameters)
        {
            Assert.NotNull(param.WaitForValueTcs);
            Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully);
#pragma warning disable CS0618 // Type or member is obsolete
            Assert.Equal(param.Value, await param.WaitForValueTcs.Task.DefaultTimeout());
#pragma warning restore CS0618 // Type or member is obsolete
        }
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithValidParametersAndDashboardEnabled_SetsRunningState()
    {
        // Arrange
        var interactionService = CreateInteractionService(disableDashboard: false);
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false);
        var parameters = new[]
        {
            CreateParameterResource("param1", "value1"),
            CreateParameterResource("param2", "value2")
        };
 
        // Act
        await parameterProcessor.InitializeParametersAsync(parameters).DefaultTimeout();
 
        // Assert
        foreach (var param in parameters)
        {
            Assert.NotNull(param.WaitForValueTcs);
            Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully);
#pragma warning disable CS0618 // Type or member is obsolete
            Assert.Equal(param.Value, await param.WaitForValueTcs.Task.DefaultTimeout());
#pragma warning restore CS0618 // Type or member is obsolete
        }
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret()
    {
        // Arrange
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var parameterProcessor = CreateParameterProcessor(notificationService: notificationService);
        var secretParam = CreateParameterResource("secret", "secretValue", secret: true);
 
        var updates = new List<(IResource Resource, CustomResourceSnapshot Snapshot)>();
        var watchTask = Task.Run(async () =>
        {
            await foreach (var resourceEvent in notificationService.WatchAsync().ConfigureAwait(false))
            {
                updates.Add((resourceEvent.Resource, resourceEvent.Snapshot));
                break; // Only collect the first update
            }
        });
 
        // Act
        await parameterProcessor.InitializeParametersAsync([secretParam]).DefaultTimeout();
 
        // Wait for the notification
        await watchTask.DefaultTimeout();
 
        // Assert
        var (resource, snapshot) = Assert.Single(updates);
        Assert.Same(secretParam, resource);
        Assert.Equal(KnownResourceStates.Running, snapshot.State?.Text);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingParameterValue_AddsToUnresolvedWhenInteractionAvailable()
    {
        // Arrange
        var interactionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
        var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]).DefaultTimeout();
 
        // Assert
        Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
        Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingOptionalParameterValue_DoesNotPrompt()
    {
        var interactionService = new TestInteractionService { IsAvailable = true };
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: interactionService);
        var parameter = CreateParameterWithMissingValue("optionalParam");
        parameter.Required = false;
 
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        Assert.NotNull(parameter.WaitForValueTcs);
        Assert.True(parameter.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Null(await parameter.WaitForValueTcs.Task.DefaultTimeout());
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        Assert.DoesNotContain(updates.Current.Snapshot.Properties, property => property.Name == KnownProperties.Parameter.Value);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithProgrammaticallySetValue_DoesNotOverwriteValue()
    {
        var interactionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
        var parameter = CreateParameterWithMissingValue("programmaticParam");
 
        Assert.True(parameter.TrySetValue("programmaticValue"));
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        Assert.Equal("programmaticValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.False(interactionService.Interactions.Reader.TryRead(out _));
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithProgrammaticallySetMissingParameterException_PromptsForValue()
    {
        var interactionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
        var parameter = CreateParameterResource("programmaticMissing", "fallback");
 
        Assert.True(parameter.TrySetException(new MissingParameterValueException("Missing parameter value.")));
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var messageBarInteraction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, messageBarInteraction.Title);
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        inputsInteraction.Inputs["programmaticMissing"].Value = "resolved";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("resolved", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task SetValueAsync_WithMissingParameterValue_ResolvesParameter()
    {
        var parameterProcessor = CreateParameterProcessor();
        var parameter = CreateParameterWithMissingValue("programmaticParam");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        Assert.NotNull(parameter.WaitForValueTcs);
        Assert.True(parameter.WaitForValueTcs.Task.IsFaulted);
 
        await parameterProcessor.SetValueAsync(parameter, "programmaticValue", cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.Equal("programmaticValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingParameterValue_SetsExceptionWhenInteractionNotAvailable()
    {
        // Arrange
        var interactionService = CreateInteractionService(disableDashboard: true);
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
        var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]).DefaultTimeout();
 
        // Assert
        Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
        Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
        Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsFaulted);
        Assert.IsType<MissingParameterValueException>(parameterWithMissingValue.WaitForValueTcs.Task.Exception?.InnerException);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingParameterValueAndDashboardEnabled_LeavesUnresolved()
    {
        // Arrange
        var interactionService = CreateInteractionService(disableDashboard: false);
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false);
        var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]).DefaultTimeout();
 
        // Assert - Parameter should remain unresolved when dashboard is enabled
        Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
        Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithNonMissingParameterException_SetsException()
    {
        // Arrange
        var parameterProcessor = CreateParameterProcessor();
        var parameterWithError = CreateParameterWithGenericError("errorParam");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithError]).DefaultTimeout();
 
        // Assert
        Assert.NotNull(parameterWithError.WaitForValueTcs);
        Assert.True(parameterWithError.WaitForValueTcs.Task.IsCompleted);
        Assert.True(parameterWithError.WaitForValueTcs.Task.IsFaulted);
        Assert.IsType<InvalidOperationException>(parameterWithError.WaitForValueTcs.Task.Exception?.InnerException);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService,
            userSecretsManager: mockUserSecretsManager);
        var param1 = CreateParameterWithMissingValue("param1");
        var param2 = CreateParameterWithMissingValue("param2");
        var secretParam = CreateParameterWithMissingValue("secretParam", secret: true);
 
        List<ParameterResource> parameters = [param1, param2, secretParam];
 
        foreach (var param in parameters)
        {
            // Initialize the parameters' WaitForValueTcs
            param.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
        }
 
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        // Act - Start handling unresolved parameters
        var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        // Assert - Wait for the first interaction (message bar)
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, messageBarInteraction.Title);
        Assert.Equal(InteractionStrings.ParametersBarMessage, messageBarInteraction.Message);
 
        // Complete the message bar interaction to proceed to inputs dialog
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true)); // Data = true (user clicked Enter Values)
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersInputsTitle, inputsInteraction.Title);
        Assert.Equal(InteractionStrings.ParametersInputsMessage, inputsInteraction.Message);
        Assert.True(inputsInteraction.Options!.EnableMessageMarkdown);
 
        Assert.Collection(inputsInteraction.Inputs,
            input =>
            {
                Assert.Equal("param1", input.Label);
                Assert.Equal(InputType.Text, input.InputType);
                Assert.True(input.Required);
            },
            input =>
            {
                Assert.Equal("param2", input.Label);
                Assert.Equal(InputType.Text, input.InputType);
                Assert.True(input.Required);
            },
            input =>
            {
                Assert.Equal("secretParam", input.Label);
                Assert.Equal(InputType.SecretText, input.InputType);
                Assert.True(input.Required);
            },
            input =>
            {
                Assert.Equal(InteractionStrings.ParametersInputsRememberLabel, input.Label);
                Assert.Equal(InputType.Boolean, input.InputType);
                Assert.False(input.Required);
            });
 
        inputsInteraction.Inputs["param1"].Value = "value1";
        inputsInteraction.Inputs["param2"].Value = "value2";
        inputsInteraction.Inputs["secretParam"].Value = "secretValue";
 
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        // Wait for the handle task to complete
        await handleTask.DefaultTimeout();
 
        // Assert - All parameters should now be resolved
        Assert.True(param1.WaitForValueTcs!.Task.IsCompletedSuccessfully);
        Assert.True(param2.WaitForValueTcs!.Task.IsCompletedSuccessfully);
        Assert.True(secretParam.WaitForValueTcs!.Task.IsCompletedSuccessfully);
        Assert.Equal("value1", await param1.WaitForValueTcs.Task.DefaultTimeout());
        Assert.Equal("value2", await param2.WaitForValueTcs.Task.DefaultTimeout());
        Assert.Equal("secretValue", await secretParam.WaitForValueTcs.Task.DefaultTimeout());
 
        // Notification service should have received updates for each parameter
        // Marking them as Running with the provided values
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "value1", isSensitive: false);
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "value2", isSensitive: false);
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "secretValue", isSensitive: true);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WhenUserDismissesNotification_WaitsWithoutShowingNotificationAgain()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var notificationDismissed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testSink = new TestSink();
        testSink.MessageLogged += context =>
        {
            if (context.Message == "Unresolved parameters notification was dismissed. The notification will not be shown again.")
            {
                notificationDismissed.TrySetResult();
            }
        };
        var testLogger = new TestLogger<ParameterProcessor>(new TestLoggerFactory(testSink, enabled: true));
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService, logger: testLogger);
        var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");
 
        // Act - Start handling unresolved parameters
        var initializeTask = parameterProcessor.InitializeParametersAsync([parameterWithMissingValue], waitForResolution: true);
 
        // Wait for the message bar interaction
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, messageBarInteraction.Title);
 
        // Dismiss the message bar interaction
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Cancel<bool>());
        await notificationDismissed.Task.DefaultTimeout();
 
        // Assert - Parameter should remain unresolved without another notification and initialization should keep waiting
        Assert.NotNull(parameterWithMissingValue.WaitForValueTcs);
        Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted);
        Assert.False(initializeTask.IsCompleted);
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
 
        await parameterWithMissingValue.SetValueAsync(null).DefaultTimeout();
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
        Assert.False(initializeTask.IsCompleted);
 
        await parameterProcessor.SetParameterCoreAsync(parameterWithMissingValue, CreateSetParameterArguments("resolvedValue"), CancellationToken.None).DefaultTimeout();
        await initializeTask.DefaultTimeout();
 
        await parameterWithMissingValue.SetValueAsync(null).DefaultTimeout();
        var newNotification = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        var secondInitializeTask = parameterProcessor.InitializeParametersAsync([parameterWithMissingValue], waitForResolution: true);
        Assert.False(newNotification.CancellationToken.IsCancellationRequested);
        await parameterWithMissingValue.SetValueAsync("resolvedAgain").DefaultTimeout();
        newNotification.CompletionTcs.SetResult(InteractionResult.Cancel<bool>());
        await secondInitializeTask.DefaultTimeout();
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully()
    {
        // Arrange
        var parameterProcessor = CreateParameterProcessor();
 
        // Act & Assert - Should not throw
        await parameterProcessor.InitializeParametersAsync([]).DefaultTimeout();
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, true)]
    public async Task InitializeParametersAsync_WhenLastParameterClearedBeforePromptExits_RestartsResolution(bool publish, bool enterInputs)
    {
        var interactions = new TestInteractionService();
        var processor = CreateParameterProcessor(
            interactionService: interactions,
            executionContext: new DistributedApplicationExecutionContext(publish ? DistributedApplicationOperation.Publish : DistributedApplicationOperation.Run));
        var parameter = CreateParameterWithMissingValue("missingParam");
        var initializeTask = processor.InitializeParametersAsync([parameter], waitForResolution: true);
        var prompt = await interactions.Interactions.Reader.ReadAsync().DefaultTimeout();
 
        try
        {
            for (var cycle = 0; cycle < 3; cycle++)
            {
                if (!publish && enterInputs)
                {
                    prompt.CompletionTcs.SetResult(InteractionResult.Ok(true));
                    prompt = await interactions.Interactions.Reader.ReadAsync().DefaultTimeout();
                }
 
                await parameter.SetValueAsync("resolved").DefaultTimeout();
                Assert.True(prompt.CancellationToken.IsCancellationRequested);
                await parameter.SetValueAsync(null).DefaultTimeout();
 
                // Keep the canceled prompt in flight until the clear has requested another resolution.
                Assert.False(initializeTask.IsCompleted);
                Assert.False(interactions.Interactions.Reader.TryRead(out _));
                if (prompt.Type == InteractionType.Notification)
                {
                    prompt.CompletionTcs.SetResult(InteractionResult.Cancel<bool>());
                }
                else
                {
                    prompt.CompletionTcs.SetResult(InteractionResult.Cancel<InteractionInputCollection>());
                }
 
                prompt = await interactions.Interactions.Reader.ReadAsync().DefaultTimeout();
                Assert.False(prompt.CancellationToken.IsCancellationRequested);
                Assert.False(initializeTask.IsCompleted);
            }
 
            if (!publish)
            {
                prompt.CompletionTcs.SetResult(InteractionResult.Ok(true));
                prompt = await interactions.Interactions.Reader.ReadAsync().DefaultTimeout();
            }
 
            prompt.Inputs[parameter.Name].Value = "finalValue";
            prompt.CompletionTcs.SetResult(InteractionResult.Ok(prompt.Inputs));
            await initializeTask.DefaultTimeout();
            Assert.Equal("finalValue", await parameter.GetValueAsync(CancellationToken.None));
            Assert.False(interactions.Interactions.Reader.TryRead(out _));
        }
        finally
        {
            await parameter.SetValueAsync("cleanup").DefaultTimeout();
            if (prompt.Type == InteractionType.Notification)
            {
                prompt.CompletionTcs.TrySetResult(InteractionResult.Cancel<bool>());
            }
            else
            {
                prompt.CompletionTcs.TrySetResult(InteractionResult.Cancel<InteractionInputCollection>());
            }
            await initializeTask.DefaultTimeout();
        }
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingParameterValue_LogsWarningWithoutException()
    {
        // Arrange
        var loggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var interactionService = CreateInteractionService();
        var parameterProcessor = CreateParameterProcessor(
            loggerService: loggerService,
            interactionService: interactionService);
        var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam");
 
        // Set up log watching
        var logsTask = ConsoleLoggingTestHelpers.WatchForLogsAsync(loggerService, 1, parameterWithMissingValue);
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]).DefaultTimeout();
 
        // Wait for logs to be written
        var logs = await logsTask.DefaultTimeout();
 
        // Assert - Should log warning without exception details
        Assert.Single(logs);
        var logEntry = logs[0];
        Assert.Contains("Parameter resource missingParam could not be initialized. Waiting for user input.", logEntry.Content);
        Assert.False(logEntry.IsErrorMessage);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithNonMissingParameterException_LogsErrorWithException()
    {
        // Arrange
        var loggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var parameterProcessor = CreateParameterProcessor(loggerService: loggerService);
        var parameterWithError = CreateParameterWithGenericError("errorParam");
 
        // Set up log watching
        var logsTask = ConsoleLoggingTestHelpers.WatchForLogsAsync(loggerService, 1, parameterWithError);
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithError]).DefaultTimeout();
 
        // Wait for logs to be written
        var logs = await logsTask.DefaultTimeout();
 
        // Assert - Should log error message
        Assert.Single(logs);
        var logEntry = logs[0];
        Assert.Contains("Failed to initialize parameter resource errorParam.", logEntry.Content);
        Assert.True(logEntry.IsErrorMessage);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WithResolvedParameter_LogsResolutionViaInteraction()
    {
        // Arrange
        var loggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var testInteractionService = new TestInteractionService();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            loggerService: loggerService,
            interactionService: testInteractionService);
        var parameter = CreateParameterWithMissingValue("testParam");
 
        parameter.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        // Set up log watching
        var logsTask = ConsoleLoggingTestHelpers.WatchForLogsAsync(loggerService, 1, parameter);
 
        // Act - Start handling unresolved parameters
        var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([parameter], CancellationToken.None);
 
        // Wait for the message bar interaction
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        inputsInteraction.Inputs["testParam"].Value = "testValue";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        // Wait for the handle task to complete
        await handleTask.DefaultTimeout();
 
        // Wait for logs to be written
        var logs = await logsTask.DefaultTimeout();
 
        // Assert - Should log that parameter was resolved via user interaction
        Assert.Single(logs);
        var logEntry = logs[0];
        Assert.Contains("Parameter resource testParam has been resolved via user interaction.", logEntry.Content);
        Assert.False(logEntry.IsErrorMessage);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WithParameterDescriptions_CreatesInputsWithDescriptions()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            userSecretsManager: mockUserSecretsManager);
 
        var param1 = CreateParameterWithMissingValue("param1");
        param1.Description = "This is a test parameter";
        param1.EnableDescriptionMarkdown = false;
 
        var param2 = CreateParameterWithMissingValue("param2");
        param2.Description = "This parameter has **markdown** formatting";
        param2.EnableDescriptionMarkdown = true;
 
        List<ParameterResource> parameters = [param1, param2];
 
        foreach (var param in parameters)
        {
            param.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
        }
 
        // Act
        _ = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        // Wait for the message bar interaction and complete it
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(3, inputsInteraction.Inputs.Count); // 2 parameters + 1 save option
 
        var param1Input = inputsInteraction.Inputs["param1"];
        Assert.Equal("param1", param1Input.Label);
        Assert.Equal("This is a test parameter", param1Input.Description);
        Assert.False(param1Input.EnableDescriptionMarkdown);
        Assert.Equal(InputType.Text, param1Input.InputType);
 
        var param2Input = inputsInteraction.Inputs["param2"];
        Assert.Equal("param2", param2Input.Label);
        Assert.Equal("This parameter has **markdown** formatting", param2Input.Description);
        Assert.True(param2Input.EnableDescriptionMarkdown);
        Assert.Equal(InputType.Text, param2Input.InputType);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WithSecretParameterWithDescription_CreatesSecretInput()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            userSecretsManager: mockUserSecretsManager);
 
        var secretParam = CreateParameterWithMissingValue("secretParam", secret: true);
        secretParam.Description = "This is a secret parameter";
        secretParam.EnableDescriptionMarkdown = false;
 
        List<ParameterResource> parameters = [secretParam];
        secretParam.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        // Act
        _ = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        // Wait for the message bar interaction and complete it
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(2, inputsInteraction.Inputs.Count); // 1 secret parameter + 1 save option
 
        var secretInput = inputsInteraction.Inputs["secretParam"];
        Assert.Equal("secretParam", secretInput.Label);
        Assert.Equal("This is a secret parameter", secretInput.Description);
        Assert.False(secretInput.EnableDescriptionMarkdown);
        Assert.Equal(InputType.SecretText, secretInput.InputType);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WhenUserSecretsNotAvailable_ShowsDisabledSaveCheckbox()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var noopUserSecretsManager = UserSecrets.NoopUserSecretsManager.Instance;
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            userSecretsManager: noopUserSecretsManager);
 
        var param = CreateParameterWithMissingValue("param1");
        param.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        List<ParameterResource> parameters = [param];
 
        // Act
        _ = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        // Wait for the message bar interaction and complete it
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync();
 
        // Assert - Should have 2 inputs (parameter + disabled save checkbox)
        Assert.Equal(2, inputsInteraction.Inputs.Count);
 
        var paramInput = inputsInteraction.Inputs["param1"];
        Assert.Equal("param1", paramInput.Label);
 
        var saveCheckbox = inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName];
        Assert.Equal(InteractionStrings.ParametersInputsRememberLabel, saveCheckbox.Label);
        Assert.Equal(InputType.Boolean, saveCheckbox.InputType);
        Assert.True(saveCheckbox.Disabled); // Should be disabled when user secrets not available
        Assert.Equal(InteractionStrings.ParametersInputsRememberDescriptionNotConfigured, saveCheckbox.Description);
        Assert.True(saveCheckbox.EnableDescriptionMarkdown);
    }
 
    [Fact]
    public async Task HandleUnresolvedParametersAsync_WhenUserSecretsAvailable_ShowsEnabledSaveCheckbox()
    {
        // Arrange
        var testInteractionService = new TestInteractionService();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            userSecretsManager: mockUserSecretsManager);
 
        var param = CreateParameterWithMissingValue("param1");
        param.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        List<ParameterResource> parameters = [param];
 
        // Act
        _ = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        // Wait for the message bar interaction and complete it
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // Wait for the inputs interaction
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync();
 
        // Assert - Should have 2 inputs (parameter + enabled save checkbox)
        Assert.Equal(2, inputsInteraction.Inputs.Count);
 
        var paramInput = inputsInteraction.Inputs["param1"];
        Assert.Equal("param1", paramInput.Label);
 
        var saveCheckbox = inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName];
        Assert.Equal(InteractionStrings.ParametersInputsRememberLabel, saveCheckbox.Label);
        Assert.Equal(InputType.Boolean, saveCheckbox.InputType);
        Assert.False(saveCheckbox.Disabled); // Should be enabled when user secrets are available
        Assert.Equal(InteractionStrings.ParametersInputsRememberDescriptionConfigured, saveCheckbox.Description);
        Assert.True(saveCheckbox.EnableDescriptionMarkdown);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_CollectsAndInitializesAllParameters()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var explicitParam = builder.AddParameter("explicitParam", () => "explicitValue");
        var referencedParam = builder.AddParameter("referencedParam", () => "referencedValue");
 
        // Create a container that references the parameter in an environment variable
        builder.AddContainer("testContainer", "nginx")
               .WithEnvironment("TEST_ENV", referencedParam);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        var explicitParameterResource = model.Resources.OfType<ParameterResource>().First(p => p.Name == "explicitParam");
        var referencedParameterResource = model.Resources.OfType<ParameterResource>().First(p => p.Name == "referencedParam");
 
        Assert.NotNull(explicitParameterResource.WaitForValueTcs);
        Assert.NotNull(referencedParameterResource.WaitForValueTcs);
        Assert.True(explicitParameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.True(referencedParameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("explicitValue", await explicitParameterResource.WaitForValueTcs.Task.DefaultTimeout());
        Assert.Equal("referencedValue", await referencedParameterResource.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_EmptyModel_CompletesSuccessfully()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act & Assert - Should not throw
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_NoParameterReferences_InitializesExplicitOnly()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var explicitParam = builder.AddParameter("explicitParam", () => "explicitValue");
 
        // Add a container without parameter references
        builder.AddContainer("testContainer", "nginx")
               .WithEnvironment("TEST_ENV", "staticValue");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        var explicitParameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.Equal("explicitParam", explicitParameterResource.Name);
        Assert.NotNull(explicitParameterResource.WaitForValueTcs);
        Assert.True(explicitParameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("explicitValue", await explicitParameterResource.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_WithEnvironmentVariableReferences()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var referencedParam = builder.AddParameter("envParam", () => "envValue");
 
        builder.AddContainer("testContainer", "nginx")
               .WithEnvironment("TEST_ENV", referencedParam);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        var parameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.Equal("envParam", parameterResource.Name);
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.True(parameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("envValue", await parameterResource.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_WaitForResolution_True()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var param = builder.AddParameter("testParam", () => "testValue");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model, waitForResolution: true).DefaultTimeout();
 
        // Assert
        var parameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.True(parameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("testValue", await parameterResource.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_WaitForResolution_False()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var param = builder.AddParameter("testParam", () => "testValue");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model, waitForResolution: false).DefaultTimeout();
 
        // Assert
        var parameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.True(parameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("testValue", await parameterResource.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_WithMissingParameterValues_HandlesCorrectly()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var missingParam = builder.AddParameter("missingParam", () => throw new MissingParameterValueException("Parameter 'missingParam' is missing"));
 
        builder.AddContainer("testContainer", "nginx")
               .WithEnvironment("TEST_ENV", missingParam);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var interactionService = CreateInteractionService();
        var parameterProcessor = CreateParameterProcessor(interactionService: interactionService);
 
        // Act
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        var parameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.False(parameterResource.WaitForValueTcs.Task.IsCompleted);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithDistributedApplicationModel_HandlesCircularReferences()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var param1 = builder.AddParameter("param1", () => "value1");
        var param2 = builder.AddParameter("param2", () => "value2");
 
        // Create a scenario that could potentially create circular references
        builder.AddContainer("container1", "nginx")
               .WithEnvironment("ENV1", param1)
               .WithEnvironment("ENV2", param2);
 
        builder.AddContainer("container2", "nginx")
               .WithEnvironment("ENV3", param1);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act - Should not hang or throw due to circular references
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        var parameters = model.Resources.OfType<ParameterResource>().ToList();
        Assert.Equal(2, parameters.Count);
 
        foreach (var param in parameters)
        {
            Assert.NotNull(param.WaitForValueTcs);
            Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully);
        }
    }
 
    [Fact]
    public async Task InitializeParametersAsync_UsesExecutionContextOptions_DoesNotThrow()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        var param = builder.AddParameter("testParam", () => "testValue");
 
        var serviceProviderAccessed = false;
        builder.AddContainer("testContainer", "nginx")
               .WithEnvironment(context =>
               {
                   // This should not throw InvalidOperationException
                   // when using the proper execution context constructor
                   var sp = context.ExecutionContext.Services;
                   serviceProviderAccessed = sp is not null;
                   context.EnvironmentVariables["TEST_ENV"] = param;
               });
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Get the ParameterProcessor from the built app's service provider
        // This ensures it has the proper execution context with ServiceProvider
        var parameterProcessor = app.Services.GetRequiredService<ParameterProcessor>();
 
        // Act - Should not throw InvalidOperationException about IServiceProvider not being available
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        Assert.True(serviceProviderAccessed);
        var parameterResource = model.Resources.OfType<ParameterResource>().Single();
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.True(parameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_SkipsResourcesExcludedFromPublish()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var param = builder.AddParameter("excludedParam", () => "excludedValue");
 
        var excludedContainer = builder.AddContainer("excludedContainer", "nginx")
               .WithEnvironment(context =>
               {
                   context.EnvironmentVariables["EXCLUDED_ENV"] = param;
               });
 
        // Mark the container as excluded from publish
        excludedContainer.ExcludeFromManifest();
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var parameterProcessor = CreateParameterProcessor();
 
        // Act - The excluded container should be skipped during parameter collection
        await parameterProcessor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Assert
        // The environment callback should have been invoked during parameter collection
        // because we now create a publish execution context to collect dependent parameters
        // However, since we filter out excluded resources, the parameter should not be initialized
        // unless it's explicitly in the model
        var parameters = model.Resources.OfType<ParameterResource>().ToList();
        Assert.Single(parameters);
 
        var parameterResource = parameters[0];
        Assert.Equal("excludedParam", parameterResource.Name);
 
        // The parameter should be initialized since it's explicitly in the model
        Assert.NotNull(parameterResource.WaitForValueTcs);
        Assert.True(parameterResource.WaitForValueTcs.Task.IsCompletedSuccessfully);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithInteractionServiceAvailable_AddsSetParameterCommand()
    {
        // Arrange
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "testValue");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        // Assert - Command should be added when interaction service is available
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .SingleOrDefault(a => a.Name == KnownResourceCommands.SetParameterCommand);
        Assert.NotNull(setValueCommand);
        Assert.Equal(CommandStrings.SetParameterName, setValueCommand.DisplayName);
        Assert.Equal(CommandStrings.SetParameterDescription, setValueCommand.DisplayDescription);
        Assert.True(setValueCommand.IsHighlighted);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithInteractionServiceAvailable_AddsSetParameterValueArgument()
    {
        // Arrange
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "testValue");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        // Assert
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.SetParameterCommand);
 
        Assert.Collection(
            setValueCommand.Arguments,
            valueInput =>
            {
                Assert.Equal(ParameterProcessor.SetParameterValueName, valueInput.Name);
                Assert.Equal("testParam", valueInput.Label);
                Assert.Null(valueInput.Value);
                Assert.NotNull(valueInput.DynamicLoading);
                Assert.True(valueInput.Required);
            },
            saveInput => Assert.Equal(ParameterProcessor.SaveToUserSecretsName, saveInput.Name));
 
        var saveToUserSecretsInput = setValueCommand.Arguments.Single(argument => argument.Name == ParameterProcessor.SaveToUserSecretsName);
        Assert.NotNull(saveToUserSecretsInput.DynamicLoading);
        var dependsOnInputs = saveToUserSecretsInput.DynamicLoading.DependsOnInputs;
        Assert.NotNull(dependsOnInputs);
        Assert.Equal(ParameterProcessor.SetParameterValueName, Assert.Single(dependsOnInputs));
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithExistingValue_LoadsSetParameterValueArgumentOnStart()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "testValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.SetParameterCommand);
        var arguments = new InteractionInputCollection(setValueCommand.Arguments);
        var valueInput = arguments[ParameterProcessor.SetParameterValueName];
 
        await valueInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = valueInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        Assert.Equal("testValue", valueInput.Value);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithExistingInputValue_DoesNotOverwriteSetParameterValueArgumentOnStart()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "testValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.SetParameterCommand);
        var arguments = new InteractionInputCollection(setValueCommand.Arguments);
        var valueInput = arguments[ParameterProcessor.SetParameterValueName];
        valueInput.Value = "callerValue";
 
        await valueInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = valueInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        Assert.Equal("callerValue", valueInput.Value);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithSavedState_DefaultsSaveArgumentToTrue()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var deploymentStateManager = new CapturingMockDeploymentStateManager();
        var parameter = CreateParameterResource("testParam", "testValue");
        var section = await deploymentStateManager.AcquireSectionAsync(parameter.ConfigurationKey).DefaultTimeout();
        section.SetValue("testValue");
        await deploymentStateManager.SaveSectionAsync(section).DefaultTimeout();
 
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: deploymentStateManager,
            userSecretsManager: new MockUserSecretsManager());
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.SetParameterCommand);
        var arguments = new InteractionInputCollection(setValueCommand.Arguments);
        var valueInput = arguments[ParameterProcessor.SetParameterValueName];
        var saveInput = arguments[ParameterProcessor.SaveToUserSecretsName];
 
        await valueInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = valueInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        await saveInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = saveInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        Assert.Equal("true", saveInput.Value);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithoutSavedState_DisablesDeleteParameterUserSecretsArgumentOnStart()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var deploymentStateManager = new CapturingMockDeploymentStateManager();
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: deploymentStateManager,
            userSecretsManager: new MockUserSecretsManager());
        var parameter = CreateParameterResource("testParam", "testValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var deleteCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.DeleteParameterCommand);
        var arguments = new InteractionInputCollection(deleteCommand.Arguments);
        var deleteInput = arguments[ParameterProcessor.DeleteFromUserSecretsName];
 
        await deleteInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = deleteInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        Assert.True(deleteInput.Disabled);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithSavedState_EnablesDeleteParameterUserSecretsArgumentOnStart()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var deploymentStateManager = new CapturingMockDeploymentStateManager();
        var parameter = CreateParameterResource("testParam", "testValue");
        var section = await deploymentStateManager.AcquireSectionAsync(parameter.ConfigurationKey).DefaultTimeout();
        section.SetValue("testValue");
        await deploymentStateManager.SaveSectionAsync(section).DefaultTimeout();
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: deploymentStateManager,
            userSecretsManager: new MockUserSecretsManager());
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var deleteCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .Single(a => a.Name == KnownResourceCommands.DeleteParameterCommand);
        var arguments = new InteractionInputCollection(deleteCommand.Arguments);
        var deleteInput = arguments[ParameterProcessor.DeleteFromUserSecretsName];
 
        await deleteInput.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            Input = deleteInput,
            AllInputs = arguments,
            Services = new ServiceCollection().BuildServiceProvider(),
            CancellationToken = CancellationToken.None
        }).DefaultTimeout();
 
        Assert.False(deleteInput.Disabled);
    }
 
    [Fact]
    public async Task ProcessParameterAsync_WithInteractionServiceNotAvailable_DoesNotAddSetParameterCommand()
    {
        // Arrange
        var testInteractionService = new TestInteractionService { IsAvailable = false };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "testValue");
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        // Assert - Command should not be added when interaction service is not available
        var setValueCommand = parameter.Annotations.OfType<ResourceCommandAnnotation>()
            .SingleOrDefault(a => a.Name == KnownResourceCommands.SetParameterCommand);
        Assert.Null(setValueCommand);
    }
 
    [Fact]
    public async Task SetParameterCoreAsync_WithUserInput_UpdatesParameterValue()
    {
        var parameterProcessor = CreateParameterProcessor();
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        parameter.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        var result = await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("newValue"), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.Equal(string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceSetParameter, parameter.Name), result.Message);
        Assert.Equal("newValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task SetParameterCoreAsync_WithMissingInput_ParameterValueUnchanged()
    {
        var parameterProcessor = CreateParameterProcessor();
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        parameter.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        var result = await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments(value: null), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.False(parameter.WaitForValueTcs.Task.IsCompleted);
    }
 
    [Fact]
    public async Task SetParameterCoreAsync_ResolvingLastParameter_CancelsPromptNotification()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterWithMissingValue("testParam");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
        Assert.False(notificationInteraction.CancellationToken.IsCancellationRequested);
 
        var result = await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("resolvedValue"), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.Equal("resolvedValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(notificationInteraction.CancellationToken.IsCancellationRequested);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithCustomInputs_PromptInputsAreNamedAfterParameters()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var firstParameter = CreateParameterWithMissingValue("firstParam");
        var secondParameter = CreateParameterWithMissingValue("secondParam");
        foreach (var parameter in new[] { firstParameter, secondParameter })
        {
            // Both generators use the same input name, which a prompt for both parameters can't contain twice.
            parameter.Annotations.Add(new InputGeneratorAnnotation(_ => new InteractionInput
            {
                Name = "ParameterInput",
                InputType = InputType.Number,
                Label = "Custom Input"
            }));
        }
 
        await parameterProcessor.InitializeParametersAsync([firstParameter, secondParameter]).DefaultTimeout();
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(["firstParam", "secondParam", "SaveToUserSecrets"], inputsInteraction.Inputs.Select(input => input.Name));
        Assert.All(inputsInteraction.Inputs.Take(2), input => Assert.Equal("Custom Input", input.Label));
 
        inputsInteraction.Inputs["firstParam"].Value = "1";
        inputsInteraction.Inputs["secondParam"].Value = "2";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("1", await firstParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("2", await secondParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithCustomInputDependingOnAnotherCustomInput_KeepsGeneratedNames()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var regionParameter = CreateParameterWithMissingValue("regionParam");
        var zoneParameter = CreateParameterWithMissingValue("zoneParam");
        regionParameter.Annotations.Add(new InputGeneratorAnnotation(_ => new InteractionInput
        {
            Name = "Region",
            InputType = InputType.Text,
            Label = "Region"
        }));
        // The zone's options depend on the region input, which the load callback reads by the name it depends on.
        zoneParameter.Annotations.Add(new InputGeneratorAnnotation(_ => new InteractionInput
        {
            Name = "Zone",
            InputType = InputType.Choice,
            Label = "Zone",
            DynamicLoading = new InputLoadOptions
            {
                LoadCallback = context =>
                {
                    context.Input.Options = [KeyValuePair.Create($"{context.AllInputs["Region"].Value}-a", "a")];
                    return Task.CompletedTask;
                },
                DependsOnInputs = ["Region"]
            }
        }));
 
        await parameterProcessor.InitializeParametersAsync([regionParameter, zoneParameter]).DefaultTimeout();
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(["Region", "Zone", "SaveToUserSecrets"], inputsInteraction.Inputs.Select(input => input.Name));
 
        inputsInteraction.Inputs["Region"].Value = "west";
        inputsInteraction.Inputs["Zone"].Value = "west-a";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("west", await regionParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("west-a", await zoneParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithCustomInputForParameterNamedLikeSaveCheckbox_KeepsGeneratedName()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterWithMissingValue(ParameterProcessor.SaveToUserSecretsName);
        parameter.Annotations.Add(new InputGeneratorAnnotation(_ => new InteractionInput
        {
            Name = "Secret",
            InputType = InputType.Text,
            Label = "Secret"
        }));
        var otherParameter = CreateParameterWithMissingValue("otherParam");
 
        await parameterProcessor.InitializeParametersAsync([parameter, otherParameter]).DefaultTimeout();
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        // The save-values checkbox owns its name in the prompt, so this input can't take its parameter's name.
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(["Secret", "otherParam", ParameterProcessor.SaveToUserSecretsName], inputsInteraction.Inputs.Select(input => input.Name));
 
        inputsInteraction.Inputs["Secret"].Value = "secret-value";
        inputsInteraction.Inputs["otherParam"].Value = "other-value";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("secret-value", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("other-value", await otherParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithMissingParameterValues_NotificationNamesUnresolvedParameters()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var resolvedParameter = CreateParameterResource("resolvedParam", "value");
        var firstParameter = CreateParameterWithMissingValue("firstParam");
        var secondParameter = CreateParameterWithMissingValue("secondParam");
 
        await parameterProcessor.InitializeParametersAsync([resolvedParameter, firstParameter, secondParameter]).DefaultTimeout();
 
        // The notification keeps its button for clients that don't know the parameter names, and names the
        // parameters so the dashboard can offer its own form for them.
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
        var options = Assert.IsType<NotificationInteractionOptions>(notificationInteraction.Options);
        Assert.Equal(InteractionStrings.ParametersBarPrimaryButtonText, options.PrimaryButtonText);
        Assert.Null(options.LinkUrl);
        Assert.Equal(["firstParam", "secondParam"], options.UnresolvedParameterNames);
    }
 
    [Fact]
    public async Task SetParameterCoreAsync_CalledTwice_UpdatesPreviousValueAndSavedState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var parameterProcessor = CreateParameterProcessor(deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterWithMissingValue("testParam");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("firstValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
        Assert.Equal("firstValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out var savedValueNode));
        Assert.Equal("firstValue", savedValueNode?.GetValue<string>());
 
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("secondValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
 
        Assert.Equal("secondValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out savedValueNode));
        Assert.Equal("secondValue", savedValueNode?.GetValue<string>());
    }
 
    [Fact]
    public async Task SetParameterAsync_WithUserInput_UpdatesParameterValueAndSavedState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var setParameterTask = parameterProcessor.SetParameterAsync(parameter, CancellationToken.None);
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.SetParameterTitle, inputsInteraction.Title);
        inputsInteraction.Inputs[ParameterProcessor.SetParameterValueName].Value = "newValue";
        inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        await setParameterTask.DefaultTimeout();
 
        Assert.Equal("newValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out var savedValueNode));
        Assert.Equal("newValue", savedValueNode?.GetValue<string>());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_RecordsResolvedSecretValues_ForRedaction()
    {
        // Assignment-time recording (https://github.com/microsoft/aspire/issues/19241): the processor records a
        // secret's resolved value into the AppHost-scoped redaction history the moment it assigns it, so
        // aspire describe/watch can redact it even before any backchannel connection has observed the value.
        var redactionHistory = new SecretRedactionHistory();
        var parameterProcessor = CreateParameterProcessor(secretRedactionHistory: redactionHistory);
 
        var secretParam = CreateParameterResource("db-password", "s3cr3t-value", secret: true);
        var nonSecretParam = CreateParameterResource("region", "public-value", secret: false);
 
        await parameterProcessor.InitializeParametersAsync([secretParam, nonSecretParam], waitForResolution: true).DefaultTimeout();
 
        // Only the secret value is recorded; the non-secret value must never enter the redaction set (asserting a
        // single element proves "public-value" was not recorded).
        var recorded = redactionHistory.AddValuesAndSnapshot([]);
        Assert.Equal("s3cr3t-value", Assert.Single(recorded));
    }
 
    [Fact]
    public async Task SetParameterCoreAsync_RecordsReplacedSecretValue_ForRedaction()
    {
        // The runtime "Set parameter" path replaces a secret's resolved value in place. Both the original and the
        // replacement value must stay redactable, because a still-current or lagging snapshot can carry the previous
        // value. Recording at assignment time accumulates both add-only, independent of any backchannel connection.
        var redactionHistory = new SecretRedactionHistory();
        var parameterProcessor = CreateParameterProcessor(secretRedactionHistory: redactionHistory);
 
        var secretParam = CreateParameterResource("db-password", "initial-secret", secret: true);
 
        await parameterProcessor.InitializeParametersAsync([secretParam], waitForResolution: true).DefaultTimeout();
 
        var result = await parameterProcessor.SetParameterCoreAsync(secretParam, CreateSetParameterArguments("replacement-secret"), CancellationToken.None).DefaultTimeout();
        Assert.True(result.Success);
 
        var recorded = redactionHistory.AddValuesAndSnapshot([]);
        Assert.Contains("initial-secret", recorded);
        Assert.Contains("replacement-secret", recorded);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ResourceSetValue_RecordsSecretBeforeNotifyingObservers(bool trySetValue)
    {
        var history = new SecretRedactionHistory();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var processor = CreateParameterProcessor(interactionService: interactionService, secretRedactionHistory: history);
        var parameter = CreateParameterWithMissingValue("secret", secret: true);
        var observed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        parameter.ValueChanged += (_, args, _) =>
        {
            Assert.NotNull(args.Value);
            Assert.Contains(args.Value, history.AddValuesAndSnapshot([]));
            observed.SetResult();
            return Task.CompletedTask;
        };
        await processor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        if (trySetValue)
        {
            Assert.True(parameter.TrySetValue("assigned-secret"));
            Assert.False(parameter.TrySetValue("rejected-secret"));
        }
        else
        {
            await parameter.SetValueAsync("assigned-secret").DefaultTimeout();
        }
 
        await observed.Task.DefaultTimeout();
        Assert.Equal("assigned-secret", Assert.Single(history.AddValuesAndSnapshot([])));
    }
 
    [Fact]
    public async Task ResourceSetValueAsync_RecordsSecretWhenNotificationIsCanceled()
    {
        var history = new SecretRedactionHistory();
        var processor = CreateParameterProcessor(secretRedactionHistory: history);
        var parameter = CreateParameterResource("secret", "initial-secret", secret: true);
        using var cts = new CancellationTokenSource();
        parameter.ValueChanged += (_, args, cancellationToken) =>
        {
            if (args.Value == "canceled-notification-secret")
            {
                cts.Cancel();
                cancellationToken.ThrowIfCancellationRequested();
            }
 
            return Task.CompletedTask;
        };
        await processor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => parameter.SetValueAsync("canceled-notification-secret", cts.Token)).DefaultTimeout();
        await parameter.SetValueAsync("replacement-secret").DefaultTimeout();
 
        var recorded = history.AddValuesAndSnapshot([]);
        Assert.Contains("initial-secret", recorded);
        Assert.Contains("canceled-notification-secret", recorded);
        Assert.Contains("replacement-secret", recorded);
    }
 
    [Fact]
    public async Task ResourceSetValueAsync_RecordsSecretWhenNotificationIsSuperseded()
    {
        var history = new SecretRedactionHistory();
        var processor = CreateParameterProcessor(secretRedactionHistory: history);
        var parameter = CreateParameterResource("secret", "initial-secret", secret: true);
        var resumeNotification = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        parameter.ValueChanged += (_, args, _) => args.Value == "superseded-secret"
            ? resumeNotification.Task
            : Task.CompletedTask;
        await processor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var supersededUpdate = parameter.SetValueAsync("superseded-secret");
        try
        {
            Assert.False(supersededUpdate.IsCompleted);
            await parameter.SetValueAsync("replacement-secret").DefaultTimeout();
        }
        finally
        {
            resumeNotification.SetResult();
        }
 
        await supersededUpdate.DefaultTimeout();
        Assert.Equal("replacement-secret", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        var recorded = history.AddValuesAndSnapshot([]);
        Assert.Contains("initial-secret", recorded);
        Assert.Contains("superseded-secret", recorded);
        Assert.Contains("replacement-secret", recorded);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_LastPromptedValue_IsSavedAfterResolutionCancelsPrompt()
    {
        var stateManager = new CapturingMockDeploymentStateManager();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var processor = CreateParameterProcessor(interactionService: interactionService, deploymentStateManager: stateManager);
        var parameter = CreateParameterWithMissingValue("missing");
        var initializeTask = processor.InitializeParametersAsync([parameter], waitForResolution: true);
 
        var notification = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        notification.CompletionTcs.SetResult(InteractionResult.Ok(true));
        var prompt = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        prompt.Inputs[parameter.Name].Value = "saved-value";
        prompt.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        prompt.CompletionTcs.SetResult(InteractionResult.Ok(prompt.Inputs));
 
        await initializeTask.DefaultTimeout();
 
        Assert.True(prompt.CancellationToken.IsCancellationRequested);
        Assert.Equal("saved-value", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("saved-value", stateManager.State[parameter.ConfigurationKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task InitializeParametersAsync_StaleMultiParameterPromptDoesNotOverwriteProgrammaticValue()
    {
        var stateManager = new CapturingMockDeploymentStateManager();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var processor = CreateParameterProcessor(interactionService: interactionService, deploymentStateManager: stateManager);
        var firstParameter = CreateParameterWithMissingValue("first");
        var secondParameter = CreateParameterWithMissingValue("second");
        var initializeTask = processor.InitializeParametersAsync([firstParameter, secondParameter], waitForResolution: true);
 
        var notification = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        notification.CompletionTcs.SetResult(InteractionResult.Ok(true));
        var prompt = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        prompt.Inputs[firstParameter.Name].Value = "stale-form-value";
        prompt.Inputs[secondParameter.Name].Value = "form-value";
        prompt.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
 
        await firstParameter.SetValueAsync("programmatic-value").DefaultTimeout();
        Assert.False(prompt.CancellationToken.IsCancellationRequested);
 
        prompt.CompletionTcs.SetResult(InteractionResult.Ok(prompt.Inputs));
        await initializeTask.DefaultTimeout();
 
        Assert.Equal("programmatic-value", await firstParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("form-value", await secondParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.False(stateManager.State.TryGetPropertyValue(firstParameter.ConfigurationKey, out _));
        Assert.Equal("form-value", stateManager.State[secondParameter.ConfigurationKey]?.GetValue<string>());
    }
 
    private static InteractionInputCollection CreateSetParameterArguments(string? value, string? saveToUserSecrets = null)
    {
        return new InteractionInputCollection([
            new InteractionInput
            {
                Name = ParameterProcessor.SetParameterValueName,
                InputType = InputType.Text,
                Value = value
            },
            new InteractionInput
            {
                Name = ParameterProcessor.SaveToUserSecretsName,
                InputType = InputType.Boolean,
                Value = saveToUserSecrets
            }
        ]);
    }
 
    private static InteractionInputCollection CreateDeleteParameterArguments(string? deleteFromUserSecrets = null)
    {
        return new InteractionInputCollection([
            new InteractionInput
            {
                Name = ParameterProcessor.DeleteFromUserSecretsName,
                InputType = InputType.Boolean,
                Value = deleteFromUserSecrets
            }
        ]);
    }
 
    private static ParameterProcessor CreateParameterProcessor(
        ResourceNotificationService? notificationService = null,
        ResourceLoggerService? loggerService = null,
        IInteractionService? interactionService = null,
        ILogger<ParameterProcessor>? logger = null,
        bool disableDashboard = true,
        DistributedApplicationExecutionContext? executionContext = null,
        IDeploymentStateManager? deploymentStateManager = null,
        IUserSecretsManager? userSecretsManager = null,
        SecretRedactionHistory? secretRedactionHistory = null)
    {
        return new ParameterProcessor(
            notificationService ?? ResourceNotificationServiceTestHelpers.Create(),
            loggerService ?? new ResourceLoggerService(),
            interactionService ?? CreateInteractionService(disableDashboard),
            logger ?? new NullLogger<ParameterProcessor>(),
            executionContext ?? new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            deploymentStateManager ?? new MockDeploymentStateManager(),
            userSecretsManager ?? UserSecrets.NoopUserSecretsManager.Instance)
        {
            // Mirror the DI wiring in DistributedApplicationBuilder so tests can observe assignment-time secret
            // recording. Left null by default (property is a no-op then), matching a processor created outside the
            // AppHost container.
            SecretRedactionHistory = secretRedactionHistory
        };
    }
 
    private static InteractionService CreateInteractionService(bool disableDashboard = false)
    {
        return new InteractionService(
            new NullLogger<InteractionService>(),
            new DistributedApplicationOptions { DisableDashboard = disableDashboard },
            new ServiceCollection().BuildServiceProvider(),
            new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(),
            new TestInteractionFileUploadStore());
    }
 
    private sealed class MockDeploymentStateManager : IDeploymentStateManager
    {
        public string? StateFilePath => null;
 
        public Task<DeploymentStateSection> AcquireSectionAsync(string sectionName, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new DeploymentStateSection(sectionName, [], 0));
        }
 
        public Task<DeploymentStateSection> AcquireCurrentSectionAsync(string sectionName, CancellationToken cancellationToken = default)
            => AcquireSectionAsync(sectionName, cancellationToken);
 
        public Task SaveSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }
 
        public Task ClearAllStateAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
 
        public Task DeleteSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }
    }
 
    private static ParameterResource CreateParameterResource(string name, string value, bool secret = false)
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?> { [$"Parameters:{name}"] = value })
            .Build();
 
        return new ParameterResource(name, _ => configuration[$"Parameters:{name}"] ?? throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret);
    }
 
    private static void AssertParameterValueProperty(CustomResourceSnapshot snapshot, string expectedValue, bool isSensitive)
    {
        var property = Assert.Single(snapshot.Properties, p => p.Name == KnownProperties.Parameter.Value);
 
        Assert.Equal(expectedValue, property.Value);
        Assert.Equal(isSensitive, property.IsSensitive);
        Assert.Equal(MessageStrings.ResourcePropertyParameterValueDisplayName, property.DisplayName);
        Assert.True(property.IsHighlighted);
        Assert.Equal(0, property.SortOrder);
    }
 
    private static ParameterResource CreateParameterWithMissingValue(string name, bool secret = false)
    {
        return new ParameterResource(name, _ => throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret: secret);
    }
 
    private static ParameterResource CreateParameterWithGenericError(string name)
    {
        return new ParameterResource(name, _ => throw new InvalidOperationException($"Generic error for parameter '{name}'"), secret: false);
    }
 
    [Fact]
    public async Task InitializeParametersAsync_WithGenerateParameterDefaultInPublishMode_DoesNotThrowWhenValueExists()
    {
        // Arrange
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?> { ["Parameters:generatedParam"] = "existingValue" })
            .Build();
 
        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(configuration);
        var serviceProvider = services.BuildServiceProvider();
 
        var executionContext = new DistributedApplicationExecutionContext(
            new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Publish, "manifest")
            {
                Services = serviceProvider
            });
 
        var parameterProcessor = CreateParameterProcessor(executionContext: executionContext);
 
        var parameterWithGenerateDefault = new ParameterResource(
            "generatedParam",
            parameterDefault => configuration["Parameters:generatedParam"] ?? parameterDefault?.GetDefaultValue() ?? throw new MissingParameterValueException("Parameter 'generatedParam' is missing"),
            secret: false)
        {
            Default = new GenerateParameterDefault()
        };
 
        // Act
        await parameterProcessor.InitializeParametersAsync([parameterWithGenerateDefault]).DefaultTimeout();
 
        // Assert - Should succeed because value exists in configuration
        Assert.NotNull(parameterWithGenerateDefault.WaitForValueTcs);
        Assert.True(parameterWithGenerateDefault.WaitForValueTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("existingValue", await parameterWithGenerateDefault.WaitForValueTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task ConnectionStringParameterStateIsSavedWithCorrectKey()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager,
            userSecretsManager: mockUserSecretsManager);
 
        var connectionStringParam = new ConnectionStringParameterResource(
            "mydb",
            _ => throw new MissingParameterValueException("Connection string 'mydb' is missing"),
            null);
        connectionStringParam.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        List<ParameterResource> parameters = [connectionStringParam];
 
        var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        inputsInteraction.Inputs["mydb"].Value = "Server=localhost;Database=mydb";
        inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        await handleTask.DefaultTimeout();
 
        // Verify the value was saved correctly in the flattened state
        Assert.True(capturingStateManager.State.TryGetPropertyValue("ConnectionStrings:mydb", out var valueNode));
        Assert.Equal("Server=localhost;Database=mydb", valueNode?.GetValue<string>());
 
        // Verify the entire state structure as JSON (mimics what gets saved to disk)
        await VerifyJson(capturingStateManager.State.ToJsonString());
    }
 
    [Fact]
    public async Task RegularParameterStateIsSavedWithCorrectKey()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager,
            userSecretsManager: mockUserSecretsManager);
 
        var regularParam = new ParameterResource(
            "myparam",
            _ => throw new MissingParameterValueException("Parameter 'myparam' is missing"),
            secret: false);
        regularParam.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        List<ParameterResource> parameters = [regularParam];
 
        var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        inputsInteraction.Inputs["myparam"].Value = "myvalue";
        inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        await handleTask.DefaultTimeout();
 
        // Verify the value was saved correctly in the flattened state
        Assert.True(capturingStateManager.State.TryGetPropertyValue("Parameters:myparam", out var valueNode));
        Assert.Equal("myvalue", valueNode?.GetValue<string>());
 
        // Verify the entire state structure as JSON (mimics what gets saved to disk)
        await VerifyJson(capturingStateManager.State.ToJsonString());
    }
 
    [Fact]
    public async Task CustomConfigurationKeyParameterStateIsSavedWithCorrectKey()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var mockUserSecretsManager = new MockUserSecretsManager();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager,
            userSecretsManager: mockUserSecretsManager);
 
        var customParam = new ParameterResource(
            "customparam",
            _ => throw new MissingParameterValueException("Parameter 'customparam' is missing"),
            secret: false)
        {
            ConfigurationKey = "MyCustomSection:MyCustomKey"
        };
        customParam.WaitForValueTcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        List<ParameterResource> parameters = [customParam];
 
        var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters, CancellationToken.None);
 
        var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        messageBarInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        inputsInteraction.Inputs["customparam"].Value = "customvalue";
        inputsInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        await handleTask.DefaultTimeout();
 
        // Verify the value was saved correctly in the flattened state
        Assert.True(capturingStateManager.State.TryGetPropertyValue("MyCustomSection:MyCustomKey", out var valueNode));
        Assert.Equal("customvalue", valueNode?.GetValue<string>());
 
        // Verify the entire state structure as JSON (mimics what gets saved to disk)
        await VerifyJson(capturingStateManager.State.ToJsonString());
    }
 
    [Fact]
    public async Task DeleteParameterCoreAsync_DeletesFromDeploymentState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var parameterProcessor = CreateParameterProcessor(deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("savedValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
        Assert.True(capturingStateManager.State.Count > 0);
 
        var result = await parameterProcessor.DeleteParameterCoreAsync(parameter, CreateDeleteParameterArguments(deleteFromUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.Equal(string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceDeletedParameter, parameter.Name), result.Message);
        var section = await capturingStateManager.AcquireSectionAsync($"Parameters:{parameter.Name}").DefaultTimeout();
        Assert.Empty(section.Data);
    }
 
    [Fact]
    public async Task DeleteParameterCoreAsync_WithoutDeleteFromUserSecrets_DoesNotDeleteDeploymentState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var parameterProcessor = CreateParameterProcessor(deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("savedValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
 
        var result = await parameterProcessor.DeleteParameterCoreAsync(parameter, new InteractionInputCollection([]), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out var savedValueNode));
        Assert.Equal("savedValue", savedValueNode?.GetValue<string>());
    }
 
    [Fact]
    public async Task DeleteParameterCoreAsync_WhenDeploymentStateDeleteFails_ReturnsFailure()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager { ThrowOnDeleteSection = true };
        var parameterProcessor = CreateParameterProcessor(deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("savedValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
 
        var result = await parameterProcessor.DeleteParameterCoreAsync(parameter, CreateDeleteParameterArguments(deleteFromUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
 
        Assert.False(result.Success);
        Assert.Equal("Failed to delete parameter 'testParam'.", result.Message);
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out var savedValueNode));
        Assert.Equal("savedValue", savedValueNode?.GetValue<string>());
    }
 
    [Fact]
    public async Task DeleteParameterCoreAsync_AddsParameterBackToUnresolvedAndStartsResolutionTask()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("savedValue", saveToUserSecrets: "true"), CancellationToken.None).DefaultTimeout();
        await parameterProcessor.DeleteParameterCoreAsync(parameter, CreateDeleteParameterArguments(), CancellationToken.None).DefaultTimeout();
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
 
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersInputsTitle, inputsInteraction.Title);
        Assert.True(inputsInteraction.Inputs.ContainsName("testParam"));
 
        inputsInteraction.Inputs["testParam"].Value = "newValue";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("newValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task DeleteParameterCoreAsync_ForOptionalParameterClearsValueWithoutPrompting()
    {
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "initialValue");
        parameter.Required = false;
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await parameterProcessor.SetParameterCoreAsync(parameter, CreateSetParameterArguments("savedValue"), CancellationToken.None).DefaultTimeout();
 
        var result = await parameterProcessor.DeleteParameterCoreAsync(parameter, CreateDeleteParameterArguments(), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.Success);
        Assert.Null(await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
    }
 
    [Fact]
    public async Task SetValueAsync_UpdatesCurrentValueAndDeploymentState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
 
        await parameterProcessor.SetValueAsync(parameter, "programmaticValue", saveToUserSecrets: true, CancellationToken.None).DefaultTimeout();
 
        Assert.Equal("programmaticValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out var savedValueNode));
        Assert.Equal("programmaticValue", savedValueNode?.GetValue<string>());
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "programmaticValue", isSensitive: false);
    }
 
    [Fact]
    public async Task SetValueAsync_WhenClearedDuringPersistence_RemainsUnresolved()
    {
        var saveStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var resumeSave = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var stateManager = new CapturingMockDeploymentStateManager
        {
            BeforeSaveAsync = async cancellationToken =>
            {
                saveStarted.TrySetResult();
                await resumeSave.Task.WaitAsync(cancellationToken);
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var processor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: interactionService,
            deploymentStateManager: stateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
        await using var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await processor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
 
        using var saveCts = new CancellationTokenSource();
        var saveTask = processor.SetValueAsync(parameter, "savedValue", saveToUserSecrets: true, saveCts.Token);
        InteractionData? notification = null;
        Task? resolutionTask = null;
        try
        {
            await saveStarted.Task.DefaultTimeout();
            await updates.MoveNextAsync().DefaultTimeout();
            await parameter.SetValueAsync(null).DefaultTimeout();
            await updates.MoveNextAsync().DefaultTimeout();
            Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
            notification = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
            resolutionTask = processor.InitializeParametersAsync([parameter], waitForResolution: true);
            Assert.False(notification.CancellationToken.IsCancellationRequested);
 
            resumeSave.TrySetResult();
            await saveTask.DefaultTimeout();
 
            await Assert.ThrowsAsync<MissingParameterValueException>(() => parameter.GetValueAsync(CancellationToken.None).AsTask()).DefaultTimeout();
            Assert.False(notification.CancellationToken.IsCancellationRequested);
            Assert.False(resolutionTask.IsCompleted);
        }
        finally
        {
            resumeSave.TrySetResult();
            saveCts.Cancel();
            await saveTask.DefaultTimeout();
            await parameter.SetValueAsync("cleanupValue").DefaultTimeout();
            notification?.CompletionTcs.TrySetResult(InteractionResult.Cancel<bool>());
            if (resolutionTask is not null)
            {
                await resolutionTask.DefaultTimeout();
            }
        }
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task HandleUnresolvedParametersAsync_WhenClearedDuringPersistence_PromptsAgain(bool secret, bool keepOtherParameterUnresolved)
    {
        var saveStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var resumeSave = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var stateManager = new CapturingMockDeploymentStateManager
        {
            BeforeSaveAsync = async cancellationToken =>
            {
                saveStarted.TrySetResult();
                await resumeSave.Task.WaitAsync(cancellationToken);
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var processor = CreateParameterProcessor(interactionService: interactionService, deploymentStateManager: stateManager);
        var parameter = CreateParameterWithMissingValue("testParam", secret);
        var otherParameter = CreateParameterWithMissingValue("otherParam");
        // Exercise both an active loop and a canceled generation while the last value is being saved.
        var initializeTask = processor.InitializeParametersAsync(
            keepOtherParameterUnresolved ? [parameter, otherParameter] : [parameter], waitForResolution: true);
        InteractionData? interaction = null;
        try
        {
            interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
            interaction.CompletionTcs.SetResult(InteractionResult.Ok(true));
            interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
            interaction.Inputs[parameter.Name].Value = "savedValue";
            interaction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
            interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
            await saveStarted.Task.DefaultTimeout();
            await parameter.SetValueAsync(null).DefaultTimeout();
            resumeSave.TrySetResult();
 
            interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
            interaction.CompletionTcs.SetResult(InteractionResult.Ok(true));
            interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
 
            Assert.True(interaction.Inputs.ContainsName(parameter.Name));
            Assert.Equal(keepOtherParameterUnresolved, interaction.Inputs.ContainsName(otherParameter.Name));
            Assert.False(interaction.CancellationToken.IsCancellationRequested);
            Assert.False(initializeTask.IsCompleted);
            await Assert.ThrowsAsync<MissingParameterValueException>(() => parameter.GetValueAsync(CancellationToken.None).AsTask()).DefaultTimeout();
        }
        finally
        {
            resumeSave.TrySetResult();
            await parameter.SetValueAsync("cleanupValue").DefaultTimeout();
            await otherParameter.SetValueAsync("cleanupValue").DefaultTimeout();
            if (interaction is { Type: InteractionType.Notification })
            {
                interaction.CompletionTcs.TrySetResult(InteractionResult.Cancel<bool>());
            }
            else
            {
                interaction?.CompletionTcs.TrySetResult(InteractionResult.Cancel<InteractionInputCollection>());
            }
            await initializeTask.DefaultTimeout();
        }
    }
 
    [Theory]
    [InlineData(false, "Parameters:testParam")]
    [InlineData(false, "ConnectionStrings:testParam")]
    [InlineData(false, "Custom:testParam")]
    [InlineData(true, "Parameters:testParam")]
    public async Task InitializeParametersAsync_AfterClearingSavedValue_ReloadsExpectedDeploymentState(bool required, string configurationKey)
    {
        var stateManager = new CapturingMockDeploymentStateManager();
        var processor = CreateParameterProcessor(
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            deploymentStateManager: stateManager);
        var parameter = CreateParameterResource("testParam", "old-value");
        parameter.Required = required;
        parameter.ConfigurationKey = configurationKey;
        var unchangedParameter = CreateParameterResource("unchanged", "retained-value");
        var model = new DistributedApplicationModel([parameter, unchangedParameter]);
 
        var section = await stateManager.AcquireSectionAsync(configurationKey).DefaultTimeout();
        section.SetValue("old-value");
        await stateManager.SaveSectionAsync(section).DefaultTimeout();
        Assert.Equal("old-value", stateManager.State[configurationKey]?.GetValue<string>());
 
        await parameter.SetValueAsync(null).DefaultTimeout();
        await processor.InitializeParametersAsync(model).DefaultTimeout();
 
        // Load the serialized, flattened state through the same configuration provider as LoadDeploymentState.
        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(stateManager.State.ToJsonString()));
        var configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();
        var reloadedParameter = new ParameterResource("testParam", _ =>
            configuration[configurationKey] ?? throw new MissingParameterValueException("Parameter is missing."))
        {
            Required = required
        };
 
        Assert.Equal(required ? "old-value" : null, await reloadedParameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.Equal("retained-value", configuration[unchangedParameter.ConfigurationKey]);
    }
 
    [Fact]
    public async Task ParameterSetValueAsync_AfterInitializationUpdatesDashboardState()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
 
        await parameter.SetValueAsync("resourceValue").DefaultTimeout();
 
        Assert.Equal("resourceValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.False(capturingStateManager.State.TryGetPropertyValue($"Parameters:{parameter.Name}", out _));
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "resourceValue", isSensitive: false);
    }
 
    [Fact]
    public async Task ParameterSetValueAsync_WithMissingParameterResolvesPromptAndUpdatesDashboardState()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService);
        var parameter = CreateParameterWithMissingValue("testParam");
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
        Assert.False(notificationInteraction.CancellationToken.IsCancellationRequested);
 
        await parameter.SetValueAsync("resourceValue").DefaultTimeout();
 
        Assert.Equal("resourceValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        Assert.True(notificationInteraction.CancellationToken.IsCancellationRequested);
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        AssertParameterValueProperty(updates.Current.Snapshot, "resourceValue", isSensitive: false);
    }
 
    [Fact]
    public async Task ParameterSetValueAsync_WithOptionalNullValueClearsDashboardValue()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var parameterProcessor = CreateParameterProcessor(notificationService: notificationService);
        var parameter = CreateParameterResource("testParam", "initialValue");
        parameter.Required = false;
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
 
        await parameter.SetValueAsync(null).DefaultTimeout();
 
        Assert.Null(await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        Assert.DoesNotContain(updates.Current.Snapshot.Properties, property => property.Name == KnownProperties.Parameter.Value);
    }
 
    [Fact]
    public async Task RequiredParameterChangedToOptional_ResolvesPromptAndUpdatesDashboard()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: interactionService);
        var parameter = CreateParameterWithMissingValue("testParam");
        await using var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
        var interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.False(interaction.CancellationToken.IsCancellationRequested);
        var waitingValue = parameter.GetValueAsync(CancellationToken.None).AsTask();
        Assert.False(waitingValue.IsCompleted);
 
        parameter.Required = false;
 
        Assert.Null(await waitingValue.DefaultTimeout());
        Assert.Null(await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        Assert.DoesNotContain(updates.Current.Snapshot.Properties, property => property.Name == KnownProperties.Parameter.Value);
        Assert.True(interaction.CancellationToken.IsCancellationRequested);
    }
 
    [Fact]
    public async Task OptionalParameterChangedToRequired_StartsPromptAndUpdatesDashboard()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: interactionService);
        var parameter = CreateParameterWithMissingValue("testParam");
        parameter.Required = false;
        await using var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
 
        parameter.Required = true;
 
        await Assert.ThrowsAsync<MissingParameterValueException>(
            () => parameter.GetValueAsync(CancellationToken.None).AsTask()).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
        var interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.False(interaction.CancellationToken.IsCancellationRequested);
 
        await parameter.SetValueAsync("resolvedValue").DefaultTimeout();
        Assert.True(interaction.CancellationToken.IsCancellationRequested);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ParameterSetValueAsync_CanceledAfterCommit_ReconcilesLatestValue(bool clearValue)
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var processor = CreateParameterProcessor(notificationService: notificationService, interactionService: interactionService);
        var parameter = CreateParameterResource("testParam", "initialValue");
        var resumeNotifications = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        // Hold both notifications before the processor observes them, so the older version is stale.
        parameter.ValueChanged += (_, args, _) => args.Value == "initialValue"
            ? Task.CompletedTask
            : resumeNotifications.Task;
        await using var updates = notificationService.WatchAsync().GetAsyncEnumerator();
        await processor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
 
        using var cts = new CancellationTokenSource();
        var olderUpdate = parameter.SetValueAsync("olderValue");
        var latestUpdate = parameter.SetValueAsync(clearValue ? null : "latestValue", cts.Token);
        try
        {
            Assert.False(olderUpdate.IsCompleted);
            Assert.False(latestUpdate.IsCompleted);
            cts.Cancel();
        }
        finally
        {
            resumeNotifications.TrySetResult();
        }
 
        try
        {
            await Task.WhenAll(olderUpdate, latestUpdate).DefaultTimeout();
            await updates.MoveNextAsync().DefaultTimeout();
            Assert.Equal(clearValue ? KnownResourceStates.ValueMissing : KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
            if (clearValue)
            {
                await Assert.ThrowsAsync<MissingParameterValueException>(() => parameter.GetValueAsync(CancellationToken.None).AsTask()).DefaultTimeout();
                var notification = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
                Assert.Equal(InteractionStrings.ParametersBarTitle, notification.Title);
                Assert.False(notification.CancellationToken.IsCancellationRequested);
                await parameter.SetValueAsync("resolvedValue").DefaultTimeout();
                Assert.True(notification.CancellationToken.IsCancellationRequested);
            }
            else
            {
                Assert.Equal("latestValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
                AssertParameterValueProperty(updates.Current.Snapshot, "latestValue", isSensitive: false);
            }
        }
        finally
        {
            await parameter.SetValueAsync("cleanupValue").DefaultTimeout();
        }
    }
 
    [Fact]
    public async Task ParameterSetValueAsync_WithRequiredNullValueMarksParameterMissing()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService);
        var parameter = CreateParameterResource("testParam", "initialValue");
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
 
        await parameter.SetValueAsync(null).DefaultTimeout();
 
        await Assert.ThrowsAsync<MissingParameterValueException>(() => parameter.GetValueAsync(CancellationToken.None).AsTask()).DefaultTimeout();
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersInputsTitle, inputsInteraction.Title);
        inputsInteraction.Inputs["testParam"].Value = "resolvedValue";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("resolvedValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    [Fact]
    public async Task ParameterTrySetValue_AfterInitializationUpdatesDashboardState()
    {
        var notificationService = ResourceNotificationServiceTestHelpers.Create();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            notificationService: notificationService,
            interactionService: testInteractionService);
        var parameter = CreateParameterWithMissingValue("testParam");
        var updates = notificationService.WatchAsync().GetAsyncEnumerator();
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.ValueMissing, updates.Current.Snapshot.State?.Text);
 
        Assert.True(parameter.TrySetValue("trySetValue"));
 
        Assert.Equal("trySetValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
 
        await updates.MoveNextAsync().DefaultTimeout();
        Assert.Equal(KnownResourceStates.Running, updates.Current.Snapshot.State?.Text);
        var valueProperty = Assert.Single(updates.Current.Snapshot.Properties, property => property.Name == KnownProperties.Parameter.Value);
        Assert.Equal("trySetValue", valueProperty.Value);
    }
 
    [Fact]
    public async Task DeleteParameterAsync_DeletesFromDeploymentState()
    {
        var capturingStateManager = new CapturingMockDeploymentStateManager();
        var testInteractionService = new TestInteractionService { IsAvailable = true };
        var parameterProcessor = CreateParameterProcessor(
            interactionService: testInteractionService,
            deploymentStateManager: capturingStateManager);
        var parameter = CreateParameterResource("testParam", "initialValue");
 
        await parameterProcessor.InitializeParametersAsync([parameter]).DefaultTimeout();
 
        var setParameterTask = parameterProcessor.SetParameterAsync(parameter, CancellationToken.None);
 
        var setInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        setInteraction.Inputs[ParameterProcessor.SetParameterValueName].Value = "savedValue";
        setInteraction.Inputs[ParameterProcessor.SaveToUserSecretsName].Value = "true";
        setInteraction.CompletionTcs.SetResult(InteractionResult.Ok(setInteraction.Inputs));
 
        await setParameterTask.DefaultTimeout();
        Assert.True(capturingStateManager.State.Count > 0);
 
        var deleteParameterTask = parameterProcessor.DeleteParameterAsync(parameter, CancellationToken.None);
 
        var deleteInteraction = await testInteractionService.Interactions.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(InteractionStrings.DeleteParameterTitle, deleteInteraction.Title);
        Assert.True(deleteInteraction.Inputs.ContainsName(ParameterProcessor.DeleteFromUserSecretsName));
        Assert.Null(deleteInteraction.Inputs[ParameterProcessor.DeleteFromUserSecretsName].Value);
        deleteInteraction.Inputs[ParameterProcessor.DeleteFromUserSecretsName].Value = "true";
        deleteInteraction.CompletionTcs.SetResult(InteractionResult.Ok(deleteInteraction.Inputs));
 
        await deleteParameterTask.DefaultTimeout();
 
        var section = await capturingStateManager.AcquireSectionAsync($"Parameters:{parameter.Name}").DefaultTimeout();
        Assert.Empty(section.Data);
 
        var notificationInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersBarTitle, notificationInteraction.Title);
        notificationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().DefaultTimeout();
        Assert.Equal(InteractionStrings.ParametersInputsTitle, inputsInteraction.Title);
        inputsInteraction.Inputs["testParam"].Value = "newValue";
        inputsInteraction.CompletionTcs.SetResult(InteractionResult.Ok(inputsInteraction.Inputs));
 
        Assert.Equal("newValue", await parameter.GetValueAsync(CancellationToken.None).DefaultTimeout());
    }
 
    private sealed class CapturingMockDeploymentStateManager : IDeploymentStateManager
    {
        // Stores the entire state in an unflattened structure in memory, then flattens for verification
        // to mimic FileDeploymentStateManager behavior
        private readonly JsonObject _unflattenedState = [];
        private JsonObject? _flattenedState;
 
        // Provides the flattened state for verification, matching what FileDeploymentStateManager saves to disk
        public JsonObject State => _flattenedState ?? [];
        public string? StateFilePath => null;
        public bool ThrowOnDeleteSection { get; init; }
        public Func<CancellationToken, Task>? BeforeSaveAsync { get; init; }
 
        public Task<DeploymentStateSection> AcquireSectionAsync(string sectionName, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            // Return existing section data if it exists, otherwise return empty
            var sectionData = _unflattenedState.TryGetPropertyValue(sectionName, out var sectionNode) && sectionNode is JsonObject obj
                ? obj.DeepClone().AsObject()
                : null;
 
            return Task.FromResult(new DeploymentStateSection(sectionName, sectionData, 0));
        }
 
        public Task<DeploymentStateSection> AcquireCurrentSectionAsync(string sectionName, CancellationToken cancellationToken = default)
            => AcquireSectionAsync(sectionName, cancellationToken);
 
        public async Task SaveSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            if (BeforeSaveAsync is { } beforeSave)
            {
                await beforeSave(cancellationToken);
            }
 
            // Increment version to allow multiple saves with the same instance (mimics FileDeploymentStateManager)
            section.Version++;
 
            // Store the section data in the unflattened state object
            _unflattenedState[section.SectionName] = section.Data.DeepClone().AsObject();
 
            // Flatten the state to mimic what FileDeploymentStateManager saves to disk
            _flattenedState = JsonFlattener.FlattenJsonObject(_unflattenedState);
        }
 
        public Task ClearAllStateAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
 
        public Task DeleteSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            if (ThrowOnDeleteSection)
            {
                throw new IOException("Failed to delete section.");
            }
 
            // Increment version to allow multiple saves with the same instance (mimics FileDeploymentStateManager)
            section.Version++;
 
            // Remove the section from the unflattened state object
            _unflattenedState.Remove(section.SectionName);
 
            // Flatten the state to mimic what FileDeploymentStateManager saves to disk
            _flattenedState = JsonFlattener.FlattenJsonObject(_unflattenedState);
 
            return Task.CompletedTask;
        }
    }
}