up
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Signals.Tests.GroundTruth;
|
||||
|
||||
/// <summary>
|
||||
/// Ground truth sample manifest.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthManifest
|
||||
{
|
||||
[JsonPropertyName("sampleId")]
|
||||
public required string SampleId { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("language")]
|
||||
public required string Language { get; init; }
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public required string Category { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public required string Description { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerabilities")]
|
||||
public IReadOnlyList<GroundTruthVulnerability>? Vulnerabilities { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability reference in manifest.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthVulnerability
|
||||
{
|
||||
[JsonPropertyName("vulnId")]
|
||||
public required string VulnId { get; init; }
|
||||
|
||||
[JsonPropertyName("purl")]
|
||||
public required string Purl { get; init; }
|
||||
|
||||
[JsonPropertyName("affectedSymbol")]
|
||||
public required string AffectedSymbol { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ground truth document for reachability validation.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthDocument
|
||||
{
|
||||
[JsonPropertyName("schema")]
|
||||
public required string Schema { get; init; }
|
||||
|
||||
[JsonPropertyName("sampleId")]
|
||||
public required string SampleId { get; init; }
|
||||
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("generator")]
|
||||
public required GroundTruthGenerator Generator { get; init; }
|
||||
|
||||
[JsonPropertyName("targets")]
|
||||
public required IReadOnlyList<GroundTruthTarget> Targets { get; init; }
|
||||
|
||||
[JsonPropertyName("entryPoints")]
|
||||
public required IReadOnlyList<GroundTruthEntryPoint> EntryPoints { get; init; }
|
||||
|
||||
[JsonPropertyName("expectedUncertainty")]
|
||||
public GroundTruthUncertainty? ExpectedUncertainty { get; init; }
|
||||
|
||||
[JsonPropertyName("expectedGateDecisions")]
|
||||
public IReadOnlyList<GroundTruthGateDecision>? ExpectedGateDecisions { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generator metadata.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthGenerator
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyName("annotator")]
|
||||
public string? Annotator { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target symbol with expected outcomes.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthTarget
|
||||
{
|
||||
[JsonPropertyName("symbolId")]
|
||||
public required string SymbolId { get; init; }
|
||||
|
||||
[JsonPropertyName("display")]
|
||||
public string? Display { get; init; }
|
||||
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
|
||||
[JsonPropertyName("expected")]
|
||||
public required GroundTruthExpected Expected { get; init; }
|
||||
|
||||
[JsonPropertyName("reasoning")]
|
||||
public required string Reasoning { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expected outcomes for a target.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthExpected
|
||||
{
|
||||
[JsonPropertyName("latticeState")]
|
||||
public required string LatticeState { get; init; }
|
||||
|
||||
[JsonPropertyName("bucket")]
|
||||
public required string Bucket { get; init; }
|
||||
|
||||
[JsonPropertyName("reachable")]
|
||||
public bool? Reachable { get; init; }
|
||||
|
||||
[JsonPropertyName("confidence")]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
[JsonPropertyName("pathLength")]
|
||||
public int? PathLength { get; init; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public IReadOnlyList<string>? Path { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point definition.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthEntryPoint
|
||||
{
|
||||
[JsonPropertyName("symbolId")]
|
||||
public required string SymbolId { get; init; }
|
||||
|
||||
[JsonPropertyName("display")]
|
||||
public string? Display { get; init; }
|
||||
|
||||
[JsonPropertyName("phase")]
|
||||
public required string Phase { get; init; }
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public required string Source { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expected uncertainty state.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthUncertainty
|
||||
{
|
||||
[JsonPropertyName("states")]
|
||||
public IReadOnlyList<GroundTruthUncertaintyState>? States { get; init; }
|
||||
|
||||
[JsonPropertyName("aggregateTier")]
|
||||
public required string AggregateTier { get; init; }
|
||||
|
||||
[JsonPropertyName("riskScore")]
|
||||
public double RiskScore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual uncertainty state.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthUncertaintyState
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public required string Code { get; init; }
|
||||
|
||||
[JsonPropertyName("entropy")]
|
||||
public required double Entropy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expected gate decision.
|
||||
/// </summary>
|
||||
public sealed record GroundTruthGateDecision
|
||||
{
|
||||
[JsonPropertyName("vulnId")]
|
||||
public required string VulnId { get; init; }
|
||||
|
||||
[JsonPropertyName("targetSymbol")]
|
||||
public required string TargetSymbol { get; init; }
|
||||
|
||||
[JsonPropertyName("requestedStatus")]
|
||||
public required string RequestedStatus { get; init; }
|
||||
|
||||
[JsonPropertyName("expectedDecision")]
|
||||
public required string ExpectedDecision { get; init; }
|
||||
|
||||
[JsonPropertyName("expectedBlockedBy")]
|
||||
public string? ExpectedBlockedBy { get; init; }
|
||||
|
||||
[JsonPropertyName("expectedReason")]
|
||||
public string? ExpectedReason { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Signals.Lattice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.GroundTruth;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that validate ground truth samples against lattice and uncertainty tier logic.
|
||||
/// </summary>
|
||||
public class GroundTruthValidatorTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all ground truth samples have valid lattice state codes.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_HasValidLatticeStates(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
foreach (var target in document.Targets)
|
||||
{
|
||||
var state = ReachabilityLatticeStateExtensions.FromCode(target.Expected.LatticeState);
|
||||
|
||||
// Verify the state is valid (not defaulting to Unknown for invalid input)
|
||||
Assert.True(
|
||||
target.Expected.LatticeState == "U" || state != ReachabilityLatticeState.Unknown,
|
||||
$"Invalid lattice state '{target.Expected.LatticeState}' for target {target.SymbolId} in {samplePath}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that lattice state and bucket are consistent.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_LatticeStateMatchesBucket(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
foreach (var target in document.Targets)
|
||||
{
|
||||
var state = ReachabilityLatticeStateExtensions.FromCode(target.Expected.LatticeState);
|
||||
var expectedBucket = state.ToV0Bucket();
|
||||
|
||||
Assert.True(
|
||||
target.Expected.Bucket == expectedBucket,
|
||||
$"Bucket mismatch for {target.SymbolId} in {samplePath}: " +
|
||||
$"expected '{target.Expected.Bucket}' for state '{target.Expected.LatticeState}' but ToV0Bucket returns '{expectedBucket}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that uncertainty tiers are valid.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_HasValidUncertaintyTiers(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
if (document.ExpectedUncertainty is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var validTiers = new[] { "T1", "T2", "T3", "T4" };
|
||||
Assert.Contains(document.ExpectedUncertainty.AggregateTier, validTiers);
|
||||
|
||||
if (document.ExpectedUncertainty.States is not null)
|
||||
{
|
||||
var validCodes = new[] { "U1", "U2", "U3", "U4" };
|
||||
foreach (var state in document.ExpectedUncertainty.States)
|
||||
{
|
||||
Assert.Contains(state.Code, validCodes);
|
||||
Assert.InRange(state.Entropy, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that gate decisions reference valid target symbols.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_GateDecisionsReferenceValidTargets(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
if (document.ExpectedGateDecisions is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetSymbols = document.Targets.Select(t => t.SymbolId).ToHashSet();
|
||||
|
||||
foreach (var decision in document.ExpectedGateDecisions)
|
||||
{
|
||||
Assert.True(
|
||||
targetSymbols.Contains(decision.TargetSymbol),
|
||||
$"Gate decision references unknown target '{decision.TargetSymbol}' in {samplePath}");
|
||||
|
||||
var validDecisions = new[] { "allow", "block", "warn" };
|
||||
Assert.Contains(decision.ExpectedDecision, validDecisions);
|
||||
|
||||
var validStatuses = new[] { "affected", "not_affected", "under_investigation", "fixed" };
|
||||
Assert.Contains(decision.RequestedStatus, validStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that reachable targets have paths, unreachable do not.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_PathConsistencyWithReachability(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
foreach (var target in document.Targets)
|
||||
{
|
||||
if (target.Expected.Reachable == true)
|
||||
{
|
||||
Assert.True(
|
||||
target.Expected.PathLength.HasValue && target.Expected.PathLength > 0,
|
||||
$"Reachable target '{target.SymbolId}' should have pathLength > 0 in {samplePath}");
|
||||
|
||||
Assert.True(
|
||||
target.Expected.Path is not null && target.Expected.Path.Count > 0,
|
||||
$"Reachable target '{target.SymbolId}' should have non-empty path in {samplePath}");
|
||||
}
|
||||
else if (target.Expected.Reachable == false)
|
||||
{
|
||||
Assert.True(
|
||||
target.Expected.PathLength is null || target.Expected.PathLength == 0,
|
||||
$"Unreachable target '{target.SymbolId}' should have null or 0 pathLength in {samplePath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that entry points have valid phases.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_EntryPointsHaveValidPhases(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
var validPhases = new[] { "load", "init", "runtime", "main", "fini" };
|
||||
|
||||
foreach (var entry in document.EntryPoints)
|
||||
{
|
||||
Assert.Contains(entry.Phase, validPhases);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all targets have reasoning explanations.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_AllTargetsHaveReasoning(string samplePath, GroundTruthDocument document)
|
||||
{
|
||||
foreach (var target in document.Targets)
|
||||
{
|
||||
Assert.False(
|
||||
string.IsNullOrWhiteSpace(target.Reasoning),
|
||||
$"Target '{target.SymbolId}' missing reasoning in {samplePath}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides ground truth samples from the datasets directory.
|
||||
/// </summary>
|
||||
public static IEnumerable<object[]> GetGroundTruthSamples()
|
||||
{
|
||||
// Find the datasets directory (relative to test execution)
|
||||
var currentDir = Directory.GetCurrentDirectory();
|
||||
var searchDirs = new[]
|
||||
{
|
||||
Path.Combine(currentDir, "datasets", "reachability", "samples"),
|
||||
Path.Combine(currentDir, "..", "..", "..", "..", "..", "..", "datasets", "reachability", "samples"),
|
||||
Path.Combine(currentDir, "..", "..", "..", "..", "..", "..", "..", "datasets", "reachability", "samples"),
|
||||
};
|
||||
|
||||
string? datasetsPath = null;
|
||||
foreach (var dir in searchDirs)
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
datasetsPath = dir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (datasetsPath is null)
|
||||
{
|
||||
// Return empty if datasets not found (allows tests to pass in CI without samples)
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var groundTruthFile in Directory.EnumerateFiles(datasetsPath, "ground-truth.json", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(datasetsPath, groundTruthFile);
|
||||
var json = File.ReadAllText(groundTruthFile);
|
||||
var document = JsonSerializer.Deserialize<GroundTruthDocument>(json, JsonOptions);
|
||||
|
||||
if (document is not null)
|
||||
{
|
||||
yield return new object[] { relativePath, document };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using StellaOps.Signals.Lattice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class ReachabilityLatticeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyUnreachable, ReachabilityLatticeState.Contested)]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.RuntimeObserved, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyUnreachable, ReachabilityLatticeState.RuntimeUnobserved, ReachabilityLatticeState.ConfirmedUnreachable)]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeObserved, ReachabilityLatticeState.StaticallyUnreachable, ReachabilityLatticeState.Contested)]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedReachable, ReachabilityLatticeState.ConfirmedUnreachable, ReachabilityLatticeState.Contested)]
|
||||
[InlineData(ReachabilityLatticeState.Contested, ReachabilityLatticeState.Unknown, ReachabilityLatticeState.Contested)]
|
||||
public void Join_ReturnsExpectedState(ReachabilityLatticeState a, ReachabilityLatticeState b, ReachabilityLatticeState expected)
|
||||
{
|
||||
var result = ReachabilityLattice.Join(a, b);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.Unknown)]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedReachable, ReachabilityLatticeState.RuntimeObserved, ReachabilityLatticeState.RuntimeObserved)]
|
||||
[InlineData(ReachabilityLatticeState.Contested, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData(ReachabilityLatticeState.Contested, ReachabilityLatticeState.Unknown, ReachabilityLatticeState.Unknown)]
|
||||
public void Meet_ReturnsExpectedState(ReachabilityLatticeState a, ReachabilityLatticeState b, ReachabilityLatticeState expected)
|
||||
{
|
||||
var result = ReachabilityLattice.Meet(a, b);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Join_IsCommutative()
|
||||
{
|
||||
var states = Enum.GetValues<ReachabilityLatticeState>();
|
||||
foreach (var a in states)
|
||||
{
|
||||
foreach (var b in states)
|
||||
{
|
||||
Assert.Equal(ReachabilityLattice.Join(a, b), ReachabilityLattice.Join(b, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Meet_IsCommutative()
|
||||
{
|
||||
var states = Enum.GetValues<ReachabilityLatticeState>();
|
||||
foreach (var a in states)
|
||||
{
|
||||
foreach (var b in states)
|
||||
{
|
||||
Assert.Equal(ReachabilityLattice.Meet(a, b), ReachabilityLattice.Meet(b, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinAll_WithEmptySequence_ReturnsUnknown()
|
||||
{
|
||||
var result = ReachabilityLattice.JoinAll(Array.Empty<ReachabilityLatticeState>());
|
||||
Assert.Equal(ReachabilityLatticeState.Unknown, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinAll_StopsEarlyOnContested()
|
||||
{
|
||||
var states = new[] { ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.Contested, ReachabilityLatticeState.Unknown };
|
||||
var result = ReachabilityLattice.JoinAll(states);
|
||||
Assert.Equal(ReachabilityLatticeState.Contested, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false, false, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData(false, false, false, ReachabilityLatticeState.StaticallyUnreachable)]
|
||||
[InlineData(null, false, false, ReachabilityLatticeState.Unknown)]
|
||||
[InlineData(true, true, true, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData(false, true, false, ReachabilityLatticeState.ConfirmedUnreachable)]
|
||||
[InlineData(false, true, true, ReachabilityLatticeState.Contested)]
|
||||
public void FromEvidence_ReturnsExpectedState(bool? staticReachable, bool hasRuntimeEvidence, bool runtimeObserved, ReachabilityLatticeState expected)
|
||||
{
|
||||
var result = ReachabilityLattice.FromEvidence(staticReachable, hasRuntimeEvidence, runtimeObserved);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("entrypoint", false, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData("direct", false, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData("direct", true, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData("runtime", false, ReachabilityLatticeState.RuntimeObserved)]
|
||||
[InlineData("unreachable", false, ReachabilityLatticeState.StaticallyUnreachable)]
|
||||
[InlineData("unreachable", true, ReachabilityLatticeState.Contested)]
|
||||
[InlineData("unknown", false, ReachabilityLatticeState.Unknown)]
|
||||
public void FromV0Bucket_ReturnsExpectedState(string bucket, bool hasRuntimeHits, ReachabilityLatticeState expected)
|
||||
{
|
||||
var result = ReachabilityLattice.FromV0Bucket(bucket, hasRuntimeHits);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
public class ReachabilityLatticeStateExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, "U")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, "SR")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyUnreachable, "SU")]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeObserved, "RO")]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeUnobserved, "RU")]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedReachable, "CR")]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedUnreachable, "CU")]
|
||||
[InlineData(ReachabilityLatticeState.Contested, "X")]
|
||||
public void ToCode_ReturnsExpectedCode(ReachabilityLatticeState state, string expectedCode)
|
||||
{
|
||||
Assert.Equal(expectedCode, state.ToCode());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("U", ReachabilityLatticeState.Unknown)]
|
||||
[InlineData("SR", ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData("SU", ReachabilityLatticeState.StaticallyUnreachable)]
|
||||
[InlineData("RO", ReachabilityLatticeState.RuntimeObserved)]
|
||||
[InlineData("RU", ReachabilityLatticeState.RuntimeUnobserved)]
|
||||
[InlineData("CR", ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData("CU", ReachabilityLatticeState.ConfirmedUnreachable)]
|
||||
[InlineData("X", ReachabilityLatticeState.Contested)]
|
||||
[InlineData("invalid", ReachabilityLatticeState.Unknown)]
|
||||
[InlineData("", ReachabilityLatticeState.Unknown)]
|
||||
[InlineData(null, ReachabilityLatticeState.Unknown)]
|
||||
public void FromCode_ReturnsExpectedState(string? code, ReachabilityLatticeState expected)
|
||||
{
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedUnreachable, "unreachable")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyUnreachable, "unreachable")]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeUnobserved, "unreachable")]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedReachable, "runtime")]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeObserved, "runtime")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, "direct")]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, "unknown")]
|
||||
[InlineData(ReachabilityLatticeState.Contested, "unknown")]
|
||||
public void ToV0Bucket_ReturnsExpectedBucket(ReachabilityLatticeState state, string expectedBucket)
|
||||
{
|
||||
Assert.Equal(expectedBucket, state.ToV0Bucket());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -82,6 +82,8 @@ public class ReachabilityScoringServiceTests
|
||||
Assert.Contains("target", state.Evidence.RuntimeHits);
|
||||
|
||||
Assert.Equal(0.405, fact.Score, 3);
|
||||
Assert.Equal(0.405, fact.RiskScore, 3);
|
||||
Assert.Null(fact.Uncertainty);
|
||||
Assert.Equal("1", fact.Metadata?["fact.version"]);
|
||||
Assert.False(string.IsNullOrWhiteSpace(fact.Metadata?["fact.digest"]));
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ public class ReachabilityUnionIngestionServiceTests
|
||||
var tempRoot = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "signals-union-test-" + Guid.NewGuid().ToString("N")));
|
||||
var signalsOptions = new SignalsOptions();
|
||||
signalsOptions.Storage.RootPath = tempRoot.FullName;
|
||||
signalsOptions.Mongo.ConnectionString = "mongodb://localhost";
|
||||
signalsOptions.Mongo.Database = "stub";
|
||||
|
||||
var options = Microsoft.Extensions.Options.Options.Create(signalsOptions);
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using StellaOps.Signals.Lattice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class UncertaintyTierTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, 0.50)]
|
||||
[InlineData(UncertaintyTier.T2, 0.25)]
|
||||
[InlineData(UncertaintyTier.T3, 0.10)]
|
||||
[InlineData(UncertaintyTier.T4, 0.00)]
|
||||
public void GetRiskModifier_ReturnsExpectedValue(UncertaintyTier tier, double expected)
|
||||
{
|
||||
Assert.Equal(expected, tier.GetRiskModifier());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, true)]
|
||||
[InlineData(UncertaintyTier.T2, false)]
|
||||
[InlineData(UncertaintyTier.T3, false)]
|
||||
[InlineData(UncertaintyTier.T4, false)]
|
||||
public void BlocksNotAffected_ReturnsExpected(UncertaintyTier tier, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, tier.BlocksNotAffected());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, true)]
|
||||
[InlineData(UncertaintyTier.T2, true)]
|
||||
[InlineData(UncertaintyTier.T3, false)]
|
||||
[InlineData(UncertaintyTier.T4, false)]
|
||||
public void RequiresWarning_ReturnsExpected(UncertaintyTier tier, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, tier.RequiresWarning());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, "High")]
|
||||
[InlineData(UncertaintyTier.T2, "Medium")]
|
||||
[InlineData(UncertaintyTier.T3, "Low")]
|
||||
[InlineData(UncertaintyTier.T4, "Negligible")]
|
||||
public void ToDisplayName_ReturnsExpected(UncertaintyTier tier, string expected)
|
||||
{
|
||||
Assert.Equal(expected, tier.ToDisplayName());
|
||||
}
|
||||
}
|
||||
|
||||
public class UncertaintyTierCalculatorTests
|
||||
{
|
||||
// U1 (MissingSymbolResolution) tier calculation
|
||||
[Theory]
|
||||
[InlineData("U1", 0.7, UncertaintyTier.T1)]
|
||||
[InlineData("U1", 0.8, UncertaintyTier.T1)]
|
||||
[InlineData("U1", 0.4, UncertaintyTier.T2)]
|
||||
[InlineData("U1", 0.5, UncertaintyTier.T2)]
|
||||
[InlineData("U1", 0.3, UncertaintyTier.T3)]
|
||||
[InlineData("U1", 0.0, UncertaintyTier.T3)]
|
||||
public void CalculateTier_U1_ReturnsExpected(string code, double entropy, UncertaintyTier expected)
|
||||
{
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
// U2 (MissingPurl) tier calculation
|
||||
[Theory]
|
||||
[InlineData("U2", 0.5, UncertaintyTier.T2)]
|
||||
[InlineData("U2", 0.6, UncertaintyTier.T2)]
|
||||
[InlineData("U2", 0.4, UncertaintyTier.T3)]
|
||||
[InlineData("U2", 0.0, UncertaintyTier.T3)]
|
||||
public void CalculateTier_U2_ReturnsExpected(string code, double entropy, UncertaintyTier expected)
|
||||
{
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
// U3 (UntrustedAdvisory) tier calculation
|
||||
[Theory]
|
||||
[InlineData("U3", 0.6, UncertaintyTier.T3)]
|
||||
[InlineData("U3", 0.8, UncertaintyTier.T3)]
|
||||
[InlineData("U3", 0.5, UncertaintyTier.T4)]
|
||||
[InlineData("U3", 0.0, UncertaintyTier.T4)]
|
||||
public void CalculateTier_U3_ReturnsExpected(string code, double entropy, UncertaintyTier expected)
|
||||
{
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
// U4 (Unknown) always T1
|
||||
[Theory]
|
||||
[InlineData("U4", 0.0, UncertaintyTier.T1)]
|
||||
[InlineData("U4", 0.5, UncertaintyTier.T1)]
|
||||
[InlineData("U4", 1.0, UncertaintyTier.T1)]
|
||||
public void CalculateTier_U4_AlwaysReturnsT1(string code, double entropy, UncertaintyTier expected)
|
||||
{
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
// Unknown code defaults to T4
|
||||
[Theory]
|
||||
[InlineData("Unknown", 0.5, UncertaintyTier.T4)]
|
||||
[InlineData("", 0.5, UncertaintyTier.T4)]
|
||||
public void CalculateTier_UnknownCode_ReturnsT4(string code, double entropy, UncertaintyTier expected)
|
||||
{
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_WithEmptySequence_ReturnsT4()
|
||||
{
|
||||
var result = UncertaintyTierCalculator.CalculateAggregateTier(Array.Empty<(string, double)>());
|
||||
Assert.Equal(UncertaintyTier.T4, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_ReturnsMaxSeverity()
|
||||
{
|
||||
var states = new[] { ("U1", 0.3), ("U2", 0.6), ("U3", 0.5) }; // T3, T2, T4
|
||||
var result = UncertaintyTierCalculator.CalculateAggregateTier(states);
|
||||
Assert.Equal(UncertaintyTier.T2, result); // Maximum severity (lowest enum value)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_StopsAtT1()
|
||||
{
|
||||
var states = new[] { ("U4", 1.0), ("U1", 0.3) }; // T1, T3
|
||||
var result = UncertaintyTierCalculator.CalculateAggregateTier(states);
|
||||
Assert.Equal(UncertaintyTier.T1, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.5, UncertaintyTier.T4, 0.1, 0.5, 0.525)] // No tier modifier for T4, but entropy boost applies
|
||||
[InlineData(0.5, UncertaintyTier.T3, 0.1, 0.5, 0.575)] // +10% + entropy boost
|
||||
[InlineData(0.5, UncertaintyTier.T2, 0.1, 0.5, 0.65)] // +25% + entropy boost
|
||||
[InlineData(0.5, UncertaintyTier.T1, 0.1, 0.5, 0.775)] // +50% + entropy boost
|
||||
public void CalculateRiskScore_AppliesModifiers(
|
||||
double baseScore,
|
||||
UncertaintyTier tier,
|
||||
double meanEntropy,
|
||||
double entropyMultiplier,
|
||||
double expected)
|
||||
{
|
||||
var result = UncertaintyTierCalculator.CalculateRiskScore(
|
||||
baseScore, tier, meanEntropy, entropyMultiplier, 0.5);
|
||||
Assert.Equal(expected, result, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateRiskScore_ClampsToCeiling()
|
||||
{
|
||||
var result = UncertaintyTierCalculator.CalculateRiskScore(
|
||||
0.9, UncertaintyTier.T1, 0.8, 0.5, 0.5);
|
||||
Assert.Equal(1.0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateUnknownState_ReturnsU4WithMaxEntropy()
|
||||
{
|
||||
var (code, name, entropy) = UncertaintyTierCalculator.CreateUnknownState();
|
||||
Assert.Equal("U4", code);
|
||||
Assert.Equal("Unknown", name);
|
||||
Assert.Equal(1.0, entropy);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10, 100, 0.1)]
|
||||
[InlineData(50, 100, 0.5)]
|
||||
[InlineData(0, 100, 0.0)]
|
||||
[InlineData(0, 0, 0.0)]
|
||||
public void CreateMissingSymbolState_CalculatesEntropy(int unknowns, int total, double expectedEntropy)
|
||||
{
|
||||
var (code, name, entropy) = UncertaintyTierCalculator.CreateMissingSymbolState(unknowns, total);
|
||||
Assert.Equal("U1", code);
|
||||
Assert.Equal("MissingSymbolResolution", name);
|
||||
Assert.Equal(expectedEntropy, entropy, 5);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user