// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors. // Sprint: SPRINT_20260110_012_008_POLICY // Task: FCG-004 - FixChain Gate Unit Tests using System.Collections.Immutable; using Moq; using StellaOps.Policy.Confidence.Models; using StellaOps.Policy.Gates; using StellaOps.Policy.TrustLattice; using Xunit; namespace StellaOps.Policy.Tests.Gates; /// /// Unit tests for evaluation scenarios. /// [Trait("Category", "Unit")] public sealed class FixChainGateTests { private readonly Mock _timeProviderMock; private readonly DateTimeOffset _fixedTime = new(2026, 1, 11, 12, 0, 0, TimeSpan.Zero); private readonly FixChainGateOptions _defaultOptions; public FixChainGateTests() { _timeProviderMock = new Mock(); _timeProviderMock.Setup(t => t.GetUtcNow()).Returns(_fixedTime); _defaultOptions = new FixChainGateOptions { Enabled = true, RequiredSeverities = ["critical", "high"], MinimumConfidence = 0.85m, AllowInconclusive = false, GracePeriodDays = 7 }; } private FixChainGate CreateGate(FixChainGateOptions? options = null) { return new FixChainGate(options ?? _defaultOptions, _timeProviderMock.Object); } [Fact] public async Task EvaluateAsync_WhenDisabled_ReturnsPass() { // Arrange var options = new FixChainGateOptions { Enabled = false }; var gate = CreateGate(options); var context = CreatePolicyContext("critical"); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("disabled", result.Reason); } [Fact] public async Task EvaluateAsync_LowSeverity_ReturnsPass() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("low"); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("does not require", result.Reason); } [Fact] public async Task EvaluateAsync_MediumSeverity_ReturnsPass() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("medium"); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); } [Fact] public async Task EvaluateAsync_CriticalSeverity_NoAttestation_ReturnsFail() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("critical"); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("no FixChain attestation", result.Reason); } [Fact] public async Task EvaluateAsync_HighSeverity_NoAttestation_ReturnsFail() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("high"); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); } [Fact] public async Task EvaluateAsync_CriticalSeverity_WithFixedAttestation_ReturnsPass() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "fixed", ["fixchain.confidence"] = "0.95" }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("verified", result.Reason); } [Fact] public async Task EvaluateAsync_FixedVerdict_BelowMinConfidence_ReturnsFail() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "fixed", ["fixchain.confidence"] = "0.70" // Below 0.85 minimum }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("below minimum", result.Reason); } [Fact] public async Task EvaluateAsync_PartialVerdict_ReturnsFail() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "partial", ["fixchain.confidence"] = "0.90" }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("Partial fix", result.Reason); } [Fact] public async Task EvaluateAsync_NotFixedVerdict_ReturnsFail() { // Arrange var gate = CreateGate(); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "not_fixed", ["fixchain.confidence"] = "0.95" }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("NOT fixed", result.Reason); } [Fact] public async Task EvaluateAsync_InconclusiveVerdict_WhenNotAllowed_ReturnsFail() { // Arrange var options = _defaultOptions with { AllowInconclusive = false }; var gate = CreateGate(options); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "inconclusive", ["fixchain.confidence"] = "0.50" }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("inconclusive", result.Reason); } [Fact] public async Task EvaluateAsync_InconclusiveVerdict_WhenAllowed_ReturnsPass() { // Arrange var options = _defaultOptions with { AllowInconclusive = true }; var gate = CreateGate(options); var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "inconclusive", ["fixchain.confidence"] = "0.50" }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("allowed by policy", result.Reason); } [Fact] public async Task EvaluateAsync_WithinGracePeriod_ReturnsPass() { // Arrange var gate = CreateGate(); var cvePublished = _fixedTime.AddDays(-3); // 3 days ago, within 7-day grace var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.cvePublishedAt"] = cvePublished.ToString("o") }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("grace period", result.Reason); } [Fact] public async Task EvaluateAsync_AfterGracePeriod_NoAttestation_ReturnsFail() { // Arrange var gate = CreateGate(); var cvePublished = _fixedTime.AddDays(-10); // 10 days ago, past 7-day grace var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.cvePublishedAt"] = cvePublished.ToString("o") }); var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); } [Fact] public void EvaluateDirect_FixedVerdict_ReturnsAllow() { // Arrange var gate = CreateGate(); var fixChainContext = new FixChainGateContext { HasAttestation = true, Verdict = "fixed", Confidence = 0.95m, VerifiedAt = _fixedTime, AttestationDigest = "sha256:test" }; // Act var result = gate.EvaluateDirect(fixChainContext, "production", "critical"); // Assert Assert.True(result.Passed); Assert.Equal(FixChainGateResult.DecisionAllow, result.Decision); } [Fact] public void EvaluateDirect_NoAttestation_ReturnsBlock() { // Arrange var gate = CreateGate(); var fixChainContext = new FixChainGateContext { HasAttestation = false }; // Act var result = gate.EvaluateDirect(fixChainContext, "production", "critical"); // Assert Assert.False(result.Passed); Assert.Equal(FixChainGateResult.DecisionBlock, result.Decision); } [Fact] public void EvaluateDirect_InconclusiveAllowed_ReturnsWarn() { // Arrange var options = _defaultOptions with { AllowInconclusive = true }; var gate = CreateGate(options); var fixChainContext = new FixChainGateContext { HasAttestation = true, Verdict = "inconclusive", Confidence = 0.50m }; // Act var result = gate.EvaluateDirect(fixChainContext, "production", "critical"); // Assert Assert.True(result.Passed); Assert.Equal(FixChainGateResult.DecisionAllow, result.Decision); } [Fact] public async Task EvaluateAsync_ProductionEnvironment_UsesHigherConfidence() { // Arrange var options = new FixChainGateOptions { Enabled = true, RequiredSeverities = ["critical"], MinimumConfidence = 0.70m, EnvironmentConfidence = new Dictionary { ["production"] = 0.95m, ["staging"] = 0.80m } }; var gate = CreateGate(options); // Context with 0.90 confidence - passes staging but not production var context = CreatePolicyContext("critical", new Dictionary { ["fixchain.hasAttestation"] = "true", ["fixchain.verdict"] = "fixed", ["fixchain.confidence"] = "0.90" }); context = context with { Environment = "production" }; var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.False(result.Passed); Assert.Contains("below minimum", result.Reason); } [Fact] public async Task EvaluateAsync_DevelopmentEnvironment_NoRequirements() { // Arrange var options = new FixChainGateOptions { Enabled = true, RequiredSeverities = ["critical"], EnvironmentSeverities = new Dictionary> { ["production"] = ["critical", "high"], ["development"] = [] // No requirements } }; var gate = CreateGate(options); var context = CreatePolicyContext("critical") with { Environment = "development" }; var mergeResult = CreateMergeResult(); // Act var result = await gate.EvaluateAsync(mergeResult, context); // Assert Assert.True(result.Passed); Assert.Contains("No severity requirements", result.Reason); } [Fact] public void Options_DefaultValues_AreReasonable() { // Arrange & Act var options = new FixChainGateOptions(); // Assert Assert.True(options.Enabled); Assert.Contains("critical", options.RequiredSeverities); Assert.Contains("high", options.RequiredSeverities); Assert.True(options.MinimumConfidence >= 0.80m); Assert.False(options.AllowInconclusive); Assert.True(options.GracePeriodDays > 0); } private static PolicyGateContext CreatePolicyContext( string severity, Dictionary? metadata = null) { return new PolicyGateContext { Environment = "production", Severity = severity, CveId = "CVE-2024-1234", SubjectKey = "pkg:generic/test@1.0.0", Metadata = metadata }; } private static MergeResult CreateMergeResult() { var emptyClaim = new ScoredClaim { SourceId = "test", Status = VexStatus.Affected, OriginalScore = 1.0, AdjustedScore = 1.0, ScopeSpecificity = 1, Accepted = true, Reason = "test" }; return new MergeResult { Status = VexStatus.Affected, Confidence = 0.9, HasConflicts = false, AllClaims = [emptyClaim], WinningClaim = emptyClaim, Conflicts = [] }; } }