using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Server.Phase7; namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Phase7; /// /// PR B.4 — pins the precedence order Phase7Composer uses to pick an /// : /// driver-provided > DI-registered > none. Driver wins so a future /// GalaxyDriver-as-IAlarmHistorianWriter takes the write path directly, /// preserving the v1 invariant where a driver that natively owns the /// historian client doesn't bounce through the sidecar IPC. /// [Trait("Category", "Unit")] public sealed class Phase7ComposerWriterSelectionTests { [Fact] public async Task No_driver_no_injected_writer_returns_null() { await using var host = new DriverHost(); var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injectedWriter: null, out var source); writer.ShouldBeNull(); source.ShouldBeNull(); } [Fact] public async Task Injected_writer_only_is_selected() { await using var host = new DriverHost(); var injected = new RecordingWriter("from-di"); var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injected, out var source); writer.ShouldBeSameAs(injected); source.ShouldStartWith("di:"); } [Fact] public async Task Driver_writer_wins_over_injected() { await using var host = new DriverHost(); var driver = new FakeDriverWithWriter("drv-1", "drv-out"); await host.RegisterAsync(driver, driverConfigJson: "{}", CancellationToken.None); var injected = new RecordingWriter("from-di"); var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injected, out var source); writer.ShouldBeSameAs(driver); source.ShouldBe("driver:drv-1"); } [Fact] public async Task First_driver_implementing_writer_wins() { await using var host = new DriverHost(); var driverNoWriter = new FakeDriverWithoutWriter("drv-1"); var driverWithWriter = new FakeDriverWithWriter("drv-2", "drv-out"); await host.RegisterAsync(driverNoWriter, "{}", CancellationToken.None); await host.RegisterAsync(driverWithWriter, "{}", CancellationToken.None); var writer = Phase7Composer.SelectAlarmHistorianWriter(host, injectedWriter: null, out var source); writer.ShouldBeSameAs(driverWithWriter); source.ShouldBe("driver:drv-2"); } private sealed class RecordingWriter : IAlarmHistorianWriter { public string Tag { get; } public RecordingWriter(string tag) { Tag = tag; } public Task> WriteBatchAsync( IReadOnlyList batch, CancellationToken cancellationToken) { var outcomes = new HistorianWriteOutcome[batch.Count]; for (var i = 0; i < outcomes.Length; i++) outcomes[i] = HistorianWriteOutcome.Ack; return Task.FromResult>(outcomes); } } private sealed class FakeDriverWithoutWriter : IDriver { public FakeDriverWithoutWriter(string id) { DriverInstanceId = id; } public string DriverInstanceId { get; } public string DriverType => "FakeNoWriter"; public Task InitializeAsync(string c, CancellationToken ct) => Task.CompletedTask; public Task ReinitializeAsync(string c, CancellationToken ct) => Task.CompletedTask; public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask; public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); public long GetMemoryFootprint() => 0; public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask; } private sealed class FakeDriverWithWriter : IDriver, IAlarmHistorianWriter { private readonly RecordingWriter _writer; public FakeDriverWithWriter(string id, string tag) { DriverInstanceId = id; _writer = new RecordingWriter(tag); } public string DriverInstanceId { get; } public string DriverType => "FakeWithWriter"; public Task InitializeAsync(string c, CancellationToken ct) => Task.CompletedTask; public Task ReinitializeAsync(string c, CancellationToken ct) => Task.CompletedTask; public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask; public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); public long GetMemoryFootprint() => 0; public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask; public Task> WriteBatchAsync( IReadOnlyList batch, CancellationToken cancellationToken) => _writer.WriteBatchAsync(batch, cancellationToken); } }