Files
git.stella-ops.org/src/Cli/__Tests/StellaOps.Cli.Commands.Setup.Tests/Steps/SetupStepImplementationsTests.cs

912 lines
28 KiB
C#

using FluentAssertions;
using StellaOps.Cli.Commands.Setup.Steps;
using StellaOps.Cli.Commands.Setup.Steps.Implementations;
using StellaOps.Doctor.Detection;
using Xunit;
namespace StellaOps.Cli.Commands.Setup.Tests.Steps;
[Trait("Category", "Unit")]
public sealed class SetupStepImplementationsTests
{
[Fact]
public void DatabaseSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new DatabaseSetupStep();
// Assert
step.Id.Should().Be("database");
step.Name.Should().Be("PostgreSQL Database");
step.Category.Should().Be(SetupCategory.Infrastructure);
step.IsRequired.Should().BeTrue();
step.IsSkippable.Should().BeFalse();
step.Order.Should().Be(10);
step.Dependencies.Should().BeEmpty();
step.ValidationChecks.Should().Contain("check.database.connectivity");
}
[Fact]
public void CacheSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new CacheSetupStep();
// Assert
step.Id.Should().Be("cache");
step.Name.Should().Be("Valkey/Redis Cache");
step.Category.Should().Be(SetupCategory.Infrastructure);
step.IsRequired.Should().BeTrue();
step.Dependencies.Should().Contain("database");
step.Order.Should().Be(20);
}
[Fact]
public void VaultSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new VaultSetupStep();
// Assert
step.Id.Should().Be("vault");
step.Name.Should().Be("Secrets Vault");
step.Category.Should().Be(SetupCategory.Security);
step.IsRequired.Should().BeFalse();
step.IsSkippable.Should().BeTrue();
step.ValidationChecks.Should().Contain("check.integration.vault.connectivity");
}
[Fact]
public void SettingsStoreSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new SettingsStoreSetupStep();
// Assert
step.Id.Should().Be("settingsstore");
step.Name.Should().Be("Settings Store");
step.Category.Should().Be(SetupCategory.Configuration);
step.IsRequired.Should().BeFalse();
step.IsSkippable.Should().BeTrue();
step.ValidationChecks.Should().Contain("check.integration.settingsstore.connectivity");
}
[Fact]
public void RegistrySetupStep_HasCorrectMetadata()
{
// Arrange
var step = new RegistrySetupStep();
// Assert
step.Id.Should().Be("registry");
step.Name.Should().Be("Container Registry");
step.Category.Should().Be(SetupCategory.Integration);
step.IsRequired.Should().BeFalse();
step.ValidationChecks.Should().Contain("check.integration.registry.connectivity");
}
[Fact]
public void TelemetrySetupStep_HasCorrectMetadata()
{
// Arrange
var step = new TelemetrySetupStep();
// Assert
step.Id.Should().Be("telemetry");
step.Name.Should().Be("OpenTelemetry");
step.Category.Should().Be(SetupCategory.Observability);
step.IsRequired.Should().BeFalse();
step.ValidationChecks.Should().Contain("check.telemetry.otlp.connectivity");
}
[Fact]
public async Task DatabaseSetupStep_CheckPrerequisites_Passes_WhenInteractive()
{
// Arrange
var step = new DatabaseSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = false
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert
result.Met.Should().BeTrue();
}
[Fact]
public async Task DatabaseSetupStep_CheckPrerequisites_Fails_WhenNonInteractiveWithoutConfig()
{
// Arrange
var step = new DatabaseSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert
result.Met.Should().BeFalse();
result.MissingPrerequisites.Should().Contain(s => s.Contains("database"));
}
[Fact]
public async Task DatabaseSetupStep_CheckPrerequisites_Passes_WhenNonInteractiveWithConnectionString()
{
// Arrange
var step = new DatabaseSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true,
ConfigValues = new Dictionary<string, string>
{
["database.connectionString"] = "Host=localhost;Database=test"
}
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert
result.Met.Should().BeTrue();
}
[Fact]
public async Task DatabaseSetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new DatabaseSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["database.host"] = "localhost",
["database.port"] = "5432",
["database.database"] = "testdb",
["database.user"] = "testuser",
["database.password"] = "testpass"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig.Should().ContainKey("database.host");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task CacheSetupStep_CheckPrerequisites_Fails_WhenNonInteractiveWithoutConfig()
{
// Arrange
var step = new CacheSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert
result.Met.Should().BeFalse();
}
[Fact]
public async Task CacheSetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new CacheSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["cache.host"] = "localhost",
["cache.port"] = "6379"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig.Should().ContainKey("cache.host");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task SettingsStoreSetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected()
{
// Arrange
var step = new SettingsStoreSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true // No provider in config, non-interactive mode
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Skipped);
}
[Fact]
public async Task SettingsStoreSetupStep_Execute_DryRun_ConfiguresConsul()
{
// Arrange
var step = new SettingsStoreSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["settingsstore.provider"] = "consul",
["settingsstore.address"] = "http://localhost:8500",
["settingsstore.prefix"] = "stellaops/"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["settingsstore.provider"].Should().Be("consul");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task VaultSetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected()
{
// Arrange
var step = new VaultSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Skipped);
}
[Fact]
public async Task TelemetrySetupStep_Execute_ReturnsSkipped_WhenNoEndpointProvided()
{
// Arrange
var step = new TelemetrySetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Skipped);
}
[Fact]
public async Task TelemetrySetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new TelemetrySetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["telemetry.otlpEndpoint"] = "http://localhost:4317",
["telemetry.serviceName"] = "test-service"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["telemetry.otlpEndpoint"].Should().Be("http://localhost:4317");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task RegistrySetupStep_Execute_ReturnsSkipped_WhenNoUrlProvided()
{
// Arrange
var step = new RegistrySetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Skipped);
}
[Fact]
public async Task RegistrySetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new RegistrySetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["registry.url"] = "https://registry.example.com",
["registry.username"] = "admin"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["registry.url"].Should().Be("https://registry.example.com");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public void AllSteps_HaveUniqueIds()
{
// Arrange
var steps = new ISetupStep[]
{
new DatabaseSetupStep(),
new CacheSetupStep(),
new VaultSetupStep(),
new SettingsStoreSetupStep(),
new RegistrySetupStep(),
new TelemetrySetupStep()
};
// Assert
var ids = steps.Select(s => s.Id).ToList();
ids.Should().OnlyHaveUniqueItems();
}
[Fact]
public void AllSteps_CanBeAddedToCatalog()
{
// Arrange
var catalog = new SetupStepCatalog();
var steps = new ISetupStep[]
{
new DatabaseSetupStep(),
new CacheSetupStep(),
new VaultSetupStep(),
new SettingsStoreSetupStep(),
new RegistrySetupStep(),
new TelemetrySetupStep()
};
// Act
foreach (var step in steps)
{
catalog.Register(step);
}
// Assert
catalog.AllSteps.Should().HaveCount(6);
}
[Fact]
public void StepCatalog_ResolvesExecutionOrder_WithDependencies()
{
// Arrange
var catalog = new SetupStepCatalog();
catalog.Register(new DatabaseSetupStep());
catalog.Register(new CacheSetupStep());
catalog.Register(new VaultSetupStep());
catalog.Register(new SettingsStoreSetupStep());
// Act
var orderedSteps = catalog.ResolveExecutionOrder().ToList();
// Assert
orderedSteps.Should().HaveCount(4);
// Database must come before Cache (Cache depends on Database)
var databaseIndex = orderedSteps.FindIndex(s => s.Id == "database");
var cacheIndex = orderedSteps.FindIndex(s => s.Id == "cache");
databaseIndex.Should().BeLessThan(cacheIndex);
}
// =====================================
// Sprint 7-9 Setup Steps Tests
// =====================================
[Fact]
public void AuthoritySetupStep_HasCorrectMetadata()
{
// Arrange
var step = new AuthoritySetupStep();
// Assert
step.Id.Should().Be("authority");
step.Name.Should().Be("Authentication Provider");
step.Category.Should().Be(SetupCategory.Security);
step.IsRequired.Should().BeTrue();
step.IsSkippable.Should().BeFalse();
step.Order.Should().Be(1);
step.ValidationChecks.Should().Contain("check.authority.plugin.configured");
}
[Fact]
public async Task AuthoritySetupStep_CheckPrerequisites_Passes_WhenInteractive()
{
// Arrange
var step = new AuthoritySetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = false
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert
result.Met.Should().BeTrue();
}
[Fact]
public async Task AuthoritySetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new AuthoritySetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["authority.provider"] = "standard",
["authority.standard.passwordPolicy.minLength"] = "12"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig.Should().ContainKey("authority.provider");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public void UsersSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new UsersSetupStep();
// Assert
step.Id.Should().Be("users");
step.Name.Should().Be("User Management");
step.Category.Should().Be(SetupCategory.Security);
step.IsRequired.Should().BeTrue();
step.IsSkippable.Should().BeFalse();
step.Order.Should().Be(2);
step.Dependencies.Should().Contain("authority");
step.ValidationChecks.Should().Contain("check.users.superuser.exists");
}
[Fact]
public async Task UsersSetupStep_Execute_DryRun_ReturnsSuccess()
{
// Arrange
var step = new UsersSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["users.superuser.username"] = "admin",
["users.superuser.email"] = "admin@example.com",
["users.superuser.password"] = "SecurePass123!"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig.Should().ContainKey("users.superuser.username");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public void NotifySetupStep_HasCorrectMetadata()
{
// Arrange
var step = new NotifySetupStep();
// Assert
step.Id.Should().Be("notify");
step.Name.Should().Be("Notifications");
step.Category.Should().Be(SetupCategory.Integration);
step.IsRequired.Should().BeFalse();
step.IsSkippable.Should().BeTrue();
step.Order.Should().Be(70);
step.ValidationChecks.Should().Contain("check.notify.channel.configured");
}
[Fact]
public async Task NotifySetupStep_Execute_ReturnsSkipped_WhenNoProviderSelected()
{
// Arrange
var step = new NotifySetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Skipped);
}
[Fact]
public async Task NotifySetupStep_Execute_DryRun_ConfiguresEmail()
{
// Arrange
var step = new NotifySetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["notify.provider"] = "email",
["notify.email.smtpHost"] = "smtp.example.com",
["notify.email.smtpPort"] = "587",
["notify.email.fromAddress"] = "noreply@example.com"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["notify.provider"].Should().Be("email");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public void LlmSetupStep_HasCorrectMetadata()
{
// Arrange
var step = new LlmSetupStep();
// Assert
step.Id.Should().Be("llm");
step.Name.Should().Be("AI/LLM Provider");
step.Category.Should().Be(SetupCategory.Integration);
step.IsRequired.Should().BeFalse();
step.IsSkippable.Should().BeTrue();
step.Order.Should().Be(80);
step.ValidationChecks.Should().Contain("check.ai.llm.config");
step.ValidationChecks.Should().Contain("check.ai.provider.openai");
step.ValidationChecks.Should().Contain("check.ai.provider.claude");
step.ValidationChecks.Should().Contain("check.ai.provider.gemini");
}
[Fact]
public async Task LlmSetupStep_CheckPrerequisites_AlwaysPasses()
{
// Arrange
var step = new LlmSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = true
};
// Act
var result = await step.CheckPrerequisitesAsync(context);
// Assert - LLM setup has no prerequisites
result.Met.Should().BeTrue();
}
[Fact]
public async Task LlmSetupStep_Execute_ReturnsSuccess_WhenNoneSelected()
{
// Arrange
var step = new LlmSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
NonInteractive = false,
ConfigValues = new Dictionary<string, string>
{
["llm.provider"] = "none"
},
Output = msg => output.Add(msg),
PromptForChoice = (prompt, options, defaultVal) => "none"
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig.Should().ContainKey("AdvisoryAI:Enabled");
result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("false");
}
[Fact]
public async Task LlmSetupStep_Execute_DryRun_ConfiguresOpenAi()
{
// Arrange
var step = new LlmSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["llm.provider"] = "openai",
["llm.openai.apiKey"] = "sk-test-key-12345",
["llm.openai.model"] = "gpt-4o"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true");
result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("openai");
result.AppliedConfig["AdvisoryAI:LlmProviders:OpenAI:Model"].Should().Be("gpt-4o");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task LlmSetupStep_Execute_DryRun_ConfiguresClaude()
{
// Arrange
var step = new LlmSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["llm.provider"] = "claude",
["llm.claude.apiKey"] = "sk-ant-test-key-12345",
["llm.claude.model"] = "claude-sonnet-4-20250514"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true");
result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("claude");
result.AppliedConfig["AdvisoryAI:LlmProviders:Claude:Model"].Should().Be("claude-sonnet-4-20250514");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task LlmSetupStep_Execute_DryRun_ConfiguresGemini()
{
// Arrange
var step = new LlmSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["llm.provider"] = "gemini",
["llm.gemini.apiKey"] = "AIzaSy-test-key-12345",
["llm.gemini.model"] = "gemini-1.5-flash"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true");
result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("gemini");
result.AppliedConfig["AdvisoryAI:LlmProviders:Gemini:Model"].Should().Be("gemini-1.5-flash");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task LlmSetupStep_Execute_DryRun_ConfiguresOllama()
{
// Arrange
var step = new LlmSetupStep();
var output = new List<string>();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
DryRun = true,
ConfigValues = new Dictionary<string, string>
{
["llm.provider"] = "ollama",
["llm.ollama.endpoint"] = "http://localhost:11434",
["llm.ollama.model"] = "llama3:8b"
},
Output = msg => output.Add(msg)
};
// Act
var result = await step.ExecuteAsync(context);
// Assert
result.Status.Should().Be(SetupStepStatus.Completed);
result.AppliedConfig["AdvisoryAI:Enabled"].Should().Be("true");
result.AppliedConfig["AdvisoryAI:DefaultProvider"].Should().Be("ollama");
result.AppliedConfig["AdvisoryAI:LlmProviders:Ollama:Enabled"].Should().Be("true");
result.AppliedConfig["AdvisoryAI:LlmProviders:Ollama:Endpoint"].Should().Be("http://localhost:11434");
output.Should().Contain(s => s.Contains("DRY RUN"));
}
[Fact]
public async Task LlmSetupStep_Validate_ReturnsSuccess_WhenDisabled()
{
// Arrange
var step = new LlmSetupStep();
var context = new SetupStepContext
{
SessionId = "test-session",
Runtime = RuntimeEnvironment.Bare,
ConfigValues = new Dictionary<string, string>
{
["AdvisoryAI:Enabled"] = "false"
}
};
// Act
var result = await step.ValidateAsync(context);
// Assert
result.IsValid.Should().BeTrue();
}
[Fact]
public void AllSetupSteps_HaveUniqueIds_IncludingSprint7_9Steps()
{
// Arrange
var steps = new ISetupStep[]
{
new DatabaseSetupStep(),
new CacheSetupStep(),
new VaultSetupStep(),
new SettingsStoreSetupStep(),
new RegistrySetupStep(),
new TelemetrySetupStep(),
new AuthoritySetupStep(),
new UsersSetupStep(),
new NotifySetupStep(),
new LlmSetupStep()
};
// Assert
var ids = steps.Select(s => s.Id).ToList();
ids.Should().OnlyHaveUniqueItems();
}
[Fact]
public void StepCatalog_ResolvesExecutionOrder_WithAllSteps()
{
// Arrange
var catalog = new SetupStepCatalog();
catalog.Register(new AuthoritySetupStep());
catalog.Register(new UsersSetupStep());
catalog.Register(new DatabaseSetupStep());
catalog.Register(new CacheSetupStep());
catalog.Register(new NotifySetupStep());
catalog.Register(new LlmSetupStep());
// Act
var orderedSteps = catalog.ResolveExecutionOrder().ToList();
// Assert
orderedSteps.Should().HaveCount(6);
// Authority must come before Users (Users depends on Authority)
var authorityIndex = orderedSteps.FindIndex(s => s.Id == "authority");
var usersIndex = orderedSteps.FindIndex(s => s.Id == "users");
authorityIndex.Should().BeLessThan(usersIndex);
// Database must come before Cache (Cache depends on Database)
var databaseIndex = orderedSteps.FindIndex(s => s.Id == "database");
var cacheIndex = orderedSteps.FindIndex(s => s.Id == "cache");
databaseIndex.Should().BeLessThan(cacheIndex);
}
}