synergy moats product advisory implementations

This commit is contained in:
master
2026-01-17 01:30:03 +02:00
parent 77ff029205
commit d8d9c0a6e3
106 changed files with 20603 additions and 123 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
}

View File

@@ -489,6 +489,236 @@ public sealed class DeterminismReplayGoldenTests
#endregion
#region Explain Block Golden Tests (Sprint 026 - WHY-004)
/// <summary>
/// Verifies that explain block JSON output matches golden snapshot.
/// Sprint: SPRINT_20260117_026_CLI_why_blocked_command
/// </summary>
[Fact]
public void ExplainBlock_Json_MatchesGolden()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Act
var actual = JsonSerializer.Serialize(explanation, JsonOptions).NormalizeLf();
// Assert - Golden snapshot
var expected = """
{
"artifact": "sha256:abc123def456789012345678901234567890123456789012345678901234",
"status": "BLOCKED",
"gate": "VexTrust",
"reason": "Trust score below threshold (0.45 \u003C 0.70)",
"suggestion": "Obtain VEX statement from trusted issuer or add issuer to trust registry",
"evaluationTime": "2026-01-15T10:30:00+00:00",
"policyVersion": "v2.3.0",
"evidence": [
{
"type": "REACH",
"id": "reach:sha256:789abc123def456",
"source": "static-analysis",
"timestamp": "2026-01-15T08:00:00+00:00"
},
{
"type": "VEX",
"id": "vex:sha256:def456789abc123",
"source": "vendor-x",
"timestamp": "2026-01-15T09:00:00+00:00"
}
],
"replayCommand": "stella verify verdict --verdict urn:stella:verdict:sha256:abc123:v2.3.0:1737108000",
"replayToken": "urn:stella:verdict:sha256:abc123:v2.3.0:1737108000",
"evaluationTrace": [
{
"step": 1,
"gate": "SbomPresent",
"result": "PASS",
"durationMs": 15
},
{
"step": 2,
"gate": "VexTrust",
"result": "FAIL",
"durationMs": 45
},
{
"step": 3,
"gate": "VulnScan",
"result": "PASS",
"durationMs": 250
}
],
"determinismHash": "sha256:e3b0c44298fc1c14"
}
""".NormalizeLf();
actual.Should().Be(expected);
}
/// <summary>
/// Verifies that explain block table output matches golden snapshot.
/// </summary>
[Fact]
public void ExplainBlock_Table_MatchesGolden()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Act
var actual = FormatBlockExplanationTable(explanation, showEvidence: false, showTrace: false).NormalizeLf();
// Assert - Golden snapshot
var expected = """
Artifact: sha256:abc123def456789012345678901234567890123456789012345678901234
Status: BLOCKED
Gate: VexTrust
Reason: Trust score below threshold (0.45 < 0.70)
Suggestion: Obtain VEX statement from trusted issuer or add issuer to trust registry
Evidence:
[REACH ] reach:sha256...def456 static-analysis 2026-01-15T08:00:00Z
[VEX ] vex:sha256:d...bc123 vendor-x 2026-01-15T09:00:00Z
Replay: stella verify verdict --verdict urn:stella:verdict:sha256:abc123:v2.3.0:1737108000
""".NormalizeLf();
actual.Trim().Should().Be(expected.Trim());
}
/// <summary>
/// Verifies that explain block markdown output matches golden snapshot.
/// </summary>
[Fact]
public void ExplainBlock_Markdown_MatchesGolden()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Act
var actual = FormatBlockExplanationMarkdown(explanation, showEvidence: false, showTrace: false).NormalizeLf();
// Assert - Key elements present
actual.Should().Contain("## Block Explanation");
actual.Should().Contain("**Artifact:** `sha256:abc123def456789012345678901234567890123456789012345678901234`");
actual.Should().Contain("**Status:** BLOCKED");
actual.Should().Contain("### Gate Decision");
actual.Should().Contain("| Property | Value |");
actual.Should().Contain("| Gate | VexTrust |");
actual.Should().Contain("| Reason | Trust score below threshold");
actual.Should().Contain("### Evidence");
actual.Should().Contain("| Type | ID | Source | Timestamp |");
actual.Should().Contain("### Verification");
actual.Should().Contain("```bash");
actual.Should().Contain("stella verify verdict --verdict");
}
/// <summary>
/// Verifies that explain block with --show-trace includes evaluation trace.
/// </summary>
[Fact]
public void ExplainBlock_WithTrace_MatchesGolden()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Act
var actual = FormatBlockExplanationTable(explanation, showEvidence: false, showTrace: true).NormalizeLf();
// Assert
actual.Should().Contain("Evaluation Trace:");
actual.Should().Contain("1. SbomPresent");
actual.Should().Contain("PASS");
actual.Should().Contain("2. VexTrust");
actual.Should().Contain("FAIL");
actual.Should().Contain("3. VulnScan");
actual.Should().Contain("PASS");
}
/// <summary>
/// Verifies that same inputs produce identical outputs (byte-for-byte).
/// M2 moat requirement: Deterministic trace + referenced evidence artifacts.
/// </summary>
[Fact]
public void ExplainBlock_SameInputs_ProducesIdenticalOutput()
{
// Arrange
var exp1 = CreateFrozenBlockExplanation();
var exp2 = CreateFrozenBlockExplanation();
// Act
var json1 = JsonSerializer.Serialize(exp1, JsonOptions);
var json2 = JsonSerializer.Serialize(exp2, JsonOptions);
var table1 = FormatBlockExplanationTable(exp1, true, true);
var table2 = FormatBlockExplanationTable(exp2, true, true);
var md1 = FormatBlockExplanationMarkdown(exp1, true, true);
var md2 = FormatBlockExplanationMarkdown(exp2, true, true);
// Assert - All formats must be identical
json1.Should().Be(json2, "JSON output must be deterministic");
table1.Should().Be(table2, "Table output must be deterministic");
md1.Should().Be(md2, "Markdown output must be deterministic");
}
/// <summary>
/// Verifies that evidence is sorted by timestamp for deterministic ordering.
/// </summary>
[Fact]
public void ExplainBlock_EvidenceIsSortedByTimestamp()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Assert - Evidence should be sorted by timestamp (ascending)
var timestamps = explanation.Evidence.Select(e => e.Timestamp).ToList();
timestamps.Should().BeInAscendingOrder();
}
/// <summary>
/// Verifies that evaluation trace is sorted by step number.
/// </summary>
[Fact]
public void ExplainBlock_TraceIsSortedByStep()
{
// Arrange
var explanation = CreateFrozenBlockExplanation();
// Assert - Trace should be sorted by step number
var steps = explanation.EvaluationTrace.Select(t => t.Step).ToList();
steps.Should().BeInAscendingOrder();
}
/// <summary>
/// Verifies that not-blocked artifacts produce deterministic output.
/// </summary>
[Fact]
public void ExplainBlock_NotBlocked_MatchesGolden()
{
// Arrange
var explanation = CreateFrozenNotBlockedExplanation();
// Act
var actual = JsonSerializer.Serialize(explanation, JsonOptions).NormalizeLf();
// Assert - Golden snapshot for not blocked
var expected = """
{
"artifact": "sha256:fedcba9876543210",
"status": "NOT_BLOCKED",
"message": "Artifact passed all policy gates",
"gatesEvaluated": 5,
"evaluationTime": "2026-01-15T10:30:00+00:00",
"policyVersion": "v2.3.0"
}
""".NormalizeLf();
actual.Should().Be(expected);
}
#endregion
#region Cross-Platform Golden Tests
/// <summary>
@@ -753,6 +983,174 @@ public sealed class DeterminismReplayGoldenTests
explanation.DeterminismHash = $"sha256:{Convert.ToHexStringLower(hashBytes)[..16]}";
}
// Explain Block helpers (Sprint 026 - WHY-004)
private static BlockExplanation CreateFrozenBlockExplanation()
{
return new BlockExplanation
{
Artifact = "sha256:abc123def456789012345678901234567890123456789012345678901234",
Status = "BLOCKED",
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 = FixedTimestamp,
PolicyVersion = "v2.3.0",
Evidence =
[
new BlockEvidence
{
Type = "REACH",
Id = "reach:sha256:789abc123def456",
Source = "static-analysis",
Timestamp = FixedTimestamp.AddHours(-2.5) // 08:00
},
new BlockEvidence
{
Type = "VEX",
Id = "vex:sha256:def456789abc123",
Source = "vendor-x",
Timestamp = FixedTimestamp.AddHours(-1.5) // 09:00
}
],
ReplayCommand = "stella verify verdict --verdict urn:stella:verdict:sha256:abc123:v2.3.0:1737108000",
ReplayToken = "urn:stella:verdict:sha256:abc123:v2.3.0:1737108000",
EvaluationTrace =
[
new BlockTraceStep { Step = 1, Gate = "SbomPresent", Result = "PASS", DurationMs = 15 },
new BlockTraceStep { Step = 2, Gate = "VexTrust", Result = "FAIL", DurationMs = 45 },
new BlockTraceStep { Step = 3, Gate = "VulnScan", Result = "PASS", DurationMs = 250 }
],
DeterminismHash = "sha256:e3b0c44298fc1c14"
};
}
private static NotBlockedExplanation CreateFrozenNotBlockedExplanation()
{
return new NotBlockedExplanation
{
Artifact = "sha256:fedcba9876543210",
Status = "NOT_BLOCKED",
Message = "Artifact passed all policy gates",
GatesEvaluated = 5,
EvaluationTime = FixedTimestamp,
PolicyVersion = "v2.3.0"
};
}
private static string FormatBlockExplanationTable(BlockExplanation exp, bool showEvidence, bool showTrace)
{
var sb = new StringBuilder();
sb.AppendLine($"Artifact: {exp.Artifact}");
sb.AppendLine($"Status: {exp.Status}");
sb.AppendLine();
sb.AppendLine($"Gate: {exp.Gate}");
sb.AppendLine($"Reason: {exp.Reason}");
sb.AppendLine($"Suggestion: {exp.Suggestion}");
sb.AppendLine();
sb.AppendLine("Evidence:");
foreach (var evidence in exp.Evidence.OrderBy(e => e.Timestamp))
{
var truncatedId = TruncateBlockId(evidence.Id);
sb.AppendLine($" [{evidence.Type,-6}] {truncatedId,-20} {evidence.Source,-15} {evidence.Timestamp:yyyy-MM-ddTHH:mm:ssZ}");
}
if (showTrace && exp.EvaluationTrace.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Evaluation Trace:");
foreach (var step in exp.EvaluationTrace.OrderBy(t => t.Step))
{
sb.AppendLine($" {step.Step}. {step.Gate,-15} {step.Result,-6} ({step.DurationMs}ms)");
}
}
if (showEvidence)
{
sb.AppendLine();
sb.AppendLine("Evidence Details:");
foreach (var evidence in exp.Evidence.OrderBy(e => e.Timestamp))
{
sb.AppendLine($" - Type: {evidence.Type}");
sb.AppendLine($" ID: {evidence.Id}");
sb.AppendLine($" Source: {evidence.Source}");
sb.AppendLine($" Retrieve: stella evidence get {evidence.Id}");
sb.AppendLine();
}
}
sb.AppendLine();
sb.AppendLine($"Replay: {exp.ReplayCommand}");
return sb.ToString();
}
private static string FormatBlockExplanationMarkdown(BlockExplanation exp, bool showEvidence, bool showTrace)
{
var sb = new StringBuilder();
sb.AppendLine("## Block Explanation");
sb.AppendLine();
sb.AppendLine($"**Artifact:** `{exp.Artifact}`");
sb.AppendLine($"**Status:** {exp.Status}");
sb.AppendLine();
sb.AppendLine("### Gate Decision");
sb.AppendLine();
sb.AppendLine("| Property | Value |");
sb.AppendLine("|----------|-------|");
sb.AppendLine($"| Gate | {exp.Gate} |");
sb.AppendLine($"| Reason | {exp.Reason} |");
sb.AppendLine($"| Suggestion | {exp.Suggestion} |");
sb.AppendLine($"| Policy Version | {exp.PolicyVersion} |");
sb.AppendLine();
sb.AppendLine("### Evidence");
sb.AppendLine();
sb.AppendLine("| Type | ID | Source | Timestamp |");
sb.AppendLine("|------|-----|--------|-----------|");
foreach (var evidence in exp.Evidence.OrderBy(e => e.Timestamp))
{
var truncatedId = TruncateBlockId(evidence.Id);
sb.AppendLine($"| {evidence.Type} | `{truncatedId}` | {evidence.Source} | {evidence.Timestamp:yyyy-MM-dd HH:mm} |");
}
sb.AppendLine();
if (showTrace && exp.EvaluationTrace.Count > 0)
{
sb.AppendLine("### Evaluation Trace");
sb.AppendLine();
sb.AppendLine("| Step | Gate | Result | Duration |");
sb.AppendLine("|------|------|--------|----------|");
foreach (var step in exp.EvaluationTrace.OrderBy(t => t.Step))
{
sb.AppendLine($"| {step.Step} | {step.Gate} | {step.Result} | {step.DurationMs}ms |");
}
sb.AppendLine();
}
sb.AppendLine("### Verification");
sb.AppendLine();
sb.AppendLine("```bash");
sb.AppendLine(exp.ReplayCommand);
sb.AppendLine("```");
return sb.ToString();
}
private static string TruncateBlockId(string id)
{
if (id.Length <= 20)
{
return id;
}
var prefix = id[..12];
var suffix = id[^6..];
return $"{prefix}...{suffix}";
}
#endregion
#region Test Models
@@ -934,6 +1332,98 @@ public sealed class DeterminismReplayGoldenTests
public string? Details { get; set; }
}
// Explain Block models (Sprint 026 - WHY-004)
private sealed class BlockExplanation
{
[JsonPropertyName("artifact")]
public string Artifact { get; set; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("gate")]
public string Gate { get; set; } = string.Empty;
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
[JsonPropertyName("suggestion")]
public string Suggestion { get; set; } = string.Empty;
[JsonPropertyName("evaluationTime")]
public DateTimeOffset EvaluationTime { get; set; }
[JsonPropertyName("policyVersion")]
public string PolicyVersion { get; set; } = string.Empty;
[JsonPropertyName("evidence")]
public List<BlockEvidence> Evidence { get; set; } = [];
[JsonPropertyName("replayCommand")]
public string ReplayCommand { get; set; } = string.Empty;
[JsonPropertyName("replayToken")]
public string ReplayToken { get; set; } = string.Empty;
[JsonPropertyName("evaluationTrace")]
public List<BlockTraceStep> EvaluationTrace { get; set; } = [];
[JsonPropertyName("determinismHash")]
public string DeterminismHash { get; set; } = string.Empty;
}
private sealed class BlockEvidence
{
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public DateTimeOffset Timestamp { get; set; }
}
private sealed class BlockTraceStep
{
[JsonPropertyName("step")]
public int Step { get; set; }
[JsonPropertyName("gate")]
public string Gate { get; set; } = string.Empty;
[JsonPropertyName("result")]
public string Result { get; set; } = string.Empty;
[JsonPropertyName("durationMs")]
public int DurationMs { get; set; }
}
private sealed class NotBlockedExplanation
{
[JsonPropertyName("artifact")]
public string Artifact { get; set; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
[JsonPropertyName("gatesEvaluated")]
public int GatesEvaluated { get; set; }
[JsonPropertyName("evaluationTime")]
public DateTimeOffset EvaluationTime { get; set; }
[JsonPropertyName("policyVersion")]
public string PolicyVersion { get; set; } = string.Empty;
}
#endregion
}