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:
master
2026-01-11 10:09:07 +02:00
parent a3b2f30a11
commit 7f7eb8b228
232 changed files with 58979 additions and 91 deletions

View File

@@ -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 = []
};
}
}