// // SPDX-License-Identifier: BUSL-1.1 // namespace StellaOps.Signals.Ebpf.Tests; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.Reachability; using StellaOps.Scanner.Reachability.Runtime; using StellaOps.Scanner.Reachability.Slices; using StellaOps.Signals.Ebpf.Schema; using Xunit; /// /// Tests for . /// public sealed class EbpfSignalMergerTests { private readonly EbpfSignalMerger _merger; private readonly RuntimeStaticMerger _baseMerger; public EbpfSignalMergerTests() { _baseMerger = new RuntimeStaticMerger(); _merger = new EbpfSignalMerger( _baseMerger, NullLogger.Instance); } [Fact] public void Merge_WithNoSignals_ReturnsSameGraph() { var graph = CreateTestGraph(); var result = _merger.Merge(graph, null); Assert.Same(graph, result.MergedGraph); Assert.Empty(result.Evidence); Assert.Equal(2, result.Statistics.StaticEdgeCount); Assert.Equal(0, result.Statistics.RuntimeEventCount); } [Fact] public void Merge_WithEmptySignals_ReturnsSameGraph() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 0, CallPaths = [], ObservedSymbols = [], }; var result = _merger.Merge(graph, signals); Assert.Same(graph, result.MergedGraph); Assert.Empty(result.Evidence); } [Fact] public void Merge_WithMatchingSignals_CreatesConfirmedEvidence() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 100, CallPaths = new List { new() { Symbols = ["main", "processRequest"], ObservationCount = 50, FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastObservedAt = DateTimeOffset.UtcNow, }, }, ObservedSymbols = ["main", "processRequest"], }; var result = _merger.Merge(graph, signals); Assert.NotEmpty(result.Evidence); Assert.Contains(result.Evidence, e => e.Type == RuntimeEvidenceType.RuntimeConfirmed); } [Fact] public void Merge_WithRuntimeOnlyPath_CreatesRuntimeOnlyEvidence() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 100, CallPaths = new List { new() { // Path not in static graph Symbols = ["dynamic_dispatch", "hidden_method"], ObservationCount = 20, FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastObservedAt = DateTimeOffset.UtcNow, }, }, ObservedSymbols = ["dynamic_dispatch", "hidden_method"], }; var result = _merger.Merge(graph, signals); Assert.Contains(result.Evidence, e => e.Type == RuntimeEvidenceType.RuntimeOnly); Assert.True(result.Statistics.RuntimeOnlyPathCount > 0); } [Fact] public void Merge_WithDetectedRuntimes_CreatesRuntimeDetectedEvidence() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 100, CallPaths = [], ObservedSymbols = [], DetectedRuntimes = [RuntimeType.Node, RuntimeType.Python], }; var result = _merger.Merge(graph, signals); Assert.Contains(result.Evidence, e => e.Type == RuntimeEvidenceType.RuntimeDetected && e.RuntimeType == "Node"); Assert.Contains(result.Evidence, e => e.Type == RuntimeEvidenceType.RuntimeDetected && e.RuntimeType == "Python"); } [Fact] public void ValidatePath_WithValidPath_ReturnsConfirmed() { var graph = CreateTestGraph(); var path = new ObservedCallPath { Symbols = ["main", "processRequest"], ObservationCount = 10, FirstObservedAt = DateTimeOffset.UtcNow, LastObservedAt = DateTimeOffset.UtcNow, }; var result = _merger.ValidatePath(graph, path); Assert.True(result.IsValid); Assert.Equal(PathType.Confirmed, result.PathType); Assert.Equal(1.0, result.MatchRatio); } [Fact] public void ValidatePath_WithUnknownPath_ReturnsRuntimeOnly() { var graph = CreateTestGraph(); var path = new ObservedCallPath { Symbols = ["unknown", "method"], ObservationCount = 10, FirstObservedAt = DateTimeOffset.UtcNow, LastObservedAt = DateTimeOffset.UtcNow, }; var result = _merger.ValidatePath(graph, path); Assert.True(result.IsValid); Assert.Equal(PathType.RuntimeOnly, result.PathType); Assert.Equal(0.0, result.MatchRatio); } [Fact] public void ValidatePath_WithShortPath_ReturnsInvalid() { var graph = CreateTestGraph(); var path = new ObservedCallPath { Symbols = ["single"], ObservationCount = 10, FirstObservedAt = DateTimeOffset.UtcNow, LastObservedAt = DateTimeOffset.UtcNow, }; var result = _merger.ValidatePath(graph, path); Assert.False(result.IsValid); Assert.Equal(PathType.Invalid, result.PathType); } [Fact] public void Merge_StatisticsAreAccurate() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 500, CallPaths = new List { new() { Symbols = ["main", "processRequest"], ObservationCount = 100, FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastObservedAt = DateTimeOffset.UtcNow, }, }, ObservedSymbols = ["main", "processRequest"], DroppedEvents = 10, }; var result = _merger.Merge(graph, signals); Assert.Equal(2, result.Statistics.StaticEdgeCount); Assert.Equal(500, result.Statistics.RuntimeEventCount); Assert.Equal(1, result.Statistics.CallPathCount); Assert.Equal(10, result.Statistics.DroppedEventCount); } [Fact] public void RuntimeEvidence_ContainsContainerId() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "my-container-id", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 100, CallPaths = new List { new() { Symbols = ["main", "processRequest"], ObservationCount = 10, FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastObservedAt = DateTimeOffset.UtcNow, }, }, ObservedSymbols = [], }; var result = _merger.Merge(graph, signals); Assert.All(result.Evidence, e => Assert.Equal("my-container-id", e.ContainerId)); } [Fact] public void EvidenceSource_IsEbpf() { var graph = CreateTestGraph(); var signals = new RuntimeSignalSummary { ContainerId = "container-123", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 100, CallPaths = new List { new() { Symbols = ["main", "processRequest"], ObservationCount = 10, FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastObservedAt = DateTimeOffset.UtcNow, }, }, ObservedSymbols = [], }; var result = _merger.Merge(graph, signals); Assert.All(result.Evidence, e => Assert.Equal(EvidenceSource.Ebpf, e.Source)); } private static RichGraph CreateTestGraph() { return new RichGraph( Nodes: new List { new("main", "main", null, null, "native", "entrypoint", null, null, null, null, null), new("processRequest", "processRequest", null, null, "native", "function", null, null, null, null, null), new("handleError", "handleError", null, null, "native", "function", null, null, null, null, null), }, Edges: new List { new("main", "processRequest", "call", null, null, null, 1.0, null), new("processRequest", "handleError", "call", null, null, null, 0.8, null), }, Roots: new List { new("main", "main", "entrypoint") }, Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null) ); } }