Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -8,6 +8,7 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -15,7 +16,8 @@ namespace StellaOps.Signals.Tests;
|
||||
/// </summary>
|
||||
public sealed class CallGraphSyncServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_WithValidDocument_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -40,7 +42,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
Assert.True(result.DurationMs >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_ProjectsToRepository()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +66,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
Assert.Single(repository.Entrypoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_SetsScanStatusToCompleted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +88,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
Assert.Equal("completed", repository.Scans[scanId].Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_WithEmptyDocument_ReturnsZeroCounts()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +120,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
Assert.False(result.WasUpdated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_WithNullDocument_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +136,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
service.SyncAsync(Guid.NewGuid(), "sha256:test-digest", null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_WithEmptyArtifactDigest_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +154,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
service.SyncAsync(Guid.NewGuid(), "", document));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteByScanAsync_RemovesScanFromRepository()
|
||||
{
|
||||
// Arrange
|
||||
@@ -171,7 +179,8 @@ public sealed class CallGraphSyncServiceTests
|
||||
Assert.Empty(repository.Entrypoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_OrdersNodesAndEdgesDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -17,6 +17,8 @@ using StellaOps.Signals.Storage;
|
||||
using StellaOps.Signals.Storage.Models;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class CallgraphIngestionServiceTests
|
||||
@@ -26,7 +28,8 @@ public class CallgraphIngestionServiceTests
|
||||
private readonly CallgraphNormalizationService _normalizer = new();
|
||||
private readonly TimeProvider _timeProvider = TimeProvider.System;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_normalizes_graph_and_persists_manifest_hash()
|
||||
{
|
||||
var parser = new StubParser("java");
|
||||
|
||||
@@ -6,13 +6,15 @@ using StellaOps.Signals.Parsing;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class CallgraphNormalizationServiceTests
|
||||
{
|
||||
private readonly CallgraphNormalizationService _service = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_adds_language_and_namespace_for_java()
|
||||
{
|
||||
var result = new CallgraphParseResult(
|
||||
@@ -36,7 +38,8 @@ public class CallgraphNormalizationServiceTests
|
||||
node.Name.Should().Be("com/example/Foo.bar:(I)V");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_deduplicates_edges_and_clamps_confidence()
|
||||
{
|
||||
var result = new CallgraphParseResult(
|
||||
@@ -66,7 +69,8 @@ public class CallgraphNormalizationServiceTests
|
||||
edge.Evidence.Should().BeEquivalentTo(new[] { "x" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_normalizes_gate_metadata()
|
||||
{
|
||||
var result = new CallgraphParseResult(
|
||||
|
||||
@@ -9,6 +9,8 @@ using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class EdgeBundleIngestionServiceTests
|
||||
@@ -25,7 +27,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
_service = new EdgeBundleIngestionService(NullLogger<EdgeBundleIngestionService>.Instance, options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_ParsesBundleAndStoresDocument()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +61,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.Contains("cas://reachability/edges/", result.CasUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_TracksRevokedEdgesForQuarantine()
|
||||
{
|
||||
// Arrange
|
||||
@@ -87,7 +91,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.True(result.Quarantined);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsEdgeRevokedAsync_ReturnsTrueForRevokedEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -110,7 +115,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.False(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "other_func", "some_func"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBundlesForGraphAsync_ReturnsAllBundlesForGraph()
|
||||
{
|
||||
// Arrange - ingest multiple bundles
|
||||
@@ -134,7 +140,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.Contains(bundles, b => b.BundleId == "bundle:2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRevokedEdgesAsync_ReturnsOnlyRevokedEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -162,7 +169,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.All(revokedEdges, e => Assert.True(e.Revoked));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_WithDsseStream_SetsVerifiedAndDsseFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -191,7 +199,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
Assert.EndsWith(".dsse", result.DsseCasUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_ThrowsOnMissingGraphHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -207,7 +216,8 @@ public class EdgeBundleIngestionServiceTests
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _service.IngestAsync(TestTenantId, stream, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_UpdatesExistingBundleWithSameId()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,11 +7,13 @@ using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class InMemoryEventsPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishFactUpdatedAsync_EmitsStructuredEvent()
|
||||
{
|
||||
var logger = new TestLogger<InMemoryEventsPublisher>();
|
||||
|
||||
@@ -4,9 +4,11 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public class ReachabilityFactDigestCalculatorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_ReturnsDeterministicDigest_ForEquivalentFacts()
|
||||
{
|
||||
var factA = new ReachabilityFactDocument
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Signals.Lattice;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class ReachabilityLatticeTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyUnreachable, ReachabilityLatticeState.Contested)]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.RuntimeObserved, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
@@ -19,7 +21,8 @@ public class ReachabilityLatticeTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.Unknown)]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedReachable, ReachabilityLatticeState.RuntimeObserved, ReachabilityLatticeState.RuntimeObserved)]
|
||||
[InlineData(ReachabilityLatticeState.Contested, ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.StaticallyReachable)]
|
||||
@@ -30,7 +33,8 @@ public class ReachabilityLatticeTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Join_IsCommutative()
|
||||
{
|
||||
var states = Enum.GetValues<ReachabilityLatticeState>();
|
||||
@@ -43,7 +47,8 @@ public class ReachabilityLatticeTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Meet_IsCommutative()
|
||||
{
|
||||
var states = Enum.GetValues<ReachabilityLatticeState>();
|
||||
@@ -56,14 +61,16 @@ public class ReachabilityLatticeTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JoinAll_WithEmptySequence_ReturnsUnknown()
|
||||
{
|
||||
var result = ReachabilityLattice.JoinAll(Array.Empty<ReachabilityLatticeState>());
|
||||
Assert.Equal(ReachabilityLatticeState.Unknown, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JoinAll_StopsEarlyOnContested()
|
||||
{
|
||||
var states = new[] { ReachabilityLatticeState.StaticallyReachable, ReachabilityLatticeState.Contested, ReachabilityLatticeState.Unknown };
|
||||
@@ -71,7 +78,8 @@ public class ReachabilityLatticeTests
|
||||
Assert.Equal(ReachabilityLatticeState.Contested, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true, false, false, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData(false, false, false, ReachabilityLatticeState.StaticallyUnreachable)]
|
||||
[InlineData(null, false, false, ReachabilityLatticeState.Unknown)]
|
||||
@@ -84,7 +92,8 @@ public class ReachabilityLatticeTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("entrypoint", false, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
[InlineData("direct", false, ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData("direct", true, ReachabilityLatticeState.ConfirmedReachable)]
|
||||
@@ -101,7 +110,8 @@ public class ReachabilityLatticeTests
|
||||
|
||||
public class ReachabilityLatticeStateExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.Unknown, "U")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyReachable, "SR")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyUnreachable, "SU")]
|
||||
@@ -115,7 +125,8 @@ public class ReachabilityLatticeStateExtensionsTests
|
||||
Assert.Equal(expectedCode, state.ToCode());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("U", ReachabilityLatticeState.Unknown)]
|
||||
[InlineData("SR", ReachabilityLatticeState.StaticallyReachable)]
|
||||
[InlineData("SU", ReachabilityLatticeState.StaticallyUnreachable)]
|
||||
@@ -132,7 +143,8 @@ public class ReachabilityLatticeStateExtensionsTests
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ReachabilityLatticeState.ConfirmedUnreachable, "unreachable")]
|
||||
[InlineData(ReachabilityLatticeState.StaticallyUnreachable, "unreachable")]
|
||||
[InlineData(ReachabilityLatticeState.RuntimeUnobserved, "unreachable")]
|
||||
|
||||
@@ -9,9 +9,11 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public class ReachabilityScoringServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecomputeAsync_applies_gate_multipliers_and_surfaces_gate_evidence()
|
||||
{
|
||||
var callgraph = new CallgraphDocument
|
||||
@@ -92,7 +94,8 @@ public class ReachabilityScoringServiceTests
|
||||
Assert.Equal(0.204, fact.RiskScore, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecomputeAsync_UsesConfiguredWeights()
|
||||
{
|
||||
var callgraph = new CallgraphDocument
|
||||
@@ -169,7 +172,8 @@ public class ReachabilityScoringServiceTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(fact.Metadata?["fact.digest"]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecomputeAsync_ComputesUncertaintyRiskScoreUsingConfiguredEntropyWeights()
|
||||
{
|
||||
var callgraph = new CallgraphDocument
|
||||
|
||||
@@ -8,11 +8,14 @@ using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class ReachabilityUnionIngestionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_ValidBundle_WritesFilesAndValidatesHashes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -11,11 +11,14 @@ using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class RouterEventsPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishFactUpdatedAsync_SendsEnvelopeToRouter()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -38,7 +41,8 @@ public class RouterEventsPublisherTests
|
||||
Assert.Contains(logger.Messages, m => m.Contains("Router publish succeeded"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishFactUpdatedAsync_LogsFailure()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
|
||||
@@ -10,6 +10,8 @@ using StellaOps.Signals.Storage;
|
||||
using StellaOps.Signals.Storage.Models;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class RuntimeFactsBatchIngestionTests
|
||||
@@ -17,7 +19,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
private const string TestTenantId = "test-tenant";
|
||||
private const string TestCallgraphId = "test-callgraph-123";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_ParsesNdjsonAndStoresArtifact()
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,7 +52,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
Assert.True(artifactStore.StoredArtifacts.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_HandlesGzipCompressedContent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +85,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
Assert.Equal(10, result.TotalHitCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_GroupsEventsBySubject()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +116,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
Assert.Contains("scan-2", result.SubjectKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_LinksCasUriToFactDocument()
|
||||
{
|
||||
// Arrange
|
||||
@@ -137,7 +143,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
Assert.Equal(result.BatchHash, fact.RuntimeFactsBatchHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_SkipsInvalidLines()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +170,8 @@ public class RuntimeFactsBatchIngestionTests
|
||||
Assert.Equal(3, result.TotalHitCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestBatchAsync_WorksWithoutArtifactStore()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -10,9 +10,11 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public class RuntimeFactsIngestionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_AggregatesHits_AndRecomputesReachability()
|
||||
{
|
||||
var factRepository = new InMemoryReachabilityFactRepository();
|
||||
@@ -165,7 +167,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
|
||||
#region Tenant Isolation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_IsolatesFactsBySubjectKey_NoDataLeakBetweenTenants()
|
||||
{
|
||||
// Arrange: Two tenants with different subjects
|
||||
@@ -209,7 +212,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
tenant2Facts.RuntimeFacts.Should().NotContain(f => f.SymbolId == "tenant1.secret.func");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_SubjectKeyIsDeterministic_ForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -240,7 +244,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
response1.SubjectKey.Should().Be("mylib|1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_BuildIdCorrelation_PreservesPerFactBuildId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -283,7 +288,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
cryptoFact.BuildId.Should().Be("gnu-build-id:a1b2c3d4e5f6");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_CodeIdCorrelation_PreservesPerFactCodeId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -315,7 +321,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
persisted.RuntimeFacts[0].CodeId.Should().Be("code:binary:abc123xyz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_RejectsRequest_WhenSubjectMissing()
|
||||
{
|
||||
// Arrange
|
||||
@@ -333,7 +340,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
() => service.IngestAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_RejectsRequest_WhenCallgraphIdMissing()
|
||||
{
|
||||
// Arrange
|
||||
@@ -351,7 +359,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
() => service.IngestAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_RejectsRequest_WhenEventsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -369,7 +378,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
() => service.IngestAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_RejectsRequest_WhenEventMissingSymbolId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -394,7 +404,8 @@ public class RuntimeFactsIngestionServiceTests
|
||||
|
||||
#region Evidence URI Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_PreservesEvidenceUri_FromRuntimeEvent()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,13 +5,15 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class RuntimeFactsProvenanceNormalizerTests
|
||||
{
|
||||
private readonly RuntimeFactsProvenanceNormalizer _normalizer = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_CreatesValidProvenanceFeed()
|
||||
{
|
||||
var events = new List<RuntimeFactEvent>
|
||||
@@ -33,7 +35,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(2, feed.Records.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_PopulatesAocMetadata()
|
||||
{
|
||||
var events = new List<RuntimeFactEvent>
|
||||
@@ -53,7 +56,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("ebpf-agent", feed.Metadata["request.source"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SetsRecordTypeBasedOnProcessMetadata()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -72,7 +76,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(ProvenanceSubjectType.Process, feed.Records[0].Subject.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SetsRecordTypeForNetworkConnection()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -89,7 +94,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("runtime.network.connection", feed.Records[0].RecordType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SetsRecordTypeForContainerActivity()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -107,7 +113,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(ProvenanceSubjectType.Container, feed.Records[0].Subject.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SetsRecordTypeForPackageLoaded()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -126,7 +133,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", feed.Records[0].Subject.Identifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_PopulatesRuntimeProvenanceFacts()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -164,7 +172,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("prod", facts.Metadata["env"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SetsConfidenceBasedOnEvidence()
|
||||
{
|
||||
var evtWithFullEvidence = new RuntimeFactEvent
|
||||
@@ -194,7 +203,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.True(minimalRecord.Confidence >= 0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_BuildsEvidenceWithCaptureMethod()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -217,7 +227,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("s3://evidence/trace.json", evidence.RawDataRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_NormalizesDigestWithSha256Prefix()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -236,7 +247,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.StartsWith("sha256:", evidence.SourceDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_SkipsEventsWithEmptySymbolId()
|
||||
{
|
||||
var events = new List<RuntimeFactEvent>
|
||||
@@ -254,7 +266,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("valid.symbol", feed.Records[0].Facts?.SymbolId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateContextFacts_ReturnsPopulatedContextFacts()
|
||||
{
|
||||
var events = new List<RuntimeFactEvent>
|
||||
@@ -275,7 +288,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(3, contextFacts.Provenance.Records.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_DeterminesObserverFromContainerContext()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -292,7 +306,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("container-runtime-agent", feed.Records[0].ObservedBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_DeterminesObserverFromProcessContext()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -309,7 +324,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("process-monitor-agent", feed.Records[0].ObservedBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_UsesObservedAtFromEvent()
|
||||
{
|
||||
var observedTime = DateTimeOffset.Parse("2025-12-06T08:00:00Z");
|
||||
@@ -328,7 +344,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(observedTime, feed.Records[0].OccurredAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_FallsBackToGeneratedAtWhenNoObservedAt()
|
||||
{
|
||||
var generatedTime = DateTimeOffset.Parse("2025-12-07T10:00:00Z");
|
||||
@@ -345,7 +362,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal(generatedTime, feed.Records[0].OccurredAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_BuildsSubjectIdentifierFromPurl()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -362,7 +380,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("pkg:npm/express@4.18.0", feed.Records[0].Subject.Identifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_BuildsSubjectIdentifierFromComponent()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
@@ -378,7 +397,8 @@ public class RuntimeFactsProvenanceNormalizerTests
|
||||
Assert.Equal("my-service@2.0.0", feed.Records[0].Subject.Identifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizeToFeed_UsesImageDigestAsSubjectForContainers()
|
||||
{
|
||||
var evt = new RuntimeFactEvent
|
||||
|
||||
@@ -4,6 +4,7 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class SchedulerRescanOrchestratorTests
|
||||
@@ -19,7 +20,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_sut = new SchedulerRescanOrchestrator(_mockClient, _timeProvider, _logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_CreatesJobWithCorrectPriority_Immediate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -35,7 +37,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_mockClient.LastRequest.PackageUrl.Should().Be("pkg:npm/lodash@4.17.21");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_CreatesJobWithCorrectPriority_Scheduled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,7 +52,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_mockClient.LastRequest!.Priority.Should().Be(RescanJobPriority.Normal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_CreatesJobWithCorrectPriority_Batch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +67,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_mockClient.LastRequest!.Priority.Should().Be(RescanJobPriority.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_PropagatesCorrelationId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -78,7 +83,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_mockClient.LastRequest.TenantId.Should().Be("tenant123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_ReturnsNextScheduledRescan_ForImmediate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +99,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.NextScheduledRescan.Should().Be(now.AddMinutes(15));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_ReturnsNextScheduledRescan_ForScheduled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -108,7 +115,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.NextScheduledRescan.Should().Be(now.AddHours(24));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_ReturnsNextScheduledRescan_ForBatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +131,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.NextScheduledRescan.Should().Be(now.AddDays(7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_ReturnsFailure_WhenClientFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -139,7 +148,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.ErrorMessage.Should().Be("Queue unavailable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerBatchRescanAsync_ProcessesAllItems()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +170,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.Results.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerBatchRescanAsync_EmptyList_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -175,7 +186,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
result.FailureCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_ExtractsTenantFromCallgraphId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +201,8 @@ public class SchedulerRescanOrchestratorTests
|
||||
_mockClient.LastRequest!.TenantId.Should().Be("acme-corp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerRescanAsync_UsesDefaultTenant_WhenNoCallgraphId()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -11,6 +11,7 @@ using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class ScoreExplanationServiceTests
|
||||
@@ -26,7 +27,8 @@ public class ScoreExplanationServiceTests
|
||||
NullLogger<ScoreExplanationService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithCvssOnly_ReturnsCorrectContribution()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -47,7 +49,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(50.0, result.RiskScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithEpss_ReturnsCorrectContribution()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -63,7 +66,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(5.0, epssContrib.Contribution); // 0.5 * 10.0 default multiplier
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("entrypoint", 25.0)]
|
||||
[InlineData("direct", 20.0)]
|
||||
[InlineData("runtime", 22.0)]
|
||||
@@ -83,7 +87,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(expectedContribution, reachContrib.Contribution);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("http", 15.0)]
|
||||
[InlineData("https", 15.0)]
|
||||
[InlineData("http_handler", 15.0)]
|
||||
@@ -104,7 +109,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(expectedContribution, exposureContrib.Contribution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithAuthGate_AppliesDiscount()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -120,7 +126,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(37.0, result.RiskScore); // 8.0 * 5.0 - 3.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithMultipleGates_CombinesDiscounts()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -137,7 +144,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(40.0, result.RiskScore); // 50 - 10
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithKev_AppliesBonus()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -153,7 +161,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(45.0, result.RiskScore); // 7.0 * 5.0 + 10.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_WithVexNotAffected_ReducesScore()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -169,7 +178,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.True(result.RiskScore < 50.0); // Should be significantly reduced
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_ClampsToMaxScore()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -188,7 +198,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Contains(result.Modifiers, m => m.Type == "cap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_ContributionsSumToTotal()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -205,7 +216,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(expectedSum, result.RiskScore, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_GeneratesSummary()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -220,7 +232,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Contains("risk", result.Summary, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_SetsAlgorithmVersion()
|
||||
{
|
||||
var request = new ScoreExplanationRequest { CvssScore = 5.0 };
|
||||
@@ -230,7 +243,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal("1.0.0", result.AlgorithmVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_PreservesEvidenceRef()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -244,7 +258,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal("scan:abc123", result.EvidenceRef);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeExplanationAsync_ReturnsSameAsSync()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
@@ -260,7 +275,8 @@ public class ScoreExplanationServiceTests
|
||||
Assert.Equal(syncResult.Contributions.Count, asyncResult.Contributions.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeExplanation_IsDeterministic()
|
||||
{
|
||||
var request = new ScoreExplanationRequest
|
||||
|
||||
@@ -7,11 +7,14 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public sealed class SimpleJsonCallgraphParserGateTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_parses_gate_fields_on_edges()
|
||||
{
|
||||
var json = """
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +36,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Add_ShouldAddSymbolsToCache()
|
||||
{
|
||||
// Arrange
|
||||
@@ -53,7 +55,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.True(_cache.Contains(buildId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_ShouldResolveKnownAddress()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +78,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Equal(buildId, symbol.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_ShouldReturnFalseForUnknownBuildId()
|
||||
{
|
||||
// Act
|
||||
@@ -86,7 +90,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Null(symbol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_ShouldReturnFalseForAddressOutsideSymbols()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +110,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Null(symbol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryResolve_ShouldUseBinarySearchForLargeSymbolTable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +137,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Equal(25UL, symbol.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetStatistics_ShouldReturnCorrectStats()
|
||||
{
|
||||
// Arrange
|
||||
@@ -157,7 +164,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Equal(0.5, stats.HitRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Clear_ShouldRemoveAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +186,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.Equal(0, stats.EntryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Remove_ShouldRemoveSpecificEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +207,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.True(_cache.Contains("build2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Persistence_ShouldWriteToDisk()
|
||||
{
|
||||
// Arrange
|
||||
@@ -215,7 +225,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
Assert.NotEmpty(files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustedSymbols_ShouldBeFlaggedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -239,7 +250,8 @@ public sealed class SlimSymbolCacheTests : IDisposable
|
||||
/// </summary>
|
||||
public sealed class CanonicalSymbolTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_ShouldFormatCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -258,7 +270,8 @@ public sealed class CanonicalSymbolTests
|
||||
Assert.Equal("abcdef1234567890:process_request+0x50", canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ShouldParseCanonicalString()
|
||||
{
|
||||
// Arrange
|
||||
@@ -274,7 +287,8 @@ public sealed class CanonicalSymbolTests
|
||||
Assert.Equal(0x100UL, symbol.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ShouldReturnNullForInvalidFormat()
|
||||
{
|
||||
Assert.Null(CanonicalSymbol.Parse(""));
|
||||
@@ -283,7 +297,8 @@ public sealed class CanonicalSymbolTests
|
||||
Assert.Null(CanonicalSymbol.Parse("build:func+invalid"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RoundTrip_ShouldPreserveData()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Signals.Lattice;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class UncertaintyTierTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, 0.50)]
|
||||
[InlineData(UncertaintyTier.T2, 0.25)]
|
||||
[InlineData(UncertaintyTier.T3, 0.10)]
|
||||
@@ -15,7 +17,8 @@ public class UncertaintyTierTests
|
||||
Assert.Equal(expected, tier.GetRiskModifier());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, true)]
|
||||
[InlineData(UncertaintyTier.T2, false)]
|
||||
[InlineData(UncertaintyTier.T3, false)]
|
||||
@@ -25,7 +28,8 @@ public class UncertaintyTierTests
|
||||
Assert.Equal(expected, tier.BlocksNotAffected());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, true)]
|
||||
[InlineData(UncertaintyTier.T2, true)]
|
||||
[InlineData(UncertaintyTier.T3, false)]
|
||||
@@ -35,7 +39,8 @@ public class UncertaintyTierTests
|
||||
Assert.Equal(expected, tier.RequiresWarning());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(UncertaintyTier.T1, "High")]
|
||||
[InlineData(UncertaintyTier.T2, "Medium")]
|
||||
[InlineData(UncertaintyTier.T3, "Low")]
|
||||
@@ -49,7 +54,8 @@ public class UncertaintyTierTests
|
||||
public class UncertaintyTierCalculatorTests
|
||||
{
|
||||
// U1 (MissingSymbolResolution) tier calculation
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("U1", 0.7, UncertaintyTier.T1)]
|
||||
[InlineData("U1", 0.8, UncertaintyTier.T1)]
|
||||
[InlineData("U1", 0.4, UncertaintyTier.T2)]
|
||||
@@ -62,7 +68,8 @@ public class UncertaintyTierCalculatorTests
|
||||
}
|
||||
|
||||
// U2 (MissingPurl) tier calculation
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("U2", 0.5, UncertaintyTier.T2)]
|
||||
[InlineData("U2", 0.6, UncertaintyTier.T2)]
|
||||
[InlineData("U2", 0.4, UncertaintyTier.T3)]
|
||||
@@ -73,7 +80,8 @@ public class UncertaintyTierCalculatorTests
|
||||
}
|
||||
|
||||
// U3 (UntrustedAdvisory) tier calculation
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("U3", 0.6, UncertaintyTier.T3)]
|
||||
[InlineData("U3", 0.8, UncertaintyTier.T3)]
|
||||
[InlineData("U3", 0.5, UncertaintyTier.T4)]
|
||||
@@ -84,7 +92,8 @@ public class UncertaintyTierCalculatorTests
|
||||
}
|
||||
|
||||
// U4 (Unknown) always T1
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("U4", 0.0, UncertaintyTier.T1)]
|
||||
[InlineData("U4", 0.5, UncertaintyTier.T1)]
|
||||
[InlineData("U4", 1.0, UncertaintyTier.T1)]
|
||||
@@ -94,7 +103,8 @@ public class UncertaintyTierCalculatorTests
|
||||
}
|
||||
|
||||
// Unknown code defaults to T4
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Unknown", 0.5, UncertaintyTier.T4)]
|
||||
[InlineData("", 0.5, UncertaintyTier.T4)]
|
||||
public void CalculateTier_UnknownCode_ReturnsT4(string code, double entropy, UncertaintyTier expected)
|
||||
@@ -102,14 +112,16 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(expected, UncertaintyTierCalculator.CalculateTier(code, entropy));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_WithEmptySequence_ReturnsT4()
|
||||
{
|
||||
var result = UncertaintyTierCalculator.CalculateAggregateTier(Array.Empty<(string, double)>());
|
||||
Assert.Equal(UncertaintyTier.T4, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_ReturnsMaxSeverity()
|
||||
{
|
||||
var states = new[] { ("U1", 0.3), ("U2", 0.6), ("U3", 0.5) }; // T3, T2, T4
|
||||
@@ -117,7 +129,8 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(UncertaintyTier.T2, result); // Maximum severity (lowest enum value)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateAggregateTier_StopsAtT1()
|
||||
{
|
||||
var states = new[] { ("U4", 1.0), ("U1", 0.3) }; // T1, T3
|
||||
@@ -125,7 +138,8 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(UncertaintyTier.T1, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[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
|
||||
@@ -142,7 +156,8 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(expected, result, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateRiskScore_ClampsToCeiling()
|
||||
{
|
||||
var result = UncertaintyTierCalculator.CalculateRiskScore(
|
||||
@@ -150,7 +165,8 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(1.0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateUnknownState_ReturnsU4WithMaxEntropy()
|
||||
{
|
||||
var (code, name, entropy) = UncertaintyTierCalculator.CreateUnknownState();
|
||||
@@ -159,7 +175,8 @@ public class UncertaintyTierCalculatorTests
|
||||
Assert.Equal(1.0, entropy);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(10, 100, 0.1)]
|
||||
[InlineData(50, 100, 0.5)]
|
||||
[InlineData(0, 100, 0.0)]
|
||||
|
||||
@@ -11,6 +11,8 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class UnknownsDecayServiceTests
|
||||
@@ -55,7 +57,8 @@ public class UnknownsDecayServiceTests
|
||||
|
||||
#region ApplyDecayAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayAsync_EmptySubject_ReturnsZeroCounts()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -70,7 +73,8 @@ public class UnknownsDecayServiceTests
|
||||
Assert.Equal(0, result.BandChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayAsync_SingleUnknown_UpdatesAndPersists()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -100,7 +104,8 @@ public class UnknownsDecayServiceTests
|
||||
Assert.True(updated[0].UpdatedAt >= now);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayAsync_BandChangesTracked()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -136,7 +141,8 @@ public class UnknownsDecayServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayAsync_MultipleUnknowns_ProcessesAll()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -186,7 +192,8 @@ public class UnknownsDecayServiceTests
|
||||
|
||||
#region RunNightlyDecayBatchAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunNightlyDecayBatchAsync_ProcessesAllSubjects()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -224,7 +231,8 @@ public class UnknownsDecayServiceTests
|
||||
Assert.True(result.Duration >= TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunNightlyDecayBatchAsync_RespectsMaxSubjectsLimit()
|
||||
{
|
||||
var decayOptions = new UnknownsDecayOptions { MaxSubjectsPerBatch = 1 };
|
||||
@@ -278,7 +286,8 @@ public class UnknownsDecayServiceTests
|
||||
Assert.Equal(1, result.TotalUnknowns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunNightlyDecayBatchAsync_CancellationRespected()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -311,7 +320,8 @@ public class UnknownsDecayServiceTests
|
||||
|
||||
#region ApplyDecayToUnknownAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayToUnknownAsync_UpdatesScoringFields()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -342,7 +352,8 @@ public class UnknownsDecayServiceTests
|
||||
Assert.NotNull(result.NormalizationTrace);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayToUnknownAsync_SetsNextRescanBasedOnBand()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
@@ -368,7 +379,8 @@ public class UnknownsDecayServiceTests
|
||||
|
||||
#region Decay Result Aggregation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ApplyDecayAsync_ResultCountsAreAccurate()
|
||||
{
|
||||
var (decayService, _) = CreateServices();
|
||||
|
||||
@@ -7,11 +7,13 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class UnknownsIngestionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_StoresNormalizedUnknowns()
|
||||
{
|
||||
var repo = new InMemoryUnknownsRepository();
|
||||
@@ -42,7 +44,8 @@ public class UnknownsIngestionServiceTests
|
||||
Assert.Equal("pkg:pypi/foo", repo.Stored[0].Purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_ThrowsWhenEmpty()
|
||||
{
|
||||
var repo = new InMemoryUnknownsRepository();
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -48,7 +49,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
#region End-to-End Flow Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EndToEnd_IngestScoreAndQueryByBand()
|
||||
{
|
||||
// Arrange: Create unknowns with varying factors
|
||||
@@ -140,7 +142,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
hotUnknown.NormalizationTrace.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EndToEnd_RecomputePreservesExistingData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +181,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
retrieved.SubjectKey.Should().Be(subjectKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EndToEnd_MultipleSubjectsIndependent()
|
||||
{
|
||||
// Arrange: Create unknowns in two different subjects
|
||||
@@ -235,7 +239,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
#region Rescan Scheduling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Rescan_GetDueForRescan_ReturnsCorrectBandItems()
|
||||
{
|
||||
// Arrange: Create unknowns with different bands
|
||||
@@ -281,7 +286,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
warmDue.Should().NotContain(u => u.Id == "warm-rescan", "WARM item not yet due");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Rescan_NextScheduledRescan_SetByBand()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +344,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
#region Query and Pagination Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Query_PaginationWorks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -367,7 +374,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
page1.Select(u => u.Id).Should().NotIntersectWith(page2.Select(u => u.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Query_FilterByBandReturnsOnlyMatchingItems()
|
||||
{
|
||||
// Arrange
|
||||
@@ -402,7 +410,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
#region Explain / Normalization Trace Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Explain_NormalizationTraceContainsAllFactors()
|
||||
{
|
||||
// Arrange
|
||||
@@ -474,7 +483,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
trace.AssignedBand.Should().Be(explained.Band.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Explain_TraceEnablesReplay()
|
||||
{
|
||||
// Arrange: Score an unknown
|
||||
@@ -521,7 +531,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameInputsProduceSameScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -575,7 +586,8 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
scored1.StalenessScore.Should().Be(scored2.StalenessScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_ConsecutiveRecomputesProduceSameResults()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -11,6 +11,7 @@ using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public class UnknownsScoringServiceTests
|
||||
@@ -43,7 +44,8 @@ public class UnknownsScoringServiceTests
|
||||
|
||||
#region Staleness Exponential Decay Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_ExponentialDecay_FreshEvidence_LowStaleness()
|
||||
{
|
||||
// Fresh evidence (analyzed today) should have low staleness
|
||||
@@ -66,7 +68,8 @@ public class UnknownsScoringServiceTests
|
||||
Assert.Equal(0, scored.DaysSinceLastAnalysis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_ExponentialDecay_StaleEvidence_HighStaleness()
|
||||
{
|
||||
// Old evidence (14 days) should have high staleness
|
||||
@@ -89,7 +92,8 @@ public class UnknownsScoringServiceTests
|
||||
Assert.Equal(14, scored.DaysSinceLastAnalysis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_ExponentialDecay_NeverAnalyzed_MaxStaleness()
|
||||
{
|
||||
// Never analyzed should have maximum staleness
|
||||
@@ -112,7 +116,8 @@ public class UnknownsScoringServiceTests
|
||||
Assert.Equal(_defaultOptions.StalenessMaxDays, scored.DaysSinceLastAnalysis);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0, 0.0)] // Fresh
|
||||
[InlineData(7, 0.35)] // Half tau - moderate staleness
|
||||
[InlineData(14, 0.70)] // At tau - significant staleness
|
||||
@@ -143,7 +148,8 @@ public class UnknownsScoringServiceTests
|
||||
|
||||
#region Band Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_BandAssignment_HotThreshold()
|
||||
{
|
||||
// High score should assign HOT band
|
||||
@@ -178,7 +184,8 @@ public class UnknownsScoringServiceTests
|
||||
$"Expected score >= {_defaultOptions.HotThreshold} for HOT, got {scored.Score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_BandAssignment_WarmThreshold()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -211,7 +218,8 @@ public class UnknownsScoringServiceTests
|
||||
$"Expected score < {_defaultOptions.HotThreshold} for WARM, got {scored.Score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_BandAssignment_ColdThreshold()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -238,7 +246,8 @@ public class UnknownsScoringServiceTests
|
||||
$"Expected score < {_defaultOptions.WarmThreshold} for COLD, got {scored.Score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_BandAssignment_CustomThresholds()
|
||||
{
|
||||
var customOptions = new UnknownsScoringOptions
|
||||
@@ -278,7 +287,8 @@ public class UnknownsScoringServiceTests
|
||||
|
||||
#region Weight Formula Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_WeightedFormula_VerifyComponents()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -320,7 +330,8 @@ public class UnknownsScoringServiceTests
|
||||
Assert.InRange(scored.Score, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_WeightedFormula_WeightsSumToOne()
|
||||
{
|
||||
// Verify default weights sum to 1.0
|
||||
@@ -337,7 +348,8 @@ public class UnknownsScoringServiceTests
|
||||
|
||||
#region Rescan Scheduling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_RescanScheduling_HotBand()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -369,7 +381,8 @@ public class UnknownsScoringServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_RescanScheduling_ColdBand()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -395,7 +408,8 @@ public class UnknownsScoringServiceTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScoreUnknown_Determinism_SameInputsSameOutput()
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
Reference in New Issue
Block a user