File: Commands\TerminalTapePlayCommandTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
 
namespace Aspire.Cli.Tests.Commands;
 
public class TerminalTapePlayCommandTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData("terminal tape --help", 0)]
    [InlineData("terminal tape play --help", 0)]
    [InlineData("terminal tape", 1)]
    [InlineData("terminal tape play", 1)]
    [InlineData("terminal tape play shell", 1)]
    [InlineData("terminal tape play --tape-file probe.tape", 1)]
    public async Task CommandHierarchy(string arguments, int expectedExitCode)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper,
            options => options.EnabledFeatures = [KnownFeatures.TerminalCommandsEnabled]);
        using var provider = services.BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse(arguments);
 
        Assert.Equal(expectedExitCode, await result.InvokeAsync().DefaultTimeout());
    }
 
    [Fact]
    public void RequiresTerminalFeature()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file probe.tape");
 
        Assert.NotEmpty(result.Errors);
    }
 
    [Theory]
    [InlineData("missing.tape", null, null)]
    [InlineData("broken.tape", "NotACommand", ":1:1:")]
    public async Task InvalidFileFailsBeforeResourceDiscovery(string path, string? contents, string? diagnosticLocation)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        if (contents is not null)
        {
            await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, path), contents);
        }
        using var errors = new StringWriter();
        var (provider, backchannel) = TerminalCommandTestServices.CreateProvider(workspace, outputHelper, _ => { },
            options => options.ErrorTextWriter = errors);
        using (provider)
        {
            var result = provider.GetRequiredService<RootCommand>().Parse(
                ["terminal", "tape", "play", "shell", "--tape-file", path]);
 
            Assert.Equal(CliExitCodes.InvalidCommand, await result.InvokeAsync().DefaultTimeout());
            Assert.Equal(0, backchannel.GetResourceSnapshotsCallCount);
            Assert.Contains(path, errors.ToString());
            if (diagnosticLocation is not null)
            {
                Assert.Contains(diagnosticLocation, errors.ToString());
            }
        }
    }
 
    [Theory]
    [InlineData("0")]
    [InlineData("-1")]
    [InlineData("4294968")]
    [InlineData("2147483647")]
    public async Task InvalidTimeoutFailsBeforeResourceDiscovery(string timeout)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var (provider, backchannel) = TerminalCommandTestServices.CreateProvider(workspace, outputHelper, _ => { });
        using (provider)
        {
            var result = provider.GetRequiredService<RootCommand>().Parse(
                ["terminal", "tape", "play", "shell", "--tape-file", "probe.tape", "--timeout", timeout]);
 
            Assert.Equal(CliExitCodes.InvalidCommand, await result.InvokeAsync().DefaultTimeout());
            Assert.Equal(0, backchannel.GetResourceSnapshotsCallCount);
        }
    }
 
    [Fact]
    public async Task NoRunningAppHostFails()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "probe.tape"), "");
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper,
            options => options.EnabledFeatures = [KnownFeatures.TerminalCommandsEnabled]);
        using var provider = services.BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file probe.tape");
 
        Assert.Equal(CliExitCodes.FailedToFindProject, await result.InvokeAsync().DefaultTimeout());
    }
 
    [Theory]
    [InlineData("incompatible", CliExitCodes.AppHostIncompatible)]
    [InlineData("missing", CliExitCodes.InvalidCommand)]
    [InlineData("unavailable", CliExitCodes.InvalidCommand)]
    [InlineData("replicas", CliExitCodes.InvalidCommand)]
    [InlineData("wrong-replica", CliExitCodes.InvalidCommand)]
    public async Task InvalidResourceFailsBeforeConnecting(string scenario, int expectedExitCode)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "probe.tape"), "");
        using var provider = CreateProvider(workspace, null, configure: backchannel =>
        {
            switch (scenario)
            {
                case "incompatible":
                    backchannel.SupportsTerminalsV1 = false;
                    break;
                case "missing":
                    backchannel.ResourceSnapshots = [];
                    break;
                case "unavailable":
                    backchannel.TerminalInfoResponse = new() { IsAvailable = false };
                    break;
                case "replicas":
                    backchannel.TerminalInfoResponse = new()
                    {
                        IsAvailable = true,
                        Replicas =
                        [
                            new() { ReplicaIndex = 0, Label = "shell-0", ConsumerUdsPath = "unused", IsAlive = true },
                            new() { ReplicaIndex = 1, Label = "shell-1", ConsumerUdsPath = "unused", IsAlive = true }
                        ]
                    };
                    break;
            }
        });
        string[] replicaArguments = scenario == "wrong-replica" ? ["--replica", "7"] : [];
        var result = provider.GetRequiredService<RootCommand>().Parse(
            ["terminal", "tape", "play", "shell", "--tape-file", "probe.tape", .. replicaArguments]);
 
        Assert.Equal(expectedExitCode, await result.InvokeAsync().DefaultTimeout());
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData(0)]
    [InlineData(1)]
    public async Task WaitsForProducerAndRefreshesSelectedReplicaBeforePlayback(int? exitCode)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync(101, 37);
        await host.WriteAsync("Ready for input");
        var time = new SignalingFakeTimeProvider(TimeSpan.FromMilliseconds(100));
        var requests = 0;
        var stdout = new TestOutputTextWriter(outputHelper);
        using var provider = CreateProvider(workspace, null, stdout, configure: backchannel =>
        {
            backchannel.GetTerminalInfoHandler = (resourceName, _) =>
            {
                Assert.Equal("shell", resourceName);
                var producerConnected = Interlocked.Increment(ref requests) > 1;
                return Task.FromResult(new GetTerminalInfoResponse
                {
                    IsAvailable = true,
                    Replicas =
                    [
                        new() { ReplicaIndex = 0, Label = "shell-0", ConsumerUdsPath = "unused", IsAlive = true },
                        new()
                        {
                            ReplicaIndex = 1,
                            Label = "shell-1",
                            ConsumerUdsPath = producerConnected ? host.SocketPath : "unused",
                            IsAlive = producerConnected,
                            ExitCode = exitCode
                        }
                    ]
                });
            };
        }, configureOptions: options => options.TimeProvider = time);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "probe.tape"),
            "Set TypingSpeed 0\nType \"ready\"");
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "terminal tape play shell-0 --replica 1 --tape-file probe.tape");
 
        var play = result.InvokeAsync();
        await time.TimerCreated.Task.DefaultTimeout();
        Assert.False(play.IsCompleted);
        time.Advance(TimeSpan.FromMilliseconds(100));
 
        Assert.Equal(CliExitCodes.Success, await play.DefaultTimeout());
        Assert.Equal("ready", await host.ReadInputAsync(5));
        Assert.Equal(2, Volatile.Read(ref requests));
        Assert.Equal(host.GetScreenText().TrimEnd(), string.Join('\n', stdout.Logs).TrimEnd());
        Assert.Equal((101, 37), host.GetDimensions());
        Assert.True(host.IsRunning);
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public async Task ProducerWaitHonorsTimeoutAndCancellation(bool callerCancels, bool duringRefresh)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var time = new SignalingFakeTimeProvider(TimeSpan.FromMilliseconds(100));
        var refreshStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requests = 0;
        using var cancellation = new CancellationTokenSource();
        using var errors = new StringWriter();
        using var provider = CreateProvider(workspace, null, stderr: errors, configure: backchannel =>
        {
            backchannel.GetTerminalInfoHandler = async (_, cancellationToken) =>
            {
                if (Interlocked.Increment(ref requests) > 1 && duringRefresh)
                {
                    refreshStarted.TrySetResult();
                    await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
                }
                return new GetTerminalInfoResponse
                {
                    IsAvailable = true,
                    Replicas = [new() { ReplicaIndex = 0, Label = "shell-0", ConsumerUdsPath = "unused", IsAlive = false }]
                };
            };
        }, configureOptions: options => options.TimeProvider = time);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "probe.tape"), "");
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "terminal tape play shell --tape-file probe.tape --timeout 5");
 
        var play = result.InvokeAsync(cancellationToken: cancellation.Token);
        await time.TimerCreated.Task.DefaultTimeout();
        if (duringRefresh)
        {
            time.Advance(TimeSpan.FromMilliseconds(100));
            await refreshStarted.Task.DefaultTimeout();
        }
        Assert.False(play.IsCompleted);
 
        if (callerCancels)
        {
            await cancellation.CancelAsync();
        }
        else
        {
            time.Advance(TimeSpan.FromSeconds(5));
        }
 
        Assert.Equal(callerCancels ? CliExitCodes.Cancelled : CliExitCodes.WaitTimeout, await play.DefaultTimeout());
        if (!callerCancels)
        {
            Assert.Contains("Terminal tape playback did not finish within 5 seconds.", errors.ToString());
        }
    }
 
    [Fact]
    public async Task ProducerWaitSharesPlaybackTimeoutBudget()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        var time = new SignalingFakeTimeProvider(TimeSpan.FromMilliseconds(100));
        var requests = 0;
        using var provider = CreateProvider(workspace, host.SocketPath, configure: backchannel =>
        {
            backchannel.GetTerminalInfoHandler = (_, _) => Task.FromResult(new GetTerminalInfoResponse
            {
                IsAvailable = true,
                Replicas =
                [
                    new()
                    {
                        ReplicaIndex = 0,
                        Label = "shell-0",
                        ConsumerUdsPath = host.SocketPath,
                        IsAlive = Interlocked.Increment(ref requests) > 1
                    }
                ]
            });
        }, configureOptions: options => options.TimeProvider = time);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "wait.tape"),
            "Set TypingSpeed 0\nType \"started\"\nSleep 60s");
        var result = provider.GetRequiredService<RootCommand>().Parse(
            "terminal tape play shell --tape-file wait.tape --timeout 5");
 
        var play = result.InvokeAsync();
        await time.TimerCreated.Task.DefaultTimeout();
        time.Advance(TimeSpan.FromSeconds(4));
        Assert.Equal("started", await host.ReadInputAsync(7));
        time.Advance(TimeSpan.FromSeconds(1));
 
        Assert.Equal(CliExitCodes.WaitTimeout, await play.DefaultTimeout());
        Assert.True(host.IsRunning);
    }
 
    [Theory]
    [InlineData("", 80, 24)]
    [InlineData("Ready for input", 101, 37)]
    public async Task EmptyTapeReadsInitialScreenWithoutResizingOrStoppingProducer(string screen, int width, int height)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync(width, height);
        if (screen.Length > 0)
        {
            await host.WriteAsync(screen);
        }
        var stdout = new TestOutputTextWriter(outputHelper);
        using var provider = CreateProvider(workspace, host.SocketPath, stdout);
        var directory = workspace.WorkspaceRoot.CreateSubdirectory("tape files");
        await File.WriteAllTextAsync(Path.Combine(directory.FullName, "probe.tape"), "# Inspect the current screen.");
        var result = provider.GetRequiredService<RootCommand>().Parse(
            ["terminal", "tape", "play", "shell", "--tape-file", Path.Combine("tape files", "probe.tape"), "--replica", "0"]);
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal(host.GetScreenText().TrimEnd(), string.Join('\n', stdout.Logs).TrimEnd());
        Assert.Equal((width, height), host.GetDimensions());
        Assert.True(host.IsRunning);
        await host.WriteAsync("Still running");
    }
 
    [Fact]
    public async Task PlaysInputAndWaitsForResponseFromSourcedTape()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        await host.WriteAsync("Ready for input\r\n");
        var stdout = new TestOutputTextWriter(outputHelper);
        using var provider = CreateProvider(workspace, host.SocketPath, stdout);
        var directory = workspace.WorkspaceRoot.CreateSubdirectory("scripts");
        directory.CreateSubdirectory("nested");
        await File.WriteAllTextAsync(Path.Combine(directory.FullName, "play.tape"), """
            Set TypingSpeed 0
            Source nested/input.tape
            Wait+Screen /Accepted: hello/
            """);
        await File.WriteAllTextAsync(Path.Combine(directory.FullName, "nested", "input.tape"), """
            Wait+Screen /Ready for input/
            Type "hello"
            Enter
            """);
        var result = provider.GetRequiredService<RootCommand>().Parse(
            ["terminal", "tape", "play", "shell", "--tape-file", Path.Combine("scripts", "play.tape")]);
 
        var play = result.InvokeAsync();
        Assert.Equal("hello\r", await host.ReadInputAsync(6));
        await host.WriteAsync("Accepted: hello\r\n");
 
        Assert.Equal(CliExitCodes.Success, await play.DefaultTimeout());
        Assert.Equal(host.GetScreenText().TrimEnd(), string.Join('\n', stdout.Logs).TrimEnd());
        Assert.True(host.IsRunning);
    }
 
    [Fact]
    public async Task ResolvesTextOutputBesideRootTape()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        await host.WriteAsync("Ready for input");
        using var provider = CreateProvider(workspace, host.SocketPath);
        var directory = workspace.WorkspaceRoot.CreateSubdirectory("scripts");
        directory.CreateSubdirectory("nested");
        await File.WriteAllTextAsync(Path.Combine(directory.FullName, "play.tape"),
            "Output screen.txt\nSource nested/wait.tape");
        await File.WriteAllTextAsync(Path.Combine(directory.FullName, "nested", "wait.tape"),
            "Output ignored.txt\nWait+Screen /Ready for input/");
        var result = provider.GetRequiredService<RootCommand>().Parse(
            ["terminal", "tape", "play", "shell", "--tape-file", Path.Combine("scripts", "play.tape")]);
 
        Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout());
        Assert.Collection(Directory.EnumerateFiles(directory.FullName, "*.txt", SearchOption.AllDirectories),
            path => Assert.Equal(Path.Combine(directory.FullName, "screen.txt"), path));
        var capture = await File.ReadAllTextAsync(Path.Combine(directory.FullName, "screen.txt"));
        await Verify(capture.Replace(workspace.WorkspaceRoot.FullName, "{workspace}"), "txt");
    }
 
    [Theory]
    [InlineData("Screenshot unsupported.png")]
    [InlineData("Set Width 120")]
    [InlineData("Env SECRET value")]
    [InlineData("Source missing.tape")]
    [InlineData("Output missing-directory/screen.txt")]
    public async Task InvalidTapePreflightDoesNotSendEarlierInput(string invalidCommand)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        using var provider = CreateProvider(workspace, host.SocketPath);
        var file = Path.Combine(workspace.WorkspaceRoot.FullName, "play.tape");
        await File.WriteAllTextAsync(file, $"Set TypingSpeed 0\nType \"unexpected\"\n{invalidCommand}");
        var command = provider.GetRequiredService<RootCommand>();
        var invalid = command.Parse("terminal tape play shell --tape-file play.tape");
 
        Assert.Equal(CliExitCodes.InvalidCommand, await invalid.InvokeAsync().DefaultTimeout());
 
        // A later valid tape provides a deterministic input boundary: rejected preflight must not
        // leave any "unexpected" bytes ahead of this marker, without relying on a quiet-time delay.
        await File.WriteAllTextAsync(file, "Set TypingSpeed 0\nType \"marker\"");
        var valid = command.Parse("terminal tape play shell --tape-file play.tape");
        Assert.Equal(CliExitCodes.Success, await valid.InvokeAsync().DefaultTimeout());
        Assert.Equal("marker", await host.ReadInputAsync(6));
    }
 
    [Fact]
    public async Task WaitFailurePrintsCurrentScreenAndSourceLocation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        await host.WriteAsync("Waiting for authentication");
        var stdout = new TestOutputTextWriter(outputHelper);
        using var errors = new StringWriter();
        using var provider = CreateProvider(workspace, host.SocketPath, stdout, errors);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "wait.tape"),
            "# Deliberately impossible condition\nWait+Screen@100ms /never_appears/");
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file wait.tape");
 
        Assert.Equal(CliExitCodes.FailedToExecuteResourceCommand, await result.InvokeAsync().DefaultTimeout());
        Assert.Equal(host.GetScreenText().TrimEnd(), string.Join('\n', stdout.Logs).TrimEnd());
        Assert.Contains("wait.tape:2:1:", errors.ToString());
        Assert.True(host.IsRunning);
    }
 
    [Fact]
    public async Task OverallTimeoutCancelsPlaybackWithoutStoppingProducer()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        var time = new FakeTimeProvider();
        using var provider = CreateProvider(workspace, host.SocketPath,
            configureOptions: options => options.TimeProvider = time);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "wait.tape"),
            "Set TypingSpeed 0\nType \"started\"\nSleep 60s");
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file wait.tape --timeout 5");
 
        var play = result.InvokeAsync();
        Assert.Equal("started", await host.ReadInputAsync(7));
        time.Advance(TimeSpan.FromSeconds(5));
 
        Assert.Equal(CliExitCodes.WaitTimeout, await play.DefaultTimeout());
        Assert.True(host.IsRunning);
    }
 
    [Fact]
    public async Task UserCancellationLeavesProducerRunning()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        using var provider = CreateProvider(workspace, host.SocketPath);
        using var cancellation = new CancellationTokenSource();
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "wait.tape"),
            "Set TypingSpeed 0\nType \"started\"\nSleep 60s");
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file wait.tape");
 
        var play = result.InvokeAsync(cancellationToken: cancellation.Token);
        Assert.Equal("started", await host.ReadInputAsync(7));
        await cancellation.CancelAsync();
 
        Assert.Equal(CliExitCodes.Cancelled, await play.DefaultTimeout());
        Assert.True(host.IsRunning);
    }
 
    [Fact]
    public async Task DisconnectionDuringPlaybackFailsPromptly()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        using var errors = new StringWriter();
        using var provider = CreateProvider(workspace, host.SocketPath, stderr: errors);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "wait.tape"),
            "Set TypingSpeed 0\nType \"started\"\nSleep 60s");
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file wait.tape");
 
        var play = result.InvokeAsync();
        Assert.Equal("started", await host.ReadInputAsync(7));
        await host.DisposeAsync();
 
        Assert.Equal(CliExitCodes.FailedToExecuteResourceCommand, await play.DefaultTimeout());
        Assert.Contains("connection closed", errors.ToString());
    }
 
    [Fact]
    public async Task UnreachableTerminalFails()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await using var host = await TerminalTapeTestHost.StartAsync();
        await host.DisposeAsync();
        using var provider = CreateProvider(workspace, host.SocketPath);
        await File.WriteAllTextAsync(Path.Combine(workspace.WorkspaceRoot.FullName, "probe.tape"), "");
        var result = provider.GetRequiredService<RootCommand>().Parse("terminal tape play shell --tape-file probe.tape");
 
        Assert.Equal(CliExitCodes.FailedToExecuteResourceCommand, await result.InvokeAsync().DefaultTimeout());
    }
 
    private ServiceProvider CreateProvider(
        TemporaryWorkspace workspace,
        string? socketPath,
        TestOutputTextWriter? stdout = null,
        StringWriter? stderr = null,
        Action<TestAppHostAuxiliaryBackchannel>? configure = null,
        Action<CliServiceCollectionTestOptions>? configureOptions = null)
    {
        var (provider, _) = TerminalCommandTestServices.CreateProvider(workspace, outputHelper, backchannel =>
        {
            backchannel.ResourceSnapshots =
            [
                new ResourceSnapshot { Name = "shell-0", DisplayName = "shell", ResourceType = "Executable", State = "Running" }
            ];
            backchannel.TerminalInfoResponse = new()
            {
                IsAvailable = true,
                Replicas = [new() { ReplicaIndex = 0, Label = "shell-0", ConsumerUdsPath = socketPath ?? "unused", IsAlive = true }]
            };
            configure?.Invoke(backchannel);
        }, options =>
        {
            options.OutputTextWriter = stdout;
            options.ErrorTextWriter = stderr;
            configureOptions?.Invoke(options);
        });
        return provider;
    }
}