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>
73 lines
3.3 KiB
C#
73 lines
3.3 KiB
C#
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;
|
|
}
|
|
}
|