Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors
Sprints completed: - SPRINT_20260110_012_* (golden set diff layer - 10 sprints) - SPRINT_20260110_013_* (advisory chat - 4 sprints) Build fixes applied: - Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create - Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite) - Fix VexSchemaValidationTests FluentAssertions method name - Fix FixChainGateIntegrationTests ambiguous type references - Fix AdvisoryAI test files required properties and namespace aliases - Add stub types for CveMappingController (ICveSymbolMappingService) - Fix VerdictBuilderService static context issue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
// Licensed under AGPL-3.0-or-later. 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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FixChainGate"/> evaluation scenarios.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FixChainGateTests
|
||||
{
|
||||
private readonly Mock<TimeProvider> _timeProviderMock;
|
||||
private readonly DateTimeOffset _fixedTime = new(2026, 1, 11, 12, 0, 0, TimeSpan.Zero);
|
||||
private readonly FixChainGateOptions _defaultOptions;
|
||||
|
||||
public FixChainGateTests()
|
||||
{
|
||||
_timeProviderMock = new Mock<TimeProvider>();
|
||||
_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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, decimal>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, IReadOnlyList<string>>
|
||||
{
|
||||
["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<string, string>? 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 = []
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user