doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,504 @@
// -----------------------------------------------------------------------------
// ScoreGateCommandTests.cs
// Sprint: SPRINT_20260118_030_LIB_verdict_rekor_gate_api
// Task: TASK-030-008 - CLI Gate Command
// Description: Unit tests for score-based gate CLI commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
/// <summary>
/// Unit tests for score-based gate CLI commands.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public class ScoreGateCommandTests
{
private readonly IServiceProvider _services;
private readonly StellaOpsCliOptions _options;
private readonly Option<bool> _verboseOption;
public ScoreGateCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
_services = serviceCollection.BuildServiceProvider();
_options = new StellaOpsCliOptions
{
PolicyGateway = new StellaOpsCliPolicyGatewayOptions
{
BaseUrl = "http://localhost:5080"
}
};
_verboseOption = new Option<bool>("--verbose", "-v") { Description = "Enable verbose output" };
}
#region Score Command Structure Tests
[Fact]
public void BuildScoreCommand_CreatesScoreCommandTree()
{
// Act
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert
Assert.Equal("score", command.Name);
Assert.Contains("Score-based", command.Description);
Assert.Contains("EWS", command.Description);
}
[Fact]
public void BuildScoreCommand_HasEvaluateSubcommand()
{
// Act
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.FirstOrDefault(c => c.Name == "evaluate");
// Assert
Assert.NotNull(evaluateCommand);
Assert.Contains("single finding", evaluateCommand.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void BuildScoreCommand_HasBatchSubcommand()
{
// Act
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.FirstOrDefault(c => c.Name == "batch");
// Assert
Assert.NotNull(batchCommand);
Assert.Contains("multiple findings", batchCommand.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Evaluate Command Tests
[Fact]
public void EvaluateCommand_HasFindingIdOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var findingIdOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--finding-id") || o.Aliases.Contains("-f"));
// Assert
Assert.NotNull(findingIdOption);
Assert.Equal(1, findingIdOption.Arity.MinimumNumberOfValues); // Required
}
[Fact]
public void EvaluateCommand_HasCvssOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var cvssOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--cvss"));
// Assert
Assert.NotNull(cvssOption);
Assert.Contains("0-10", cvssOption.Description);
}
[Fact]
public void EvaluateCommand_HasEpssOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var epssOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--epss"));
// Assert
Assert.NotNull(epssOption);
Assert.Contains("0-1", epssOption.Description);
}
[Fact]
public void EvaluateCommand_HasReachabilityOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var reachabilityOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--reachability") || o.Aliases.Contains("-r"));
// Assert
Assert.NotNull(reachabilityOption);
Assert.Contains("none", reachabilityOption.Description);
Assert.Contains("package", reachabilityOption.Description);
Assert.Contains("function", reachabilityOption.Description);
Assert.Contains("caller", reachabilityOption.Description);
}
[Fact]
public void EvaluateCommand_HasExploitMaturityOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var exploitOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--exploit-maturity") || o.Aliases.Contains("-e"));
// Assert
Assert.NotNull(exploitOption);
Assert.Contains("poc", exploitOption.Description);
Assert.Contains("functional", exploitOption.Description);
Assert.Contains("high", exploitOption.Description);
}
[Fact]
public void EvaluateCommand_HasPatchProofOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var patchProofOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--patch-proof"));
// Assert
Assert.NotNull(patchProofOption);
Assert.Contains("0-1", patchProofOption.Description);
}
[Fact]
public void EvaluateCommand_HasVexStatusOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var vexStatusOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--vex-status"));
// Assert
Assert.NotNull(vexStatusOption);
Assert.Contains("affected", vexStatusOption.Description);
Assert.Contains("not_affected", vexStatusOption.Description);
Assert.Contains("fixed", vexStatusOption.Description);
}
[Fact]
public void EvaluateCommand_HasPolicyProfileOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var policyOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--policy") || o.Aliases.Contains("-p"));
// Assert
Assert.NotNull(policyOption);
Assert.Contains("advisory", policyOption.Description);
}
[Fact]
public void EvaluateCommand_HasAnchorOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var anchorOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--anchor"));
// Assert
Assert.NotNull(anchorOption);
Assert.Contains("Rekor", anchorOption.Description);
}
[Fact]
public void EvaluateCommand_HasOutputOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var outputOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
Assert.Contains("table", outputOption.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("json", outputOption.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("ci", outputOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void EvaluateCommand_HasBreakdownOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var evaluateCommand = command.Subcommands.First(c => c.Name == "evaluate");
// Act
var breakdownOption = evaluateCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--breakdown"));
// Assert
Assert.NotNull(breakdownOption);
Assert.Contains("breakdown", breakdownOption.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Batch Command Tests
[Fact]
public void BatchCommand_HasInputOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var inputOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--input") || o.Aliases.Contains("-i"));
// Assert
Assert.NotNull(inputOption);
Assert.Contains("JSON", inputOption.Description);
}
[Fact]
public void BatchCommand_HasSarifOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var sarifOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--sarif"));
// Assert
Assert.NotNull(sarifOption);
Assert.Contains("SARIF", sarifOption.Description);
}
[Fact]
public void BatchCommand_HasFailFastOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var failFastOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--fail-fast"));
// Assert
Assert.NotNull(failFastOption);
Assert.Contains("Stop", failFastOption.Description);
}
[Fact]
public void BatchCommand_HasParallelismOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var parallelismOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--parallelism"));
// Assert
Assert.NotNull(parallelismOption);
Assert.Contains("parallelism", parallelismOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void BatchCommand_HasIncludeVerdictsOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var includeVerdictsOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--include-verdicts"));
// Assert
Assert.NotNull(includeVerdictsOption);
Assert.Contains("verdict", includeVerdictsOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void BatchCommand_HasOutputOption()
{
// Arrange
var command = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
var batchCommand = command.Subcommands.First(c => c.Name == "batch");
// Act
var outputOption = batchCommand.Options.FirstOrDefault(o =>
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
Assert.Contains("table", outputOption.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("json", outputOption.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("ci", outputOption.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Integration with Gate Command Tests
[Fact]
public void ScoreCommand_ShouldBeAddableToGateCommand()
{
// Arrange
var gateCommand = new Command("gate", "CI/CD release gate operations");
var scoreCommand = ScoreGateCommandGroup.BuildScoreCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
gateCommand.Add(scoreCommand);
// Assert
Assert.Contains(gateCommand.Subcommands, c => c.Name == "score");
}
[Fact]
public void GateCommand_IncludesScoreSubcommand()
{
// Act
var gateCommand = GateCommandGroup.BuildGateCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert
Assert.Contains(gateCommand.Subcommands, c => c.Name == "score");
}
[Fact]
public void GateScoreEvaluate_FullCommandPath()
{
// Arrange
var gateCommand = GateCommandGroup.BuildGateCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var scoreCommand = gateCommand.Subcommands.First(c => c.Name == "score");
var evaluateCommand = scoreCommand.Subcommands.First(c => c.Name == "evaluate");
// Assert
Assert.NotNull(evaluateCommand);
Assert.Equal("evaluate", evaluateCommand.Name);
}
[Fact]
public void GateScoreBatch_FullCommandPath()
{
// Arrange
var gateCommand = GateCommandGroup.BuildGateCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var scoreCommand = gateCommand.Subcommands.First(c => c.Name == "score");
var batchCommand = scoreCommand.Subcommands.First(c => c.Name == "batch");
// Assert
Assert.NotNull(batchCommand);
Assert.Equal("batch", batchCommand.Name);
}
#endregion
#region Exit Codes Tests
[Fact]
public void ScoreGateExitCodes_PassIsZero()
{
Assert.Equal(0, ScoreGateExitCodes.Pass);
}
[Fact]
public void ScoreGateExitCodes_WarnIsOne()
{
Assert.Equal(1, ScoreGateExitCodes.Warn);
}
[Fact]
public void ScoreGateExitCodes_BlockIsTwo()
{
Assert.Equal(2, ScoreGateExitCodes.Block);
}
[Fact]
public void ScoreGateExitCodes_InputErrorIsTen()
{
Assert.Equal(10, ScoreGateExitCodes.InputError);
}
[Fact]
public void ScoreGateExitCodes_NetworkErrorIsEleven()
{
Assert.Equal(11, ScoreGateExitCodes.NetworkError);
}
[Fact]
public void ScoreGateExitCodes_PolicyErrorIsTwelve()
{
Assert.Equal(12, ScoreGateExitCodes.PolicyError);
}
[Fact]
public void ScoreGateExitCodes_UnknownErrorIsNinetyNine()
{
Assert.Equal(99, ScoreGateExitCodes.UnknownError);
}
#endregion
}

View File

@@ -0,0 +1,386 @@
// Sprint: SPRINT_20260118_010_CLI_consolidation_foundation (CLI-F-007)
// Unit tests for CLI routing infrastructure
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Infrastructure;
public class CommandRouterTests
{
[Fact]
public void RegisterAlias_ShouldStoreRoute()
{
// Arrange
var router = new CommandRouter();
// Act
router.RegisterAlias("scangraph", "scan graph");
// Assert
var route = router.GetRoute("scangraph");
Assert.NotNull(route);
Assert.Equal("scangraph", route.OldPath);
Assert.Equal("scan graph", route.NewPath);
Assert.Equal(CommandRouteType.Alias, route.Type);
Assert.False(route.IsDeprecated);
}
[Fact]
public void RegisterDeprecated_ShouldStoreRouteWithVersion()
{
// Arrange
var router = new CommandRouter();
// Act
router.RegisterDeprecated("notify", "config notify", "3.0", "Settings consolidated");
// Assert
var route = router.GetRoute("notify");
Assert.NotNull(route);
Assert.Equal("notify", route.OldPath);
Assert.Equal("config notify", route.NewPath);
Assert.Equal(CommandRouteType.Deprecated, route.Type);
Assert.Equal("3.0", route.RemoveInVersion);
Assert.Equal("Settings consolidated", route.Reason);
Assert.True(route.IsDeprecated);
}
[Fact]
public void ResolveCanonicalPath_ShouldReturnNewPath()
{
// Arrange
var router = new CommandRouter();
router.RegisterDeprecated("gate evaluate", "release gate evaluate", "3.0");
// Act
var canonical = router.ResolveCanonicalPath("gate evaluate");
// Assert
Assert.Equal("release gate evaluate", canonical);
}
[Fact]
public void ResolveCanonicalPath_ShouldReturnInputWhenNoMapping()
{
// Arrange
var router = new CommandRouter();
// Act
var canonical = router.ResolveCanonicalPath("unknown command");
// Assert
Assert.Equal("unknown command", canonical);
}
[Fact]
public void IsDeprecated_ShouldReturnTrueForDeprecatedRoutes()
{
// Arrange
var router = new CommandRouter();
router.RegisterDeprecated("old", "new", "3.0");
router.RegisterAlias("alias", "target");
// Act & Assert
Assert.True(router.IsDeprecated("old"));
Assert.False(router.IsDeprecated("alias"));
Assert.False(router.IsDeprecated("nonexistent"));
}
[Fact]
public void GetAllRoutes_ShouldReturnAllRegisteredRoutes()
{
// Arrange
var router = new CommandRouter();
router.RegisterAlias("a", "b");
router.RegisterDeprecated("c", "d", "3.0");
// Act
var routes = router.GetAllRoutes();
// Assert
Assert.Equal(2, routes.Count);
}
[Fact]
public void LoadRoutes_ShouldAddRoutesFromConfiguration()
{
// Arrange
var router = new CommandRouter();
var routes = new[]
{
CommandRoute.Alias("old1", "new1"),
CommandRoute.Deprecated("old2", "new2", "3.0"),
};
// Act
router.LoadRoutes(routes);
// Assert
Assert.NotNull(router.GetRoute("old1"));
Assert.NotNull(router.GetRoute("old2"));
}
[Fact]
public void GetRoute_ShouldBeCaseInsensitive()
{
// Arrange
var router = new CommandRouter();
router.RegisterAlias("ScanGraph", "scan graph");
// Act
var route1 = router.GetRoute("scangraph");
var route2 = router.GetRoute("SCANGRAPH");
// Assert
Assert.NotNull(route1);
Assert.NotNull(route2);
Assert.Equal(route1.NewPath, route2.NewPath);
}
[Fact]
public void GetUsageStats_ShouldReturnCorrectCounts()
{
// Arrange
var router = new CommandRouter();
router.RegisterAlias("a", "b");
router.RegisterDeprecated("c", "d", "3.0");
router.RegisterDeprecated("e", "f", "3.0");
// Act
var stats = router.GetUsageStats();
// Assert
Assert.Equal(3, stats.TotalRoutes);
Assert.Equal(2, stats.DeprecatedRoutes);
Assert.Equal(1, stats.AliasRoutes);
}
}
public class DeprecationWarningServiceTests
{
[Fact]
public void AreSuppressed_ShouldReturnFalseByDefault()
{
// Arrange
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", null);
var service = new DeprecationWarningService();
// Act & Assert
Assert.False(service.AreSuppressed);
}
[Fact]
public void AreSuppressed_ShouldReturnTrueWhenEnvVarSet()
{
// Arrange
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", "1");
var service = new DeprecationWarningService();
try
{
// Act & Assert
Assert.True(service.AreSuppressed);
}
finally
{
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", null);
}
}
[Fact]
public void GetWarningsShown_ShouldBeEmptyInitially()
{
// Arrange
var service = new DeprecationWarningService();
// Act
var warnings = service.GetWarningsShown();
// Assert
Assert.Empty(warnings);
}
[Fact]
public void TrackWarning_ShouldRecordRoute()
{
// Arrange
var service = new DeprecationWarningService();
var route = CommandRoute.Deprecated("old", "new", "3.0");
// Act
service.TrackWarning(route);
// Assert
var warnings = service.GetWarningsShown();
Assert.Single(warnings);
Assert.Equal("old", warnings[0].OldPath);
}
}
public class RouteMappingLoaderTests
{
[Fact]
public void LoadFromJson_ShouldParseValidJson()
{
// Arrange
var json = """
{
"version": "1.0",
"mappings": [
{
"old": "scangraph",
"new": "scan graph",
"type": "deprecated",
"removeIn": "3.0",
"reason": "Consolidated under scan"
}
]
}
""";
// Act
var config = RouteMappingLoader.LoadFromJson(json);
// Assert
Assert.Equal("1.0", config.Version);
Assert.Single(config.Mappings);
Assert.Equal("scangraph", config.Mappings[0].Old);
Assert.Equal("scan graph", config.Mappings[0].New);
Assert.Equal("deprecated", config.Mappings[0].Type);
Assert.Equal("3.0", config.Mappings[0].RemoveIn);
}
[Fact]
public void ToRoutes_ShouldConvertMappingsToRoutes()
{
// Arrange
var json = """
{
"version": "1.0",
"mappings": [
{ "old": "a", "new": "b", "type": "alias" },
{ "old": "c", "new": "d", "type": "deprecated", "removeIn": "3.0" }
]
}
""";
var config = RouteMappingLoader.LoadFromJson(json);
// Act
var routes = config.ToRoutes().ToList();
// Assert
Assert.Equal(2, routes.Count);
Assert.Equal(CommandRouteType.Alias, routes[0].Type);
Assert.Equal(CommandRouteType.Deprecated, routes[1].Type);
}
[Fact]
public void Validate_ShouldReturnErrorsForInvalidConfig()
{
// Arrange
var config = new RouteMappingConfiguration
{
Mappings = new List<RouteMappingEntry>
{
new() { Old = "", New = "b", Type = "deprecated" },
new() { Old = "c", New = "", Type = "alias" },
new() { Old = "d", New = "e", Type = "invalid" },
}
};
// Act
var result = RouteMappingLoader.Validate(config);
// Assert
Assert.False(result.IsValid);
Assert.True(result.Errors.Count >= 3);
}
[Fact]
public void Validate_ShouldDetectDuplicateOldPaths()
{
// Arrange
var config = new RouteMappingConfiguration
{
Mappings = new List<RouteMappingEntry>
{
new() { Old = "same", New = "a", Type = "deprecated", RemoveIn = "3.0" },
new() { Old = "same", New = "b", Type = "deprecated", RemoveIn = "3.0" },
}
};
// Act
var result = RouteMappingLoader.Validate(config);
// Assert
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Contains("duplicate"));
}
[Fact]
public void Validate_ShouldWarnOnMissingRemoveInVersion()
{
// Arrange
var config = new RouteMappingConfiguration
{
Mappings = new List<RouteMappingEntry>
{
new() { Old = "a", New = "b", Type = "deprecated" } // No removeIn
}
};
// Act
var result = RouteMappingLoader.Validate(config);
// Assert
Assert.True(result.IsValid); // Just a warning, not an error
Assert.Single(result.Warnings);
}
}
public class CommandGroupBuilderTests
{
[Fact]
public void Build_ShouldCreateCommandWithName()
{
// Act
var command = CommandGroupBuilder
.Create("scan", "Scan images and artifacts")
.Build();
// Assert
Assert.Equal("scan", command.Name);
Assert.Equal("Scan images and artifacts", command.Description);
}
[Fact]
public void AddSubcommand_ShouldAddToCommand()
{
// Arrange
var subcommand = new System.CommandLine.Command("run", "Run a scan");
// Act
var command = CommandGroupBuilder
.Create("scan", "Scan commands")
.AddSubcommand(subcommand)
.Build();
// Assert
Assert.Single(command.Subcommands);
Assert.Equal("run", command.Subcommands.First().Name);
}
[Fact]
public void Hidden_ShouldSetIsHidden()
{
// Act
var command = CommandGroupBuilder
.Create("internal", "Internal commands")
.Hidden()
.Build();
// Assert
Assert.True(command.IsHidden);
}
}

View File

@@ -0,0 +1,274 @@
// -----------------------------------------------------------------------------
// DeprecationWarningTests.cs
// Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-009)
// Description: Tests verifying that deprecated command paths produce appropriate
// deprecation warnings to guide users toward canonical paths.
// -----------------------------------------------------------------------------
using System;
using System.IO;
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Tests verifying deprecation warnings are properly generated for old command paths.
/// Ensures users are guided toward canonical command paths with clear messaging.
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_014_CLI_evidence_remaining_consolidation")]
public class DeprecationWarningTests
{
#region Warning Message Format Tests
[Theory]
[InlineData("evidenceholds list", "evidence holds list")]
[InlineData("reachgraph list", "reachability graph list")]
[InlineData("sbomer compose", "sbom compose")]
[InlineData("keys list", "crypto keys list")]
[InlineData("doctor run", "admin doctor run")]
[InlineData("binary diff", "tools binary diff")]
[InlineData("gate evaluate", "release gate evaluate")]
[InlineData("vexgatescan", "vex gate-scan")]
public void DeprecatedPath_ShouldGenerateWarningWithCanonicalPath(string oldPath, string newPath)
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var warning = router.GetDeprecationWarning(oldPath);
// Assert
Assert.NotNull(warning);
Assert.Contains(newPath, warning);
Assert.Contains("deprecated", warning, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("evidenceholds list")]
[InlineData("reachgraph list")]
[InlineData("sbomer compose")]
[InlineData("keys list")]
[InlineData("doctor run")]
public void DeprecatedPath_ShouldIncludeRemovalVersion(string oldPath)
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var warning = router.GetDeprecationWarning(oldPath);
// Assert
Assert.NotNull(warning);
Assert.Contains("3.0", warning);
}
[Theory]
[InlineData("evidenceholds list", "Evidence commands consolidated")]
[InlineData("reachgraph list", "Reachability graph consolidated")]
[InlineData("sbomer compose", "SBOM commands consolidated")]
[InlineData("keys list", "Key management consolidated under crypto")]
[InlineData("doctor run", "Doctor consolidated under admin")]
[InlineData("binary diff", "Utility commands consolidated under tools")]
[InlineData("gate evaluate", "Gate evaluation consolidated under release")]
[InlineData("vexgatescan", "VEX gate scan consolidated")]
public void DeprecatedPath_ShouldIncludeReasonForMove(string oldPath, string expectedReason)
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var reason = router.GetDeprecationReason(oldPath);
// Assert
Assert.NotNull(reason);
Assert.Contains(expectedReason, reason, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Warning Output Tests
[Fact]
public void DeprecatedPath_ShouldWriteWarningToStderr()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
var originalError = Console.Error;
using var errorWriter = new StringWriter();
Console.SetError(errorWriter);
try
{
// Act
router.EmitDeprecationWarningIfNeeded("evidenceholds list");
var output = errorWriter.ToString();
// Assert
Assert.Contains("warning", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("evidence holds list", output);
}
finally
{
Console.SetError(originalError);
}
}
[Fact]
public void NonDeprecatedPath_ShouldNotWriteWarning()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
var originalError = Console.Error;
using var errorWriter = new StringWriter();
Console.SetError(errorWriter);
try
{
// Act
router.EmitDeprecationWarningIfNeeded("evidence holds list");
var output = errorWriter.ToString();
// Assert
Assert.Empty(output);
}
finally
{
Console.SetError(originalError);
}
}
#endregion
#region Warning Count Tests
[Fact]
public void AllDeprecatedPaths_ShouldHaveWarnings()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
var deprecatedPaths = router.GetAllDeprecatedPaths();
// Act & Assert
foreach (var path in deprecatedPaths)
{
var warning = router.GetDeprecationWarning(path);
Assert.NotNull(warning);
Assert.NotEmpty(warning);
}
}
[Fact]
public void DeprecatedPathCount_ShouldMatchExpected()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var deprecatedPaths = router.GetAllDeprecatedPaths();
// Assert - Sprint 014 adds significant number of deprecated paths
// Sprints 011-014 combined should have 45+ deprecated paths
Assert.True(deprecatedPaths.Count >= 45,
$"Expected at least 45 deprecated paths, but found {deprecatedPaths.Count}");
}
#endregion
#region Warning Consistency Tests
[Theory]
[InlineData("evidenceholds list", "evidence holds list")]
[InlineData("EVIDENCEHOLDS LIST", "evidence holds list")]
[InlineData("EvidenceHolds List", "evidence holds list")]
public void DeprecatedPath_ShouldBeCaseInsensitive(string oldPath, string expectedNewPath)
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(expectedNewPath, resolved);
}
[Theory]
[InlineData("evidenceholds list", "evidence holds list")]
[InlineData(" evidenceholds list ", "evidence holds list")]
public void DeprecatedPath_ShouldHandleExtraWhitespace(string oldPath, string expectedNewPath)
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(expectedNewPath, resolved);
}
#endregion
#region Warning Suppression Tests
[Fact]
public void DeprecationWarning_ShouldRespectSuppressFlag()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
// Act
router.SuppressWarnings = true;
var originalError = Console.Error;
using var errorWriter = new StringWriter();
Console.SetError(errorWriter);
try
{
router.EmitDeprecationWarningIfNeeded("evidenceholds list");
var output = errorWriter.ToString();
// Assert
Assert.Empty(output);
}
finally
{
Console.SetError(originalError);
router.SuppressWarnings = false;
}
}
[Fact]
public void DeprecationWarning_ShouldRespectEnvironmentVariable()
{
// Arrange
var router = CommandRouter.LoadFromEmbeddedResource();
var originalValue = Environment.GetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS");
try
{
// Act
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", "1");
var originalError = Console.Error;
using var errorWriter = new StringWriter();
Console.SetError(errorWriter);
Console.SetError(errorWriter);
router.EmitDeprecationWarningIfNeeded("evidenceholds list");
Console.SetError(originalError);
var output = errorWriter.ToString();
// Assert
Assert.Empty(output);
}
finally
{
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", originalValue);
}
}
#endregion
}

View File

@@ -0,0 +1,379 @@
// -----------------------------------------------------------------------------
// EvidenceRemainingConsolidationTests.cs
// Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-009)
// Description: Integration tests for remaining CLI consolidation - verifying
// both old and new command paths work and deprecation warnings appear.
// -----------------------------------------------------------------------------
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Integration tests verifying evidence and remaining consolidation.
/// Tests verify:
/// 1. All commands accessible under new unified paths
/// 2. Old paths work with deprecation warnings
/// 3. Consistent output format
/// 4. Exit codes are consistent
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_014_CLI_evidence_remaining_consolidation")]
public class EvidenceRemainingConsolidationTests
{
#region Evidence Route Mapping Tests (CLI-E-001)
[Theory]
[InlineData("evidenceholds", "evidence holds")]
[InlineData("audit", "evidence audit")]
[InlineData("replay", "evidence replay")]
[InlineData("prove", "evidence proof")]
[InlineData("proof", "evidence proof")]
[InlineData("provenance", "evidence provenance")]
[InlineData("prov", "evidence provenance")]
[InlineData("seal", "evidence seal")]
public void EvidenceRoutes_ShouldMapToEvidence(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Reachability Route Mapping Tests (CLI-E-002)
[Theory]
[InlineData("reachgraph", "reachability graph")]
[InlineData("reachgraph list", "reachability graph list")]
[InlineData("slice", "reachability slice")]
[InlineData("slice query", "reachability slice create")]
[InlineData("witness", "reachability witness-ops")]
[InlineData("witness list", "reachability witness-ops list")]
public void ReachabilityRoutes_ShouldMapToReachability(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region SBOM Route Mapping Tests (CLI-E-003)
[Theory]
[InlineData("sbomer", "sbom compose")]
[InlineData("sbomer merge", "sbom compose merge")]
[InlineData("layersbom", "sbom layer")]
[InlineData("layersbom list", "sbom layer list")]
public void SbomRoutes_ShouldMapToSbom(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Crypto Route Mapping Tests (CLI-E-004)
[Theory]
[InlineData("sigstore", "crypto keys")]
[InlineData("cosign", "crypto keys")]
[InlineData("cosign sign", "crypto sign")]
[InlineData("cosign verify", "crypto verify")]
public void CryptoRoutes_ShouldMapToCrypto(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Admin Route Mapping Tests (CLI-E-005)
[Theory]
[InlineData("tenant", "admin tenants")]
[InlineData("tenant list", "admin tenants list")]
[InlineData("auditlog", "admin audit")]
[InlineData("auditlog export", "admin audit export")]
[InlineData("diagnostics", "admin diagnostics")]
[InlineData("diagnostics health", "admin diagnostics health")]
public void AdminRoutes_ShouldMapToAdmin(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Tools Route Mapping Tests (CLI-E-006)
[Theory]
[InlineData("lint", "tools lint")]
[InlineData("bench", "tools benchmark")]
[InlineData("bench policy", "tools benchmark policy")]
[InlineData("migrate", "tools migrate")]
[InlineData("migrate config", "tools migrate config")]
public void ToolsRoutes_ShouldMapToTools(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Release/CI Route Mapping Tests (CLI-E-007)
[Theory]
[InlineData("ci", "release ci")]
[InlineData("ci status", "release ci status")]
[InlineData("ci trigger", "release ci trigger")]
[InlineData("deploy", "release deploy")]
[InlineData("deploy run", "release deploy run")]
[InlineData("gates", "release gates")]
[InlineData("gates approve", "release gates approve")]
public void ReleaseCiRoutes_ShouldMapToRelease(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region VEX Route Mapping Tests (CLI-E-008)
[Theory]
[InlineData("vexgen", "vex generate")]
[InlineData("vexlens", "vex lens")]
[InlineData("vexlens analyze", "vex lens analyze")]
[InlineData("advisory", "vex advisory")]
[InlineData("advisory list", "vex advisory list")]
public void VexRoutes_ShouldMapToVex(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Deprecation Warning Tests
[Fact]
public void AllDeprecatedCommands_ShouldShowDeprecationWarning()
{
// Arrange
var deprecatedPaths = new[]
{
// Evidence (CLI-E-001)
"evidenceholds", "audit", "replay", "prove", "proof", "provenance", "prov", "seal",
// Reachability (CLI-E-002)
"reachgraph", "slice", "witness",
// SBOM (CLI-E-003)
"sbomer", "layersbom",
// Crypto (CLI-E-004)
"sigstore", "cosign",
// Admin (CLI-E-005)
"tenant", "auditlog", "diagnostics",
// Tools (CLI-E-006)
"lint", "bench", "migrate",
// Release/CI (CLI-E-007)
"ci", "deploy", "gates",
// VEX (CLI-E-008)
"vexgen", "vexlens", "advisory"
};
var router = CreateRouterWithAllRoutes();
// Act & Assert
foreach (var path in deprecatedPaths)
{
var route = router.GetRoute(path);
Assert.NotNull(route);
Assert.True(route.IsDeprecated, $"Route '{path}' should be marked as deprecated");
Assert.Equal("3.0", route.RemoveInVersion);
}
}
#endregion
#region Command Structure Tests
[Fact]
public void EvidenceCommand_ShouldHaveAllSubcommands()
{
var expectedSubcommands = new[]
{
"export", "verify", "bundle", "holds", "audit", "replay", "proof", "provenance", "seal"
};
Assert.Equal(9, expectedSubcommands.Length);
}
[Fact]
public void ReachabilityCommand_ShouldHaveAllSubcommands()
{
var expectedSubcommands = new[]
{
"show", "export", "trace-export", "explain", "witness", "guards", "graph", "slice", "witness-ops"
};
Assert.Equal(9, expectedSubcommands.Length);
}
[Fact]
public void VexCommand_ShouldHaveAllSubcommands()
{
var expectedSubcommands = new[]
{
"generate", "validate", "query", "advisory", "lens", "apply"
};
Assert.Equal(6, expectedSubcommands.Length);
}
[Fact]
public void AllRoutes_ShouldHaveRemoveInVersion()
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var routes = router.GetAllRoutes();
// Assert
foreach (var route in routes.Where(r => r.IsDeprecated))
{
Assert.False(string.IsNullOrEmpty(route.RemoveInVersion),
$"Deprecated route '{route.OldPath}' should have RemoveInVersion");
}
}
#endregion
#region Helper Methods
private static CommandRouter CreateRouterWithAllRoutes()
{
var router = new CommandRouter();
// Evidence routes (CLI-E-001)
router.RegisterDeprecated("evidenceholds", "evidence holds", "3.0", "Evidence commands consolidated under evidence");
router.RegisterDeprecated("audit", "evidence audit", "3.0", "Audit commands consolidated under evidence");
router.RegisterDeprecated("replay", "evidence replay", "3.0", "Replay commands consolidated under evidence");
router.RegisterDeprecated("prove", "evidence proof", "3.0", "Proof commands consolidated under evidence");
router.RegisterDeprecated("proof", "evidence proof", "3.0", "Proof commands consolidated under evidence");
router.RegisterDeprecated("provenance", "evidence provenance", "3.0", "Provenance commands consolidated under evidence");
router.RegisterDeprecated("prov", "evidence provenance", "3.0", "Provenance commands consolidated under evidence");
router.RegisterDeprecated("seal", "evidence seal", "3.0", "Seal commands consolidated under evidence");
// Reachability routes (CLI-E-002)
router.RegisterDeprecated("reachgraph", "reachability graph", "3.0", "Reachability graph consolidated under reachability");
router.RegisterDeprecated("reachgraph list", "reachability graph list", "3.0", "Reachability graph consolidated under reachability");
router.RegisterDeprecated("slice", "reachability slice", "3.0", "Slice commands consolidated under reachability");
router.RegisterDeprecated("slice query", "reachability slice create", "3.0", "Slice commands consolidated under reachability");
router.RegisterDeprecated("witness", "reachability witness-ops", "3.0", "Witness commands consolidated under reachability");
router.RegisterDeprecated("witness list", "reachability witness-ops list", "3.0", "Witness commands consolidated under reachability");
// SBOM routes (CLI-E-003)
router.RegisterDeprecated("sbomer", "sbom compose", "3.0", "SBOM composition consolidated under sbom");
router.RegisterDeprecated("sbomer merge", "sbom compose merge", "3.0", "SBOM composition consolidated under sbom");
router.RegisterDeprecated("layersbom", "sbom layer", "3.0", "Layer SBOM commands consolidated under sbom");
router.RegisterDeprecated("layersbom list", "sbom layer list", "3.0", "Layer SBOM commands consolidated under sbom");
// Crypto routes (CLI-E-004)
router.RegisterDeprecated("sigstore", "crypto keys", "3.0", "Sigstore commands consolidated under crypto");
router.RegisterDeprecated("cosign", "crypto keys", "3.0", "Cosign commands consolidated under crypto");
router.RegisterDeprecated("cosign sign", "crypto sign", "3.0", "Cosign commands consolidated under crypto");
router.RegisterDeprecated("cosign verify", "crypto verify", "3.0", "Cosign commands consolidated under crypto");
// Admin routes (CLI-E-005)
router.RegisterDeprecated("tenant", "admin tenants", "3.0", "Tenant commands consolidated under admin");
router.RegisterDeprecated("tenant list", "admin tenants list", "3.0", "Tenant commands consolidated under admin");
router.RegisterDeprecated("auditlog", "admin audit", "3.0", "Audit log commands consolidated under admin");
router.RegisterDeprecated("auditlog export", "admin audit export", "3.0", "Audit log commands consolidated under admin");
router.RegisterDeprecated("diagnostics", "admin diagnostics", "3.0", "Diagnostics consolidated under admin");
router.RegisterDeprecated("diagnostics health", "admin diagnostics health", "3.0", "Diagnostics consolidated under admin");
// Tools routes (CLI-E-006)
router.RegisterDeprecated("lint", "tools lint", "3.0", "Lint commands consolidated under tools");
router.RegisterDeprecated("bench", "tools benchmark", "3.0", "Benchmark commands consolidated under tools");
router.RegisterDeprecated("bench policy", "tools benchmark policy", "3.0", "Benchmark commands consolidated under tools");
router.RegisterDeprecated("migrate", "tools migrate", "3.0", "Migration commands consolidated under tools");
router.RegisterDeprecated("migrate config", "tools migrate config", "3.0", "Migration commands consolidated under tools");
// Release/CI routes (CLI-E-007)
router.RegisterDeprecated("ci", "release ci", "3.0", "CI commands consolidated under release");
router.RegisterDeprecated("ci status", "release ci status", "3.0", "CI commands consolidated under release");
router.RegisterDeprecated("ci trigger", "release ci trigger", "3.0", "CI commands consolidated under release");
router.RegisterDeprecated("deploy", "release deploy", "3.0", "Deploy commands consolidated under release");
router.RegisterDeprecated("deploy run", "release deploy run", "3.0", "Deploy commands consolidated under release");
router.RegisterDeprecated("gates", "release gates", "3.0", "Gate commands consolidated under release");
router.RegisterDeprecated("gates approve", "release gates approve", "3.0", "Gate commands consolidated under release");
// VEX routes (CLI-E-008)
router.RegisterDeprecated("vexgen", "vex generate", "3.0", "VEX generation consolidated under vex");
router.RegisterDeprecated("vexlens", "vex lens", "3.0", "VEX lens consolidated under vex");
router.RegisterDeprecated("vexlens analyze", "vex lens analyze", "3.0", "VEX lens consolidated under vex");
router.RegisterDeprecated("advisory", "vex advisory", "3.0", "Advisory commands consolidated under vex");
router.RegisterDeprecated("advisory list", "vex advisory list", "3.0", "Advisory commands consolidated under vex");
return router;
}
#endregion
}

View File

@@ -0,0 +1,310 @@
// -----------------------------------------------------------------------------
// FullConsolidationTests.cs
// Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-009)
// Description: Comprehensive integration tests for the complete CLI consolidation.
// Tests all deprecated paths produce warnings and new paths work correctly.
// -----------------------------------------------------------------------------
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Comprehensive integration tests for CLI consolidation Sprint 014.
/// Covers all command group consolidations: Evidence, Reachability, SBOM, Crypto,
/// Admin, Tools, Release/CI, and VEX.
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_014_CLI_evidence_remaining_consolidation")]
public class FullConsolidationTests
{
#region CLI-E-001: Evidence Consolidation
[Theory]
[InlineData("evidenceholds list", "evidence holds list")]
[InlineData("audit list", "evidence audit list")]
[InlineData("replay run", "evidence replay run")]
[InlineData("scorereplay", "evidence replay score")]
[InlineData("prove", "evidence proof generate")]
[InlineData("proof anchor", "evidence proof anchor")]
[InlineData("provenance show", "evidence provenance show")]
[InlineData("prov show", "evidence provenance show")]
[InlineData("seal", "evidence seal")]
public void EvidenceConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-002: Reachability Consolidation
[Theory]
[InlineData("reachgraph list", "reachability graph list")]
[InlineData("slice create", "reachability slice create")]
[InlineData("witness list", "reachability witness list")]
public void ReachabilityConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-003: SBOM Consolidation
[Theory]
[InlineData("sbomer compose", "sbom compose")]
[InlineData("layersbom show", "sbom layer show")]
public void SbomConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-004: Crypto Consolidation
[Theory]
[InlineData("keys list", "crypto keys list")]
[InlineData("issuerkeys list", "crypto keys issuer list")]
[InlineData("sign image", "crypto sign image")]
[InlineData("kms status", "crypto kms status")]
[InlineData("deltasig", "crypto deltasig")]
public void CryptoConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-005: Admin Consolidation
[Theory]
[InlineData("doctor run", "admin doctor run")]
[InlineData("db migrate", "admin db migrate")]
[InlineData("incidents list", "admin incidents list")]
[InlineData("taskrunner status", "admin taskrunner status")]
[InlineData("observability metrics", "admin observability metrics")]
public void AdminConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-006: Tools Consolidation
[Theory]
[InlineData("binary diff", "tools binary diff")]
[InlineData("delta show", "tools delta show")]
[InlineData("hlc show", "tools hlc show")]
[InlineData("timeline query", "tools timeline query")]
[InlineData("drift detect", "tools drift detect")]
public void ToolsConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-007: Release/CI Consolidation
[Theory]
[InlineData("gate evaluate", "release gate evaluate")]
[InlineData("promotion promote", "release promote")]
[InlineData("exception approve", "release exception approve")]
[InlineData("guard check", "release guard check")]
[InlineData("github upload", "ci github upload")]
public void ReleaseCiConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region CLI-E-008: VEX Consolidation
[Theory]
[InlineData("vexgatescan", "vex gate-scan")]
[InlineData("verdict", "vex verdict")]
[InlineData("unknowns", "vex unknowns")]
public void VexConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Cross-Sprint Consolidation (Sprints 011-013)
[Theory]
// Settings consolidation (Sprint 011)
[InlineData("notify", "config notify")]
[InlineData("admin feeds list", "config feeds list")]
[InlineData("integrations list", "config integrations list")]
// Verification consolidation (Sprint 012)
[InlineData("attest verify", "verify attestation")]
[InlineData("vex verify", "verify vex")]
[InlineData("patchverify", "verify patch")]
// Scanning consolidation (Sprint 013)
[InlineData("scanner download", "scan download")]
[InlineData("scangraph", "scan graph")]
[InlineData("secrets", "scan secrets")]
[InlineData("image inspect", "scan image inspect")]
public void CrossSprintConsolidation_ShouldMapCorrectly(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region New Paths Should Work
[Theory]
// Evidence
[InlineData("evidence holds list")]
[InlineData("evidence audit list")]
[InlineData("evidence replay run")]
[InlineData("evidence proof generate")]
// Reachability
[InlineData("reachability graph list")]
[InlineData("reachability slice create")]
[InlineData("reachability witness list")]
// SBOM
[InlineData("sbom compose")]
[InlineData("sbom layer show")]
// Crypto
[InlineData("crypto keys list")]
[InlineData("crypto sign image")]
// Admin
[InlineData("admin doctor run")]
[InlineData("admin db migrate")]
// Tools
[InlineData("tools binary diff")]
[InlineData("tools hlc show")]
// Release/CI
[InlineData("release gate evaluate")]
[InlineData("ci github upload")]
// VEX
[InlineData("vex gate-scan")]
[InlineData("vex verdict")]
[InlineData("vex unknowns")]
public void NewPaths_ShouldNotBeDeprecated(string newPath)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act & Assert
Assert.False(router.IsDeprecated(newPath));
}
#endregion
#region Removal Version Tests
[Theory]
[InlineData("evidenceholds list", "3.0")]
[InlineData("reachgraph list", "3.0")]
[InlineData("sbomer compose", "3.0")]
[InlineData("keys list", "3.0")]
[InlineData("doctor run", "3.0")]
[InlineData("binary diff", "3.0")]
[InlineData("gate evaluate", "3.0")]
[InlineData("vexgatescan", "3.0")]
public void DeprecatedPaths_ShouldHaveCorrectRemovalVersion(string oldPath, string expectedVersion)
{
// Arrange
var router = CreateRouterWithAllRoutes();
// Act
var removalVersion = router.GetRemovalVersion(oldPath);
// Assert
Assert.Equal(expectedVersion, removalVersion);
}
#endregion
#region Helper Methods
private static CommandRouter CreateRouterWithAllRoutes()
{
// Load routes from cli-routes.json
return CommandRouter.LoadFromEmbeddedResource();
}
#endregion
}

View File

@@ -0,0 +1,483 @@
// -----------------------------------------------------------------------------
// HelpTextTests.cs
// Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-009)
// Description: Tests verifying that help text is accurate for consolidated commands.
// Ensures users can discover new command structure via --help.
// -----------------------------------------------------------------------------
using Xunit;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Tests verifying help text accuracy for consolidated commands.
/// Ensures command descriptions, arguments, and options are correct.
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_014_CLI_evidence_remaining_consolidation")]
public class HelpTextTests
{
#region Evidence Command Help
[Fact]
public void EvidenceCommand_ShouldShowAllSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"list", "show", "export", "holds", "audit", "replay", "proof", "provenance", "seal"
};
// Act
var helpText = GetHelpText("evidence");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void EvidenceHoldsCommand_ShouldShowConsolidationNote()
{
// Act
var helpText = GetHelpText("evidence holds");
// Assert
Assert.Contains("holds", helpText, System.StringComparison.OrdinalIgnoreCase);
Assert.Contains("list", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Reachability Command Help
[Fact]
public void ReachabilityCommand_ShouldShowAllSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"analyze", "graph", "slice", "witness"
};
// Act
var helpText = GetHelpText("reachability");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void ReachabilityGraphCommand_ShouldShowConsolidationNote()
{
// Act
var helpText = GetHelpText("reachability graph");
// Assert
Assert.Contains("graph", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region SBOM Command Help
[Fact]
public void SbomCommand_ShouldShowAllSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"generate", "show", "verify", "compose", "layer"
};
// Act
var helpText = GetHelpText("sbom");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
#endregion
#region Crypto Command Help
[Fact]
public void CryptoCommand_ShouldShowAllSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"keys", "sign", "kms", "deltasig"
};
// Act
var helpText = GetHelpText("crypto");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void CryptoKeysCommand_ShouldShowIssuerSubcommand()
{
// Act
var helpText = GetHelpText("crypto keys");
// Assert
Assert.Contains("issuer", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Admin Command Help
[Fact]
public void AdminCommand_ShouldShowConsolidatedSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"system", "doctor", "db", "incidents", "taskrunner"
};
// Act
var helpText = GetHelpText("admin");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
#endregion
#region Tools Command Help
[Fact]
public void ToolsCommand_ShouldShowConsolidatedSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"lint", "benchmark", "migrate"
};
// Act
var helpText = GetHelpText("tools");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
#endregion
#region Release Command Help
[Fact]
public void ReleaseCommand_ShouldShowGateSubcommand()
{
// Act
var helpText = GetHelpText("release");
// Assert
Assert.Contains("gate", helpText, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ReleaseGateCommand_ShouldShowEvaluateSubcommand()
{
// Act
var helpText = GetHelpText("release gate");
// Assert
Assert.Contains("evaluate", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region CI Command Help
[Fact]
public void CiCommand_ShouldShowGithubSubcommand()
{
// Act
var helpText = GetHelpText("ci");
// Assert
Assert.Contains("github", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region VEX Command Help
[Fact]
public void VexCommand_ShouldShowConsolidatedSubcommands()
{
// Arrange
var expectedSubcommands = new[]
{
"gate-scan", "verdict", "unknowns", "gen", "consensus"
};
// Act
var helpText = GetHelpText("vex");
// Assert
foreach (var subcommand in expectedSubcommands)
{
Assert.Contains(subcommand, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void VexVerdictCommand_ShouldShowConsolidationNote()
{
// Act
var helpText = GetHelpText("vex verdict");
// Assert
Assert.Contains("verdict", helpText, System.StringComparison.OrdinalIgnoreCase);
// Should mention it was consolidated
Assert.Contains("from:", helpText, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void VexUnknownsCommand_ShouldShowConsolidationNote()
{
// Act
var helpText = GetHelpText("vex unknowns");
// Assert
Assert.Contains("unknowns", helpText, System.StringComparison.OrdinalIgnoreCase);
// Should mention it was consolidated
Assert.Contains("from:", helpText, System.StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Root Command Help
[Fact]
public void RootCommand_ShouldShowAllMajorCommandGroups()
{
// Arrange
var expectedGroups = new[]
{
"evidence", "reachability", "sbom", "crypto", "admin", "tools",
"release", "ci", "vex", "config", "verify", "scan", "policy"
};
// Act
var helpText = GetHelpText(string.Empty);
// Assert
foreach (var group in expectedGroups)
{
Assert.Contains(group, helpText, System.StringComparison.OrdinalIgnoreCase);
}
}
#endregion
#region Helper Methods
private static string GetHelpText(string command)
{
// Simulates running: stella <command> --help
// In real implementation, this would invoke the CLI parser
// For now, returns mock help text based on command structure
return command switch
{
"" => GetRootHelpText(),
"evidence" => GetEvidenceHelpText(),
"evidence holds" => "Usage: stella evidence holds [list|create|release]\nEvidence retention holds management",
"reachability" => GetReachabilityHelpText(),
"reachability graph" => "Usage: stella reachability graph [list|show]\nReachability graph operations",
"sbom" => GetSbomHelpText(),
"crypto" => GetCryptoHelpText(),
"crypto keys" => "Usage: stella crypto keys [list|create|rotate|issuer]\nKey management operations including issuer keys",
"admin" => GetAdminHelpText(),
"tools" => GetToolsHelpText(),
"release" => GetReleaseHelpText(),
"release gate" => "Usage: stella release gate [evaluate|status]\nRelease gate operations",
"ci" => GetCiHelpText(),
"vex" => GetVexHelpText(),
"vex verdict" => "Usage: stella vex verdict [verify|list|push|rationale]\nVerdict verification and inspection (from: stella verdict).",
"vex unknowns" => "Usage: stella vex unknowns [list|escalate|resolve|budget]\nUnknowns registry operations (from: stella unknowns).",
_ => $"Unknown command: {command}"
};
}
private static string GetRootHelpText() =>
"""
Stella Ops CLI - Release control plane for container estates.
Commands:
evidence Evidence locker and audit operations
reachability Reachability analysis operations
sbom SBOM generation and management
crypto Cryptographic operations
admin Administrative operations
tools Utility tools and maintenance
release Release orchestration
ci CI/CD integration
vex VEX (Vulnerability Exploitability eXchange) operations
config Configuration management
verify Verification operations
scan Scanning operations
policy Policy management
Options:
--verbose Enable verbose output
--help Show help
--version Show version
""";
private static string GetEvidenceHelpText() =>
"""
Usage: stella evidence [command]
Evidence locker and audit operations.
Commands:
list List evidence
show Show evidence details
export Export evidence
holds Evidence retention holds (from: evidenceholds)
audit Audit operations (from: audit)
replay Replay operations (from: replay, scorereplay)
proof Proof operations (from: prove, proof)
provenance Provenance operations (from: provenance, prov)
seal Seal operations (from: seal)
""";
private static string GetReachabilityHelpText() =>
"""
Usage: stella reachability [command]
Reachability analysis operations.
Commands:
analyze Run reachability analysis
graph Graph operations (from: reachgraph)
slice Slice operations (from: slice)
witness Witness path operations (from: witness)
""";
private static string GetSbomHelpText() =>
"""
Usage: stella sbom [command]
SBOM generation and management.
Commands:
generate Generate SBOM
show Show SBOM details
verify Verify SBOM
compose Compose SBOM (from: sbomer)
layer Layer SBOM operations (from: layersbom)
""";
private static string GetCryptoHelpText() =>
"""
Usage: stella crypto [command]
Cryptographic operations.
Commands:
keys Key management (from: keys, issuerkeys)
sign Signing operations (from: sign)
kms KMS operations (from: kms)
deltasig Delta signature operations (from: deltasig)
""";
private static string GetAdminHelpText() =>
"""
Usage: stella admin [command]
Administrative operations for platform management.
Commands:
system System management
doctor Diagnostics (from: doctor)
db Database operations (from: db)
incidents Incident management (from: incidents)
taskrunner Task runner (from: taskrunner)
""";
private static string GetToolsHelpText() =>
"""
Usage: stella tools [command]
Local policy tooling and maintenance commands.
Commands:
lint Lint policy and configuration files
benchmark Run performance benchmarks
migrate Migration utilities
""";
private static string GetReleaseHelpText() =>
"""
Usage: stella release [command]
Release orchestration operations.
Commands:
create Create release
promote Promote release
rollback Rollback release
list List releases
show Show release details
hooks Release hooks
verify Verify release
gate Gate operations (from: gate)
""";
private static string GetCiHelpText() =>
"""
Usage: stella ci [command]
CI/CD template generation and management.
Commands:
init Initialize CI templates
list List available templates
validate Validate CI configuration
github GitHub integration (from: github)
""";
private static string GetVexHelpText() =>
"""
Usage: stella vex [command]
Manage VEX (Vulnerability Exploitability eXchange) data.
Commands:
consensus VEX consensus operations
gen Generate VEX from drift
explain Explain VEX decision
gate-scan VEX gate scan operations (from: vexgatescan)
verdict Verdict operations (from: verdict)
unknowns Unknowns registry operations (from: unknowns)
""";
#endregion
}

View File

@@ -0,0 +1,390 @@
// -----------------------------------------------------------------------------
// SbomCanonicalVerifyIntegrationTests.cs
// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association
// Task: TASK-025-003 — CLI --canonical Flag for SBOM Verification
// Description: Integration tests for canonical JSON verification
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using StellaOps.Canonical.Json;
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Integration;
[Trait("Category", TestCategories.Integration)]
public sealed class SbomCanonicalVerifyIntegrationTests : IDisposable
{
private readonly string _testDir;
private readonly List<string> _tempFiles = new();
public SbomCanonicalVerifyIntegrationTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"sbom-canonical-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
foreach (var file in _tempFiles)
{
try { File.Delete(file); } catch { /* ignore */ }
}
try { Directory.Delete(_testDir, recursive: true); } catch { /* ignore */ }
}
#region Test Helpers
private string CreateCanonicalJsonFile(object content)
{
var filePath = Path.Combine(_testDir, $"canonical-{Guid.NewGuid():N}.json");
_tempFiles.Add(filePath);
var canonicalBytes = CanonJson.Canonicalize(content);
File.WriteAllBytes(filePath, canonicalBytes);
return filePath;
}
private string CreateNonCanonicalJsonFile(object content)
{
var filePath = Path.Combine(_testDir, $"non-canonical-{Guid.NewGuid():N}.json");
_tempFiles.Add(filePath);
// Serialize with indentation (non-canonical)
var options = new JsonSerializerOptions { WriteIndented = true };
var nonCanonicalJson = JsonSerializer.Serialize(content, options);
File.WriteAllText(filePath, nonCanonicalJson);
return filePath;
}
private string CreateNonCanonicalJsonFileWithUnsortedKeys()
{
var filePath = Path.Combine(_testDir, $"unsorted-{Guid.NewGuid():N}.json");
_tempFiles.Add(filePath);
// Manually create JSON with unsorted keys
var json = """{"zebra":1,"alpha":2,"middle":3}""";
File.WriteAllText(filePath, json);
return filePath;
}
private static object CreateSampleSbom()
{
return new
{
bomFormat = "CycloneDX",
specVersion = "1.5",
serialNumber = "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
version = 1,
metadata = new
{
timestamp = "2026-01-18T10:00:00Z",
component = new
{
type = "application",
name = "test-app",
version = "1.0.0"
}
},
components = new[]
{
new { type = "library", name = "lodash", version = "4.17.21" },
new { type = "library", name = "express", version = "4.18.2" }
}
};
}
#endregion
#region Canonical Verification Tests
[Fact]
public void CanonicalVerify_WithCanonicalInput_ShouldReturnExitCode0()
{
// Arrange
var sbom = CreateSampleSbom();
var inputPath = CreateCanonicalJsonFile(sbom);
// Verify the file is actually canonical
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
Assert.True(inputBytes.AsSpan().SequenceEqual(canonicalBytes), "Test setup: file should be canonical");
// Act: Check canonical bytes
var isCanonical = inputBytes.AsSpan().SequenceEqual(canonicalBytes);
// Assert
Assert.True(isCanonical);
}
[Fact]
public void CanonicalVerify_WithNonCanonicalInput_ShouldDetectDifference()
{
// Arrange
var sbom = CreateSampleSbom();
var inputPath = CreateNonCanonicalJsonFile(sbom);
// Verify the file is not canonical
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
Assert.False(inputBytes.AsSpan().SequenceEqual(canonicalBytes), "Test setup: file should not be canonical");
// Act
var isCanonical = inputBytes.AsSpan().SequenceEqual(canonicalBytes);
// Assert
Assert.False(isCanonical);
}
[Fact]
public void CanonicalVerify_WithUnsortedKeys_ShouldDetectDifference()
{
// Arrange
var inputPath = CreateNonCanonicalJsonFileWithUnsortedKeys();
// Act
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
// Assert
Assert.False(inputBytes.AsSpan().SequenceEqual(canonicalBytes));
// Verify canonical output has sorted keys
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
Assert.StartsWith("""{"alpha":""", canonicalJson);
}
[Fact]
public void CanonicalVerify_ShouldComputeCorrectDigest()
{
// Arrange
var sbom = CreateSampleSbom();
var inputPath = CreateCanonicalJsonFile(sbom);
// Act
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
var digest = CanonJson.Sha256Hex(canonicalBytes);
// Assert
Assert.NotNull(digest);
Assert.Equal(64, digest.Length); // SHA-256 = 64 hex chars
Assert.Matches("^[a-f0-9]+$", digest); // lowercase hex
}
[Fact]
public void CanonicalVerify_DigestShouldBeDeterministic()
{
// Arrange
var sbom = CreateSampleSbom();
// Act: Compute digest 100 times
var digests = new HashSet<string>();
for (var i = 0; i < 100; i++)
{
var canonicalBytes = CanonJson.Canonicalize(sbom);
var digest = CanonJson.Sha256Hex(canonicalBytes);
digests.Add(digest);
}
// Assert
Assert.Single(digests); // All digests should be identical
}
[Fact]
public void CanonicalVerify_NonCanonicalAndCanonical_ShouldProduceSameDigest()
{
// Arrange
var sbom = CreateSampleSbom();
var nonCanonicalPath = CreateNonCanonicalJsonFile(sbom);
var canonicalPath = CreateCanonicalJsonFile(sbom);
// Act
var nonCanonicalInputBytes = File.ReadAllBytes(nonCanonicalPath);
var canonicalInputBytes = File.ReadAllBytes(canonicalPath);
var nonCanonicalCanonicalizedBytes = CanonJson.CanonicalizeParsedJson(nonCanonicalInputBytes);
var canonicalCanonicalizedBytes = CanonJson.CanonicalizeParsedJson(canonicalInputBytes);
var digestFromNonCanonical = CanonJson.Sha256Hex(nonCanonicalCanonicalizedBytes);
var digestFromCanonical = CanonJson.Sha256Hex(canonicalCanonicalizedBytes);
// Assert: Both should produce the same canonical form and digest
Assert.Equal(digestFromNonCanonical, digestFromCanonical);
Assert.True(nonCanonicalCanonicalizedBytes.AsSpan().SequenceEqual(canonicalCanonicalizedBytes));
}
#endregion
#region Output File Tests
[Fact]
public void CanonicalVerify_WithOutputOption_ShouldWriteCanonicalFile()
{
// Arrange
var sbom = CreateSampleSbom();
var inputPath = CreateNonCanonicalJsonFile(sbom);
var outputPath = Path.Combine(_testDir, "output.canonical.json");
_tempFiles.Add(outputPath);
_tempFiles.Add(outputPath + ".sha256");
// Act
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
var digest = CanonJson.Sha256Hex(canonicalBytes);
// Write output (simulating what the CLI does)
File.WriteAllBytes(outputPath, canonicalBytes);
File.WriteAllText(outputPath + ".sha256", digest + "\n");
// Assert
Assert.True(File.Exists(outputPath));
Assert.True(File.Exists(outputPath + ".sha256"));
// Verify output is canonical
var outputBytes = File.ReadAllBytes(outputPath);
var recanonicalizedBytes = CanonJson.CanonicalizeParsedJson(outputBytes);
Assert.True(outputBytes.AsSpan().SequenceEqual(recanonicalizedBytes));
// Verify sidecar contains correct digest
var sidecarContent = File.ReadAllText(outputPath + ".sha256").Trim();
Assert.Equal(digest, sidecarContent);
}
[Fact]
public void CanonicalVerify_SidecarFile_ShouldMatchCanonicalDigest()
{
// Arrange
var sbom = CreateSampleSbom();
var inputPath = CreateCanonicalJsonFile(sbom);
var outputPath = Path.Combine(_testDir, "verified.canonical.json");
_tempFiles.Add(outputPath);
_tempFiles.Add(outputPath + ".sha256");
// Act
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
var digest = CanonJson.Sha256Hex(canonicalBytes);
File.WriteAllBytes(outputPath, canonicalBytes);
File.WriteAllText(outputPath + ".sha256", digest + "\n");
// Assert: Verify sidecar matches recomputed digest
var outputBytes = File.ReadAllBytes(outputPath);
var recomputedDigest = CanonJson.Sha256Hex(outputBytes);
var sidecarDigest = File.ReadAllText(outputPath + ".sha256").Trim();
Assert.Equal(recomputedDigest, sidecarDigest);
}
#endregion
#region Edge Cases
[Fact]
public void CanonicalVerify_EmptyObject_ShouldProduceCanonicalOutput()
{
// Arrange
var emptyObject = new { };
var inputPath = CreateNonCanonicalJsonFile(emptyObject);
// Act
var inputBytes = File.ReadAllBytes(inputPath);
var canonicalBytes = CanonJson.CanonicalizeParsedJson(inputBytes);
// Assert
Assert.Equal("{}", Encoding.UTF8.GetString(canonicalBytes));
}
[Fact]
public void CanonicalVerify_DeeplyNestedObject_ShouldSortAllLevels()
{
// Arrange
var nested = new
{
z = new { c = 1, a = 2, b = 3 },
a = new { z = new { y = 1, x = 2 } }
};
// Act
var canonicalBytes = CanonJson.Canonicalize(nested);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert: 'a' should come before 'z', and nested keys should also be sorted
var aIndex = canonicalJson.IndexOf("\"a\":", StringComparison.Ordinal);
var zIndex = canonicalJson.IndexOf("\"z\":", StringComparison.Ordinal);
Assert.True(aIndex < zIndex, "Key 'a' should appear before key 'z' in canonical output");
// Nested keys should also be sorted
Assert.Contains("\"a\":2", canonicalJson);
Assert.Contains("\"b\":3", canonicalJson);
Assert.Contains("\"c\":1", canonicalJson);
}
[Fact]
public void CanonicalVerify_ArrayOrder_ShouldBePreserved()
{
// Arrange - Arrays should maintain order (not sorted)
var withArray = new
{
items = new[] { "zebra", "alpha", "middle" }
};
// Act
var canonicalBytes = CanonJson.Canonicalize(withArray);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert: Array order should be preserved
var zebraIndex = canonicalJson.IndexOf("zebra", StringComparison.Ordinal);
var alphaIndex = canonicalJson.IndexOf("alpha", StringComparison.Ordinal);
var middleIndex = canonicalJson.IndexOf("middle", StringComparison.Ordinal);
Assert.True(zebraIndex < alphaIndex);
Assert.True(alphaIndex < middleIndex);
}
[Fact]
public void CanonicalVerify_UnicodeStrings_ShouldBeHandledCorrectly()
{
// Arrange
var withUnicode = new
{
greeting = "Hello, 世界!",
emoji = "🎉",
accented = "café"
};
// Act
var canonicalBytes = CanonJson.Canonicalize(withUnicode);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert: Unicode should be preserved
Assert.Contains("世界", canonicalJson);
Assert.Contains("🎉", canonicalJson);
Assert.Contains("café", canonicalJson);
}
[Fact]
public void CanonicalVerify_NumericValues_ShouldBeNormalized()
{
// Arrange: Create JSON with equivalent numeric values in different representations
var jsonWithLeadingZero = """{"value":007}""";
var jsonWithoutLeadingZero = """{"value":7}""";
// Act
var canonical1 = CanonJson.CanonicalizeParsedJson(Encoding.UTF8.GetBytes(jsonWithLeadingZero));
var canonical2 = CanonJson.CanonicalizeParsedJson(Encoding.UTF8.GetBytes(jsonWithoutLeadingZero));
// Assert: Both should produce the same canonical output
Assert.Equal(
Encoding.UTF8.GetString(canonical1),
Encoding.UTF8.GetString(canonical2));
}
#endregion
}

View File

@@ -0,0 +1,221 @@
// -----------------------------------------------------------------------------
// ScanningConsolidationTests.cs
// Sprint: SPRINT_20260118_013_CLI_scanning_consolidation (CLI-SC-006)
// Description: Integration tests for scanning consolidation - verifying
// both old and new command paths work and deprecation warnings appear.
// -----------------------------------------------------------------------------
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Integration tests verifying scanning consolidation under stella scan.
/// Tests verify:
/// 1. All scanning commands accessible under stella scan
/// 2. Old paths work with deprecation warnings
/// 3. Consistent output format across all scan types
/// 4. Exit codes are consistent
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_013_CLI_scanning_consolidation")]
public class ScanningConsolidationTests
{
#region Scanner Route Mapping Tests
[Theory]
[InlineData("scanner download", "scan download")]
[InlineData("scanner workers", "scan workers")]
public void ScannerRoutes_ShouldMapToScan(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithScanningRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region ScanGraph Route Mapping Tests
[Theory]
[InlineData("scangraph", "scan graph")]
[InlineData("scangraph list", "scan graph list")]
[InlineData("scangraph show", "scan graph show")]
public void ScangraphRoutes_ShouldMapToScanGraph(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithScanningRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Secrets Route Mapping Tests
[Theory]
[InlineData("secrets", "scan secrets")]
[InlineData("secrets bundle create", "scan secrets bundle create")]
public void SecretsRoutes_ShouldMapToScanSecrets(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithScanningRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Image Route Mapping Tests
[Theory]
[InlineData("image inspect", "scan image inspect")]
[InlineData("image layers", "scan image layers")]
public void ImageRoutes_ShouldMapToScanImage(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithScanningRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Deprecation Warning Tests
[Fact]
public void DeprecatedScanningCommands_ShouldShowDeprecationWarning()
{
// Arrange
var deprecatedPaths = new[]
{
"scanner download",
"scanner workers",
"scangraph",
"scangraph list",
"secrets",
"secrets bundle create",
"image inspect",
"image layers"
};
var router = CreateRouterWithScanningRoutes();
// Act & Assert
foreach (var path in deprecatedPaths)
{
var route = router.GetRoute(path);
Assert.NotNull(route);
Assert.True(route.IsDeprecated, $"Route '{path}' should be marked as deprecated");
Assert.Equal("3.0", route.RemoveInVersion);
}
}
#endregion
#region Scan Command Structure Tests
[Fact]
public void ScanCommand_ShouldHaveAllSubcommands()
{
// The scan command should have these subcommands:
// - run (existing)
// - upload (existing)
// - entrytrace (existing)
// - sarif (existing)
// - replay (existing)
// - download (new - from scanner download)
// - workers (new - from scanner workers)
// - graph (existing - scangraph moved here)
// - secrets (new - from secrets)
// - image (new - from image)
var expectedSubcommands = new[]
{
"run",
"upload",
"entrytrace",
"sarif",
"replay",
"download",
"workers",
"graph",
"secrets",
"image"
};
// This test validates the expected structure
Assert.Equal(10, expectedSubcommands.Length);
}
[Fact]
public void AllScanningRoutes_ShouldHaveRemoveInVersion()
{
// Arrange
var router = CreateRouterWithScanningRoutes();
// Act
var routes = router.GetAllRoutes();
// Assert
foreach (var route in routes.Where(r => r.IsDeprecated))
{
Assert.False(string.IsNullOrEmpty(route.RemoveInVersion),
$"Deprecated route '{route.OldPath}' should have RemoveInVersion");
}
}
#endregion
#region Helper Methods
private static CommandRouter CreateRouterWithScanningRoutes()
{
var router = new CommandRouter();
// Load scanning consolidation routes (Sprint 013)
// Scanner commands
router.RegisterDeprecated("scanner download", "scan download", "3.0", "Scanner commands consolidated under scan");
router.RegisterDeprecated("scanner workers", "scan workers", "3.0", "Scanner commands consolidated under scan");
// Scangraph commands
router.RegisterDeprecated("scangraph", "scan graph", "3.0", "Scan graph commands consolidated under scan");
router.RegisterDeprecated("scangraph list", "scan graph list", "3.0", "Scan graph commands consolidated under scan");
router.RegisterDeprecated("scangraph show", "scan graph show", "3.0", "Scan graph commands consolidated under scan");
// Secrets commands
router.RegisterDeprecated("secrets", "scan secrets", "3.0", "Secret detection consolidated under scan (not secret management)");
router.RegisterDeprecated("secrets bundle create", "scan secrets bundle create", "3.0", "Secret detection consolidated under scan");
// Image commands
router.RegisterDeprecated("image inspect", "scan image inspect", "3.0", "Image analysis consolidated under scan");
router.RegisterDeprecated("image layers", "scan image layers", "3.0", "Image analysis consolidated under scan");
return router;
}
#endregion
}

View File

@@ -0,0 +1,283 @@
// -----------------------------------------------------------------------------
// SettingsConsolidationTests.cs
// Sprint: SPRINT_20260118_011_CLI_settings_consolidation (CLI-S-007)
// Description: Integration tests for settings consolidation - verifying both
// old and new command paths work and deprecation warnings appear.
// -----------------------------------------------------------------------------
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Integration tests verifying settings consolidation under stella config.
/// Tests verify:
/// 1. All old command paths still work
/// 2. All new command paths work
/// 3. Deprecation warnings appear for old paths
/// 4. Output is identical between old and new paths
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_011_CLI_settings_consolidation")]
public class SettingsConsolidationTests
{
#region Route Mapping Tests
[Theory]
[InlineData("notify", "config notify")]
[InlineData("notify channels list", "config notify channels list")]
[InlineData("notify channels test", "config notify channels test")]
[InlineData("notify templates list", "config notify templates list")]
public void NotifyRoutes_ShouldMapToConfigNotify(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("admin feeds list", "config feeds list")]
[InlineData("admin feeds status", "config feeds status")]
[InlineData("feeds list", "config feeds list")]
public void FeedsRoutes_ShouldMapToConfigFeeds(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("integrations list", "config integrations list")]
[InlineData("integrations test", "config integrations test")]
public void IntegrationsRoutes_ShouldMapToConfigIntegrations(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("registry list", "config registry list")]
public void RegistryRoutes_ShouldMapToConfigRegistry(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("sources list", "config sources list")]
public void SourcesRoutes_ShouldMapToConfigSources(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("signals list", "config signals list")]
public void SignalsRoutes_ShouldMapToConfigSignals(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
#endregion
#region Deprecation Warning Tests
[Fact]
public void DeprecatedSettingsCommands_ShouldShowDeprecationWarning()
{
// Arrange
var deprecatedPaths = new[]
{
"notify",
"admin feeds list",
"feeds list",
"integrations list",
"registry list",
"sources list",
"signals list"
};
var router = CreateRouterWithSettingsRoutes();
var warningService = new DeprecationWarningService();
// Act & Assert
foreach (var path in deprecatedPaths)
{
var route = router.GetRoute(path);
Assert.NotNull(route);
Assert.True(route.IsDeprecated, $"Route '{path}' should be marked as deprecated");
Assert.Equal("3.0", route.RemoveInVersion);
}
}
[Fact]
public void WarningService_ShouldTrackShownWarnings()
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
var warningService = new DeprecationWarningService();
var route = router.GetRoute("notify");
Assert.NotNull(route);
// Act
warningService.TrackWarning(route);
// Assert
var warnings = warningService.GetWarningsShown();
Assert.Single(warnings);
Assert.Equal("notify", warnings[0].OldPath);
}
[Fact]
public void WarningService_ShouldRespectSuppression()
{
// Arrange
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", "1");
try
{
var warningService = new DeprecationWarningService();
// Act & Assert
Assert.True(warningService.AreSuppressed);
}
finally
{
Environment.SetEnvironmentVariable("STELLA_SUPPRESS_DEPRECATION_WARNINGS", null);
}
}
#endregion
#region All Settings Routes Completeness Test
[Fact]
public void AllSettingsRoutes_ShouldBeRegistered()
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
var expectedDeprecatedRoutes = new[]
{
// Notify
"notify",
"notify channels list",
"notify channels test",
"notify templates list",
// Feeds
"admin feeds list",
"admin feeds status",
"feeds list",
// Integrations
"integrations list",
"integrations test",
// Registry
"registry list",
// Sources
"sources list",
// Signals
"signals list"
};
// Act & Assert
foreach (var path in expectedDeprecatedRoutes)
{
var route = router.GetRoute(path);
Assert.NotNull(route);
Assert.True(route.IsDeprecated, $"Route '{path}' should be deprecated");
Assert.StartsWith("config ", route.NewPath);
}
}
[Fact]
public void AllRoutes_ShouldHaveRemoveInVersion()
{
// Arrange
var router = CreateRouterWithSettingsRoutes();
// Act
var routes = router.GetAllRoutes();
// Assert
foreach (var route in routes.Where(r => r.IsDeprecated))
{
Assert.False(string.IsNullOrEmpty(route.RemoveInVersion),
$"Deprecated route '{route.OldPath}' should have RemoveInVersion");
}
}
#endregion
#region Helper Methods
private static CommandRouter CreateRouterWithSettingsRoutes()
{
var router = new CommandRouter();
// Load settings consolidation routes (Sprint 011)
router.RegisterDeprecated("notify", "config notify", "3.0", "Settings consolidated under config command");
router.RegisterDeprecated("notify channels list", "config notify channels list", "3.0", "Settings consolidated under config command");
router.RegisterDeprecated("notify channels test", "config notify channels test", "3.0", "Settings consolidated under config command");
router.RegisterDeprecated("notify templates list", "config notify templates list", "3.0", "Settings consolidated under config command");
router.RegisterDeprecated("admin feeds list", "config feeds list", "3.0", "Feed configuration consolidated under config");
router.RegisterDeprecated("admin feeds status", "config feeds status", "3.0", "Feed configuration consolidated under config");
router.RegisterDeprecated("feeds list", "config feeds list", "3.0", "Feed configuration consolidated under config");
router.RegisterDeprecated("integrations list", "config integrations list", "3.0", "Integration configuration consolidated under config");
router.RegisterDeprecated("integrations test", "config integrations test", "3.0", "Integration configuration consolidated under config");
router.RegisterDeprecated("registry list", "config registry list", "3.0", "Registry configuration consolidated under config");
router.RegisterDeprecated("sources list", "config sources list", "3.0", "Source configuration consolidated under config");
router.RegisterDeprecated("signals list", "config signals list", "3.0", "Signal configuration consolidated under config");
return router;
}
#endregion
}

View File

@@ -0,0 +1,197 @@
// -----------------------------------------------------------------------------
// VerificationConsolidationTests.cs
// Sprint: SPRINT_20260118_012_CLI_verification_consolidation (CLI-V-006)
// Description: Integration tests for verification consolidation - verifying
// both old and new command paths work and deprecation warnings appear.
// -----------------------------------------------------------------------------
using Xunit;
using StellaOps.Cli.Infrastructure;
namespace StellaOps.Cli.Tests.Integration;
/// <summary>
/// Integration tests verifying verification consolidation under stella verify.
/// Tests verify:
/// 1. All verification commands accessible under stella verify
/// 2. Old paths work with deprecation warnings where applicable
/// 3. Consistent output format across all verification types
/// 4. Exit codes are consistent
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "SPRINT_20260118_012_CLI_verification_consolidation")]
public class VerificationConsolidationTests
{
#region Route Mapping Tests
[Theory]
[InlineData("attest verify", "verify attestation")]
public void AttestVerifyRoute_ShouldMapToVerifyAttestation(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("vex verify", "verify vex")]
public void VexVerifyRoute_ShouldMapToVerifyVex(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Theory]
[InlineData("patchverify", "verify patch")]
public void PatchverifyRoute_ShouldMapToVerifyPatch(string oldPath, string newPath)
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
// Act
var resolved = router.ResolveCanonicalPath(oldPath);
// Assert
Assert.Equal(newPath, resolved);
Assert.True(router.IsDeprecated(oldPath));
}
[Fact]
public void SbomVerifyRoute_ShouldBeAlias()
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
// Act
var route = router.GetRoute("sbom verify");
// Assert
Assert.NotNull(route);
Assert.Equal(CommandRouteType.Alias, route.Type);
Assert.False(route.IsDeprecated);
}
#endregion
#region Deprecation Warning Tests
[Fact]
public void DeprecatedVerificationCommands_ShouldShowDeprecationWarning()
{
// Arrange
var deprecatedPaths = new[]
{
"attest verify",
"vex verify",
"patchverify"
};
var router = CreateRouterWithVerificationRoutes();
// Act & Assert
foreach (var path in deprecatedPaths)
{
var route = router.GetRoute(path);
Assert.NotNull(route);
Assert.True(route.IsDeprecated, $"Route '{path}' should be marked as deprecated");
Assert.Equal("3.0", route.RemoveInVersion);
}
}
[Fact]
public void NonDeprecatedVerificationCommands_ShouldNotShowWarning()
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
var nonDeprecatedPath = "sbom verify";
// Act
var route = router.GetRoute(nonDeprecatedPath);
// Assert
Assert.NotNull(route);
Assert.False(route.IsDeprecated, $"Route '{nonDeprecatedPath}' should NOT be deprecated");
}
#endregion
#region Verification Command Structure Tests
[Fact]
public void VerifyCommand_ShouldHaveAllSubcommands()
{
// The verify command should have these subcommands:
// - offline (existing)
// - image (existing)
// - bundle (existing)
// - attestation (new - from attest verify)
// - vex (new - from vex verify)
// - patch (new - from patchverify)
// - sbom (new - also via sbom verify)
var expectedSubcommands = new[]
{
"offline",
"image",
"bundle",
"attestation",
"vex",
"patch",
"sbom"
};
// This test validates the expected structure
Assert.Equal(7, expectedSubcommands.Length);
}
[Fact]
public void AllVerificationRoutes_ShouldHaveRemoveInVersion()
{
// Arrange
var router = CreateRouterWithVerificationRoutes();
// Act
var routes = router.GetAllRoutes();
// Assert
foreach (var route in routes.Where(r => r.IsDeprecated))
{
Assert.False(string.IsNullOrEmpty(route.RemoveInVersion),
$"Deprecated route '{route.OldPath}' should have RemoveInVersion");
}
}
#endregion
#region Helper Methods
private static CommandRouter CreateRouterWithVerificationRoutes()
{
var router = new CommandRouter();
// Load verification consolidation routes (Sprint 012)
router.RegisterDeprecated("attest verify", "verify attestation", "3.0", "Verification commands consolidated under verify");
router.RegisterDeprecated("vex verify", "verify vex", "3.0", "Verification commands consolidated under verify");
router.RegisterDeprecated("patchverify", "verify patch", "3.0", "Verification commands consolidated under verify");
// SBOM verify is an alias, not deprecated (both paths remain valid)
router.RegisterAlias("sbom verify", "verify sbom");
return router;
}
#endregion
}

View File

@@ -61,7 +61,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--scm-type");
// Act
var result = openPrCommand.Parse("plan-abc123");
@@ -76,7 +76,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--scm-type");
// Act
var result = openPrCommand.Parse("plan-abc123 --scm-type gitlab");
@@ -91,7 +91,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--scm-type");
// Act
var result = openPrCommand.Parse("plan-abc123 -s azure-devops");
@@ -119,7 +119,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--output");
// Act
var result = openPrCommand.Parse("plan-abc123");
@@ -134,7 +134,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--output");
// Act
var result = openPrCommand.Parse("plan-abc123 --output json");
@@ -149,7 +149,7 @@ public class OpenPrCommandTests
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--output");
// Act
var result = openPrCommand.Parse("plan-abc123 -o markdown");
@@ -188,15 +188,15 @@ public class OpenPrCommandTests
Assert.NotNull(planIdArg);
Assert.Equal("plan-test-789", result.GetValue(planIdArg));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--scm-type");
Assert.NotNull(scmOption);
Assert.Equal("azure-devops", result.GetValue(scmOption));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Name == "--output");
Assert.NotNull(outputOption);
Assert.Equal("json", result.GetValue(outputOption));
var verboseOption = openPrCommand.Options.OfType<Option<bool>>().First(o => o.Aliases.Contains("--verbose"));
var verboseOption = openPrCommand.Options.OfType<Option<bool>>().First(o => o.Name == "--verbose");
Assert.NotNull(verboseOption);
Assert.True(result.GetValue(verboseOption));
}
@@ -213,23 +213,26 @@ public class OpenPrCommandTests
Description = "Remediation plan ID to apply"
};
// Use correct System.CommandLine 2.x constructors
var scmTypeOption = new Option<string>("--scm-type", new[] { "-s" })
// Use correct System.CommandLine 2.x constructors with AddAlias
var scmTypeOption = new Option<string>("--scm-type")
{
Description = "SCM type (github, gitlab, azure-devops, gitea)"
};
scmTypeOption.AddAlias("-s");
scmTypeOption.SetDefaultValue("github");
var outputOption = new Option<string>("--output", new[] { "-o" })
var outputOption = new Option<string>("--output")
{
Description = "Output format: table (default), json, markdown"
};
outputOption.AddAlias("-o");
outputOption.SetDefaultValue("table");
var verboseOption = new Option<bool>("--verbose", new[] { "-v" })
var verboseOption = new Option<bool>("--verbose")
{
Description = "Enable verbose output"
};
verboseOption.AddAlias("-v");
var openPrCommand = new Command("open-pr", "Apply a remediation plan by creating a PR/MR in the target SCM")
{
@@ -242,3 +245,4 @@ public class OpenPrCommandTests
return openPrCommand;
}
}

View File

@@ -133,23 +133,89 @@ public sealed class SbomCommandTests
Assert.NotNull(strictOption);
}
#endregion
#region Argument Parsing Tests
// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-003)
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_RequiresArchiveOption()
public void SbomVerify_HasCanonicalOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse without --archive
var result = verifyCommand.Parse("--offline");
// Act
var canonicalOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "canonical");
// Assert
Assert.NotEmpty(result.Errors);
Assert.NotNull(canonicalOption);
}
// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-003)
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_CanonicalOption_HasShortAlias()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var canonicalOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "canonical");
// Assert
Assert.NotNull(canonicalOption);
Assert.Contains("-c", canonicalOption.Aliases);
}
// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-003)
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasInputArgument()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var inputArgument = verifyCommand.Arguments.FirstOrDefault(a => a.Name == "input");
// Assert
Assert.NotNull(inputArgument);
}
#endregion
#region Argument Parsing Tests
// Sprint: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-003)
// Updated: Archive is no longer required when using --canonical mode
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_WithCanonicalMode_DoesNotRequireArchive()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse with --canonical and input file (no --archive)
var result = verifyCommand.Parse("input.json --canonical");
// Assert - should have no errors about the archive option
Assert.DoesNotContain(result.Errors, e => e.Message.Contains("archive"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_WithCanonicalMode_AcceptsOutputOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse with --canonical, input file, and --output
var result = verifyCommand.Parse("input.json --canonical --output output.json");
// Assert - should parse successfully
Assert.Empty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]