// // SPDX-License-Identifier: AGPL-3.0-or-later // Sprint: SPRINT_20260112_005_SIGNALS_runtime_nodehash (PW-SIG-003) // namespace StellaOps.Signals.Ebpf.Tests; using StellaOps.Signals.Ebpf.Schema; using Xunit; /// /// Tests for node hash emission and callstack hash determinism. /// Sprint: SPRINT_20260112_005_SIGNALS_runtime_nodehash (PW-SIG-003) /// [Trait("Category", "Unit")] public sealed class RuntimeNodeHashTests { [Fact] public void RuntimeCallEvent_NodeHashFields_HaveCorrectDefaults() { // Arrange & Act var evt = new RuntimeCallEvent { EventId = Guid.NewGuid(), ContainerId = "container-123", Pid = 1234, Tid = 5678, TimestampNs = 1000000000, Symbol = "vulnerable_func", }; // Assert - New fields should be null by default Assert.Null(evt.FunctionSignature); Assert.Null(evt.BinaryDigest); Assert.Null(evt.BinaryOffset); Assert.Null(evt.NodeHash); Assert.Null(evt.CallstackHash); } [Fact] public void RuntimeCallEvent_WithNodeHashFields_PreservesValues() { // Arrange & Act var evt = new RuntimeCallEvent { EventId = Guid.NewGuid(), ContainerId = "container-123", Pid = 1234, Tid = 5678, TimestampNs = 1000000000, Symbol = "vulnerable_func", Purl = "pkg:npm/lodash@4.17.21", FunctionSignature = "lodash.merge(object, ...sources)", BinaryDigest = "sha256:abc123def456", BinaryOffset = 0x1234, NodeHash = "sha256:nodehash123", CallstackHash = "sha256:callstackhash456" }; // Assert Assert.Equal("lodash.merge(object, ...sources)", evt.FunctionSignature); Assert.Equal("sha256:abc123def456", evt.BinaryDigest); Assert.Equal((ulong)0x1234, evt.BinaryOffset); Assert.Equal("sha256:nodehash123", evt.NodeHash); Assert.Equal("sha256:callstackhash456", evt.CallstackHash); } [Fact] public void ObservedCallPath_NodeHashFields_HaveCorrectDefaults() { // Arrange & Act var path = new ObservedCallPath { Symbols = ["main", "processRequest", "vulnerable_func"], ObservationCount = 100, Purl = "pkg:npm/lodash@4.17.21", }; // Assert - New fields should be null/empty by default Assert.Null(path.NodeHashes); Assert.Null(path.PathHash); Assert.Null(path.CallstackHash); Assert.Null(path.FunctionSignatures); Assert.Null(path.BinaryDigests); Assert.Null(path.BinaryOffsets); } [Fact] public void ObservedCallPath_WithNodeHashes_PreservesValues() { // Arrange var nodeHashes = new List { "sha256:hash1", "sha256:hash2", "sha256:hash3" }; var functionSignatures = new List { "main()", "process(req)", "vuln(data)" }; var binaryDigests = new List { "sha256:bin1", "sha256:bin2", "sha256:bin3" }; var binaryOffsets = new List { 0x1000, 0x2000, 0x3000 }; // Act var path = new ObservedCallPath { Symbols = ["main", "process", "vuln"], ObservationCount = 50, Purl = "pkg:golang/example.com/pkg@1.0.0", NodeHashes = nodeHashes, PathHash = "sha256:pathhash123", CallstackHash = "sha256:callstackhash456", FunctionSignatures = functionSignatures, BinaryDigests = binaryDigests, BinaryOffsets = binaryOffsets }; // Assert Assert.Equal(3, path.NodeHashes!.Count); Assert.Equal("sha256:hash1", path.NodeHashes[0]); Assert.Equal("sha256:pathhash123", path.PathHash); Assert.Equal("sha256:callstackhash456", path.CallstackHash); Assert.Equal(3, path.FunctionSignatures!.Count); Assert.Equal(3, path.BinaryDigests!.Count); Assert.Equal(3, path.BinaryOffsets!.Count); } [Fact] public void RuntimeSignalSummary_NodeHashFields_HaveCorrectDefaults() { // Arrange & Act var summary = new RuntimeSignalSummary { ContainerId = "container-456", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 1000, }; // Assert Assert.Null(summary.ObservedNodeHashes); Assert.Null(summary.ObservedPathHashes); Assert.Null(summary.CombinedPathHash); } [Fact] public void RuntimeSignalSummary_WithNodeHashes_PreservesValues() { // Arrange var observedNodeHashes = new List { "sha256:node1", "sha256:node2" }; var observedPathHashes = new List { "sha256:path1", "sha256:path2" }; // Act var summary = new RuntimeSignalSummary { ContainerId = "container-456", StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), StoppedAt = DateTimeOffset.UtcNow, TotalEvents = 1000, ObservedNodeHashes = observedNodeHashes, ObservedPathHashes = observedPathHashes, CombinedPathHash = "sha256:combinedhash" }; // Assert Assert.Equal(2, summary.ObservedNodeHashes!.Count); Assert.Equal(2, summary.ObservedPathHashes!.Count); Assert.Equal("sha256:combinedhash", summary.CombinedPathHash); } [Fact] public void NodeHashes_AreDeterministicallySorted() { // Arrange - Create hashes in unsorted order var unsortedHashes = new List { "sha256:zzz", "sha256:aaa", "sha256:mmm" }; // Act - Sort for determinism var sortedHashes = unsortedHashes.Order().ToList(); // Assert - Should be sorted alphabetically Assert.Equal("sha256:aaa", sortedHashes[0]); Assert.Equal("sha256:mmm", sortedHashes[1]); Assert.Equal("sha256:zzz", sortedHashes[2]); } [Fact] public void CallstackHash_DeterminismTest() { // Arrange - Same symbols should produce same path var path1 = new ObservedCallPath { Symbols = ["main", "process", "vulnerable_func"], Purl = "pkg:npm/lodash@4.17.21" }; var path2 = new ObservedCallPath { Symbols = ["main", "process", "vulnerable_func"], Purl = "pkg:npm/lodash@4.17.21" }; // Assert - Both paths have identical structure Assert.Equal(path1.Symbols.Count, path2.Symbols.Count); for (int i = 0; i < path1.Symbols.Count; i++) { Assert.Equal(path1.Symbols[i], path2.Symbols[i]); } Assert.Equal(path1.Purl, path2.Purl); } [Fact] public void NodeHash_MissingPurl_HandledGracefully() { // Arrange & Act var evt = new RuntimeCallEvent { EventId = Guid.NewGuid(), ContainerId = "container-123", Pid = 1234, Tid = 5678, TimestampNs = 1000000000, Symbol = "unknown_func", Purl = null, // Missing PURL FunctionSignature = "unknown_func()", }; // Assert - Should not throw, node hash will be null Assert.Null(evt.Purl); Assert.NotNull(evt.FunctionSignature); } [Fact] public void NodeHash_MissingSymbol_HandledGracefully() { // Arrange & Act var evt = new RuntimeCallEvent { EventId = Guid.NewGuid(), ContainerId = "container-123", Pid = 1234, Tid = 5678, TimestampNs = 1000000000, Symbol = null, // Missing symbol Purl = "pkg:npm/lodash@4.17.21", }; // Assert - Should not throw Assert.Null(evt.Symbol); Assert.NotNull(evt.Purl); } [Fact] public void RuntimeType_AllValuesSupported() { // Arrange & Act - Test all runtime types var runtimeTypes = Enum.GetValues(); // Assert Assert.Contains(RuntimeType.Unknown, runtimeTypes); Assert.Contains(RuntimeType.Native, runtimeTypes); Assert.Contains(RuntimeType.Jvm, runtimeTypes); Assert.Contains(RuntimeType.Node, runtimeTypes); Assert.Contains(RuntimeType.Python, runtimeTypes); Assert.Contains(RuntimeType.DotNet, runtimeTypes); Assert.Contains(RuntimeType.Go, runtimeTypes); Assert.Contains(RuntimeType.Ruby, runtimeTypes); } [Fact] public void PathHash_DifferentSymbolOrder_DifferentHash() { // Arrange - Same symbols but different order var path1 = new ObservedCallPath { Symbols = ["main", "process", "vulnerable_func"], PathHash = "sha256:path1hash" }; var path2 = new ObservedCallPath { Symbols = ["vulnerable_func", "process", "main"], PathHash = "sha256:path2hash" }; // Assert - Different order should produce different hash Assert.NotEqual(path1.PathHash, path2.PathHash); } }