server: DriverNodeManager prefers IAlarmSource ack over IWritable (PR B.3)
Thirteenth PR of the alarms-over-gateway epic (docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver implements IAlarmSource, merged). When DriverNodeManager registers an AlarmConditionState with AlarmConditionService, it now picks the acknowledger: - Driver implements IAlarmSource → DriverAlarmSourceAcknowledger routes the operator comment through IAlarmSource.AcknowledgeAsync via the existing AlarmSurfaceInvoker (Phase 6.1 resilience pipeline, no-retry per decision #143). Preserves operator-comment fidelity end-to-end — the value-driven sub-attribute write collapses the comment into a single string write that loses MxAccess metadata. - Driver does not implement IAlarmSource → DriverWritableAcknowledger fallback (existing behaviour for AbCip / Modbus / S7 / etc). The dedup logic that prefers driver-native transitions over sub-attribute synthesis lives in AlarmConditionService and is already in place — drivers that surface OnAlarmEvent (B.2) feed the service directly, while sub-attribute writes still flow through DriverNodeManager's ConditionSink so a Galaxy template without $Alarm extensions stays functional. Tests: - 2 new routing-decision tests in DriverAlarmSourceAcknowledgerRoutingTests pin the IAlarmSource detection used at registration time. - Server build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// PR B.3 — pins the routing decision DriverNodeManager makes when registering
|
||||
/// an AlarmConditionState: drivers that implement <see cref="IAlarmSource"/>
|
||||
/// get an acknowledger that calls AcknowledgeAsync (driver-native path); drivers
|
||||
/// that don't fall back to the IWritable sub-attribute write.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DriverAlarmSourceAcknowledgerRoutingTests
|
||||
{
|
||||
[Fact]
|
||||
public void Driver_with_IAlarmSource_is_recognized()
|
||||
{
|
||||
IDriver driver = new FakeDriverWithAlarmSource("drv-1");
|
||||
(driver is IAlarmSource).ShouldBeTrue(
|
||||
"fakes that participate in the routing-test fixture must report IAlarmSource");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Driver_without_IAlarmSource_falls_to_writable_path()
|
||||
{
|
||||
IDriver driver = new FakeDriverNoAlarmSource("drv-2");
|
||||
(driver is IAlarmSource).ShouldBeFalse(
|
||||
"drivers without IAlarmSource take the legacy DriverWritableAcknowledger path");
|
||||
}
|
||||
|
||||
private sealed class FakeDriverWithAlarmSource(string id) : IDriver, IAlarmSource
|
||||
{
|
||||
public string DriverInstanceId { get; } = id;
|
||||
public string DriverType => "FakeAlarmSource";
|
||||
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<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IAlarmSubscriptionHandle>(new FakeHandle("h"));
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
private void NoUnusedWarning() => OnAlarmEvent?.Invoke(this, null!);
|
||||
}
|
||||
|
||||
private sealed class FakeDriverNoAlarmSource(string id) : IDriver
|
||||
{
|
||||
public string DriverInstanceId { get; } = id;
|
||||
public string DriverType => "FakeNoAlarmSource";
|
||||
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 FakeHandle(string id) : IAlarmSubscriptionHandle
|
||||
{
|
||||
public string DiagnosticId { get; } = id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user