synergy moats product advisory implementations

This commit is contained in:
master
2026-01-17 01:30:03 +02:00
parent 77ff029205
commit 702a27ac83
112 changed files with 21356 additions and 127 deletions

View File

@@ -0,0 +1,821 @@
// -----------------------------------------------------------------------------
// ExplainBlockCommandTests.cs
// Sprint: SPRINT_20260117_026_CLI_why_blocked_command
// Task: WHY-005 - Unit and Integration Tests
// Description: Tests for stella explain block command
// -----------------------------------------------------------------------------
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
/// <summary>
/// Tests for the explain block command.
/// Validates M2 moat: "Explainability with proof, not narrative."
/// </summary>
public class ExplainBlockCommandTests
{
#region Digest Normalization Tests
[Theory]
[InlineData("sha256:abc123def456", "sha256:abc123def456")]
[InlineData("SHA256:ABC123DEF456", "sha256:abc123def456")]
[InlineData("abc123def456789012345678901234567890123456789012345678901234", "sha256:abc123def456789012345678901234567890123456789012345678901234")]
[InlineData("registry.example.com/image@sha256:abc123", "sha256:abc123")]
public void NormalizeDigest_ValidFormats_ReturnsNormalized(string input, string expected)
{
// Arrange & Act
var result = NormalizeDigestForTest(input);
// Assert
result.Should().Be(expected);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void NormalizeDigest_EmptyOrNull_ReturnsEmpty(string? input)
{
// Arrange & Act
var result = NormalizeDigestForTest(input ?? string.Empty);
// Assert
result.Should().BeEmpty();
}
#endregion
#region Output Format Tests
[Fact]
public void RenderTable_BlockedArtifact_ContainsRequiredFields()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderTableForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
output.Should().Contain("Status: BLOCKED");
output.Should().Contain("Gate: VexTrust");
output.Should().Contain("Reason:");
output.Should().Contain("Suggestion:");
output.Should().Contain("Evidence:");
output.Should().Contain("stella verify verdict");
}
[Fact]
public void RenderTable_WithShowEvidence_IncludesEvidenceDetails()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderTableForTest(explanation, showEvidence: true, showTrace: false, includeReplayToken: false);
// Assert
output.Should().Contain("Evidence Details:");
output.Should().Contain("stella evidence get");
}
[Fact]
public void RenderTable_WithShowTrace_IncludesEvaluationTrace()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderTableForTest(explanation, showEvidence: false, showTrace: true, includeReplayToken: false);
// Assert
output.Should().Contain("Evaluation Trace:");
output.Should().Contain("SbomPresent");
output.Should().Contain("VulnScan");
output.Should().Contain("VexTrust");
output.Should().Contain("PASS");
output.Should().Contain("FAIL");
}
[Fact]
public void RenderTable_WithReplayToken_IncludesToken()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderTableForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: true);
// Assert
output.Should().Contain("Replay Token:");
output.Should().Contain("urn:stella:verdict:");
}
[Fact]
public void RenderJson_BlockedArtifact_ValidJsonWithRequiredFields()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderJsonForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
var json = JsonDocument.Parse(output);
json.RootElement.GetProperty("status").GetString().Should().Be("BLOCKED");
json.RootElement.GetProperty("gate").GetString().Should().Be("VexTrust");
json.RootElement.GetProperty("reason").GetString().Should().NotBeNullOrEmpty();
json.RootElement.GetProperty("suggestion").GetString().Should().NotBeNullOrEmpty();
json.RootElement.GetProperty("evidence").GetArrayLength().Should().BeGreaterThan(0);
json.RootElement.GetProperty("replayCommand").GetString().Should().Contain("stella verify verdict");
}
[Fact]
public void RenderJson_WithTrace_IncludesEvaluationTrace()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderJsonForTest(explanation, showEvidence: false, showTrace: true, includeReplayToken: false);
// Assert
var json = JsonDocument.Parse(output);
json.RootElement.TryGetProperty("evaluationTrace", out var trace).Should().BeTrue();
trace.GetArrayLength().Should().Be(3);
}
[Fact]
public void RenderMarkdown_BlockedArtifact_ValidMarkdownFormat()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output = RenderMarkdownForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
output.Should().Contain("## Block Explanation");
output.Should().Contain("**Artifact:**");
output.Should().Contain("**Status:** ");
output.Should().Contain("### Gate Decision");
output.Should().Contain("| Property | Value |");
output.Should().Contain("### Evidence");
output.Should().Contain("### Verification");
output.Should().Contain("```bash");
}
#endregion
#region Not Blocked Tests
[Fact]
public void RenderNotBlocked_JsonFormat_ReturnsNotBlockedStatus()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = false
};
// Act
var output = RenderNotBlockedForTest(explanation, "json");
// Assert
var json = JsonDocument.Parse(output);
json.RootElement.GetProperty("status").GetString().Should().Be("NOT_BLOCKED");
json.RootElement.GetProperty("message").GetString().Should().Contain("passed all policy gates");
}
[Fact]
public void RenderNotBlocked_TableFormat_ReturnsNotBlockedMessage()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = false
};
// Act
var output = RenderNotBlockedForTest(explanation, "table");
// Assert
output.Should().Contain("NOT blocked");
output.Should().Contain("All policy gates passed");
}
#endregion
#region ID Truncation Tests
[Theory]
[InlineData("short", "short")]
[InlineData("vex:sha256:abcdef123456789012345678901234567890", "vex:sha256:ab...67890")]
public void TruncateId_VariousLengths_TruncatesCorrectly(string input, string expectedPattern)
{
// Arrange & Act
var result = TruncateIdForTest(input);
// Assert
if (input.Length <= 25)
{
result.Should().Be(input);
}
else
{
result.Should().Contain("...");
result.Length.Should().BeLessThan(input.Length);
}
}
#endregion
#region Determinism Tests
[Fact]
public void RenderJson_SameInput_ProducesSameOutput()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output1 = RenderJsonForTest(explanation, showEvidence: true, showTrace: true, includeReplayToken: true);
var output2 = RenderJsonForTest(explanation, showEvidence: true, showTrace: true, includeReplayToken: true);
// Assert
output1.Should().Be(output2, "output should be deterministic");
}
[Fact]
public void RenderTable_SameInput_ProducesSameOutput()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var output1 = RenderTableForTest(explanation, showEvidence: true, showTrace: true, includeReplayToken: true);
var output2 = RenderTableForTest(explanation, showEvidence: true, showTrace: true, includeReplayToken: true);
// Assert
output1.Should().Be(output2, "output should be deterministic");
}
#endregion
#region Error Handling Tests
[Fact]
public void RenderArtifactNotFound_JsonFormat_ReturnsNotFoundStatus()
{
// Arrange
var digest = "sha256:nonexistent123456789";
// Act
var output = RenderArtifactNotFoundForTest(digest, "json");
// Assert
var json = JsonDocument.Parse(output);
json.RootElement.GetProperty("status").GetString().Should().Be("NOT_FOUND");
json.RootElement.GetProperty("artifact").GetString().Should().Be(digest);
json.RootElement.GetProperty("message").GetString().Should().Contain("not found");
}
[Fact]
public void RenderArtifactNotFound_TableFormat_ReturnsNotFoundMessage()
{
// Arrange
var digest = "sha256:nonexistent123456789";
// Act
var output = RenderArtifactNotFoundForTest(digest, "table");
// Assert
output.Should().Contain("not found");
output.Should().Contain(digest);
}
[Fact]
public void RenderApiError_JsonFormat_ReturnsErrorStatus()
{
// Arrange
var errorMessage = "Policy service unavailable";
// Act
var output = RenderApiErrorForTest(errorMessage, "json");
// Assert
var json = JsonDocument.Parse(output);
json.RootElement.GetProperty("status").GetString().Should().Be("ERROR");
json.RootElement.GetProperty("error").GetString().Should().Be(errorMessage);
}
[Fact]
public void RenderApiError_TableFormat_ReturnsErrorMessage()
{
// Arrange
var errorMessage = "Policy service unavailable";
// Act
var output = RenderApiErrorForTest(errorMessage, "table");
// Assert
output.Should().Contain("Error");
output.Should().Contain(errorMessage);
}
[Theory]
[InlineData("connection_timeout", "Connection timeout")]
[InlineData("auth_failed", "Authentication failed")]
[InlineData("rate_limited", "Rate limited")]
public void RenderApiError_VariousErrors_ContainsErrorType(string errorCode, string expectedMessage)
{
// Act
var output = RenderApiErrorForTest(expectedMessage, "table");
// Assert
output.Should().Contain(expectedMessage);
}
#endregion
#region Exit Code Tests
[Fact]
public void DetermineExitCode_Blocked_ReturnsOne()
{
// Arrange
var explanation = CreateSampleBlockExplanation();
// Act
var exitCode = DetermineExitCodeForTest(explanation, apiError: null);
// Assert
exitCode.Should().Be(1, "blocked artifacts should return exit code 1");
}
[Fact]
public void DetermineExitCode_NotBlocked_ReturnsZero()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = false
};
// Act
var exitCode = DetermineExitCodeForTest(explanation, apiError: null);
// Assert
exitCode.Should().Be(0, "non-blocked artifacts should return exit code 0");
}
[Fact]
public void DetermineExitCode_ApiError_ReturnsTwo()
{
// Act
var exitCode = DetermineExitCodeForTest(null, apiError: "Service unavailable");
// Assert
exitCode.Should().Be(2, "API errors should return exit code 2");
}
[Fact]
public void DetermineExitCode_ArtifactNotFound_ReturnsTwo()
{
// Act
var exitCode = DetermineExitCodeForTest(null, apiError: null); // null explanation, no error = not found
// Assert
exitCode.Should().Be(2, "artifact not found should return exit code 2");
}
#endregion
#region Edge Case Tests
[Fact]
public void RenderTable_NoEvidence_ShowsNoEvidenceMessage()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = true,
Gate = "PolicyCheck",
Reason = "Manual block applied",
Suggestion = "Contact administrator",
Evidence = new List<TestEvidenceReference>(), // Empty evidence
ReplayToken = "urn:stella:verdict:sha256:xyz",
EvaluationTrace = new List<TestTraceStep>()
};
// Act
var output = RenderTableForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
output.Should().Contain("Evidence:");
// Should handle empty evidence gracefully
}
[Fact]
public void RenderJson_SpecialCharactersInReason_ProperlyEscaped()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = true,
Gate = "VulnCheck",
Reason = "CVE-2024-1234: SQL injection via \"user\" parameter",
Suggestion = "Upgrade to version >= 2.0",
Evidence = new List<TestEvidenceReference>(),
ReplayToken = "urn:stella:verdict:sha256:xyz",
EvaluationTime = DateTimeOffset.UtcNow,
PolicyVersion = "v1.0.0",
EvaluationTrace = new List<TestTraceStep>()
};
// Act
var output = RenderJsonForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
// Should be valid JSON (no exception)
var action = () => JsonDocument.Parse(output);
action.Should().NotThrow();
var json = JsonDocument.Parse(output);
json.RootElement.GetProperty("reason").GetString().Should().Contain("SQL injection");
}
[Fact]
public void RenderMarkdown_LongReason_DoesNotBreakTable()
{
// Arrange
var explanation = new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123",
IsBlocked = true,
Gate = "VulnCheck",
Reason = "This is a very long reason that spans multiple words and might cause issues with table rendering in markdown if not handled properly with appropriate escaping and formatting",
Suggestion = "Fix the issue",
Evidence = new List<TestEvidenceReference>(),
ReplayToken = "urn:stella:verdict:sha256:xyz",
EvaluationTime = DateTimeOffset.UtcNow,
PolicyVersion = "v1.0.0",
EvaluationTrace = new List<TestTraceStep>()
};
// Act
var output = RenderMarkdownForTest(explanation, showEvidence: false, showTrace: false, includeReplayToken: false);
// Assert
output.Should().Contain("| Reason |");
output.Should().Contain("very long reason");
}
#endregion
#region Test Helpers
private static TestBlockExplanation CreateSampleBlockExplanation()
{
return new TestBlockExplanation
{
ArtifactDigest = "sha256:abc123def456789012345678901234567890123456789012345678901234",
IsBlocked = true,
Gate = "VexTrust",
Reason = "Trust score below threshold (0.45 < 0.70)",
Suggestion = "Obtain VEX statement from trusted issuer or add issuer to trust registry",
EvaluationTime = new DateTimeOffset(2026, 1, 17, 10, 0, 0, TimeSpan.Zero),
PolicyVersion = "v2.3.0",
Evidence = new List<TestEvidenceReference>
{
new()
{
Type = "VEX",
Id = "vex:sha256:def456789abc123",
Source = "vendor-x",
Timestamp = new DateTimeOffset(2026, 1, 17, 9, 0, 0, TimeSpan.Zero)
},
new()
{
Type = "REACH",
Id = "reach:sha256:789abc123def456",
Source = "static-analysis",
Timestamp = new DateTimeOffset(2026, 1, 17, 8, 0, 0, TimeSpan.Zero)
}
},
ReplayToken = "urn:stella:verdict:sha256:abc123:v2.3.0:1737108000",
EvaluationTrace = new List<TestTraceStep>
{
new() { Step = 1, Gate = "SbomPresent", Result = "PASS", Duration = TimeSpan.FromMilliseconds(15) },
new() { Step = 2, Gate = "VulnScan", Result = "PASS", Duration = TimeSpan.FromMilliseconds(250) },
new() { Step = 3, Gate = "VexTrust", Result = "FAIL", Duration = TimeSpan.FromMilliseconds(45) }
}
};
}
// Mirror the private methods from ExplainCommandGroup for testing
private static string NormalizeDigestForTest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return string.Empty;
}
digest = digest.Trim();
if (digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ||
digest.StartsWith("sha512:", StringComparison.OrdinalIgnoreCase))
{
return digest.ToLowerInvariant();
}
if (digest.Length == 64 && digest.All(c => char.IsAsciiHexDigit(c)))
{
return $"sha256:{digest.ToLowerInvariant()}";
}
var atIndex = digest.IndexOf('@');
if (atIndex > 0)
{
return digest[(atIndex + 1)..].ToLowerInvariant();
}
return digest.ToLowerInvariant();
}
private static string RenderTableForTest(TestBlockExplanation explanation, bool showEvidence, bool showTrace, bool includeReplayToken)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Artifact: {explanation.ArtifactDigest}");
sb.AppendLine($"Status: BLOCKED");
sb.AppendLine();
sb.AppendLine($"Gate: {explanation.Gate}");
sb.AppendLine($"Reason: {explanation.Reason}");
sb.AppendLine($"Suggestion: {explanation.Suggestion}");
sb.AppendLine();
sb.AppendLine("Evidence:");
foreach (var evidence in explanation.Evidence)
{
var truncatedId = TruncateIdForTest(evidence.Id);
sb.AppendLine($" [{evidence.Type,-6}] {truncatedId,-25} {evidence.Source,-12} {evidence.Timestamp:yyyy-MM-ddTHH:mm:ssZ}");
}
if (showEvidence)
{
sb.AppendLine();
sb.AppendLine("Evidence Details:");
foreach (var evidence in explanation.Evidence)
{
sb.AppendLine($" - Type: {evidence.Type}");
sb.AppendLine($" ID: {evidence.Id}");
sb.AppendLine($" Source: {evidence.Source}");
sb.AppendLine($" Timestamp: {evidence.Timestamp:o}");
sb.AppendLine($" Retrieve: stella evidence get {evidence.Id}");
sb.AppendLine();
}
}
if (showTrace && explanation.EvaluationTrace.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Evaluation Trace:");
foreach (var step in explanation.EvaluationTrace)
{
var resultText = step.Result == "PASS" ? "PASS" : "FAIL";
sb.AppendLine($" {step.Step}. {step.Gate,-15} {resultText,-6} ({step.Duration.TotalMilliseconds:F0}ms)");
}
}
sb.AppendLine();
sb.AppendLine($"Replay: stella verify verdict --verdict {explanation.ReplayToken}");
if (includeReplayToken)
{
sb.AppendLine();
sb.AppendLine($"Replay Token: {explanation.ReplayToken}");
}
return sb.ToString();
}
private static string RenderJsonForTest(TestBlockExplanation explanation, bool showEvidence, bool showTrace, bool includeReplayToken)
{
var result = new Dictionary<string, object?>
{
["artifact"] = explanation.ArtifactDigest,
["status"] = "BLOCKED",
["gate"] = explanation.Gate,
["reason"] = explanation.Reason,
["suggestion"] = explanation.Suggestion,
["evaluationTime"] = explanation.EvaluationTime.ToString("o"),
["policyVersion"] = explanation.PolicyVersion,
["evidence"] = explanation.Evidence.Select(e => new
{
type = e.Type,
id = e.Id,
source = e.Source,
timestamp = e.Timestamp.ToString("o"),
retrieveCommand = $"stella evidence get {e.Id}"
}).ToList(),
["replayCommand"] = $"stella verify verdict --verdict {explanation.ReplayToken}"
};
if (showTrace)
{
result["evaluationTrace"] = explanation.EvaluationTrace.Select(t => new
{
step = t.Step,
gate = t.Gate,
result = t.Result,
durationMs = t.Duration.TotalMilliseconds
}).ToList();
}
if (includeReplayToken)
{
result["replayToken"] = explanation.ReplayToken;
}
return JsonSerializer.Serialize(result, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
private static string RenderMarkdownForTest(TestBlockExplanation explanation, bool showEvidence, bool showTrace, bool includeReplayToken)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("## Block Explanation");
sb.AppendLine();
sb.AppendLine($"**Artifact:** `{explanation.ArtifactDigest}`");
sb.AppendLine($"**Status:** BLOCKED");
sb.AppendLine();
sb.AppendLine("### Gate Decision");
sb.AppendLine();
sb.AppendLine($"| Property | Value |");
sb.AppendLine($"|----------|-------|");
sb.AppendLine($"| Gate | {explanation.Gate} |");
sb.AppendLine($"| Reason | {explanation.Reason} |");
sb.AppendLine($"| Suggestion | {explanation.Suggestion} |");
sb.AppendLine($"| Policy Version | {explanation.PolicyVersion} |");
sb.AppendLine();
sb.AppendLine("### Evidence");
sb.AppendLine();
sb.AppendLine("| Type | ID | Source | Timestamp |");
sb.AppendLine("|------|-----|--------|-----------|");
foreach (var evidence in explanation.Evidence)
{
var truncatedId = TruncateIdForTest(evidence.Id);
sb.AppendLine($"| {evidence.Type} | `{truncatedId}` | {evidence.Source} | {evidence.Timestamp:yyyy-MM-dd HH:mm} |");
}
sb.AppendLine();
if (showTrace && explanation.EvaluationTrace.Count > 0)
{
sb.AppendLine("### Evaluation Trace");
sb.AppendLine();
sb.AppendLine("| Step | Gate | Result | Duration |");
sb.AppendLine("|------|------|--------|----------|");
foreach (var step in explanation.EvaluationTrace)
{
sb.AppendLine($"| {step.Step} | {step.Gate} | {step.Result} | {step.Duration.TotalMilliseconds:F0}ms |");
}
sb.AppendLine();
}
sb.AppendLine("### Verification");
sb.AppendLine();
sb.AppendLine("```bash");
sb.AppendLine($"stella verify verdict --verdict {explanation.ReplayToken}");
sb.AppendLine("```");
if (includeReplayToken)
{
sb.AppendLine();
sb.AppendLine($"**Replay Token:** `{explanation.ReplayToken}`");
}
return sb.ToString();
}
private static string RenderNotBlockedForTest(TestBlockExplanation explanation, string format)
{
if (format == "json")
{
return JsonSerializer.Serialize(new
{
artifact = explanation.ArtifactDigest,
status = "NOT_BLOCKED",
message = "Artifact passed all policy gates"
}, new JsonSerializerOptions { WriteIndented = true });
}
return $"Artifact {explanation.ArtifactDigest} is NOT blocked. All policy gates passed.";
}
private static string TruncateIdForTest(string id)
{
if (id.Length <= 25)
{
return id;
}
var prefix = id[..12];
var suffix = id[^8..];
return $"{prefix}...{suffix}";
}
private static string RenderArtifactNotFoundForTest(string digest, string format)
{
if (format == "json")
{
return JsonSerializer.Serialize(new
{
artifact = digest,
status = "NOT_FOUND",
message = $"Artifact {digest} not found in registry or evidence store"
}, new JsonSerializerOptions { WriteIndented = true });
}
return $"Error: Artifact {digest} not found in registry or evidence store.";
}
private static string RenderApiErrorForTest(string errorMessage, string format)
{
if (format == "json")
{
return JsonSerializer.Serialize(new
{
status = "ERROR",
error = errorMessage
}, new JsonSerializerOptions { WriteIndented = true });
}
return $"Error: {errorMessage}";
}
private static int DetermineExitCodeForTest(TestBlockExplanation? explanation, string? apiError)
{
// Exit codes: 0 = not blocked, 1 = blocked, 2 = error
if (!string.IsNullOrEmpty(apiError))
{
return 2; // API error
}
if (explanation == null)
{
return 2; // Not found
}
return explanation.IsBlocked ? 1 : 0;
}
#endregion
#region Test Models
private sealed class TestBlockExplanation
{
public required string ArtifactDigest { get; init; }
public bool IsBlocked { get; init; }
public string Gate { get; init; } = string.Empty;
public string Reason { get; init; } = string.Empty;
public string Suggestion { get; init; } = string.Empty;
public DateTimeOffset EvaluationTime { get; init; }
public string PolicyVersion { get; init; } = string.Empty;
public List<TestEvidenceReference> Evidence { get; init; } = new();
public string ReplayToken { get; init; } = string.Empty;
public List<TestTraceStep> EvaluationTrace { get; init; } = new();
}
private sealed class TestEvidenceReference
{
public string Type { get; init; } = string.Empty;
public string Id { get; init; } = string.Empty;
public string Source { get; init; } = string.Empty;
public DateTimeOffset Timestamp { get; init; }
}
private sealed class TestTraceStep
{
public int Step { get; init; }
public string Gate { get; init; } = string.Empty;
public string Result { get; init; } = string.Empty;
public TimeSpan Duration { get; init; }
}
#endregion
}