using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using StellaOps.Determinism; using StellaOps.Signals.Models; using StellaOps.Signals.Options; using StellaOps.Signals.Services; using Xunit; using StellaOps.TestKit; namespace StellaOps.Signals.Tests; public class InMemoryEventsPublisherTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task PublishFactUpdatedAsync_EmitsStructuredEvent() { var logger = new TestLogger(); var options = new SignalsOptions(); options.Events.Driver = "inmemory"; options.Events.Stream = "signals.fact.updated.v1"; options.Events.DefaultTenant = "tenant-default"; var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System, SystemGuidProvider.Instance); var publisher = new InMemoryEventsPublisher(logger, builder); var fact = new ReachabilityFactDocument { SubjectKey = "tenant:image@sha256:abc", CallgraphId = "cg-123", ComputedAt = System.DateTimeOffset.Parse("2025-11-18T12:00:00Z"), States = new List { new() { Target = "pkg:pypi/django", Reachable = true, Confidence = 0.9, Bucket = "runtime", Weight = 0.45 }, new() { Target = "pkg:pypi/requests", Reachable = false, Confidence = 0.2, Bucket = "runtime", Weight = 0.45 } }, RuntimeFacts = new List { new() { SymbolId = "funcA", HitCount = 3 } } }; var envelope = builder.Build(fact); await publisher.PublishFactUpdatedAsync(fact, CancellationToken.None); Assert.Equal("signals.fact.updated.v1", envelope.Topic); Assert.Equal("signals.fact.updated@v1", envelope.Version); Assert.False(string.IsNullOrWhiteSpace(envelope.EventId)); Assert.Equal("tenant-default", envelope.Tenant); Assert.Equal("tenant:image@sha256:abc", envelope.SubjectKey); Assert.Equal("cg-123", envelope.CallgraphId); Assert.Equal(1, envelope.Summary.ReachableCount); Assert.Equal(1, envelope.Summary.UnreachableCount); Assert.Equal(1, envelope.Summary.RuntimeFactsCount); Assert.Equal("runtime", envelope.Summary.Bucket); Assert.Equal(2, envelope.Summary.StateCount); Assert.Contains("pkg:pypi/django", envelope.Summary.Targets); Assert.Contains("pkg:pypi/requests", envelope.Summary.Targets); Assert.Contains("signals.fact.updated.v1", logger.LastMessage); } private sealed class TestLogger : ILogger { public string LastMessage { get; private set; } = string.Empty; public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func formatter) { LastMessage = formatter(state, exception); } private sealed class NullScope : IDisposable { public static readonly NullScope Instance = new(); public void Dispose() { } } } }