File: Pipelines\DistributedApplicationPipelineTests.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.
 
#pragma warning disable ASPIREPUBLISHERS001
#pragma warning disable ASPIREPIPELINES001
#pragma warning disable IDE0005
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Tests.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Tests.Pipelines;
 
public class DistributedApplicationPipelineTests
{
    [Fact]
    public async Task ExecuteAsync_WithNoSteps_CompletesSuccessfully()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
        var context = CreateDeployingContext(builder.Build());
 
        await pipeline.ExecuteAsync(context);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithSingleStep_ExecutesStep()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var stepExecuted = false;
        pipeline.AddStep("step1", async (context) =>
        {
            stepExecuted = true;
            await Task.CompletedTask;
        });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.True(stepExecuted);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleIndependentSteps_ExecutesAllSteps()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(3, executedSteps.Count);
        Assert.Contains("step1", executedSteps);
        Assert.Contains("step2", executedSteps);
        Assert.Contains("step3", executedSteps);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithDependsOn_ExecutesInOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        }, dependsOn: "step1");
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        }, dependsOn: "step2");
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(["step1", "step2", "step3"], executedSteps);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithRequiredBy_ExecutesInCorrectOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        }, requiredBy: "step2");
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        }, requiredBy: "step3");
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(["step1", "step2", "step3"], executedSteps);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMixedDependsOnAndRequiredBy_ExecutesInCorrectOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        }, requiredBy: "step3");
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        }, dependsOn: "step1");
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(3, executedSteps.Count);
        var step1Index = executedSteps.IndexOf("step1");
        var step2Index = executedSteps.IndexOf("step2");
        var step3Index = executedSteps.IndexOf("step3");
 
        Assert.True(step1Index < step3Index, "step1 should execute before step3");
        Assert.True(step2Index < step3Index, "step2 should execute before step3");
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleLevels_ExecutesLevelsInOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executionOrder = new List<(string step, DateTime time)>();
        var level1Complete = new TaskCompletionSource();
        var level2Complete = new TaskCompletionSource();
 
        pipeline.AddStep("level1-step1", async (context) =>
        {
            executionOrder.Add(("level1-step1", DateTime.UtcNow));
            await Task.Delay(10);
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("level1-step2", async (context) =>
        {
            executionOrder.Add(("level1-step2", DateTime.UtcNow));
            await Task.Delay(10);
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("level2-step1", async (context) =>
        {
            executionOrder.Add(("level2-step1", DateTime.UtcNow));
            await Task.CompletedTask;
        }, dependsOn: "level1-step1");
 
        pipeline.AddStep("level2-step2", async (context) =>
        {
            executionOrder.Add(("level2-step2", DateTime.UtcNow));
            await Task.CompletedTask;
        }, dependsOn: "level1-step2");
 
        pipeline.AddStep("level3-step1", async (context) =>
        {
            executionOrder.Add(("level3-step1", DateTime.UtcNow));
            await Task.CompletedTask;
        }, dependsOn: "level2-step1");
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(5, executionOrder.Count);
 
        var level1Steps = executionOrder.Where(x => x.step.StartsWith("level1-")).ToList();
        var level2Steps = executionOrder.Where(x => x.step.StartsWith("level2-")).ToList();
        var level3Steps = executionOrder.Where(x => x.step.StartsWith("level3-")).ToList();
 
        Assert.True(level1Steps.All(l1 => level2Steps.All(l2 => l1.time <= l2.time)),
            "All level 1 steps should start before or at same time as level 2 steps");
        Assert.True(level2Steps.All(l2 => level3Steps.All(l3 => l2.time <= l3.time)),
            "All level 2 steps should start before or at same time as level 3 steps");
    }
 
    [Fact]
    public async Task ExecuteAsync_WithPipelineStepAnnotation_ExecutesAnnotatedSteps()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var executedSteps = new List<string>();
        var resource = builder.AddResource(new CustomResource("test-resource"))
            .WithAnnotation(new PipelineStepAnnotation(() => new PipelineStep
            {
                Name = "annotated-step",
                Action = async (ctx) =>
                {
                    executedSteps.Add("annotated-step");
                    await Task.CompletedTask;
                }
            }));
 
        var pipeline = new DistributedApplicationPipeline();
        pipeline.AddStep("regular-step", async (context) =>
        {
            executedSteps.Add("regular-step");
            await Task.CompletedTask;
        });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(2, executedSteps.Count);
        Assert.Contains("annotated-step", executedSteps);
        Assert.Contains("regular-step", executedSteps);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultiplePipelineStepAnnotations_ExecutesAllAnnotatedSteps()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var executedSteps = new List<string>();
        var resource = builder.AddResource(new CustomResource("test-resource"))
            .WithAnnotation(new PipelineStepAnnotation(() => new[]
            {
                new PipelineStep
                {
                    Name = "annotated-step-1",
                    Action = async (ctx) =>
                    {
                        executedSteps.Add("annotated-step-1");
                        await Task.CompletedTask;
                    }
                },
                new PipelineStep
                {
                    Name = "annotated-step-2",
                    Action = async (ctx) =>
                    {
                        executedSteps.Add("annotated-step-2");
                        await Task.CompletedTask;
                    }
                }
            }));
 
        var pipeline = new DistributedApplicationPipeline();
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(2, executedSteps.Count);
        Assert.Contains("annotated-step-1", executedSteps);
        Assert.Contains("annotated-step-2", executedSteps);
    }
 
    [Fact]
    public void AddStep_WithDuplicateStepNames_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) => await Task.CompletedTask);
 
        var ex = Assert.Throws<InvalidOperationException>(() => pipeline.AddStep("step1", async (context) => await Task.CompletedTask));
        Assert.Contains("A step with the name 'step1' has already been added", ex.Message);
        Assert.Contains("step1", ex.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithUnknownDependency_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) => await Task.CompletedTask, dependsOn: "unknown-step");
 
        var context = CreateDeployingContext(builder.Build());
 
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("depends on unknown step", ex.Message);
        Assert.Contains("unknown-step", ex.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithUnknownRequiredBy_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) => await Task.CompletedTask, requiredBy: "unknown-step");
 
        var context = CreateDeployingContext(builder.Build());
 
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("required by unknown step", ex.Message);
        Assert.Contains("unknown-step", ex.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithCircularDependency_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var step1 = new PipelineStep
        {
            Name = "step1",
            Action = async (context) => await Task.CompletedTask
        };
        step1.DependsOn("step2");
 
        var step2 = new PipelineStep
        {
            Name = "step2",
            Action = async (context) => await Task.CompletedTask
        };
        step2.DependsOn("step1");
 
        pipeline.AddStep(step1);
        pipeline.AddStep(step2);
 
        var context = CreateDeployingContext(builder.Build());
 
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("Circular dependency", ex.Message);
        Assert.Contains("step1", ex.Message);
        Assert.Contains("step2", ex.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WhenStepThrows_WrapsExceptionWithStepName()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var exceptionMessage = "Test exception";
        pipeline.AddStep("failing-step", async (context) =>
        {
            await Task.CompletedTask;
            throw new NotSupportedException(exceptionMessage);
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("failing-step", ex.Message);
        Assert.Contains("failed", ex.Message);
        Assert.NotNull(ex.InnerException);
        Assert.Equal(exceptionMessage, ex.InnerException.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithComplexDependencyGraph_ExecutesInCorrectOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
 
        pipeline.AddStep("a", async (context) =>
        {
            executedSteps.Add("a");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("b", async (context) =>
        {
            executedSteps.Add("b");
            await Task.CompletedTask;
        }, dependsOn: "a");
 
        pipeline.AddStep("c", async (context) =>
        {
            executedSteps.Add("c");
            await Task.CompletedTask;
        }, dependsOn: "a");
 
        pipeline.AddStep("d", async (context) =>
        {
            executedSteps.Add("d");
            await Task.CompletedTask;
        }, dependsOn: "b", requiredBy: "e");
 
        pipeline.AddStep("e", async (context) =>
        {
            executedSteps.Add("e");
            await Task.CompletedTask;
        }, dependsOn: "c");
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        Assert.Equal(5, executedSteps.Count);
 
        var aIndex = executedSteps.IndexOf("a");
        var bIndex = executedSteps.IndexOf("b");
        var cIndex = executedSteps.IndexOf("c");
        var dIndex = executedSteps.IndexOf("d");
        var eIndex = executedSteps.IndexOf("e");
 
        Assert.True(aIndex < bIndex, "a should execute before b");
        Assert.True(aIndex < cIndex, "a should execute before c");
        Assert.True(bIndex < dIndex, "b should execute before d");
        Assert.True(cIndex < eIndex, "c should execute before e");
        Assert.True(dIndex < eIndex, "d should execute before e (requiredBy relationship)");
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleDependencies_ExecutesInCorrectOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        }, dependsOn: new[] { "step1", "step2" });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        var step1Index = executedSteps.IndexOf("step1");
        var step2Index = executedSteps.IndexOf("step2");
        var step3Index = executedSteps.IndexOf("step3");
 
        Assert.True(step1Index < step3Index, "step1 should execute before step3");
        Assert.True(step2Index < step3Index, "step2 should execute before step3");
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleRequiredBy_ExecutesInCorrectOrder()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
        pipeline.AddStep("step1", async (context) =>
        {
            executedSteps.Add("step1");
            await Task.CompletedTask;
        }, requiredBy: new[] { "step2", "step3" });
 
        pipeline.AddStep("step2", async (context) =>
        {
            executedSteps.Add("step2");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step3", async (context) =>
        {
            executedSteps.Add("step3");
            await Task.CompletedTask;
        });
 
        var context = CreateDeployingContext(builder.Build());
        await pipeline.ExecuteAsync(context);
 
        var step1Index = executedSteps.IndexOf("step1");
        var step2Index = executedSteps.IndexOf("step2");
        var step3Index = executedSteps.IndexOf("step3");
 
        Assert.True(step1Index < step2Index, "step1 should execute before step2");
        Assert.True(step1Index < step3Index, "step1 should execute before step3");
    }
 
    [Fact]
    public async Task ExecuteAsync_WithUnknownRequiredByStep_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) =>
        {
            await Task.CompletedTask;
        }, requiredBy: "unknown-step");
 
        var context = CreateDeployingContext(builder.Build());
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("Step 'step1' is required by unknown step 'unknown-step'", exception.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithUnknownRequiredByStepInList_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) =>
        {
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("step2", async (context) =>
        {
            await Task.CompletedTask;
        }, requiredBy: new[] { "step1", "unknown-step" });
 
        var context = CreateDeployingContext(builder.Build());
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("Step 'step2' is required by unknown step 'unknown-step'", exception.Message);
    }
 
    [Fact]
    public void AddStep_WithInvalidDependsOnType_ThrowsArgumentException()
    {
        var pipeline = new DistributedApplicationPipeline();
 
        var exception = Assert.Throws<ArgumentException>(() =>
            pipeline.AddStep("step1", async (context) => await Task.CompletedTask, dependsOn: 123));
 
        Assert.Contains("The dependsOn parameter must be a string or IEnumerable<string>", exception.Message);
    }
 
    [Fact]
    public void AddStep_WithInvalidRequiredByType_ThrowsArgumentException()
    {
        var pipeline = new DistributedApplicationPipeline();
 
        var exception = Assert.Throws<ArgumentException>(() =>
            pipeline.AddStep("step1", async (context) => await Task.CompletedTask, requiredBy: 123));
 
        Assert.Contains("The requiredBy parameter must be a string or IEnumerable<string>", exception.Message);
    }
 
    [Fact]
    public void AddStep_WithDuplicateName_ThrowsInvalidOperationException()
    {
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("step1", async (context) => await Task.CompletedTask);
 
        var exception = Assert.Throws<InvalidOperationException>(() =>
            pipeline.AddStep("step1", async (context) => await Task.CompletedTask));
 
        Assert.Contains("A step with the name 'step1' has already been added to the pipeline", exception.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithDuplicateAnnotationStepNames_ThrowsInvalidOperationException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var resource1 = builder.AddResource(new CustomResource("resource1"))
            .WithAnnotation(new PipelineStepAnnotation(() => new PipelineStep
            {
                Name = "duplicate-step",
                Action = async (ctx) => await Task.CompletedTask
            }));
 
        var resource2 = builder.AddResource(new CustomResource("resource2"))
            .WithAnnotation(new PipelineStepAnnotation(() => new PipelineStep
            {
                Name = "duplicate-step",
                Action = async (ctx) => await Task.CompletedTask
            }));
 
        var pipeline = new DistributedApplicationPipeline();
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("Duplicate step name", exception.Message);
        Assert.Contains("duplicate-step", exception.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleStepsFailingAtSameLevel_ThrowsAggregateException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("failing-step1", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error from step 1");
        });
 
        pipeline.AddStep("failing-step2", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error from step 2");
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("Multiple pipeline steps failed", exception.Message);
        Assert.Equal(2, exception.InnerExceptions.Count);
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step1"));
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step2"));
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMixOfSuccessfulAndFailingStepsAtSameLevel_ThrowsAggregateException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var successfulStepExecuted = false;
 
        pipeline.AddStep("successful-step", async (context) =>
        {
            successfulStepExecuted = true;
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("failing-step1", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error from step 1");
        });
 
        pipeline.AddStep("failing-step2", async (context) =>
        {
            await Task.CompletedTask;
            throw new NotSupportedException("Error from step 2");
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
        Assert.True(successfulStepExecuted, "Successful step should have executed");
        Assert.Contains("Multiple pipeline steps failed", exception.Message);
        Assert.Equal(2, exception.InnerExceptions.Count);
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step1"));
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step2"));
    }
 
    [Fact]
    public async Task ExecuteAsync_WithMultipleFailuresAtSameLevel_StopsExecutionOfNextLevel()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var nextLevelStepExecuted = false;
 
        pipeline.AddStep("failing-step1", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error from step 1");
        });
 
        pipeline.AddStep("failing-step2", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error from step 2");
        });
 
        pipeline.AddStep("next-level-step", async (context) =>
        {
            nextLevelStepExecuted = true;
            await Task.CompletedTask;
        }, dependsOn: "failing-step1");
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
        Assert.False(nextLevelStepExecuted, "Next level step should not have executed");
        Assert.Equal(2, exception.InnerExceptions.Count);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithThreeStepsFailingAtSameLevel_CapturesAllExceptions()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("failing-step1", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error 1");
        });
 
        pipeline.AddStep("failing-step2", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error 2");
        });
 
        pipeline.AddStep("failing-step3", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Error 3");
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
        Assert.Equal(3, exception.InnerExceptions.Count);
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step1"));
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step2"));
        Assert.Contains(exception.InnerExceptions, e => e.Message.Contains("failing-step3"));
    }
 
    [Fact]
    public async Task ExecuteAsync_WithDifferentExceptionTypesAtSameLevel_CapturesAllTypes()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("invalid-op-step", async (context) =>
        {
            await Task.CompletedTask;
            throw new InvalidOperationException("Invalid operation");
        });
 
        pipeline.AddStep("not-supported-step", async (context) =>
        {
            await Task.CompletedTask;
            throw new NotSupportedException("Not supported");
        });
 
        pipeline.AddStep("argument-step", async (context) =>
        {
            await Task.CompletedTask;
            throw new ArgumentException("Bad argument");
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
        Assert.Equal(3, exception.InnerExceptions.Count);
 
        var innerExceptions = exception.InnerExceptions.ToList();
        Assert.Contains(innerExceptions, e => e is InvalidOperationException && e.Message.Contains("invalid-op-step"));
        Assert.Contains(innerExceptions, e => e is InvalidOperationException && e.Message.Contains("not-supported-step"));
        Assert.Contains(innerExceptions, e => e is InvalidOperationException && e.Message.Contains("argument-step"));
    }
 
    [Fact]
    public async Task ExecuteAsync_WithFailingStep_PreservesOriginalStackTrace()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        pipeline.AddStep("failing-step", async (context) =>
        {
            await Task.CompletedTask;
            ThrowHelperMethod();
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline.ExecuteAsync(context));
        Assert.Contains("failing-step", exception.Message);
        Assert.NotNull(exception.InnerException);
        Assert.Contains("ThrowHelperMethod", exception.InnerException.StackTrace);
    }
 
    [Fact]
    public async Task ExecuteAsync_WithParallelSuccessfulAndFailingSteps_OnlyFailuresReported()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
        var pipeline = new DistributedApplicationPipeline();
 
        var executedSteps = new List<string>();
 
        pipeline.AddStep("success1", async (context) =>
        {
            executedSteps.Add("success1");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("fail1", async (context) =>
        {
            executedSteps.Add("fail1");
            await Task.CompletedTask;
            throw new InvalidOperationException("Failure 1");
        });
 
        pipeline.AddStep("success2", async (context) =>
        {
            executedSteps.Add("success2");
            await Task.CompletedTask;
        });
 
        pipeline.AddStep("fail2", async (context) =>
        {
            executedSteps.Add("fail2");
            await Task.CompletedTask;
            throw new InvalidOperationException("Failure 2");
        });
 
        var context = CreateDeployingContext(builder.Build());
 
        var exception = await Assert.ThrowsAsync<AggregateException>(() => pipeline.ExecuteAsync(context));
 
        // All steps should have attempted to execute
        Assert.Contains("success1", executedSteps);
        Assert.Contains("success2", executedSteps);
        Assert.Contains("fail1", executedSteps);
        Assert.Contains("fail2", executedSteps);
 
        // Only failures should be in the exception
        Assert.Equal(2, exception.InnerExceptions.Count);
        Assert.All(exception.InnerExceptions, e => Assert.IsType<InvalidOperationException>(e));
    }
 
    [Fact]
    public async Task PublishAsync_Deploy_WithNoResourcesAndNoPipelineSteps_ReturnsError()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
 
        // Act
        await publisher.PublishAsync(app.Services.GetRequiredService<DistributedApplicationModel>(), CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundErrorActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                activity.Data.IsError &&
                activity.Data.CompletionMessage == "No deployment steps found in the application pipeline.")
            {
                foundErrorActivity = true;
                break;
            }
        }
 
        Assert.True(foundErrorActivity, "Expected to find a task activity with error about no deployment steps found");
    }
 
    [Fact]
    public async Task PublishAsync_Deploy_WithNoResourcesButHasPipelineSteps_Succeeds()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var pipeline = new DistributedApplicationPipeline();
        pipeline.AddStep("test-step", async (context) => await Task.CompletedTask);
 
        builder.Services.AddSingleton<IDistributedApplicationPipeline>(pipeline);
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Act
        await publisher.PublishAsync(model, CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundSuccessActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                !activity.Data.IsError &&
                activity.Data.CompletionMessage == "Found deployment steps in the application pipeline.")
            {
                foundSuccessActivity = true;
                break;
            }
        }
 
        Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline");
    }
 
    [Fact]
    public async Task PublishAsync_Deploy_WithResourcesAndPipelineSteps_ShowsStepsMessage()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var resource = builder.AddResource(new CustomResource("test-resource"))
            .WithAnnotation(new PipelineStepAnnotation(() => new PipelineStep
            {
                Name = "annotated-step",
                Action = async (ctx) => await Task.CompletedTask
            }));
 
        var pipeline = new DistributedApplicationPipeline();
        pipeline.AddStep("direct-step", async (context) => await Task.CompletedTask);
 
        builder.Services.AddSingleton<IDistributedApplicationPipeline>(pipeline);
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Act
        await publisher.PublishAsync(model, CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundSuccessActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                !activity.Data.IsError &&
                activity.Data.CompletionMessage == "Found deployment steps in the application pipeline.")
            {
                foundSuccessActivity = true;
                break;
            }
        }
 
        Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline");
    }
 
    [Fact]
    public async Task PublishAsync_Deploy_WithOnlyResources_ShowsStepsMessage()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: true);
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var resource = builder.AddResource(new CustomResource("test-resource"))
            .WithAnnotation(new PipelineStepAnnotation(() => new PipelineStep
            {
                Name = "annotated-step",
                Action = async (ctx) => await Task.CompletedTask
            }));
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Act
        await publisher.PublishAsync(model, CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundSuccessActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                !activity.Data.IsError &&
                activity.Data.CompletionMessage == "Found deployment steps in the application pipeline.")
            {
                foundSuccessActivity = true;
                break;
            }
        }
 
        Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline");
    }
 
    [Fact]
    public async Task PublishAsync_Publish_WithNoResources_ReturnsError()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: false);
 
        builder.Services.Configure<PublishingOptions>(options =>
        {
            options.OutputPath = Path.GetTempPath();
        });
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Act
        await publisher.PublishAsync(model, CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundErrorActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                activity.Data.IsError &&
                activity.Data.CompletionMessage == "No resources in the distributed application model support publishing.")
            {
                foundErrorActivity = true;
                break;
            }
        }
 
        Assert.True(foundErrorActivity, "Expected to find a task activity with error about no resources supporting publishing");
    }
 
    [Fact]
    public async Task PublishAsync_Publish_WithResources_ShowsResourceCount()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default", isDeploy: false);
 
        builder.Services.Configure<PublishingOptions>(options =>
        {
            options.OutputPath = Path.GetTempPath();
        });
 
        var interactionService = PublishingActivityReporterTests.CreateInteractionService();
        var reporter = new PublishingActivityReporter(interactionService, NullLogger<PublishingActivityReporter>.Instance);
 
        builder.Services.AddSingleton<IPublishingActivityReporter>(reporter);
 
        var resource = builder.AddResource(new CustomResource("test-resource"))
            .WithAnnotation(new PublishingCallbackAnnotation(async (context) => await Task.CompletedTask));
 
        var app = builder.Build();
        var publisher = app.Services.GetRequiredKeyedService<IDistributedApplicationPublisher>("default");
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Act
        await publisher.PublishAsync(model, CancellationToken.None);
 
        // Assert
        var activityReader = reporter.ActivityItemUpdated.Reader;
        var foundSuccessActivity = false;
 
        while (activityReader.TryRead(out var activity))
        {
            if (activity.Type == PublishingActivityTypes.Task &&
                !activity.Data.IsError &&
                activity.Data.CompletionMessage?.StartsWith("Found 1 resources that support publishing.") == true)
            {
                foundSuccessActivity = true;
                break;
            }
        }
 
        Assert.True(foundSuccessActivity, "Expected to find a task activity with message about resources supporting publishing");
    }
 
    private static void ThrowHelperMethod()
    {
        throw new NotSupportedException("Test exception for stack trace");
    }
 
    private static DeployingContext CreateDeployingContext(DistributedApplication app)
    {
        return new DeployingContext(
            app.Services.GetRequiredService<DistributedApplicationModel>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>(),
            app.Services,
            NullLogger.Instance,
            CancellationToken.None,
            outputPath: null);
    }
 
    private sealed class CustomResource(string name) : Resource(name)
    {
    }
}