Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Confidence.Configuration;
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Confidence.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Confidence;
|
||||
|
||||
public sealed class ConfidenceCalculatorTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2025, 12, 22, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void Calculate_AllHighFactors_ReturnsVeryHighConfidence()
|
||||
{
|
||||
var calculator = CreateCalculator();
|
||||
var input = CreateInput(
|
||||
reachability: ReachabilityState.ConfirmedUnreachable,
|
||||
runtime: RuntimePosture.Supports,
|
||||
vex: VexStatus.NotAffected,
|
||||
provenance: ProvenanceLevel.SlsaLevel3,
|
||||
policyStrength: 1.0m);
|
||||
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
result.Tier.Should().Be(ConfidenceTier.VeryHigh);
|
||||
result.Value.Should().BeGreaterThanOrEqualTo(0.9m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_AllLowFactors_ReturnsLowConfidence()
|
||||
{
|
||||
var calculator = CreateCalculator();
|
||||
var input = CreateInput(
|
||||
reachability: ReachabilityState.Unknown,
|
||||
runtime: RuntimePosture.Contradicts,
|
||||
vex: VexStatus.UnderInvestigation,
|
||||
provenance: ProvenanceLevel.Unsigned,
|
||||
policyStrength: 0.3m);
|
||||
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
result.Tier.Should().Be(ConfidenceTier.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_MissingEvidence_UsesFallbackValues()
|
||||
{
|
||||
var calculator = CreateCalculator();
|
||||
var input = new ConfidenceInput();
|
||||
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
result.Value.Should().BeApproximately(0.47m, 0.05m);
|
||||
result.Factors.Should().AllSatisfy(f => f.Reason.Should().Contain("No"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_GeneratesImprovements_ForLowFactors()
|
||||
{
|
||||
var calculator = CreateCalculator();
|
||||
var input = CreateInput(reachability: ReachabilityState.Unknown);
|
||||
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
result.Improvements.Should().Contain(i => i.Factor == ConfidenceFactorType.Reachability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WeightsSumToOne()
|
||||
{
|
||||
var options = new ConfidenceWeightOptions();
|
||||
|
||||
options.Validate().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_FactorContributions_SumToValue()
|
||||
{
|
||||
var calculator = CreateCalculator();
|
||||
var input = CreateInput();
|
||||
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
var sumOfContributions = result.Factors.Sum(f => f.Contribution);
|
||||
result.Value.Should().BeApproximately(sumOfContributions, 0.001m);
|
||||
}
|
||||
|
||||
private static ConfidenceInput CreateInput(
|
||||
ReachabilityState reachability = ReachabilityState.StaticUnreachable,
|
||||
RuntimePosture runtime = RuntimePosture.Supports,
|
||||
VexStatus vex = VexStatus.NotAffected,
|
||||
ProvenanceLevel provenance = ProvenanceLevel.Signed,
|
||||
decimal policyStrength = 0.8m)
|
||||
{
|
||||
return new ConfidenceInput
|
||||
{
|
||||
Reachability = new ReachabilityEvidence
|
||||
{
|
||||
State = reachability,
|
||||
AnalysisConfidence = 1.0m,
|
||||
GraphDigests = ["sha256:reachability"]
|
||||
},
|
||||
Runtime = new RuntimeEvidence
|
||||
{
|
||||
Posture = runtime,
|
||||
ObservationCount = 3,
|
||||
LastObserved = FixedTimestamp,
|
||||
SessionDigests = ["sha256:runtime"]
|
||||
},
|
||||
Vex = new VexEvidence
|
||||
{
|
||||
Statements =
|
||||
[
|
||||
new VexStatement
|
||||
{
|
||||
Status = vex,
|
||||
Issuer = "NVD",
|
||||
TrustScore = 0.95m,
|
||||
Timestamp = FixedTimestamp,
|
||||
StatementDigest = "sha256:vex"
|
||||
}
|
||||
]
|
||||
},
|
||||
Provenance = new ProvenanceEvidence
|
||||
{
|
||||
Level = provenance,
|
||||
SbomCompleteness = 0.95m,
|
||||
AttestationDigests = ["sha256:attestation"]
|
||||
},
|
||||
Policy = new PolicyEvidence
|
||||
{
|
||||
RuleName = "rule-1",
|
||||
MatchStrength = policyStrength,
|
||||
EvaluationDigest = "sha256:policy"
|
||||
},
|
||||
EvaluationTimestamp = FixedTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfidenceCalculator CreateCalculator()
|
||||
{
|
||||
return new ConfidenceCalculator(new StaticOptionsMonitor<ConfidenceWeightOptions>(new ConfidenceWeightOptions()));
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public StaticOptionsMonitor(T value) => _value = value;
|
||||
|
||||
public T CurrentValue => _value;
|
||||
|
||||
public T Get(string? name) => _value;
|
||||
|
||||
public IDisposable OnChange(Action<T, string> listener) => NullDisposable.Instance;
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Freshness;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Freshness;
|
||||
|
||||
public sealed class EvidenceTtlEnforcerTests
|
||||
{
|
||||
private readonly EvidenceTtlEnforcer _enforcer;
|
||||
private readonly EvidenceTtlOptions _options;
|
||||
|
||||
public EvidenceTtlEnforcerTests()
|
||||
{
|
||||
_options = new EvidenceTtlOptions();
|
||||
_enforcer = new EvidenceTtlEnforcer(
|
||||
Options.Create(_options),
|
||||
NullLogger<EvidenceTtlEnforcer>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_AllFresh_ReturnsFresh()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
Reachability = new ReachabilityEvidence { ComputedAt = now.AddHours(-1) },
|
||||
VexStatus = new VexEvidence { Timestamp = now.AddHours(-1) },
|
||||
Provenance = new ProvenanceEvidence { BuildTime = now.AddDays(-1) }
|
||||
};
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Fresh, result.OverallStatus);
|
||||
Assert.True(result.IsAcceptable);
|
||||
Assert.False(result.HasWarnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_ReachabilityNearExpiry_ReturnsWarning()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
// 7 day TTL, 20% warning threshold = warn after 5.6 days
|
||||
// At 6 days old, should be in warning state
|
||||
Reachability = new ReachabilityEvidence { ComputedAt = now.AddDays(-6) }
|
||||
};
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Warning, result.OverallStatus);
|
||||
Assert.True(result.IsAcceptable);
|
||||
Assert.True(result.HasWarnings);
|
||||
|
||||
var reachabilityCheck = result.Checks.First(c => c.Type == EvidenceType.Reachability);
|
||||
Assert.Equal(FreshnessStatus.Warning, reachabilityCheck.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_BoundaryExpired_ReturnsStale()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
// 72 hour TTL, so 5 days is definitely expired
|
||||
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) }
|
||||
};
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Stale, result.OverallStatus);
|
||||
Assert.False(result.IsAcceptable);
|
||||
|
||||
var boundaryCheck = result.Checks.First(c => c.Type == EvidenceType.Boundary);
|
||||
Assert.Equal(FreshnessStatus.Stale, boundaryCheck.Status);
|
||||
Assert.True(boundaryCheck.Remaining == TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(EvidenceType.Sbom, 30)]
|
||||
[InlineData(EvidenceType.Boundary, 3)]
|
||||
[InlineData(EvidenceType.Reachability, 7)]
|
||||
[InlineData(EvidenceType.Vex, 14)]
|
||||
[InlineData(EvidenceType.PolicyDecision, 1)]
|
||||
[InlineData(EvidenceType.HumanApproval, 30)]
|
||||
[InlineData(EvidenceType.CallStack, 7)]
|
||||
public void GetTtl_ReturnsConfiguredValue(EvidenceType type, int expectedDays)
|
||||
{
|
||||
var ttl = _enforcer.GetTtl(type);
|
||||
|
||||
Assert.Equal(expectedDays, ttl.TotalDays, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_MixedStates_ReturnsStaleOverall()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
Reachability = new ReachabilityEvidence { ComputedAt = now.AddHours(-1) }, // Fresh
|
||||
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) }, // Stale (72h TTL)
|
||||
VexStatus = new VexEvidence { Timestamp = now.AddDays(-2) } // Fresh
|
||||
};
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Stale, result.OverallStatus);
|
||||
Assert.False(result.IsAcceptable);
|
||||
Assert.Equal(3, result.Checks.Count);
|
||||
|
||||
var freshChecks = result.Checks.Where(c => c.Status == FreshnessStatus.Fresh).ToList();
|
||||
var staleChecks = result.Checks.Where(c => c.Status == FreshnessStatus.Stale).ToList();
|
||||
|
||||
Assert.Equal(2, freshChecks.Count);
|
||||
Assert.Single(staleChecks);
|
||||
Assert.Equal(EvidenceType.Boundary, staleChecks[0].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeExpiration_CalculatesCorrectly()
|
||||
{
|
||||
var createdAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var expiresAt = _enforcer.ComputeExpiration(EvidenceType.Boundary, createdAt);
|
||||
|
||||
// Boundary TTL is 72 hours = 3 days
|
||||
Assert.Equal(createdAt.AddHours(72), expiresAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_EmptyBundle_ReturnsEmptyChecks()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle();
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Fresh, result.OverallStatus);
|
||||
Assert.Empty(result.Checks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_CustomOptions_UsesCustomTtl()
|
||||
{
|
||||
var customOptions = new EvidenceTtlOptions
|
||||
{
|
||||
BoundaryTtl = TimeSpan.FromDays(1), // Custom: 1 day instead of default 3 days
|
||||
WarningThresholdPercent = 0.5 // Custom: 50% instead of default 20%
|
||||
};
|
||||
|
||||
var customEnforcer = new EvidenceTtlEnforcer(
|
||||
Options.Create(customOptions),
|
||||
NullLogger<EvidenceTtlEnforcer>.Instance);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
// 1 day TTL with 50% warning threshold = warn after 12 hours
|
||||
// At 16 hours old, should be in warning state
|
||||
Boundary = new BoundaryEvidence { ObservedAt = now.AddHours(-16) }
|
||||
};
|
||||
|
||||
var result = customEnforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(FreshnessStatus.Warning, result.OverallStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckType_GeneratesCorrectMessage()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
Reachability = new ReachabilityEvidence { ComputedAt = now.AddDays(-8) } // Expired (7 day TTL)
|
||||
};
|
||||
|
||||
var result = _enforcer.CheckFreshness(bundle, now);
|
||||
|
||||
var check = result.Checks.First();
|
||||
Assert.Equal(EvidenceType.Reachability, check.Type);
|
||||
Assert.Contains("expired", check.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("ago", check.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckFreshness_RecommendedAction_BasedOnConfiguration()
|
||||
{
|
||||
var blockOptions = new EvidenceTtlOptions
|
||||
{
|
||||
StaleAction = StaleEvidenceAction.Block
|
||||
};
|
||||
|
||||
var blockEnforcer = new EvidenceTtlEnforcer(
|
||||
Options.Create(blockOptions),
|
||||
NullLogger<EvidenceTtlEnforcer>.Instance);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bundle = new EvidenceBundle
|
||||
{
|
||||
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) } // Stale
|
||||
};
|
||||
|
||||
var result = blockEnforcer.CheckFreshness(bundle, now);
|
||||
|
||||
Assert.Equal(StaleEvidenceAction.Block, result.RecommendedAction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public sealed class ClaimScoreMergerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_SelectsHighestScore()
|
||||
{
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 2,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.7, BaseTrust = 0.7, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-b",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 3,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-02T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.9, BaseTrust = 0.9, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
};
|
||||
|
||||
var merger = new ClaimScoreMerger();
|
||||
var result = merger.Merge(claims, new MergePolicy());
|
||||
|
||||
result.Status.Should().Be(VexStatus.NotAffected);
|
||||
result.WinningClaim.SourceId.Should().Be("source-b");
|
||||
result.Confidence.Should().Be(0.9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_AppliesConflictPenalty()
|
||||
{
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = VexStatus.NotAffected,
|
||||
ScopeSpecificity = 2,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.8, BaseTrust = 0.8, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-b",
|
||||
Status = VexStatus.Affected,
|
||||
ScopeSpecificity = 1,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.7, BaseTrust = 0.7, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
};
|
||||
|
||||
var merger = new ClaimScoreMerger();
|
||||
var result = merger.Merge(claims, new MergePolicy { ConflictPenalty = 0.25 });
|
||||
|
||||
result.HasConflicts.Should().BeTrue();
|
||||
result.RequiresReplayProof.Should().BeTrue();
|
||||
result.Conflicts.Should().HaveCount(1);
|
||||
result.AllClaims.Should().Contain(c => c.SourceId == "source-b" && c.AdjustedScore == 0.525);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_IsDeterministic()
|
||||
{
|
||||
var claims = new List<(VexClaim, ClaimScoreResult)>
|
||||
{
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = VexStatus.Fixed,
|
||||
ScopeSpecificity = 1,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.6, BaseTrust = 0.6, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
(new VexClaim
|
||||
{
|
||||
SourceId = "source-b",
|
||||
Status = VexStatus.Fixed,
|
||||
ScopeSpecificity = 1,
|
||||
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
}, new ClaimScoreResult { Score = 0.6, BaseTrust = 0.6, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
|
||||
};
|
||||
|
||||
var merger = new ClaimScoreMerger();
|
||||
var expected = merger.Merge(claims, new MergePolicy());
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
merger.Merge(claims, new MergePolicy()).WinningClaim.SourceId.Should().Be(expected.WinningClaim.SourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public sealed class PolicyGateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Registry_StopsOnFirstFailure()
|
||||
{
|
||||
var registry = new PolicyGateRegistry(new StubServiceProvider(), new PolicyGateRegistryOptions { StopOnFirstFailure = true });
|
||||
registry.Register<FailingGate>("fail");
|
||||
registry.Register<PassingGate>("pass");
|
||||
|
||||
var mergeResult = CreateMergeResult();
|
||||
var context = new PolicyGateContext();
|
||||
|
||||
var evaluation = await registry.EvaluateAsync(mergeResult, context);
|
||||
|
||||
evaluation.Results.Should().HaveCount(1);
|
||||
evaluation.Results[0].GateName.Should().Be("fail");
|
||||
evaluation.AllPassed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_CollectsAllWhenConfigured()
|
||||
{
|
||||
var registry = new PolicyGateRegistry(new StubServiceProvider(), new PolicyGateRegistryOptions { StopOnFirstFailure = false });
|
||||
registry.Register<FailingGate>("fail");
|
||||
registry.Register<PassingGate>("pass");
|
||||
|
||||
var mergeResult = CreateMergeResult();
|
||||
var context = new PolicyGateContext();
|
||||
|
||||
var evaluation = await registry.EvaluateAsync(mergeResult, context);
|
||||
|
||||
evaluation.Results.Should().HaveCount(2);
|
||||
evaluation.Results.Select(r => r.GateName).Should().ContainInOrder("fail", "pass");
|
||||
}
|
||||
|
||||
private static MergeResult CreateMergeResult()
|
||||
{
|
||||
var winner = new ScoredClaim
|
||||
{
|
||||
SourceId = "source",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.9,
|
||||
AdjustedScore = 0.9,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.9,
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
WinningClaim = winner,
|
||||
AllClaims = ImmutableArray.Create(winner),
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
|
||||
private sealed class FailingGate : IPolicyGate
|
||||
{
|
||||
public Task<GateResult> EvaluateAsync(MergeResult mergeResult, PolicyGateContext context, CancellationToken ct = default)
|
||||
=> Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(FailingGate),
|
||||
Passed = false,
|
||||
Reason = "fail",
|
||||
Details = ImmutableDictionary<string, object>.Empty,
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class PassingGate : IPolicyGate
|
||||
{
|
||||
public Task<GateResult> EvaluateAsync(MergeResult mergeResult, PolicyGateContext context, CancellationToken ct = default)
|
||||
=> Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(PassingGate),
|
||||
Passed = true,
|
||||
Reason = null,
|
||||
Details = ImmutableDictionary<string, object>.Empty,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.TrustLattice;
|
||||
|
||||
public sealed class PolicyGatesTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MinimumConfidenceGate_FailsBelowThreshold()
|
||||
{
|
||||
var gate = new MinimumConfidenceGate();
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.7);
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("confidence_below_threshold");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownsBudgetGate_FailsWhenBudgetExceeded()
|
||||
{
|
||||
var gate = new UnknownsBudgetGate(new UnknownsBudgetGateOptions { MaxUnknownCount = 1, MaxCumulativeUncertainty = 0.5 });
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.9);
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
UnknownCount = 2,
|
||||
UnknownClaimScores = new[] { 0.4, 0.3 }
|
||||
};
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("unknowns_budget_exceeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SourceQuotaGate_FailsWithoutCorroboration()
|
||||
{
|
||||
var gate = new SourceQuotaGate(new SourceQuotaGateOptions { MaxInfluencePercent = 60, CorroborationDelta = 0.10 });
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.8,
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
WinningClaim = new ScoredClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.9,
|
||||
AdjustedScore = 0.9,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
},
|
||||
AllClaims = ImmutableArray.Create(
|
||||
new ScoredClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.9,
|
||||
AdjustedScore = 0.9,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
},
|
||||
new ScoredClaim
|
||||
{
|
||||
SourceId = "source-b",
|
||||
Status = VexStatus.NotAffected,
|
||||
OriginalScore = 0.1,
|
||||
AdjustedScore = 0.1,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = false,
|
||||
Reason = "initial",
|
||||
}),
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty,
|
||||
};
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, new PolicyGateContext());
|
||||
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("source_quota_exceeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReachabilityRequirementGate_FailsWithoutProof()
|
||||
{
|
||||
var gate = new ReachabilityRequirementGate();
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.9);
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
Severity = "CRITICAL",
|
||||
HasReachabilityProof = false,
|
||||
};
|
||||
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("reachability_proof_missing");
|
||||
}
|
||||
|
||||
private static MergeResult CreateMergeResult(VexStatus status, double confidence)
|
||||
{
|
||||
var winner = new ScoredClaim
|
||||
{
|
||||
SourceId = "source-a",
|
||||
Status = status,
|
||||
OriginalScore = confidence,
|
||||
AdjustedScore = confidence,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "winner",
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = confidence,
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
WinningClaim = winner,
|
||||
AllClaims = ImmutableArray.Create(winner),
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user