// ----------------------------------------------------------------------------- // DeterminismReplayGoldenTests.cs // Sprint: SPRINT_20260117_014_CLI_determinism_replay // Task: DRP-004 - Golden file tests for replay verification // Description: Golden output tests for HLC, Timeline, and Score Explain commands // ----------------------------------------------------------------------------- using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using FluentAssertions; using StellaOps.TestKit; using Xunit; namespace StellaOps.Cli.Tests.GoldenOutput; /// /// Golden output tests for determinism and replay CLI commands. /// Verifies that HLC status, timeline query, and score explain /// produce consistent, deterministic outputs matching frozen snapshots. /// Task: DRP-004 /// /// HOW TO UPDATE GOLDEN FILES: /// 1. Run tests to identify failures /// 2. Review the actual output carefully to ensure changes are intentional /// 3. Update the expected golden snapshot in this file /// 4. Document the reason for the change in the commit message /// [Trait("Category", TestCategories.Unit)] [Trait("Category", "GoldenOutput")] [Trait("Category", "Determinism")] [Trait("Sprint", "20260117-014")] public sealed class DeterminismReplayGoldenTests { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Fixed timestamp for deterministic tests private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero); #region HLC Status Golden Tests (DRP-001) /// /// Verifies that HLC status JSON output matches golden snapshot. /// [Fact] public void HlcStatus_Json_MatchesGolden() { // Arrange var status = CreateFrozenHlcStatus(); // Act var actual = JsonSerializer.Serialize(status, JsonOptions).NormalizeLf(); // Assert - Golden snapshot var expected = """ { "nodeId": "node-01", "healthy": true, "currentTimestamp": { "physical": 1736937000000, "logical": 42, "nodeId": "node-01" }, "formattedTimestamp": "2026-01-15T10:30:00.000Z:0042:node-01", "clockDriftMs": 3.2, "ntpServer": "time.google.com", "lastNtpSync": "2026-01-15T10:25:00+00:00", "clusterState": { "totalNodes": 3, "syncedNodes": 3, "peers": [ { "nodeId": "node-01", "status": "synced", "lastSeen": "2026-01-15T10:30:00+00:00", "driftMs": 0 }, { "nodeId": "node-02", "status": "synced", "lastSeen": "2026-01-15T10:29:58+00:00", "driftMs": 1.5 }, { "nodeId": "node-03", "status": "synced", "lastSeen": "2026-01-15T10:29:55+00:00", "driftMs": 2.8 } ] }, "checkedAt": "2026-01-15T10:30:00+00:00" } """.NormalizeLf(); actual.Should().Be(expected); } /// /// Verifies that HLC status text output matches golden snapshot. /// [Fact] public void HlcStatus_Text_MatchesGolden() { // Arrange var status = CreateFrozenHlcStatus(); // Act var actual = FormatHlcStatusText(status, verbose: false).NormalizeLf(); // Assert - Golden snapshot var expected = """ HLC Node Status =============== Health: [OK] Healthy Node ID: node-01 HLC Timestamp: 2026-01-15T10:30:00.000Z:0042:node-01 Clock Drift: 3.2 ms NTP Server: time.google.com Last NTP Sync: 2026-01-15 10:25:00Z Cluster State: Nodes: 3/3 synced Checked At: 2026-01-15 10:30:00Z """.NormalizeLf(); actual.Trim().Should().Be(expected.Trim()); } /// /// Verifies that HLC status verbose text output matches golden snapshot. /// [Fact] public void HlcStatus_TextVerbose_MatchesGolden() { // Arrange var status = CreateFrozenHlcStatus(); // Act var actual = FormatHlcStatusText(status, verbose: true).NormalizeLf(); // Assert - Should contain peer table actual.Should().Contain("Peer Status:"); actual.Should().Contain("node-01"); actual.Should().Contain("node-02"); actual.Should().Contain("node-03"); actual.Should().Contain("synced"); } /// /// Verifies that HLC status produces consistent output across multiple runs. /// [Fact] public void HlcStatus_SameInputs_ProducesIdenticalOutput() { // Arrange var status1 = CreateFrozenHlcStatus(); var status2 = CreateFrozenHlcStatus(); // Act var json1 = JsonSerializer.Serialize(status1, JsonOptions); var json2 = JsonSerializer.Serialize(status2, JsonOptions); // Assert json1.Should().Be(json2); } #endregion #region Timeline Query Golden Tests (DRP-002) /// /// Verifies that timeline query JSON output matches golden snapshot. /// [Fact] public void TimelineQuery_Json_MatchesGolden() { // Arrange var result = CreateFrozenTimelineResult(); // Act var actual = JsonSerializer.Serialize(result, JsonOptions).NormalizeLf(); // Assert - Golden snapshot var expected = """ { "events": [ { "hlcTimestamp": "1737000000000000001", "type": "scan", "entityId": "sha256:abc123def456", "actor": "scanner-agent-1", "details": "SBOM generated" }, { "hlcTimestamp": "1737000000000000002", "type": "attest", "entityId": "sha256:abc123def456", "actor": "attestor-1", "details": "SLSA provenance created" }, { "hlcTimestamp": "1737000000000000003", "type": "policy", "entityId": "sha256:abc123def456", "actor": "policy-engine", "details": "Policy evaluation: PASS" }, { "hlcTimestamp": "1737000000000000004", "type": "promote", "entityId": "release-2026.01.15-001", "actor": "ops@example.com", "details": "Promoted from dev to stage" } ], "pagination": { "offset": 0, "limit": 50, "total": 4, "hasMore": false }, "determinismHash": "sha256:a1b2c3d4e5f67890" } """.NormalizeLf(); actual.Should().Be(expected); } /// /// Verifies that timeline query table output matches golden snapshot. /// [Fact] public void TimelineQuery_Table_MatchesGolden() { // Arrange var events = CreateFrozenTimelineEvents(); // Act var actual = FormatTimelineTable(events).NormalizeLf(); // Assert - Golden snapshot header actual.Should().Contain("Timeline Events"); actual.Should().Contain("HLC Timestamp"); actual.Should().Contain("Type"); actual.Should().Contain("Entity"); actual.Should().Contain("Actor"); // Events should appear in HLC timestamp order var scanIndex = actual.IndexOf("scan"); var attestIndex = actual.IndexOf("attest"); var policyIndex = actual.IndexOf("policy"); var promoteIndex = actual.IndexOf("promote"); scanIndex.Should().BeLessThan(attestIndex); attestIndex.Should().BeLessThan(policyIndex); policyIndex.Should().BeLessThan(promoteIndex); } /// /// Verifies that timeline events are sorted by HLC timestamp. /// [Fact] public void TimelineQuery_EventsAreSortedByHlcTimestamp() { // Arrange - Events in random order var events = new List { new() { HlcTimestamp = "1737000000000000004", Type = "promote", EntityId = "release-001", Actor = "ops", Details = "Promoted" }, new() { HlcTimestamp = "1737000000000000001", Type = "scan", EntityId = "sha256:abc", Actor = "scanner", Details = "Scanned" }, new() { HlcTimestamp = "1737000000000000003", Type = "policy", EntityId = "sha256:abc", Actor = "policy", Details = "Evaluated" }, new() { HlcTimestamp = "1737000000000000002", Type = "attest", EntityId = "sha256:abc", Actor = "attestor", Details = "Attested" } }; // Act - Sort as timeline query would var sorted = events.OrderBy(e => e.HlcTimestamp).ToList(); // Assert - Events should be in ascending HLC timestamp order sorted[0].Type.Should().Be("scan"); sorted[1].Type.Should().Be("attest"); sorted[2].Type.Should().Be("policy"); sorted[3].Type.Should().Be("promote"); } /// /// Verifies that timeline determinism hash is consistent. /// [Fact] public void TimelineQuery_DeterminismHashIsConsistent() { // Arrange var events1 = CreateFrozenTimelineEvents(); var events2 = CreateFrozenTimelineEvents(); // Act var hash1 = ComputeTimelineDeterminismHash(events1); var hash2 = ComputeTimelineDeterminismHash(events2); // Assert hash1.Should().Be(hash2); hash1.Should().StartWith("sha256:"); } #endregion #region Score Explain Golden Tests (DRP-003) /// /// Verifies that score explain JSON output matches golden snapshot. /// [Fact] public void ScoreExplain_Json_MatchesGolden() { // Arrange var explanation = CreateFrozenScoreExplanation(); EnsureScoreExplanationDeterminism(explanation); // Act var actual = JsonSerializer.Serialize(explanation, JsonOptions).NormalizeLf(); // Assert - Golden snapshot var expected = """ { "digest": "sha256:abc123def456789012345678901234567890123456789012345678901234", "finalScore": 7.500000, "scoreBreakdown": { "baseScore": 8.100000, "cvssScore": 8.100000, "epssAdjustment": -0.300000, "reachabilityAdjustment": -0.200000, "vexAdjustment": -0.100000, "factors": [ { "name": "CVSS Base Score", "value": 8.100000, "weight": 0.400000, "contribution": 3.240000, "source": "NVD", "details": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, { "name": "EPSS Probability", "value": 0.150000, "weight": 0.200000, "contribution": 1.500000, "source": "FIRST EPSS", "details": "15th percentile exploitation probability" }, { "name": "KEV Status", "value": 0.000000, "weight": 0.050000, "contribution": 0.000000, "source": "CISA KEV", "details": "Not in Known Exploited Vulnerabilities catalog" }, { "name": "Reachability", "value": 0.700000, "weight": 0.250000, "contribution": 1.750000, "source": "Static Analysis", "details": "Reachable via 2 call paths; confidence 0.7" }, { "name": "VEX Status", "value": 0.000000, "weight": 0.100000, "contribution": 0.000000, "source": "OpenVEX", "details": "No VEX statement available" } ] }, "computedAt": "2026-01-15T10:30:00+00:00", "profileUsed": "stella-default-v1", "determinismHash": "sha256:b3c4d5e6f7a89012" } """.NormalizeLf(); actual.Should().Be(expected); } /// /// Verifies that score explain factors are sorted alphabetically. /// [Fact] public void ScoreExplain_FactorsAreSortedAlphabetically() { // Arrange - Create explanation with unsorted factors var explanation = CreateFrozenScoreExplanation(); // Act EnsureScoreExplanationDeterminism(explanation); // Assert - Factors should be sorted by name var factorNames = explanation.ScoreBreakdown.Factors.Select(f => f.Name).ToList(); factorNames.Should().BeInAscendingOrder(); } /// /// Verifies that floating-point values have stable 6-decimal precision. /// [Fact] public void ScoreExplain_FloatingPointValuesHaveStablePrecision() { // Arrange var explanation = CreateFrozenScoreExplanation(); EnsureScoreExplanationDeterminism(explanation); // Act var json = JsonSerializer.Serialize(explanation, JsonOptions); // Assert - Values should have 6 decimal places json.Should().Contain("7.500000"); json.Should().Contain("8.100000"); json.Should().Contain("-0.300000"); json.Should().Contain("-0.200000"); json.Should().Contain("-0.100000"); } /// /// Verifies that score explain determinism hash is consistent. /// [Fact] public void ScoreExplain_DeterminismHashIsConsistent() { // Arrange var exp1 = CreateFrozenScoreExplanation(); var exp2 = CreateFrozenScoreExplanation(); // Act EnsureScoreExplanationDeterminism(exp1); EnsureScoreExplanationDeterminism(exp2); // Assert exp1.DeterminismHash.Should().Be(exp2.DeterminismHash); exp1.DeterminismHash.Should().StartWith("sha256:"); exp1.DeterminismHash.Should().HaveLength(24); // "sha256:" + 16 hex chars } /// /// Verifies that same inputs produce identical outputs (byte-for-byte). /// [Fact] public void ScoreExplain_SameInputs_ProducesIdenticalOutput() { // Arrange var exp1 = CreateFrozenScoreExplanation(); var exp2 = CreateFrozenScoreExplanation(); // Act EnsureScoreExplanationDeterminism(exp1); EnsureScoreExplanationDeterminism(exp2); var json1 = JsonSerializer.Serialize(exp1, JsonOptions); var json2 = JsonSerializer.Serialize(exp2, JsonOptions); // Assert json1.Should().Be(json2); } /// /// Verifies that different inputs produce different determinism hashes. /// [Fact] public void ScoreExplain_DifferentInputs_ProducesDifferentHash() { // Arrange var exp1 = CreateFrozenScoreExplanation(); var exp2 = CreateFrozenScoreExplanation(); exp2.FinalScore = 8.0; // Different score // Act EnsureScoreExplanationDeterminism(exp1); EnsureScoreExplanationDeterminism(exp2); // Assert exp1.DeterminismHash.Should().NotBe(exp2.DeterminismHash); } #endregion #region Explain Block Golden Tests (Sprint 026 - WHY-004) /// /// Verifies that explain block JSON output matches golden snapshot. /// Sprint: SPRINT_20260117_026_CLI_why_blocked_command /// [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); } /// /// Verifies that explain block table output matches golden snapshot. /// [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()); } /// /// Verifies that explain block markdown output matches golden snapshot. /// [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"); } /// /// Verifies that explain block with --show-trace includes evaluation trace. /// [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"); } /// /// Verifies that same inputs produce identical outputs (byte-for-byte). /// M2 moat requirement: Deterministic trace + referenced evidence artifacts. /// [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"); } /// /// Verifies that evidence is sorted by timestamp for deterministic ordering. /// [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(); } /// /// Verifies that evaluation trace is sorted by step number. /// [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(); } /// /// Verifies that not-blocked artifacts produce deterministic output. /// [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 /// /// Verifies that JSON output uses consistent line endings (LF). /// [Fact] public void AllOutputs_UseConsistentLineEndings() { // Arrange var hlcStatus = CreateFrozenHlcStatus(); var timeline = CreateFrozenTimelineResult(); var score = CreateFrozenScoreExplanation(); // Act var hlcJson = JsonSerializer.Serialize(hlcStatus, JsonOptions); var timelineJson = JsonSerializer.Serialize(timeline, JsonOptions); var scoreJson = JsonSerializer.Serialize(score, JsonOptions); // Assert - Should not contain CRLF hlcJson.Should().NotContain("\r\n"); timelineJson.Should().NotContain("\r\n"); scoreJson.Should().NotContain("\r\n"); } /// /// Verifies that timestamps use ISO 8601 format with UTC. /// [Fact] public void AllOutputs_TimestampsAreIso8601Utc() { // Arrange var hlcStatus = CreateFrozenHlcStatus(); var score = CreateFrozenScoreExplanation(); // Act var hlcJson = JsonSerializer.Serialize(hlcStatus, JsonOptions); var scoreJson = JsonSerializer.Serialize(score, JsonOptions); // Assert - Timestamps should be ISO 8601 with UTC offset hlcJson.Should().MatchRegex(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00"); scoreJson.Should().MatchRegex(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00"); } /// /// Verifies that digests are lowercase hex. /// [Fact] public void AllOutputs_DigestsAreLowercaseHex() { // Arrange var score = CreateFrozenScoreExplanation(); EnsureScoreExplanationDeterminism(score); // Act var json = JsonSerializer.Serialize(score, JsonOptions); // Assert - Digests should be lowercase json.Should().Contain("sha256:abc123def456"); json.Should().NotMatchRegex("sha256:[A-F]"); } #endregion #region Test Helpers private static HlcStatus CreateFrozenHlcStatus() { return new HlcStatus { NodeId = "node-01", Healthy = true, CurrentTimestamp = new HlcTimestamp { Physical = 1736937000000, Logical = 42, NodeId = "node-01" }, FormattedTimestamp = "2026-01-15T10:30:00.000Z:0042:node-01", ClockDriftMs = 3.2, NtpServer = "time.google.com", LastNtpSync = FixedTimestamp.AddMinutes(-5), ClusterState = new HlcClusterState { TotalNodes = 3, SyncedNodes = 3, Peers = [ new HlcPeerStatus { NodeId = "node-01", Status = "synced", LastSeen = FixedTimestamp, DriftMs = 0 }, new HlcPeerStatus { NodeId = "node-02", Status = "synced", LastSeen = FixedTimestamp.AddSeconds(-2), DriftMs = 1.5 }, new HlcPeerStatus { NodeId = "node-03", Status = "synced", LastSeen = FixedTimestamp.AddSeconds(-5), DriftMs = 2.8 } ] }, CheckedAt = FixedTimestamp }; } private static string FormatHlcStatusText(HlcStatus status, bool verbose) { var sb = new StringBuilder(); sb.AppendLine("HLC Node Status"); sb.AppendLine("==============="); sb.AppendLine(); var healthStatus = status.Healthy ? "[OK] Healthy" : "[FAIL] Unhealthy"; sb.AppendLine($"Health: {healthStatus}"); sb.AppendLine($"Node ID: {status.NodeId}"); sb.AppendLine($"HLC Timestamp: {status.FormattedTimestamp}"); sb.AppendLine($"Clock Drift: {status.ClockDriftMs} ms"); sb.AppendLine($"NTP Server: {status.NtpServer}"); sb.AppendLine($"Last NTP Sync: {status.LastNtpSync:yyyy-MM-dd HH:mm:ssZ}"); sb.AppendLine(); sb.AppendLine("Cluster State:"); sb.AppendLine($" Nodes: {status.ClusterState.SyncedNodes}/{status.ClusterState.TotalNodes} synced"); if (verbose && status.ClusterState.Peers.Count > 0) { sb.AppendLine(); sb.AppendLine("Peer Status:"); foreach (var peer in status.ClusterState.Peers) { sb.AppendLine($" {peer.NodeId}: {peer.Status} (drift: {peer.DriftMs} ms)"); } } sb.AppendLine(); sb.AppendLine($"Checked At: {status.CheckedAt:yyyy-MM-dd HH:mm:ssZ}"); return sb.ToString(); } private static List CreateFrozenTimelineEvents() { return [ new TimelineEvent { HlcTimestamp = "1737000000000000001", Type = "scan", EntityId = "sha256:abc123def456", Actor = "scanner-agent-1", Details = "SBOM generated" }, new TimelineEvent { HlcTimestamp = "1737000000000000002", Type = "attest", EntityId = "sha256:abc123def456", Actor = "attestor-1", Details = "SLSA provenance created" }, new TimelineEvent { HlcTimestamp = "1737000000000000003", Type = "policy", EntityId = "sha256:abc123def456", Actor = "policy-engine", Details = "Policy evaluation: PASS" }, new TimelineEvent { HlcTimestamp = "1737000000000000004", Type = "promote", EntityId = "release-2026.01.15-001", Actor = "ops@example.com", Details = "Promoted from dev to stage" } ]; } private static TimelineQueryResult CreateFrozenTimelineResult() { var events = CreateFrozenTimelineEvents(); return new TimelineQueryResult { Events = events, Pagination = new PaginationInfo { Offset = 0, Limit = 50, Total = events.Count, HasMore = false }, DeterminismHash = ComputeTimelineDeterminismHash(events) }; } private static string FormatTimelineTable(List events) { var sb = new StringBuilder(); sb.AppendLine("Timeline Events"); sb.AppendLine("==============="); sb.AppendLine(); sb.AppendLine($"{"HLC Timestamp",-28} {"Type",-12} {"Entity",-25} {"Actor"}"); sb.AppendLine(new string('-', 90)); foreach (var evt in events.OrderBy(e => e.HlcTimestamp)) { var entityTrunc = evt.EntityId.Length > 23 ? evt.EntityId[..23] + ".." : evt.EntityId; sb.AppendLine($"{evt.HlcTimestamp,-28} {evt.Type,-12} {entityTrunc,-25} {evt.Actor}"); } sb.AppendLine(); sb.AppendLine($"Total: {events.Count} events"); return sb.ToString(); } private static string ComputeTimelineDeterminismHash(IEnumerable events) { var combined = string.Join("|", events.OrderBy(e => e.HlcTimestamp).Select(e => $"{e.HlcTimestamp}:{e.Type}:{e.EntityId}")); var hash = SHA256.HashData(Encoding.UTF8.GetBytes(combined)); return $"sha256:{Convert.ToHexStringLower(hash)[..16]}"; } private static ScoreExplanation CreateFrozenScoreExplanation() { return new ScoreExplanation { Digest = "sha256:abc123def456789012345678901234567890123456789012345678901234", FinalScore = 7.5, ScoreBreakdown = new ScoreBreakdown { BaseScore = 8.1, CvssScore = 8.1, EpssAdjustment = -0.3, ReachabilityAdjustment = -0.2, VexAdjustment = -0.1, Factors = [ new ScoreFactor { Name = "CVSS Base Score", Value = 8.1, Weight = 0.4, Contribution = 3.24, Source = "NVD", Details = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" }, new ScoreFactor { Name = "EPSS Probability", Value = 0.15, Weight = 0.2, Contribution = 1.5, Source = "FIRST EPSS", Details = "15th percentile exploitation probability" }, new ScoreFactor { Name = "Reachability", Value = 0.7, Weight = 0.25, Contribution = 1.75, Source = "Static Analysis", Details = "Reachable via 2 call paths; confidence 0.7" }, new ScoreFactor { Name = "VEX Status", Value = 0, Weight = 0.1, Contribution = 0, Source = "OpenVEX", Details = "No VEX statement available" }, new ScoreFactor { Name = "KEV Status", Value = 0, Weight = 0.05, Contribution = 0, Source = "CISA KEV", Details = "Not in Known Exploited Vulnerabilities catalog" } ] }, ComputedAt = FixedTimestamp, ProfileUsed = "stella-default-v1" }; } private static void EnsureScoreExplanationDeterminism(ScoreExplanation explanation) { // Sort factors alphabetically by name for deterministic output explanation.ScoreBreakdown.Factors = [.. explanation.ScoreBreakdown.Factors.OrderBy(f => f.Name, StringComparer.Ordinal)]; // Compute determinism hash from stable representation var hashInput = $"{explanation.Digest}|{explanation.FinalScore:F6}|{explanation.ProfileUsed}|{string.Join(",", explanation.ScoreBreakdown.Factors.Select(f => $"{f.Name}:{f.Value:F6}:{f.Weight:F6}"))}"; var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(hashInput)); 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 private sealed class HlcStatus { [JsonPropertyName("nodeId")] public string NodeId { get; set; } = string.Empty; [JsonPropertyName("healthy")] public bool Healthy { get; set; } [JsonPropertyName("currentTimestamp")] public HlcTimestamp CurrentTimestamp { get; set; } = new(); [JsonPropertyName("formattedTimestamp")] public string FormattedTimestamp { get; set; } = string.Empty; [JsonPropertyName("clockDriftMs")] public double ClockDriftMs { get; set; } [JsonPropertyName("ntpServer")] public string NtpServer { get; set; } = string.Empty; [JsonPropertyName("lastNtpSync")] public DateTimeOffset LastNtpSync { get; set; } [JsonPropertyName("clusterState")] public HlcClusterState ClusterState { get; set; } = new(); [JsonPropertyName("checkedAt")] public DateTimeOffset CheckedAt { get; set; } } private sealed class HlcTimestamp { [JsonPropertyName("physical")] public long Physical { get; set; } [JsonPropertyName("logical")] public int Logical { get; set; } [JsonPropertyName("nodeId")] public string NodeId { get; set; } = string.Empty; } private sealed class HlcClusterState { [JsonPropertyName("totalNodes")] public int TotalNodes { get; set; } [JsonPropertyName("syncedNodes")] public int SyncedNodes { get; set; } [JsonPropertyName("peers")] public List Peers { get; set; } = []; } private sealed class HlcPeerStatus { [JsonPropertyName("nodeId")] public string NodeId { get; set; } = string.Empty; [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; [JsonPropertyName("lastSeen")] public DateTimeOffset LastSeen { get; set; } [JsonPropertyName("driftMs")] public double DriftMs { get; set; } } private sealed class TimelineQueryResult { [JsonPropertyName("events")] public List Events { get; set; } = []; [JsonPropertyName("pagination")] public PaginationInfo Pagination { get; set; } = new(); [JsonPropertyName("determinismHash")] public string DeterminismHash { get; set; } = string.Empty; } private sealed class PaginationInfo { [JsonPropertyName("offset")] public int Offset { get; set; } [JsonPropertyName("limit")] public int Limit { get; set; } [JsonPropertyName("total")] public int Total { get; set; } [JsonPropertyName("hasMore")] public bool HasMore { get; set; } } private sealed class TimelineEvent { [JsonPropertyName("hlcTimestamp")] public string HlcTimestamp { get; set; } = string.Empty; [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; [JsonPropertyName("entityId")] public string EntityId { get; set; } = string.Empty; [JsonPropertyName("actor")] public string Actor { get; set; } = string.Empty; [JsonPropertyName("details")] public string Details { get; set; } = string.Empty; } private sealed class ScoreExplanation { [JsonPropertyName("digest")] public string Digest { get; set; } = string.Empty; [JsonPropertyName("finalScore")] public double FinalScore { get; set; } [JsonPropertyName("scoreBreakdown")] public ScoreBreakdown ScoreBreakdown { get; set; } = new(); [JsonPropertyName("computedAt")] public DateTimeOffset ComputedAt { get; set; } [JsonPropertyName("profileUsed")] public string ProfileUsed { get; set; } = string.Empty; [JsonPropertyName("determinismHash")] public string? DeterminismHash { get; set; } } private sealed class ScoreBreakdown { [JsonPropertyName("baseScore")] public double BaseScore { get; set; } [JsonPropertyName("cvssScore")] public double CvssScore { get; set; } [JsonPropertyName("epssAdjustment")] public double EpssAdjustment { get; set; } [JsonPropertyName("reachabilityAdjustment")] public double ReachabilityAdjustment { get; set; } [JsonPropertyName("vexAdjustment")] public double VexAdjustment { get; set; } [JsonPropertyName("factors")] public List Factors { get; set; } = []; } private sealed class ScoreFactor { [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("value")] public double Value { get; set; } [JsonPropertyName("weight")] public double Weight { get; set; } [JsonPropertyName("contribution")] public double Contribution { get; set; } [JsonPropertyName("source")] public string Source { get; set; } = string.Empty; [JsonPropertyName("details")] 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 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 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 } /// /// Extension methods for string normalization in golden tests. /// internal static class GoldenTestStringExtensions { /// /// Normalize line endings to LF for cross-platform consistency. /// public static string NormalizeLf(this string input) { return input.Replace("\r\n", "\n"); } }