// // SPDX-License-Identifier: AGPL-3.0-or-later // Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-004) // using StellaOps.Signals.Models; using StellaOps.TestKit; using Xunit; namespace StellaOps.Signals.Tests; /// /// Tests for runtime updated event generation, idempotency, and ordering. /// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-004) /// [Trait("Category", TestCategories.Unit)] public sealed class RuntimeUpdatedEventTests { private static readonly DateTimeOffset FixedTime = new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero); [Fact] public void Factory_CreatesEventWithDeterministicId() { // Arrange & Act var event1 = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "cve:CVE-2026-1234|purl:pkg:npm/lodash@4.17.21", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime); var event2 = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "cve:CVE-2026-1234|purl:pkg:npm/lodash@4.17.21", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime); // Assert - Same inputs should produce same event ID Assert.Equal(event1.EventId, event2.EventId); } [Fact] public void Factory_DifferentEvidenceDigest_ProducesDifferentId() { // Arrange & Act var event1 = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "cve:CVE-2026-1234|purl:pkg:npm/lodash@4.17.21", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime); var event2 = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "cve:CVE-2026-1234|purl:pkg:npm/lodash@4.17.21", evidenceDigest: "sha256:different", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime); // Assert Assert.NotEqual(event1.EventId, event2.EventId); } [Fact] public void Factory_ExploitTelemetry_AlwaysTriggersReanalysis() { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.ExploitTelemetry, newState: "exploited", confidence: 0.5, fromRuntime: true, occurredAtUtc: FixedTime); // Assert Assert.True(evt.TriggerReanalysis); Assert.NotNull(evt.ReanalysisReason); } [Fact] public void Factory_StateChange_TriggersReanalysis() { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.StateChange, newState: "confirmed", confidence: 0.7, fromRuntime: true, occurredAtUtc: FixedTime, previousState: "suspected"); // Assert Assert.True(evt.TriggerReanalysis); } [Fact] public void Factory_HighConfidenceRuntime_TriggersReanalysis() { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.ConfidenceIncrease, newState: "observed", confidence: 0.95, fromRuntime: true, occurredAtUtc: FixedTime, previousState: "observed"); // Assert Assert.True(evt.TriggerReanalysis); } [Fact] public void Factory_LowConfidence_DoesNotTriggerReanalysis() { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.ConfidenceIncrease, newState: "observed", confidence: 0.3, fromRuntime: true, occurredAtUtc: FixedTime, previousState: "observed"); // Assert - Low confidence state change without state change shouldn't trigger Assert.False(evt.TriggerReanalysis); } [Fact] public void Factory_ObservedNodeHashes_PreservedInOrder() { // Arrange var nodeHashes = new List { "sha256:zzz", "sha256:aaa", "sha256:mmm" }; // Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime, observedNodeHashes: nodeHashes); // Assert - Hashes should be preserved as provided Assert.Equal(3, evt.ObservedNodeHashes.Length); Assert.Equal("sha256:zzz", evt.ObservedNodeHashes[0]); Assert.Equal("sha256:aaa", evt.ObservedNodeHashes[1]); Assert.Equal("sha256:mmm", evt.ObservedNodeHashes[2]); } [Fact] public void Factory_AllFieldsPopulated() { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "cve:CVE-2026-1234|purl:pkg:npm/lodash@4.17.21", evidenceDigest: "sha256:abc123", updateType: RuntimeUpdateType.NewCallPath, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime, cveId: "CVE-2026-1234", purl: "pkg:npm/lodash@4.17.21", callgraphId: "cg-scan-001", previousState: "suspected", runtimeMethod: "ebpf", observedNodeHashes: new List { "sha256:node1" }, pathHash: "sha256:path1", traceId: "trace-001"); // Assert Assert.Equal("test-tenant", evt.Tenant); Assert.Equal("CVE-2026-1234", evt.CveId); Assert.Equal("pkg:npm/lodash@4.17.21", evt.Purl); Assert.Equal("cg-scan-001", evt.CallgraphId); Assert.Equal("suspected", evt.PreviousState); Assert.Equal("observed", evt.NewState); Assert.Equal("ebpf", evt.RuntimeMethod); Assert.Equal("sha256:path1", evt.PathHash); Assert.Equal("trace-001", evt.TraceId); Assert.Equal(RuntimeEventTypes.Updated, evt.EventType); Assert.Equal("1.0.0", evt.Version); } [Fact] public void RuntimeEventTypes_HasCorrectConstants() { // Assert Assert.Equal("runtime.updated", RuntimeEventTypes.Updated); Assert.Equal("runtime.updated@1", RuntimeEventTypes.UpdatedV1); Assert.Equal("runtime.ingested", RuntimeEventTypes.Ingested); Assert.Equal("runtime.confirmed", RuntimeEventTypes.Confirmed); Assert.Equal("runtime.exploit_detected", RuntimeEventTypes.ExploitDetected); } [Theory] [InlineData(RuntimeUpdateType.NewObservation)] [InlineData(RuntimeUpdateType.StateChange)] [InlineData(RuntimeUpdateType.ConfidenceIncrease)] [InlineData(RuntimeUpdateType.NewCallPath)] [InlineData(RuntimeUpdateType.ExploitTelemetry)] public void Factory_AllUpdateTypes_CreateValidEvents(RuntimeUpdateType updateType) { // Arrange & Act var evt = RuntimeUpdatedEventFactory.Create( tenant: "test-tenant", subjectKey: "test-subject", evidenceDigest: "sha256:abc123", updateType: updateType, newState: "observed", confidence: 0.85, fromRuntime: true, occurredAtUtc: FixedTime); // Assert Assert.NotNull(evt); Assert.NotEmpty(evt.EventId); Assert.Equal(updateType, evt.UpdateType); } [Fact] public void Event_IdempotencyKey_IsDeterministic() { // Arrange - Create same event multiple times with same inputs var events = Enumerable.Range(0, 5) .Select(_ => RuntimeUpdatedEventFactory.Create( tenant: "tenant-1", subjectKey: "subject-1", evidenceDigest: "sha256:evidence1", updateType: RuntimeUpdateType.NewObservation, newState: "observed", confidence: 0.9, fromRuntime: true, occurredAtUtc: FixedTime)) .ToList(); // Assert - All events should have the same ID var distinctIds = events.Select(e => e.EventId).Distinct().ToList(); Assert.Single(distinctIds); } }