Files
git.stella-ops.org/src/Signals/__Tests/StellaOps.Signals.Tests/RuntimeUpdatedEventTests.cs

271 lines
9.4 KiB
C#

// <copyright file="RuntimeUpdatedEventTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-004)
// </copyright>
using StellaOps.Signals.Models;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Signals.Tests;
/// <summary>
/// Tests for runtime updated event generation, idempotency, and ordering.
/// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-004)
/// </summary>
[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<string> { "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<string> { "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);
}
}