worker(alarms): subtag value-source seam + synthesis state machine
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the subtag-fallback synthesis state machine. The machine
|
||||
/// consumes normalized subtag value changes (active/acked/priority) and
|
||||
/// emits <see cref="MxAlarmTransitionEvent"/> records mirroring the wnwrap
|
||||
/// consumer's UNACK_ALM / ACK_ALM / UNACK_RTN / ACK_RTN transitions. No COM
|
||||
/// or AVEVA install is required.
|
||||
/// </summary>
|
||||
public sealed class SubtagAlarmStateMachineTests
|
||||
{
|
||||
private static AlarmSubtagTarget Target() => new()
|
||||
{
|
||||
AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi",
|
||||
SourceObjectReference = "Tank01",
|
||||
ActiveSubtag = "Tank01.Level.HiHi.active",
|
||||
AckedSubtag = "Tank01.Level.HiHi.acked",
|
||||
AckCommentSubtag = "Tank01.Level.HiHi.ackmsg",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ActiveFalseToTrue_EmitsRaise()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||
var events = sm.Apply("Tank01.Level.HiHi.active", true, ts);
|
||||
var e = Assert.Single(events);
|
||||
Assert.Equal(MxAlarmStateKind.UnackAlm, e.Record.State);
|
||||
Assert.Equal(MxAlarmStateKind.Unspecified, e.PreviousState);
|
||||
Assert.Equal("Tank01.Level.HiHi", e.Record.TagName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AckedTrueWhileActive_EmitsAck()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||
sm.Apply("Tank01.Level.HiHi.active", true, ts);
|
||||
var events = sm.Apply("Tank01.Level.HiHi.acked", true, ts.AddSeconds(5));
|
||||
var e = Assert.Single(events);
|
||||
Assert.Equal(MxAlarmStateKind.AckAlm, e.Record.State);
|
||||
Assert.Equal(MxAlarmStateKind.UnackAlm, e.PreviousState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveTrueToFalse_WhileUnacked_EmitsUnackRtn()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||
sm.Apply("Tank01.Level.HiHi.active", true, ts);
|
||||
var events = sm.Apply("Tank01.Level.HiHi.active", false, ts.AddSeconds(10));
|
||||
var e = Assert.Single(events);
|
||||
Assert.Equal(MxAlarmStateKind.UnackRtn, e.Record.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveTrueToFalse_WhileAcked_EmitsAckRtn()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||
sm.Apply("Tank01.Level.HiHi.active", true, ts);
|
||||
sm.Apply("Tank01.Level.HiHi.acked", true, ts.AddSeconds(2));
|
||||
var events = sm.Apply("Tank01.Level.HiHi.active", false, ts.AddSeconds(10));
|
||||
var e = Assert.Single(events);
|
||||
Assert.Equal(MxAlarmStateKind.AckRtn, e.Record.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_ReflectsActiveAndAckedState()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var ts = new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc);
|
||||
sm.Apply("Tank01.Level.HiHi.active", true, ts);
|
||||
sm.Apply("Tank01.Level.HiHi.acked", true, ts);
|
||||
var snap = Assert.Single(sm.SnapshotActive());
|
||||
Assert.Equal(MxAlarmStateKind.AckAlm, snap.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownAddress_NoEvents()
|
||||
{
|
||||
var sm = new SubtagAlarmStateMachine(new[] { Target() });
|
||||
var events = sm.Apply("Some.Other.Tag.active", true, DateTime.UtcNow);
|
||||
Assert.Empty(events);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user