using System.Diagnostics; using System.Text.Json.Nodes; using FluentAssertions; using StellaOps.Findings.Ledger.Domain; using StellaOps.Findings.Ledger.Observability; using StellaOps.Findings.Ledger.Services.Incident; using Xunit; namespace StellaOps.Findings.Ledger.Tests.Observability; public class LedgerTimelineTests { [Fact] public void EmitLedgerAppended_writes_structured_log_with_event_id() { var logger = new TestLogger(); using var activity = new Activity("test").Start(); var record = CreateRecord(); LedgerTimeline.EmitLedgerAppended(logger, record, "evidence-123"); logger.Entries.Should().HaveCount(1); var entry = logger.Entries.First(); entry.EventId.Name.Should().Be("ledger.event.appended"); entry.EventId.Id.Should().Be(6101); var state = AsDictionary(entry.State); state["Tenant"].Should().Be(record.TenantId); state["EvidenceRef"].Should().Be("evidence-123"); } [Fact] public void EmitProjectionUpdated_writes_structured_log_with_status() { var logger = new TestLogger(); using var activity = new Activity("test").Start(); var record = CreateRecord(); LedgerTimeline.EmitProjectionUpdated(logger, record, "affected"); var entry = logger.Entries.Single(); entry.EventId.Name.Should().Be("ledger.projection.updated"); entry.EventId.Id.Should().Be(6201); var state = AsDictionary(entry.State); state["Status"].Should().Be("affected"); } [Fact] public void EmitIncidentModeChanged_writes_structured_log() { var logger = new TestLogger(); var snapshot = new LedgerIncidentSnapshot( IsActive: true, ActivationId: "act-123", Actor: "actor-1", Reason: "reason", TenantId: "tenant-a", ChangedAt: DateTimeOffset.UtcNow, ExpiresAt: DateTimeOffset.UtcNow.AddMinutes(10), RetentionExtensionDays: 30); LedgerTimeline.EmitIncidentModeChanged(logger, snapshot, wasReactivation: false); var entry = logger.Entries.Single(e => e.EventId.Id == 6901); var state = AsDictionary(entry.State); state["RetentionExtensionDays"].Should().Be(30); state["ActivationId"].Should().Be("act-123"); } [Fact] public void EmitIncidentLagTrace_writes_structured_log() { var logger = new TestLogger(); var sample = new ProjectionLagSample( "tenant-a", Guid.NewGuid(), 10, "finding.created", "v1", 12.5, DateTimeOffset.UtcNow.AddSeconds(-12), DateTimeOffset.UtcNow); LedgerTimeline.EmitIncidentLagTrace(logger, sample); var entry = logger.Entries.Single(e => e.EventId.Id == 6902); var state = AsDictionary(entry.State); state["LagSeconds"].Should().Be(12.5); } private static LedgerEventRecord CreateRecord() { var payload = new JsonObject { ["status"] = "affected" }; return new LedgerEventRecord( "tenant-a", Guid.NewGuid(), 1, Guid.NewGuid(), "finding.status.changed", "v1", "finding-1", "artifact-1", null, "actor-1", "operator", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, payload, "hash-event", "hash-prev", "hash-leaf", "canonical-json"); } private static IDictionary AsDictionary(object? state) { if (state is not IEnumerable> pairs) { return new Dictionary(); } return pairs.ToDictionary(k => k.Key, v => v.Value); } }