diff --git a/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs b/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs index a62e02f..d97917f 100644 --- a/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs +++ b/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs @@ -149,10 +149,13 @@ public static class ServiceCollectionExtensions sp.GetRequiredService>(), // SourceNode-stamping (Task 14): the local node identity is // threaded through so RecordEnqueueAsync can stamp the - // tracking row's SourceNode column. GetService — central - // composition roots may not register the provider, in which - // case the forwarder degrades to a null SourceNode rather - // than failing the DI resolution. + // tracking row's SourceNode column. GetService (not + // GetRequiredService) — test composition roots that build a + // stripped DI container may not register the provider, in + // which case the forwarder degrades to a null SourceNode + // rather than failing the DI resolution. Production hosts + // (site + central) always register it via + // SiteServiceRegistration.BindSharedOptions. sp.GetService())); // M3 Bundle F: bridge the store-and-forward retry-loop observer hook @@ -165,9 +168,11 @@ public static class ServiceCollectionExtensions // INodeIdentityProvider singleton can be threaded through — the // bridge stamps SiteCallOperational.SourceNode from // INodeIdentityProvider.NodeName on every cached-call lifecycle row. - // GetService (not GetRequiredService) — central composition roots may - // not register the provider, in which case the bridge degrades to a - // null SourceNode rather than failing the DI resolution. + // GetService (not GetRequiredService) — test composition roots that + // build a stripped DI container may not register the provider, in + // which case the bridge degrades to a null SourceNode rather than + // failing the DI resolution. Production hosts (site + central) + // always register it via SiteServiceRegistration.BindSharedOptions. services.AddSingleton(sp => new CachedCallLifecycleBridge( sp.GetRequiredService(), sp.GetRequiredService>(), diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs index b993083..2f87363 100644 --- a/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs +++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/DatabaseCachedWriteEmissionTests.cs @@ -328,4 +328,68 @@ public class DatabaseCachedWriteEmissionTests It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + + // ── SourceNode-stamping (Task 14) ── + + [Fact] + public async Task CachedWrite_StampsSourceNode_OnSubmitTelemetryRow() + { + // Symmetric to ExternalSystemCachedCallEmissionTests's + // CachedCall_StampsSourceNode_OnEverySiteCallOperationalRow — locks + // the DbOutbound emitter against a future refactor that drops + // _sourceNode from the Database.CachedWrite CachedSubmit row. + var gateway = new Mock(); + gateway + .Setup(g => g.CachedWriteAsync( + "myDb", "INSERT INTO t VALUES (1)", + It.IsAny?>(), + InstanceName, + It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + var forwarder = new CapturingForwarder(); + + var helper = new ScriptRuntimeContext.DatabaseHelper( + gateway.Object, + InstanceName, + NullLogger.Instance, + TestExecutionId, + auditWriter: null, + siteId: SiteId, + sourceScript: SourceScript, + cachedForwarder: forwarder, + parentExecutionId: null, + sourceNode: "node-a"); + + await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); + + var packet = Assert.Single(forwarder.Telemetry); + Assert.Equal("node-a", packet.Operational.SourceNode); + } + + [Fact] + public async Task CachedWrite_NoSourceNodeWired_LeavesSourceNodeNull() + { + // Default CreateHelper does NOT pass sourceNode — the legacy / test + // host path. The operational row carries null SourceNode, leaving + // central's SiteCalls.SourceNode NULL. + var gateway = new Mock(); + gateway + .Setup(g => g.CachedWriteAsync( + "myDb", "INSERT INTO t VALUES (1)", + It.IsAny?>(), + InstanceName, + It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + var forwarder = new CapturingForwarder(); + + var helper = CreateHelper(gateway.Object, forwarder); + await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); + + var packet = Assert.Single(forwarder.Telemetry); + Assert.Null(packet.Operational.SourceNode); + } }