using Serilog; using Serilog.Core; using Serilog.Events; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Resilience; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Observability; [Trait("Category", "Integration")] public sealed class CapabilityInvokerEnrichmentTests { /// Verifies that InvokerExecute logs inside call site with structured properties. [Fact] public async Task InvokerExecute_LogsInsideCallSite_CarryStructuredProperties() { var sink = new InMemorySink(); var logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Sink(sink) .CreateLogger(); var invoker = new CapabilityInvoker( new DriverResiliencePipelineBuilder(), driverInstanceId: "drv-live", optionsAccessor: () => new DriverResilienceOptions { Tier = DriverTier.A }, driverType: "Modbus"); await invoker.ExecuteAsync( DriverCapability.Read, "plc-1", ct => { logger.Information("inside call site"); return ValueTask.FromResult(42); }, CancellationToken.None); var evt = sink.Events.ShouldHaveSingleItem(); evt.Properties["DriverInstanceId"].ToString().ShouldBe("\"drv-live\""); evt.Properties["DriverType"].ToString().ShouldBe("\"Modbus\""); evt.Properties["CapabilityName"].ToString().ShouldBe("\"Read\""); evt.Properties.ShouldContainKey("CorrelationId"); } /// Verifies that InvokerExecute does not leak context outside the call site. [Fact] public async Task InvokerExecute_DoesNotLeak_ContextOutsideCallSite() { var sink = new InMemorySink(); var logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Sink(sink) .CreateLogger(); var invoker = new CapabilityInvoker( new DriverResiliencePipelineBuilder(), driverInstanceId: "drv-a", optionsAccessor: () => new DriverResilienceOptions { Tier = DriverTier.A }); await invoker.ExecuteAsync(DriverCapability.Read, "host", _ => ValueTask.FromResult(1), CancellationToken.None); logger.Information("outside"); var outside = sink.Events.ShouldHaveSingleItem(); outside.Properties.ContainsKey("DriverInstanceId").ShouldBeFalse(); } private sealed class InMemorySink : ILogEventSink { /// Gets the list of captured log events. public List Events { get; } = []; /// Emits a log event by adding it to the captured events list. /// The log event to emit. public void Emit(LogEvent logEvent) => Events.Add(logEvent); } }