M3 Bundle F (Task F1) wires the cached-call audit pipeline through the composition roots: - Central: register SiteCallAuditActor as a cluster singleton + proxy (mirrors AuditLogIngestActor and NotificationOutboxActor). Program.cs calls .AddSiteCallAudit() on the central role. - Site: register ICachedCallTelemetryForwarder + CachedCallLifecycleBridge in AddAuditLog (lazy factory — Central nodes degrade to audit-only emission because IOperationTrackingStore is site-only). - Site: bind CachedCallLifecycleBridge to ICachedCallLifecycleObserver so StoreAndForwardService picks it up via DI. - Site: introduce IStoreAndForwardSiteContext + Host adapter to surface the site id to StoreAndForwardService without creating a StoreAndForward -> HealthMonitoring project-reference cycle. - ScriptExecutionActor resolves ICachedCallTelemetryForwarder per script scope and threads it into ScriptRuntimeContext. CachedCallTelemetryForwarder's IOperationTrackingStore dependency is now nullable so Central DI validation succeeds with the lazy registration; the forwarder's tracking-half emission is a no-op when the store is absent. Tests: - AkkaHostedServiceAuditWiringTests: Central host builds with AddSiteCallAudit and resolves ICachedCallTelemetryForwarder; Site resolves the forwarder + bridge + observer + IStoreAndForwardSiteContext. - Full solution: 194 Host tests green, 241 SiteRuntime tests green, every other suite unchanged.
This commit is contained in:
@@ -14,6 +14,7 @@ using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.ConfigurationDatabase;
|
||||
using ScadaLink.Host;
|
||||
using ScadaLink.Host.Actors;
|
||||
using ScadaLink.StoreAndForward;
|
||||
|
||||
namespace ScadaLink.Host.Tests;
|
||||
|
||||
@@ -189,6 +190,43 @@ public class CentralAuditWiringTests : IDisposable
|
||||
Assert.NotNull(client);
|
||||
Assert.IsType<NoOpSiteStreamAuditClient>(client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (T15): the Central composition root calls
|
||||
/// <c>AddSiteCallAudit()</c>. Today that extension is a no-op placeholder,
|
||||
/// but invoking it must not throw and the central host's service collection
|
||||
/// must build successfully — the actor's Props are constructed inline in
|
||||
/// <c>AkkaHostedService</c> (via the root <see cref="IServiceProvider"/>),
|
||||
/// not from a DI factory. Asserting the host built confirms the wiring
|
||||
/// call is in place; this test guards against accidentally removing it
|
||||
/// from <c>Program.cs</c>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Central_HostBuilds_With_AddSiteCallAudit_Wired()
|
||||
{
|
||||
// Reaching _factory.Services means WebApplicationFactory built the host
|
||||
// (DI validation completed). The fact this test is in the
|
||||
// CentralAuditWiringTests fixture means it ran against the Central
|
||||
// composition root path through Program.cs.
|
||||
Assert.NotNull(_factory.Services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F: the Central composition root registers
|
||||
/// <c>ICachedCallTelemetryForwarder</c> as a lazy singleton (the
|
||||
/// forwarder degrades to audit-only emission when the site-only
|
||||
/// <c>IOperationTrackingStore</c> is absent, matching the M2 lazy chain
|
||||
/// pattern). The binding is exercised here so a future regression that
|
||||
/// removes the registration or makes IOperationTrackingStore mandatory
|
||||
/// fails on the Central node, not just at first script execution.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Central_Resolves_ICachedCallTelemetryForwarder_LazySingleton()
|
||||
{
|
||||
var forwarder = _factory.Services.GetService<ICachedCallTelemetryForwarder>();
|
||||
Assert.NotNull(forwarder);
|
||||
Assert.IsType<CachedCallTelemetryForwarder>(forwarder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -303,4 +341,66 @@ public class SiteAuditWiringTests : IDisposable
|
||||
Assert.Equal(5, opts.Value.BusyIntervalSeconds);
|
||||
Assert.Equal(30, opts.Value.IdleIntervalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (T15): the site composition root resolves the cached-call
|
||||
/// telemetry forwarder. ScriptExecutionActor consumes this through
|
||||
/// <c>GetService<ICachedCallTelemetryForwarder>()</c> on every script
|
||||
/// execution; a missing registration would silently degrade
|
||||
/// <c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c> to the
|
||||
/// "no-emission" path and break the M3 audit pipeline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Site_Resolves_ICachedCallTelemetryForwarder()
|
||||
{
|
||||
var forwarder = _host.Services.GetService<ICachedCallTelemetryForwarder>();
|
||||
Assert.NotNull(forwarder);
|
||||
Assert.IsType<CachedCallTelemetryForwarder>(forwarder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (T15): the site composition root resolves the lifecycle
|
||||
/// bridge that translates S&F retry-loop attempt notifications into
|
||||
/// cached-call telemetry packets.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Site_Resolves_CachedCallLifecycleBridge_AsSingleton()
|
||||
{
|
||||
var a = _host.Services.GetService<CachedCallLifecycleBridge>();
|
||||
var b = _host.Services.GetService<CachedCallLifecycleBridge>();
|
||||
Assert.NotNull(a);
|
||||
Assert.NotNull(b);
|
||||
Assert.Same(a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (T15): the lifecycle bridge is bound to the
|
||||
/// <see cref="ICachedCallLifecycleObserver"/> contract that
|
||||
/// StoreAndForwardService consults at construction time. Without this
|
||||
/// binding the S&F service is built with a null observer and the
|
||||
/// retry-loop telemetry never reaches the audit pipeline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Site_ICachedCallLifecycleObserver_IsTheLifecycleBridge()
|
||||
{
|
||||
var observer = _host.Services.GetService<ICachedCallLifecycleObserver>();
|
||||
var bridge = _host.Services.GetService<CachedCallLifecycleBridge>();
|
||||
Assert.NotNull(observer);
|
||||
Assert.NotNull(bridge);
|
||||
Assert.Same(bridge, observer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M3 Bundle F (T15): the Host registers an
|
||||
/// <see cref="IStoreAndForwardSiteContext"/> adapter so the S&F service
|
||||
/// can resolve the site id at composition time WITHOUT introducing a
|
||||
/// StoreAndForward → HealthMonitoring project-reference cycle.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Site_Resolves_IStoreAndForwardSiteContext_FromHost()
|
||||
{
|
||||
var ctx = _host.Services.GetService<IStoreAndForwardSiteContext>();
|
||||
Assert.NotNull(ctx);
|
||||
Assert.Equal("TestSite", ctx!.SiteId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user