save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -0,0 +1,227 @@
// <copyright file="ConfidenceCalculatorTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Unit tests for <see cref="ConfidenceCalculator"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ConfidenceCalculatorTests
{
private readonly ConfidenceCalculator _calculator = new();
[Theory]
[InlineData(LatticeState.Unknown, 0.00, 0.29)]
[InlineData(LatticeState.StaticReachable, 0.30, 0.49)]
[InlineData(LatticeState.StaticUnreachable, 0.50, 0.69)]
[InlineData(LatticeState.RuntimeObserved, 0.70, 0.89)]
[InlineData(LatticeState.RuntimeUnobserved, 0.70, 0.89)]
[InlineData(LatticeState.ConfirmedReachable, 0.90, 1.00)]
[InlineData(LatticeState.ConfirmedUnreachable, 0.90, 1.00)]
[InlineData(LatticeState.Contested, 0.00, 0.00)]
public void GetConfidenceRange_Returns_Correct_Range(LatticeState state, double expectedMin, double expectedMax)
{
// Act
var (min, max) = ConfidenceCalculator.GetConfidenceRange(state);
// Assert
min.Should().Be(expectedMin);
max.Should().Be(expectedMax);
}
[Fact]
public void Calculate_Unknown_Returns_Value_In_Range()
{
// Act
var confidence = _calculator.Calculate(LatticeState.Unknown, 0.5);
// Assert
confidence.Should().BeInRange(0.00, 0.29);
}
[Fact]
public void Calculate_ConfirmedReachable_Returns_High_Confidence()
{
// Act
var confidence = _calculator.Calculate(LatticeState.ConfirmedReachable, 0.5);
// Assert
confidence.Should().BeInRange(0.90, 1.00);
}
[Fact]
public void Calculate_WithStaticResult_PathCount_Increases_Confidence()
{
// Arrange
var baseResult = CreateStaticResult(isReachable: true, pathCount: 1);
var highPathResult = CreateStaticResult(isReachable: true, pathCount: 10);
// Act
var baseConfidence = _calculator.Calculate(
LatticeState.StaticReachable, baseResult, null);
var highPathConfidence = _calculator.Calculate(
LatticeState.StaticReachable, highPathResult, null);
// Assert
highPathConfidence.Should().BeGreaterThan(baseConfidence);
}
[Fact]
public void Calculate_WithGuards_Decreases_Confidence()
{
// Arrange
var noGuards = CreateStaticResult(isReachable: true, pathCount: 5, guardCount: 0);
var withGuards = CreateStaticResult(isReachable: true, pathCount: 5, guardCount: 2);
// Act
var noGuardConfidence = _calculator.Calculate(
LatticeState.StaticReachable, noGuards, null);
var withGuardConfidence = _calculator.Calculate(
LatticeState.StaticReachable, withGuards, null);
// Assert
withGuardConfidence.Should().BeLessThan(noGuardConfidence);
}
[Fact]
public void Calculate_WithRuntimeResult_HitCount_Increases_Confidence()
{
// Arrange
var lowHits = CreateRuntimeResult(wasObserved: true, hitCount: 1);
var highHits = CreateRuntimeResult(wasObserved: true, hitCount: 10000);
// Act
var lowHitConfidence = _calculator.Calculate(
LatticeState.RuntimeObserved, null, lowHits);
var highHitConfidence = _calculator.Calculate(
LatticeState.RuntimeObserved, null, highHits);
// Assert
highHitConfidence.Should().BeGreaterThan(lowHitConfidence);
}
[Fact]
public void Calculate_MultipleEnvironments_Increases_Confidence()
{
// Arrange
var singleEnv = CreateRuntimeResult(wasObserved: true, hitCount: 100, environmentCount: 1);
var multiEnv = CreateRuntimeResult(wasObserved: true, hitCount: 100, environmentCount: 3);
// Act
var singleConfidence = _calculator.Calculate(
LatticeState.RuntimeObserved, null, singleEnv);
var multiConfidence = _calculator.Calculate(
LatticeState.RuntimeObserved, null, multiEnv);
// Assert
multiConfidence.Should().BeGreaterThan(singleConfidence);
}
[Fact]
public void Calculate_Confidence_Always_Within_State_Range()
{
// Arrange
var staticResult = CreateStaticResult(isReachable: true, pathCount: 100);
var runtimeResult = CreateRuntimeResult(wasObserved: true, hitCount: 1000000, environmentCount: 10);
// Act
var confidence = _calculator.Calculate(
LatticeState.RuntimeObserved, staticResult, runtimeResult);
// Assert
var (min, max) = ConfidenceCalculator.GetConfidenceRange(LatticeState.RuntimeObserved);
confidence.Should().BeInRange(min, max);
}
[Fact]
public void Calculate_Contested_Returns_Zero()
{
// Act
var confidence = _calculator.Calculate(LatticeState.Contested, 0.5);
// Assert
confidence.Should().Be(0.0);
}
[Fact]
public void CustomWeights_Affect_Calculation()
{
// Arrange - default pathCountBonus is 0.02, custom is 0.05
// With pathCount=5, default adds 5*0.02=0.1 bonus, custom adds 5*0.05=0.25 (capped)
var customWeights = new ConfidenceWeights
{
PathCountBonus = 0.05,
MaxPathBonus = 0.20
};
var customCalculator = new ConfidenceCalculator(customWeights);
var defaultCalculator = new ConfidenceCalculator();
var staticResult = CreateStaticResult(isReachable: true, pathCount: 5);
// Act
var customConfidence = customCalculator.Calculate(
LatticeState.StaticReachable, staticResult, null);
var defaultConfidence = defaultCalculator.Calculate(
LatticeState.StaticReachable, staticResult, null);
// Assert - both should be in StaticReachable range (0.30-0.49) but custom should be higher
// due to larger path count bonus
customConfidence.Should().BeGreaterThanOrEqualTo(defaultConfidence);
}
private static StaticReachabilityResult CreateStaticResult(
bool isReachable,
int pathCount,
int guardCount = 0)
{
var guards = Enumerable.Range(0, guardCount)
.Select(i => new GuardCondition { Type = "FeatureFlag", Key = $"flag_{i}" })
.ToImmutableArray();
return new StaticReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
IsReachable = isReachable,
PathCount = pathCount,
Guards = guards,
AnalyzedAt = DateTimeOffset.UtcNow
};
}
private static RuntimeReachabilityResult CreateRuntimeResult(
bool wasObserved,
long hitCount,
int environmentCount = 1)
{
var contexts = Enumerable.Range(0, environmentCount)
.Select(i => new ExecutionContext { Environment = $"env_{i}" })
.ToImmutableArray();
return new RuntimeReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
WasObserved = wasObserved,
HitCount = hitCount,
Contexts = contexts,
ObservationWindow = TimeSpan.FromDays(7),
WindowStart = DateTimeOffset.UtcNow.AddDays(-7),
WindowEnd = DateTimeOffset.UtcNow
};
}
private static SymbolRef CreateTestSymbol() => new()
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
}

View File

@@ -0,0 +1,217 @@
// <copyright file="EvidenceUriBuilderTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Unit tests for <see cref="EvidenceUriBuilder"/> and <see cref="EvidenceUri"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class EvidenceUriBuilderTests
{
private readonly EvidenceUriBuilder _builder = new();
[Fact]
public void BuildReachGraphUri_Returns_Valid_Uri()
{
// Act
var uri = _builder.BuildReachGraphUri("sha256:abc123");
// Assert
uri.Should().Be("stella://reachgraph/sha256:abc123");
}
[Fact]
public void BuildReachGraphUri_Adds_Sha256_Prefix_If_Missing()
{
// Act
var uri = _builder.BuildReachGraphUri("abc123");
// Assert
uri.Should().Be("stella://reachgraph/sha256:abc123");
}
[Fact]
public void BuildReachGraphSliceUri_Returns_Uri_With_Symbol()
{
// Act
var uri = _builder.BuildReachGraphSliceUri("sha256:abc123", "symbol_id_123");
// Assert
uri.Should().StartWith("stella://reachgraph/sha256:abc123/slice?symbol=");
uri.Should().Contain("symbol_id_123");
}
[Fact]
public void BuildReachGraphSliceUri_Escapes_SymbolId()
{
// Arrange
var symbolWithSpecialChars = "System.IO/File.Read()";
// Act
var uri = _builder.BuildReachGraphSliceUri("sha256:abc123", symbolWithSpecialChars);
// Assert
uri.Should().NotContain("/File"); // Should be escaped
uri.Should().Contain(Uri.EscapeDataString(symbolWithSpecialChars));
}
[Fact]
public void BuildRuntimeFactsUri_Returns_Uri_With_Tenant()
{
// Act
var uri = _builder.BuildRuntimeFactsUri("tenant1", "sha256:abc123");
// Assert
uri.Should().Be("stella://signals/runtime/tenant1/sha256:abc123");
}
[Fact]
public void BuildRuntimeFactsUri_WithSymbol_Returns_Full_Uri()
{
// Act
var uri = _builder.BuildRuntimeFactsUri("tenant1", "sha256:abc123", "symbol_id");
// Assert
uri.Should().Be("stella://signals/runtime/tenant1/sha256:abc123?symbol=symbol_id");
}
[Fact]
public void BuildRuntimeFactsUri_Escapes_TenantId()
{
// Arrange
var tenantWithSpace = "tenant with space";
// Act
var uri = _builder.BuildRuntimeFactsUri(tenantWithSpace, "sha256:abc123");
// Assert
uri.Should().Contain(Uri.EscapeDataString(tenantWithSpace));
uri.Should().NotContain(" ");
}
[Fact]
public void BuildCveMappingUri_Returns_Valid_Uri()
{
// Act
var uri = _builder.BuildCveMappingUri("CVE-2024-1234");
// Assert
uri.Should().Be("stella://cvemap/CVE-2024-1234");
}
[Fact]
public void BuildAttestationUri_Returns_Valid_Uri()
{
// Act
var uri = _builder.BuildAttestationUri("sha256:attestation_digest");
// Assert
uri.Should().Be("stella://attestation/sha256:attestation_digest");
}
[Fact]
public void BuildHybridResultUri_Returns_Valid_Uri()
{
// Act
var uri = _builder.BuildHybridResultUri("sha256:result_digest");
// Assert
uri.Should().Be("stella://hybrid/sha256:result_digest");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void BuildReachGraphUri_Throws_On_Invalid_Digest(string? digest)
{
// Act & Assert
var act = () => _builder.BuildReachGraphUri(digest!);
act.Should().Throw<ArgumentException>();
}
// EvidenceUri parsing tests
[Fact]
public void Parse_Valid_ReachGraph_Uri()
{
// Act
var parsed = EvidenceUri.Parse("stella://reachgraph/sha256:abc123");
// Assert
parsed.Should().NotBeNull();
parsed!.Scheme.Should().Be("stella");
parsed.Type.Should().Be("reachgraph");
parsed.Segments.Should().HaveCount(1);
parsed.Segments[0].Should().Be("sha256:abc123");
}
[Fact]
public void Parse_Uri_With_Query_Parameters()
{
// Act
var parsed = EvidenceUri.Parse("stella://reachgraph/sha256:abc123/slice?symbol=test_symbol");
// Assert
parsed.Should().NotBeNull();
parsed!.Type.Should().Be("reachgraph");
parsed.Segments.Should().Contain("slice");
parsed.Query.Should().ContainKey("symbol");
parsed.Query["symbol"].Should().Be("test_symbol");
}
[Fact]
public void Parse_Uri_With_Multiple_Query_Parameters()
{
// Act
var parsed = EvidenceUri.Parse("stella://signals/runtime/tenant?symbol=sym&version=1");
// Assert
parsed.Should().NotBeNull();
parsed!.Query.Should().HaveCount(2);
parsed.Query["symbol"].Should().Be("sym");
parsed.Query["version"].Should().Be("1");
}
[Fact]
public void Parse_Decodes_Escaped_Values()
{
// Act
var parsed = EvidenceUri.Parse("stella://cvemap/CVE%2D2024%2D1234");
// Assert
parsed.Should().NotBeNull();
// Note: path segments aren't automatically decoded in this implementation
// but query params are
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("http://example.com")]
[InlineData("stella://")]
public void Parse_Returns_Null_For_Invalid_Uris(string? uri)
{
// Act
var parsed = EvidenceUri.Parse(uri!);
// Assert
parsed.Should().BeNull();
}
[Fact]
public void Parse_Is_Case_Insensitive_For_Scheme()
{
// Act
var parsed = EvidenceUri.Parse("STELLA://reachgraph/sha256:abc");
// Assert
parsed.Should().NotBeNull();
parsed!.Type.Should().Be("reachgraph");
}
}

View File

@@ -0,0 +1,184 @@
// <copyright file="HybridReachabilityResultTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Unit tests for <see cref="HybridReachabilityResult"/> and related types.
/// </summary>
[Trait("Category", "Unit")]
public sealed class HybridReachabilityResultTests
{
[Fact]
public void ContentDigest_Is_Deterministic()
{
// Arrange
var now = DateTimeOffset.Parse("2026-01-09T12:00:00Z");
var result1 = CreateResult(now);
var result2 = CreateResult(now);
// Act & Assert
result1.ContentDigest.Should().Be(result2.ContentDigest);
}
[Fact]
public void ContentDigest_Has_Sha256_Prefix()
{
// Arrange
var result = CreateResult(DateTimeOffset.UtcNow);
// Act & Assert
result.ContentDigest.Should().StartWith("sha256:");
result.ContentDigest.Should().HaveLength(7 + 64); // sha256: + 64 hex chars
}
[Fact]
public void ContentDigest_Changes_With_LatticeState()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var result1 = CreateResult(now, LatticeState.ConfirmedReachable);
var result2 = CreateResult(now, LatticeState.ConfirmedUnreachable);
// Act & Assert
result1.ContentDigest.Should().NotBe(result2.ContentDigest);
}
[Fact]
public void ContentDigest_Changes_With_Confidence()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var result1 = CreateResult(now, confidence: 0.5);
var result2 = CreateResult(now, confidence: 0.9);
// Act & Assert
result1.ContentDigest.Should().NotBe(result2.ContentDigest);
}
[Fact]
public void VerdictRecommendation_Affected_Has_Correct_Status()
{
// Act
var verdict = VerdictRecommendation.Affected("Test notes");
// Assert
verdict.VexStatus.Should().Be("affected");
verdict.StatusNotes.Should().Be("Test notes");
verdict.Justification.Should().BeNull();
verdict.RequiresManualReview.Should().BeFalse();
}
[Fact]
public void VerdictRecommendation_NotAffectedUnreachable_Has_Correct_Justification()
{
// Act
var verdict = VerdictRecommendation.NotAffectedUnreachable("Test notes");
// Assert
verdict.VexStatus.Should().Be("not_affected");
verdict.Justification.Should().Be("vulnerable_code_not_in_execute_path");
verdict.StatusNotes.Should().Be("Test notes");
}
[Fact]
public void VerdictRecommendation_UnderInvestigation_Has_Correct_Status()
{
// Act
var verdict = VerdictRecommendation.UnderInvestigation("Analyzing...");
// Assert
verdict.VexStatus.Should().Be("under_investigation");
verdict.StatusNotes.Should().Be("Analyzing...");
verdict.RequiresManualReview.Should().BeFalse();
}
[Fact]
public void VerdictRecommendation_NeedsReview_Sets_ManualReviewFlag()
{
// Act
var verdict = VerdictRecommendation.NeedsReview("Conflict detected");
// Assert
verdict.VexStatus.Should().Be("under_investigation");
verdict.RequiresManualReview.Should().BeTrue();
}
[Fact]
public void EvidenceBundle_Empty_Has_No_Uris()
{
// Act
var bundle = EvidenceBundle.Empty(DateTimeOffset.UtcNow);
// Assert
bundle.Uris.Should().BeEmpty();
bundle.Static.Should().BeNull();
bundle.Runtime.Should().BeNull();
}
[Fact]
public void StaticEvidence_Contains_Required_Fields()
{
// Arrange
var evidence = new StaticEvidence
{
ReachGraphDigest = "sha256:abc123",
SliceUri = "stella://reachgraph/sha256:abc123/slice?symbol=test"
};
// Assert
evidence.ReachGraphDigest.Should().Be("sha256:abc123");
evidence.SliceUri.Should().Contain("stella://");
evidence.CallPath.Should().BeEmpty();
}
[Fact]
public void RuntimeEvidence_Contains_Required_Fields()
{
// Arrange
var evidence = new RuntimeEvidence
{
FactsUri = "stella://signals/runtime/tenant/sha256:abc123",
ObservationWindow = TimeSpan.FromDays(7)
};
// Assert
evidence.FactsUri.Should().Contain("stella://");
evidence.ObservationWindow.Should().Be(TimeSpan.FromDays(7));
evidence.SampleTraceIds.Should().BeEmpty();
}
private static HybridReachabilityResult CreateResult(
DateTimeOffset computedAt,
LatticeState state = LatticeState.ConfirmedReachable,
double confidence = 0.95)
{
var symbol = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
return new HybridReachabilityResult
{
Symbol = symbol,
ArtifactDigest = "sha256:abc123",
LatticeState = state,
Confidence = confidence,
Verdict = VerdictRecommendation.Affected(),
Evidence = new EvidenceBundle
{
Uris = ImmutableArray.Create("stella://reachgraph/sha256:abc123"),
CollectedAt = computedAt
},
ComputedAt = computedAt
};
}
}

View File

@@ -0,0 +1,303 @@
// <copyright file="ReachabilityLatticeTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Unit tests for <see cref="ReachabilityLattice"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ReachabilityLatticeTests
{
[Fact]
public void Initial_State_Is_Unknown()
{
// Arrange & Act
var lattice = new ReachabilityLattice();
// Assert
lattice.CurrentState.Should().Be(LatticeState.Unknown);
lattice.Confidence.Should().Be(0.0);
}
[Fact]
public void ApplyEvidence_StaticReachable_TransitionsToStaticReachable()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
var transition = lattice.ApplyEvidence(EvidenceType.StaticReachable);
// Assert
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.StaticReachable);
lattice.CurrentState.Should().Be(LatticeState.StaticReachable);
lattice.Confidence.Should().BeGreaterThan(0);
}
[Fact]
public void ApplyEvidence_StaticUnreachable_TransitionsToStaticUnreachable()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
var transition = lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
// Assert
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.StaticUnreachable);
lattice.CurrentState.Should().Be(LatticeState.StaticUnreachable);
}
[Fact]
public void ApplyEvidence_RuntimeObserved_TransitionsToRuntimeObserved()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
var transition = lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Assert
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.RuntimeObserved);
lattice.CurrentState.Should().Be(LatticeState.RuntimeObserved);
}
[Fact]
public void StaticReachable_Plus_RuntimeObserved_ConfirmsReachable()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
lattice.ApplyEvidence(EvidenceType.StaticReachable);
var transition = lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Assert
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.ConfirmedReachable);
lattice.CurrentState.Should().Be(LatticeState.ConfirmedReachable);
}
[Fact]
public void StaticUnreachable_Plus_RuntimeUnobserved_ConfirmsUnreachable()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
var transition = lattice.ApplyEvidence(EvidenceType.RuntimeUnobserved);
// Assert
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.ConfirmedUnreachable);
lattice.CurrentState.Should().Be(LatticeState.ConfirmedUnreachable);
}
[Fact]
public void StaticUnreachable_Plus_RuntimeObserved_CreatesContest()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
var transition = lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Assert - conflict detected
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.Contested);
lattice.CurrentState.Should().Be(LatticeState.Contested);
}
[Fact]
public void RuntimeObserved_Plus_StaticUnreachable_CreatesContest()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
var transition = lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
// Assert - conflict detected
transition.Should().NotBeNull();
transition!.ToState.Should().Be(LatticeState.Contested);
lattice.CurrentState.Should().Be(LatticeState.Contested);
}
[Fact]
public void Contested_State_Stays_Contested()
{
// Arrange
var lattice = new ReachabilityLattice();
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Act - try more evidence
var transition1 = lattice.ApplyEvidence(EvidenceType.StaticReachable);
var transition2 = lattice.ApplyEvidence(EvidenceType.RuntimeUnobserved);
// Assert - stays contested
transition1!.ToState.Should().Be(LatticeState.Contested);
transition2!.ToState.Should().Be(LatticeState.Contested);
lattice.CurrentState.Should().Be(LatticeState.Contested);
}
[Fact]
public void Confidence_Is_Clamped_To_Valid_Range()
{
// Arrange
var lattice = new ReachabilityLattice();
// Act - apply conflicting evidence that might cause negative confidence
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Assert
lattice.Confidence.Should().BeGreaterThanOrEqualTo(0.0);
lattice.Confidence.Should().BeLessThanOrEqualTo(1.0);
}
[Fact]
public void Reset_Returns_To_Unknown()
{
// Arrange
var lattice = new ReachabilityLattice();
lattice.ApplyEvidence(EvidenceType.StaticReachable);
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
// Act
lattice.Reset();
// Assert
lattice.CurrentState.Should().Be(LatticeState.Unknown);
lattice.Confidence.Should().Be(0.0);
}
[Fact]
public void Combine_StaticReachable_RuntimeObserved_Returns_ConfirmedReachable()
{
// Arrange
var staticResult = new StaticReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
IsReachable = true,
AnalyzedAt = DateTimeOffset.UtcNow
};
var runtimeResult = new RuntimeReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
WasObserved = true,
ObservationWindow = TimeSpan.FromDays(7),
WindowStart = DateTimeOffset.UtcNow.AddDays(-7),
WindowEnd = DateTimeOffset.UtcNow,
HitCount = 100
};
// Act
var (state, confidence) = ReachabilityLattice.Combine(staticResult, runtimeResult);
// Assert
state.Should().Be(LatticeState.ConfirmedReachable);
confidence.Should().BeGreaterThan(0);
}
[Fact]
public void Combine_StaticUnreachable_RuntimeUnobserved_Returns_ConfirmedUnreachable()
{
// Arrange
var staticResult = new StaticReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
IsReachable = false,
AnalyzedAt = DateTimeOffset.UtcNow
};
var runtimeResult = new RuntimeReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
WasObserved = false,
ObservationWindow = TimeSpan.FromDays(7),
WindowStart = DateTimeOffset.UtcNow.AddDays(-7),
WindowEnd = DateTimeOffset.UtcNow
};
// Act
var (state, confidence) = ReachabilityLattice.Combine(staticResult, runtimeResult);
// Assert
state.Should().Be(LatticeState.ConfirmedUnreachable);
confidence.Should().BeGreaterThan(0);
}
[Fact]
public void Combine_NullStatic_RuntimeObserved_Returns_RuntimeObserved()
{
// Arrange
var runtimeResult = new RuntimeReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
WasObserved = true,
ObservationWindow = TimeSpan.FromDays(7),
WindowStart = DateTimeOffset.UtcNow.AddDays(-7),
WindowEnd = DateTimeOffset.UtcNow
};
// Act
var (state, _) = ReachabilityLattice.Combine(null, runtimeResult);
// Assert
state.Should().Be(LatticeState.RuntimeObserved);
}
[Fact]
public void Combine_StaticReachable_NullRuntime_Returns_StaticReachable()
{
// Arrange
var staticResult = new StaticReachabilityResult
{
Symbol = CreateTestSymbol(),
ArtifactDigest = "sha256:abc123",
IsReachable = true,
AnalyzedAt = DateTimeOffset.UtcNow
};
// Act
var (state, _) = ReachabilityLattice.Combine(staticResult, null);
// Assert
state.Should().Be(LatticeState.StaticReachable);
}
[Fact]
public void Combine_BothNull_Returns_Unknown()
{
// Act
var (state, confidence) = ReachabilityLattice.Combine(null, null);
// Assert
state.Should().Be(LatticeState.Unknown);
confidence.Should().Be(0);
}
private static SymbolRef CreateTestSymbol() => new()
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Reachability.Core\StellaOps.Reachability.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="FluentAssertions" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,212 @@
// <copyright file="SymbolRefTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.Core.Tests;
/// <summary>
/// Unit tests for <see cref="SymbolRef"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class SymbolRefTests
{
[Fact]
public void CanonicalId_Is_Deterministic()
{
// Arrange
var symbol1 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
var symbol2 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
// Act & Assert
symbol1.CanonicalId.Should().Be(symbol2.CanonicalId);
}
[Fact]
public void CanonicalId_Is_64_Char_Hex()
{
// Arrange
var symbol = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
// Act & Assert
symbol.CanonicalId.Should().HaveLength(64);
symbol.CanonicalId.Should().MatchRegex("^[a-f0-9]+$");
}
[Fact]
public void Different_Symbols_Have_Different_CanonicalIds()
{
// Arrange
var symbol1 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
var symbol2 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "set"
};
// Act & Assert
symbol1.CanonicalId.Should().NotBe(symbol2.CanonicalId);
}
[Fact]
public void DisplayName_Without_Type_Excludes_Underscore()
{
// Arrange
var symbol = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
// Act & Assert
symbol.DisplayName.Should().Be("lodash.get");
}
[Fact]
public void DisplayName_With_Type_Includes_Type()
{
// Arrange
var symbol = new SymbolRef
{
Purl = "pkg:nuget/System.Text.Json@8.0.0",
Namespace = "System.Text.Json",
Type = "JsonSerializer",
Method = "Serialize",
Signature = "(object)"
};
// Act & Assert
symbol.DisplayName.Should().Be("System.Text.Json.JsonSerializer.Serialize(object)");
}
[Fact]
public void FromFullyQualified_Parses_Simple_Name()
{
// Act
var symbol = SymbolRef.FromFullyQualified("pkg:npm/lodash@4.17.21", "get");
// Assert
symbol.Namespace.Should().Be("_");
symbol.Type.Should().Be("_");
symbol.Method.Should().Be("get");
}
[Fact]
public void FromFullyQualified_Parses_With_Namespace()
{
// Act
var symbol = SymbolRef.FromFullyQualified("pkg:npm/lodash@4.17.21", "lodash.get");
// Assert
symbol.Namespace.Should().Be("lodash");
symbol.Type.Should().Be("_");
symbol.Method.Should().Be("get");
}
[Fact]
public void FromFullyQualified_Parses_With_Type()
{
// Act
var symbol = SymbolRef.FromFullyQualified(
"pkg:nuget/System.Text.Json@8.0.0",
"System.Text.Json.JsonSerializer.Serialize");
// Assert
symbol.Namespace.Should().Be("System.Text.Json");
symbol.Type.Should().Be("JsonSerializer");
symbol.Method.Should().Be("Serialize");
}
[Fact]
public void FromFullyQualified_Extracts_Signature()
{
// Act
var symbol = SymbolRef.FromFullyQualified(
"pkg:nuget/System.Text.Json@8.0.0",
"System.Text.Json.JsonSerializer.Serialize(object, Type)");
// Assert
symbol.Method.Should().Be("Serialize");
symbol.Signature.Should().Be("(object, Type)");
}
[Fact]
public void FromFullyQualified_Handles_Empty_Signature()
{
// Act
var symbol = SymbolRef.FromFullyQualified(
"pkg:npm/lodash@4.17.21",
"lodash.get");
// Assert
symbol.Signature.Should().BeEmpty();
}
[Theory]
[InlineData(null, "test")]
[InlineData("", "test")]
[InlineData(" ", "test")]
[InlineData("pkg:npm/test@1.0.0", null)]
[InlineData("pkg:npm/test@1.0.0", "")]
[InlineData("pkg:npm/test@1.0.0", " ")]
public void FromFullyQualified_Throws_On_Invalid_Input(string? purl, string? name)
{
// Act & Assert
var act = () => SymbolRef.FromFullyQualified(purl!, name!);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Equality_Works_Correctly()
{
// Arrange
var symbol1 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
var symbol2 = new SymbolRef
{
Purl = "pkg:npm/lodash@4.17.21",
Namespace = "lodash",
Type = "_",
Method = "get"
};
// Act & Assert
symbol1.Should().Be(symbol2);
(symbol1 == symbol2).Should().BeTrue();
}
}