912 lines
28 KiB
C#
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);
|
|
}
|
|
}
|