using FluentAssertions; using StellaOps.Policy.Replay; using Xunit; namespace StellaOps.Policy.Tests.Replay; public sealed class VerdictComparerTests { private readonly VerdictComparer _comparer = new(); [Fact] public void Compare_IdenticalVerdicts_ReturnsExactMatch() { var verdict = CreateVerdict(decision: ReplayDecision.Pass, score: 85.5m); var result = _comparer.Compare(verdict, verdict, VerdictComparisonOptions.Default); result.MatchStatus.Should().Be(ReplayMatchStatus.ExactMatch); result.IsDeterministic.Should().BeTrue(); result.DeterminismConfidence.Should().Be(1.0m); result.Differences.Should().BeEmpty(); } [Fact] public void Compare_DifferentDecisions_ReturnsMismatch() { var original = CreateVerdict(decision: ReplayDecision.Pass); var replayed = CreateVerdict(decision: ReplayDecision.Fail); var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default); result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch); result.IsDeterministic.Should().BeFalse(); result.Differences.Should().Contain(d => d.Field == "Decision"); } [Fact] public void Compare_ScoreWithinTolerance_ReturnsMatchWithinTolerance() { var original = CreateVerdict(score: 85.5000m); var replayed = CreateVerdict(score: 85.5005m); var result = _comparer.Compare(replayed, original, new VerdictComparisonOptions { ScoreTolerance = 0.001m, TreatMinorAsMatch = true }); result.MatchStatus.Should().Be(ReplayMatchStatus.MatchWithinTolerance); } [Fact] public void Compare_ScoreBeyondTolerance_ReturnsMismatch() { var original = CreateVerdict(score: 85.5m); var replayed = CreateVerdict(score: 86.0m); var result = _comparer.Compare(replayed, original, new VerdictComparisonOptions { ScoreTolerance = 0.001m, CriticalScoreTolerance = 0.1m }); result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch); result.Differences.Should().Contain(d => d.Field == "Score"); } [Fact] public void Compare_DifferentFindings_DetectsAddedAndRemoved() { var original = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002"); var replayed = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-003"); var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default); result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch); result.Differences.Should().Contain(d => d.Field == "Finding:CVE-2024-002" && d.ReplayedValue == "absent"); result.Differences.Should().Contain(d => d.Field == "Finding:CVE-2024-003" && d.OriginalValue == "absent"); } [Fact] public void Compare_SameFindings_DifferentOrder_ReturnsMatch() { var original = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002", "CVE-2024-003"); var replayed = CreateVerdictWithFindings("CVE-2024-003", "CVE-2024-001", "CVE-2024-002"); var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default); result.MatchStatus.Should().Be(ReplayMatchStatus.ExactMatch); } [Fact] public void Compare_ExtraFindings_DetectsAdditions() { var original = CreateVerdictWithFindings("CVE-2024-001"); var replayed = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002"); var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default); result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch); result.Differences.Should().ContainSingle(d => d.Field == "Finding:CVE-2024-002"); } [Fact] public void Compare_CalculatesCorrectConfidence() { var original = CreateVerdict(decision: ReplayDecision.Pass, score: 85.0m); var replayed = CreateVerdict(decision: ReplayDecision.Fail, score: 75.0m); var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default); result.DeterminismConfidence.Should().BeLessThan(1.0m); result.DeterminismConfidence.Should().BeGreaterThanOrEqualTo(0m); } private static ReplayedVerdict CreateVerdict( ReplayDecision decision = ReplayDecision.Pass, decimal score = 85.0m) => new() { ArtifactDigest = "sha256:test123", Decision = decision, Score = score, FindingIds = [] }; private static ReplayedVerdict CreateVerdictWithFindings(params string[] findingIds) => new() { ArtifactDigest = "sha256:test123", Decision = ReplayDecision.Pass, Score = 85.0m, FindingIds = findingIds.ToList() }; }