test(sitecall-audit): symmetric SourceNode coverage on DbOutbound emitter + clarify DI comments

Two follow-ups from the T13/T14 code review:

- M1: Add CachedWrite_StampsSourceNode_OnSubmitTelemetryRow and
  CachedWrite_NoSourceNodeWired_LeavesSourceNodeNull to DatabaseCachedWriteEmissionTests,
  mirroring the existing ApiOutbound SourceNode tests in
  ExternalSystemCachedCallEmissionTests. Site-emitter coverage now symmetric
  across both cached-call channels.
- M2: Clarify the GetService(INodeIdentityProvider) DI comments on the
  CachedCallTelemetryForwarder and CachedCallLifecycleBridge factories:
  it's test composition roots that may not register the provider, not
  central production. Both site and central hosts always register it via
  SiteServiceRegistration.BindSharedOptions.
This commit is contained in:
Joseph Doherty
2026-05-23 17:50:14 -04:00
parent 06ed0acead
commit 466e1454fe
2 changed files with 76 additions and 7 deletions

View File

@@ -149,10 +149,13 @@ public static class ServiceCollectionExtensions
sp.GetRequiredService<ILogger<CachedCallTelemetryForwarder>>(),
// 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<INodeIdentityProvider>()));
// 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<CachedCallLifecycleBridge>(sp => new CachedCallLifecycleBridge(
sp.GetRequiredService<ICachedCallTelemetryForwarder>(),
sp.GetRequiredService<ILogger<CachedCallLifecycleBridge>>(),

View File

@@ -328,4 +328,68 @@ public class DatabaseCachedWriteEmissionTests
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
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<IDatabaseGateway>();
gateway
.Setup(g => g.CachedWriteAsync(
"myDb", "INSERT INTO t VALUES (1)",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
.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<IDatabaseGateway>();
gateway
.Setup(g => g.CachedWriteAsync(
"myDb", "INSERT INTO t VALUES (1)",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
.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);
}
}