audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -0,0 +1,252 @@
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Cli.Commands.Setup.State;
using StellaOps.Cli.Commands.Setup.Steps;
using StellaOps.Doctor.Detection;
using Xunit;
namespace StellaOps.Cli.Commands.Setup.Tests.State;
[Trait("Category", "Unit")]
public sealed class FileSetupStateStoreTests : IDisposable
{
private readonly string _testDir;
private readonly FakeTimeProvider _timeProvider;
private readonly FileSetupStateStore _store;
public FileSetupStateStoreTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"setup-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero));
_store = new FileSetupStateStore(_timeProvider, _testDir);
}
public void Dispose()
{
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, true);
}
}
[Fact]
public async Task CreateSessionAsync_CreatesNewSession()
{
// Act
var session = await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
// Assert
session.Should().NotBeNull();
session.Id.Should().StartWith("setup-20260113");
session.Runtime.Should().Be(RuntimeEnvironment.DockerCompose);
session.Status.Should().Be(SetupSessionStatus.InProgress);
session.CreatedAt.Should().Be(_timeProvider.GetUtcNow());
}
[Fact]
public async Task GetLatestSessionAsync_ReturnsNull_WhenNoSessions()
{
// Act
var session = await _store.GetLatestSessionAsync();
// Assert
session.Should().BeNull();
}
[Fact]
public async Task GetLatestSessionAsync_ReturnsLatestSession()
{
// Arrange
await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
_timeProvider.Advance(TimeSpan.FromMinutes(1));
var laterSession = await _store.CreateSessionAsync(RuntimeEnvironment.Kubernetes);
// Act
var result = await _store.GetLatestSessionAsync();
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(laterSession.Id);
}
[Fact]
public async Task GetSessionAsync_ReturnsSession_WhenExists()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Systemd);
// Act
var result = await _store.GetSessionAsync(session.Id);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(session.Id);
}
[Fact]
public async Task GetSessionAsync_ReturnsNull_WhenNotExists()
{
// Act
var result = await _store.GetSessionAsync("nonexistent-session");
// Assert
result.Should().BeNull();
}
[Fact]
public async Task ListSessionsAsync_ReturnsAllSessions()
{
// Arrange
await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
_timeProvider.Advance(TimeSpan.FromMinutes(1));
await _store.CreateSessionAsync(RuntimeEnvironment.Kubernetes);
// Act
var sessions = await _store.ListSessionsAsync();
// Assert
sessions.Should().HaveCount(2);
}
[Fact]
public async Task SaveStepResultAsync_SavesResult()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
var result = SetupStepResult.Success("Test completed");
// Act
await _store.SaveStepResultAsync(session.Id, "database", result);
// Assert
var results = await _store.GetStepResultsAsync(session.Id);
results.Should().ContainKey("database");
results["database"].Status.Should().Be(SetupStepStatus.Completed);
}
[Fact]
public async Task SaveStepResultAsync_UpdatesSessionMetadata()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
var result = SetupStepResult.Success();
// Act
await _store.SaveStepResultAsync(session.Id, "database", result);
// Assert
var updatedSession = await _store.GetSessionAsync(session.Id);
updatedSession!.LastStepId.Should().Be("database");
updatedSession.UpdatedAt.Should().NotBeNull();
}
[Fact]
public async Task CompleteSessionAsync_UpdatesStatus()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
// Act
await _store.CompleteSessionAsync(session.Id);
// Assert
var result = await _store.GetSessionAsync(session.Id);
result!.Status.Should().Be(SetupSessionStatus.Completed);
result.CompletedAt.Should().NotBeNull();
}
[Fact]
public async Task FailSessionAsync_UpdatesStatusWithError()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
// Act
await _store.FailSessionAsync(session.Id, "Connection failed");
// Assert
var result = await _store.GetSessionAsync(session.Id);
result!.Status.Should().Be(SetupSessionStatus.Failed);
result.Error.Should().Be("Connection failed");
}
[Fact]
public async Task ResetStepAsync_RemovesStepResult()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
await _store.SaveStepResultAsync(session.Id, "database", SetupStepResult.Success());
await _store.SaveStepResultAsync(session.Id, "cache", SetupStepResult.Success());
// Act
await _store.ResetStepAsync(session.Id, "database");
// Assert
var results = await _store.GetStepResultsAsync(session.Id);
results.Should().NotContainKey("database");
results.Should().ContainKey("cache");
}
[Fact]
public async Task DeleteSessionAsync_RemovesSession()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
// Act
await _store.DeleteSessionAsync(session.Id);
// Assert
var result = await _store.GetSessionAsync(session.Id);
result.Should().BeNull();
}
[Fact]
public async Task DeleteAllSessionsAsync_RemovesAllSessions()
{
// Arrange
await _store.CreateSessionAsync(RuntimeEnvironment.DockerCompose);
await _store.CreateSessionAsync(RuntimeEnvironment.Kubernetes);
// Act
await _store.DeleteAllSessionsAsync();
// Assert
var sessions = await _store.ListSessionsAsync();
sessions.Should().BeEmpty();
}
[Fact]
public async Task SaveConfigValuesAsync_StoresValues()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
var values = new Dictionary<string, string>
{
["database.host"] = "localhost",
["database.port"] = "5432"
};
// Act
await _store.SaveConfigValuesAsync(session.Id, values);
// Assert
var result = await _store.GetConfigValuesAsync(session.Id);
result.Should().HaveCount(2);
result["database.host"].Should().Be("localhost");
}
[Fact]
public async Task GetConfigValuesAsync_ReturnsEmpty_WhenNoValues()
{
// Arrange
var session = await _store.CreateSessionAsync(RuntimeEnvironment.Bare);
// Act
var result = await _store.GetConfigValuesAsync(session.Id);
// Assert
result.Should().BeEmpty();
}
}

View File

@@ -5,19 +5,12 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,911 @@
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);
}
}

View File

@@ -0,0 +1 @@
This project causes MSBuild hang due to deep dependency tree. Build individually with: dotnet build StellaOps.Cli.Tests.csproj

View File

@@ -0,0 +1,404 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Commands.Advise;
using StellaOps.Cli.Services.Models.Chat;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
[Trait("Category", "Unit")]
public sealed class AdviseChatCommandTests
{
[Fact]
public async Task RenderQueryResponse_Table_RendersCorrectFormat()
{
// Arrange
var response = CreateSampleQueryResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderQueryResponseAsync(response, ChatOutputFormat.Table, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("=== Advisory Chat Response ===", output);
Assert.Contains("Response ID: resp-123", output);
Assert.Contains("Intent:", output);
Assert.Contains("vulnerability_query", output);
Assert.Contains("This is a test summary response.", output);
Assert.Contains("--- Mitigations ---", output);
Assert.Contains("[MIT-001] Update Package", output);
Assert.Contains("[RECOMMENDED]", output);
Assert.Contains("--- Evidence ---", output);
Assert.Contains("[sbom] SBOM Reference", output);
}
[Fact]
public async Task RenderQueryResponse_Json_ReturnsValidJson()
{
// Arrange
var response = CreateSampleQueryResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderQueryResponseAsync(response, ChatOutputFormat.Json, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("\"responseId\"", output);
Assert.Contains("\"resp-123\"", output);
Assert.Contains("\"intent\"", output);
Assert.Contains("\"vulnerability_query\"", output);
Assert.Contains("\"summary\"", output);
Assert.Contains("\"mitigations\"", output);
}
[Fact]
public async Task RenderQueryResponse_Markdown_RendersHeadings()
{
// Arrange
var response = CreateSampleQueryResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderQueryResponseAsync(response, ChatOutputFormat.Markdown, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("# Advisory Chat Response", output);
Assert.Contains("## Summary", output);
Assert.Contains("## Mitigations", output);
Assert.Contains("## Evidence", output);
Assert.Contains("**(Recommended)**", output);
Assert.Contains("| Type | Reference | Label |", output);
}
[Fact]
public async Task RenderDoctorResponse_Table_ShowsQuotasAndTools()
{
// Arrange
var response = CreateSampleDoctorResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderDoctorResponseAsync(response, ChatOutputFormat.Table, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("=== Advisory Chat Doctor ===", output);
Assert.Contains("Tenant: tenant-001", output);
Assert.Contains("User: user-001", output);
Assert.Contains("--- Quotas ---", output);
Assert.Contains("Requests/Minute:", output);
Assert.Contains("--- Tool Access ---", output);
Assert.Contains("Allow All: No", output);
Assert.Contains("SBOM:", output);
Assert.Contains("VEX:", output);
}
[Fact]
public async Task RenderDoctorResponse_Json_ReturnsValidJson()
{
// Arrange
var response = CreateSampleDoctorResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderDoctorResponseAsync(response, ChatOutputFormat.Json, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("\"tenantId\"", output);
Assert.Contains("\"tenant-001\"", output);
Assert.Contains("\"quotas\"", output);
Assert.Contains("\"requestsPerMinuteLimit\"", output);
Assert.Contains("\"tools\"", output);
}
[Fact]
public async Task RenderSettingsResponse_Table_ShowsEffectiveSettings()
{
// Arrange
var response = CreateSampleSettingsResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderSettingsResponseAsync(response, ChatOutputFormat.Table, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("=== Advisory Chat Settings ===", output);
Assert.Contains("Tenant: tenant-001", output);
Assert.Contains("Scope: effective", output);
Assert.Contains("--- Effective Settings ---", output);
Assert.Contains("Source: environment", output);
Assert.Contains("Quotas:", output);
Assert.Contains("Requests/Minute:", output);
}
[Fact]
public async Task RenderSettingsResponse_Json_ReturnsValidJson()
{
// Arrange
var response = CreateSampleSettingsResponse();
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderSettingsResponseAsync(response, ChatOutputFormat.Json, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("\"tenantId\"", output);
Assert.Contains("\"tenant-001\"", output);
Assert.Contains("\"effective\"", output);
Assert.Contains("\"quotas\"", output);
}
[Fact]
public async Task RenderQueryResponse_WithDeniedActions_ShowsDenialReason()
{
// Arrange
var response = new ChatQueryResponse
{
ResponseId = "resp-denied",
Intent = "action_request",
GeneratedAt = DateTimeOffset.UtcNow,
Summary = "Action was denied.",
Confidence = new ChatConfidence { Overall = 0.9, EvidenceQuality = 0.85, ModelCertainty = 0.95 },
ProposedActions =
[
new ChatProposedAction
{
Id = "ACT-001",
Tool = "vex.update",
Description = "Update VEX document",
Denied = true,
DenyReason = "Tool not in allowlist",
RequiresConfirmation = false
}
]
};
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderQueryResponseAsync(response, ChatOutputFormat.Table, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("--- Proposed Actions ---", output);
Assert.Contains("[DENIED]", output);
Assert.Contains("Reason: Tool not in allowlist", output);
}
[Fact]
public async Task RenderDoctorResponse_WithLastDenial_ShowsDenialInfo()
{
// Arrange
var response = new ChatDoctorResponse
{
TenantId = "tenant-001",
UserId = "user-001",
Quotas = new ChatQuotaStatus
{
RequestsPerMinuteLimit = 10,
RequestsPerMinuteRemaining = 0,
RequestsPerMinuteResetsAt = DateTimeOffset.UtcNow.AddMinutes(1),
RequestsPerDayLimit = 100,
RequestsPerDayRemaining = 50,
RequestsPerDayResetsAt = DateTimeOffset.UtcNow.AddHours(12),
TokensPerDayLimit = 50000,
TokensPerDayRemaining = 25000,
TokensPerDayResetsAt = DateTimeOffset.UtcNow.AddHours(12)
},
Tools = new ChatToolAccess
{
AllowAll = false,
AllowedTools = ["sbom.read", "vex.query"]
},
LastDenied = new ChatDenialInfo
{
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Reason = "Quota exceeded",
Code = "QUOTA_EXCEEDED",
Query = "What vulnerabilities affect my image?"
}
};
var sb = new StringBuilder();
await using var writer = new StringWriter(sb);
// Act
await ChatRenderer.RenderDoctorResponseAsync(response, ChatOutputFormat.Table, writer, CancellationToken.None);
var output = sb.ToString();
// Assert
Assert.Contains("--- Last Denial ---", output);
Assert.Contains("Reason: Quota exceeded", output);
Assert.Contains("Code: QUOTA_EXCEEDED", output);
Assert.Contains("Query: What vulnerabilities affect my image?", output);
}
private static ChatQueryResponse CreateSampleQueryResponse()
{
return new ChatQueryResponse
{
ResponseId = "resp-123",
BundleId = "bundle-456",
Intent = "vulnerability_query",
GeneratedAt = DateTimeOffset.UtcNow,
Summary = "This is a test summary response.",
Impact = new ChatImpactAssessment
{
Severity = "High",
AffectedComponents = ["component-a", "component-b"],
Description = "Critical vulnerability in component-a."
},
Reachability = new ChatReachabilityAssessment
{
Reachable = true,
Paths = ["/app/main.js -> /lib/vulnerable.js"],
Confidence = 0.92
},
Mitigations =
[
new ChatMitigationOption
{
Id = "MIT-001",
Title = "Update Package",
Description = "Update the vulnerable package to the latest version.",
Effort = "Low",
Recommended = true
},
new ChatMitigationOption
{
Id = "MIT-002",
Title = "Apply Workaround",
Description = "Disable the affected feature temporarily.",
Effort = "Medium",
Recommended = false
}
],
EvidenceLinks =
[
new ChatEvidenceLink
{
Type = "sbom",
Ref = "sbom:sha256:abc123",
Label = "SBOM Reference"
},
new ChatEvidenceLink
{
Type = "vex",
Ref = "vex:sha256:def456",
Label = "VEX Document"
}
],
Confidence = new ChatConfidence
{
Overall = 0.87,
EvidenceQuality = 0.9,
ModelCertainty = 0.85
},
ProposedActions =
[
new ChatProposedAction
{
Id = "ACT-001",
Tool = "sbom.read",
Description = "Read SBOM details",
RequiresConfirmation = false,
Denied = false
}
],
FollowUp = new ChatFollowUp
{
SuggestedQueries =
[
"What is the CVE severity?",
"Are there any patches available?"
],
RelatedTopics = ["CVE-2024-1234", "npm:lodash"]
},
Diagnostics = new ChatDiagnostics
{
TokensUsed = 1500,
ProcessingTimeMs = 250,
EvidenceSourcesQueried = 3
}
};
}
private static ChatDoctorResponse CreateSampleDoctorResponse()
{
return new ChatDoctorResponse
{
TenantId = "tenant-001",
UserId = "user-001",
Quotas = new ChatQuotaStatus
{
RequestsPerMinuteLimit = 10,
RequestsPerMinuteRemaining = 8,
RequestsPerMinuteResetsAt = DateTimeOffset.UtcNow.AddSeconds(45),
RequestsPerDayLimit = 100,
RequestsPerDayRemaining = 75,
RequestsPerDayResetsAt = DateTimeOffset.UtcNow.AddHours(12),
TokensPerDayLimit = 50000,
TokensPerDayRemaining = 35000,
TokensPerDayResetsAt = DateTimeOffset.UtcNow.AddHours(12)
},
Tools = new ChatToolAccess
{
AllowAll = false,
AllowedTools = ["sbom.read", "vex.query", "findings.topk"],
Providers = new ChatToolProviders
{
Sbom = true,
Vex = true,
Reachability = true,
Policy = false,
Findings = true
}
}
};
}
private static ChatSettingsResponse CreateSampleSettingsResponse()
{
return new ChatSettingsResponse
{
TenantId = "tenant-001",
UserId = "user-001",
Scope = "effective",
Effective = new ChatEffectiveSettings
{
Quotas = new ChatQuotaSettings
{
RequestsPerMinute = 10,
RequestsPerDay = 100,
TokensPerDay = 50000,
ToolCallsPerDay = 500
},
Tools = new ChatToolSettings
{
AllowAll = false,
AllowedTools = ["sbom.read", "vex.query"]
},
Source = "environment"
}
};
}
}

View File

@@ -1,132 +0,0 @@
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cli.Commands.Scan;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class BinaryDiffCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _cancellationToken;
public BinaryDiffCommandTests()
{
_services = new ServiceCollection().BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose", new[] { "-v" })
{
Description = "Enable verbose output"
};
_cancellationToken = CancellationToken.None;
}
[Fact]
public void BuildDiffCommand_HasRequiredOptions()
{
var command = BuildDiffCommand();
Assert.Contains(command.Options, option => HasAlias(option, "--base", "-b"));
Assert.Contains(command.Options, option => HasAlias(option, "--target", "-t"));
Assert.Contains(command.Options, option => HasAlias(option, "--mode", "-m"));
Assert.Contains(command.Options, option => HasAlias(option, "--emit-dsse", "-d"));
Assert.Contains(command.Options, option => HasAlias(option, "--signing-key"));
Assert.Contains(command.Options, option => HasAlias(option, "--format", "-f"));
Assert.Contains(command.Options, option => HasAlias(option, "--platform", "-p"));
Assert.Contains(command.Options, option => HasAlias(option, "--include-unchanged"));
Assert.Contains(command.Options, option => HasAlias(option, "--sections"));
Assert.Contains(command.Options, option => HasAlias(option, "--registry-auth"));
Assert.Contains(command.Options, option => HasAlias(option, "--timeout"));
Assert.Contains(command.Options, option => HasAlias(option, "--verbose", "-v"));
}
[Fact]
public void BuildDiffCommand_RequiresBaseAndTarget()
{
var command = BuildDiffCommand();
var baseOption = FindOption(command, "--base");
var targetOption = FindOption(command, "--target");
Assert.NotNull(baseOption);
Assert.NotNull(targetOption);
Assert.True(baseOption!.IsRequired);
Assert.True(targetOption!.IsRequired);
}
[Fact]
public void DiffCommand_ParsesMinimalArgs()
{
var root = BuildRoot(out _);
var result = root.Parse("scan diff --base registry.example.com/app:1 --target registry.example.com/app:2");
Assert.Empty(result.Errors);
}
[Fact]
public void DiffCommand_FailsWhenBaseMissing()
{
var root = BuildRoot(out _);
var result = root.Parse("scan diff --target registry.example.com/app:2");
Assert.NotEmpty(result.Errors);
}
[Fact]
public void DiffCommand_ParsesSectionsValues()
{
var root = BuildRoot(out var diffCommand);
var result = root.Parse("scan diff --base registry.example.com/app:1 --target registry.example.com/app:2 --sections .text,.rodata --sections .data");
Assert.Empty(result.Errors);
var sectionsOption = diffCommand.Options
.OfType<Option<string[]>>()
.Single(option => HasAlias(option, "--sections"));
var values = result.GetValueForOption(sectionsOption);
Assert.Contains(".text,.rodata", values);
Assert.Contains(".data", values);
Assert.True(sectionsOption.AllowMultipleArgumentsPerToken);
}
private Command BuildDiffCommand()
{
return BinaryDiffCommandGroup.BuildDiffCommand(_services, _verboseOption, _cancellationToken);
}
private RootCommand BuildRoot(out Command diffCommand)
{
diffCommand = BuildDiffCommand();
var scan = new Command("scan", "Scanner operations")
{
diffCommand
};
return new RootCommand { scan };
}
private static Option? FindOption(Command command, string alias)
{
return command.Options.FirstOrDefault(option =>
option.Name.Equals(alias, StringComparison.OrdinalIgnoreCase) ||
option.Name.Equals(alias.TrimStart('-'), StringComparison.OrdinalIgnoreCase) ||
option.Aliases.Contains(alias));
}
private static bool HasAlias(Option option, params string[] aliases)
{
foreach (var alias in aliases)
{
if (option.Name.Equals(alias, StringComparison.OrdinalIgnoreCase) ||
option.Name.Equals(alias.TrimStart('-'), StringComparison.OrdinalIgnoreCase) ||
option.Aliases.Contains(alias))
{
return true;
}
}
return false;
}
}

View File

@@ -37,3 +37,4 @@
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>