Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/MxGatewayAlarmMapperTests.cs
T
Joseph Doherty 9b78e6071d fix(dcl): identify MxGateway native alarms by object-relative reference
Surface native (Galaxy/MxGateway) alarms by their object-relative reference
(e.g. "Z28061.HeartbeatTimeoutAlarm") instead of the gateway's full provider
reference ("Galaxy!<area>.<object>.<alarm>"). The area is already preserved in
Category and the object reference is globally unique within the galaxy, so the
full provider prefix added only noise to the alarm identity operators see.

MxGatewayAlarmMapper.MapTransition/MapSnapshot now set SourceReference from
SourceObjectReference, falling back to AlarmFullReference only when the gateway
omits the object reference. +2 mapper tests; full DCL suite green (158).
2026-06-16 19:46:44 -04:00

203 lines
7.3 KiB
C#

using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
using CommonsTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
using ProtoConditionState = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState;
using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
/// <summary>Task-12: pure MxGateway alarm-feed proto → NativeAlarmTransition mapping.</summary>
public class MxGatewayAlarmMapperTests
{
[Fact]
public void MapTransition_AckTransition_IsAcknowledgedWithOperator()
{
var ev = new OnAlarmTransitionEvent
{
AlarmFullReference = "Tank01.Level.HiHi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
TransitionKind = ProtoTransitionKind.Acknowledge,
Severity = 600,
OperatorUser = "operator1",
OperatorComment = "ack",
Category = "Process",
Description = "hi"
};
var t = MxGatewayAlarmMapper.MapTransition(ev);
Assert.Equal(CommonsTransitionKind.Acknowledge, t.Kind);
Assert.True(t.Condition.Active);
Assert.True(t.Condition.Acknowledged);
Assert.Equal(600, t.Condition.Severity);
Assert.Equal("operator1", t.OperatorUser);
Assert.Equal("Tank01", t.SourceObjectReference);
}
[Fact]
public void MapConditionState_ActiveAcked_To_ActiveTrue_AckTrue()
{
var c = MxGatewayAlarmMapper.MapConditionState(ProtoConditionState.ActiveAcked, severity: 600);
Assert.True(c.Active);
Assert.True(c.Acknowledged);
Assert.Equal(600, c.Severity);
}
[Fact]
public void MapSnapshot_ActiveUnacked_IsSnapshotKind()
{
var snap = new ActiveAlarmSnapshot
{
AlarmFullReference = "Tank01.Level.Hi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.Hi",
CurrentState = ProtoConditionState.Active,
Severity = 1500 // out of range — must clamp
};
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
Assert.Equal(CommonsTransitionKind.Snapshot, t.Kind);
Assert.True(t.Condition.Active);
Assert.False(t.Condition.Acknowledged);
Assert.Equal(1000, t.Condition.Severity);
}
[Fact]
public void SourceReference_IsObjectRelative_NotFullProviderReference()
{
// The condition identity surfaced upward is the object-relative reference
// (e.g. "Z28061.HeartbeatTimeoutAlarm"), not the gateway's full provider
// reference ("Galaxy!<area>.<object>.<alarm>"). Area lives in Category.
var snap = new ActiveAlarmSnapshot
{
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
AlarmTypeName = "Syst",
Category = "CVDAisle_1",
CurrentState = ProtoConditionState.Active,
Severity = 400
};
var ev = new OnAlarmTransitionEvent
{
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
AlarmTypeName = "Syst",
TransitionKind = ProtoTransitionKind.Raise,
Severity = 400
};
var snapT = MxGatewayAlarmMapper.MapSnapshot(snap);
var liveT = MxGatewayAlarmMapper.MapTransition(ev);
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", snapT.SourceReference);
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", liveT.SourceReference);
Assert.Equal("CVDAisle_1", snapT.Category);
}
[Fact]
public void SourceReference_FallsBackToFullReference_WhenObjectReferenceEmpty()
{
var snap = new ActiveAlarmSnapshot
{
AlarmFullReference = "Galaxy!Area.Obj.Alarm",
SourceObjectReference = "",
CurrentState = ProtoConditionState.Active,
Severity = 100
};
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
Assert.Equal("Galaxy!Area.Obj.Alarm", t.SourceReference);
}
// ── CurrentValue / LimitValue (M2.13 / #27) ──────────────────────────────
[Fact]
public void MapTransition_CurrentAndLimitValue_PopulatedFromProto()
{
// The gateway proto OnAlarmTransitionEvent carries current_value and
// limit_value as MxValue union fields. Verify both are mapped through
// MxValueToString into the neutral NativeAlarmTransition strings.
var ev = new OnAlarmTransitionEvent
{
AlarmFullReference = "Tank01.Level.HiHi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
TransitionKind = ProtoTransitionKind.Raise,
Severity = 800,
CurrentValue = 95.3.ToMxValue(),
LimitValue = 90.0.ToMxValue()
};
var t = MxGatewayAlarmMapper.MapTransition(ev);
Assert.Equal("95.3", t.CurrentValue);
Assert.Equal("90", t.LimitValue);
}
[Fact]
public void MapTransition_AbsentCurrentAndLimitValue_YieldsEmpty()
{
// When the gateway sends events without current/limit value fields (optional),
// the resulting transition must have empty strings — never null.
var ev = new OnAlarmTransitionEvent
{
AlarmFullReference = "Tank01.Level.Hi",
SourceObjectReference = "Tank01",
AlarmTypeName = "AnalogLimitAlarm.Hi",
TransitionKind = ProtoTransitionKind.Raise,
Severity = 600
// CurrentValue and LimitValue not set → proto default (null reference)
};
var t = MxGatewayAlarmMapper.MapTransition(ev);
Assert.Equal("", t.CurrentValue);
Assert.Equal("", t.LimitValue);
}
[Fact]
public void MapSnapshot_CurrentAndLimitValue_PopulatedFromProto()
{
// ActiveAlarmSnapshot also carries current_value and limit_value.
var snap = new ActiveAlarmSnapshot
{
AlarmFullReference = "Pump01.Vibration.HiHi",
SourceObjectReference = "Pump01",
AlarmTypeName = "AnalogLimitAlarm.HiHi",
CurrentState = ProtoConditionState.Active,
Severity = 900,
CurrentValue = 12.7.ToMxValue(),
LimitValue = 10.0.ToMxValue()
};
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
Assert.Equal("12.7", t.CurrentValue);
Assert.Equal("10", t.LimitValue);
}
[Fact]
public void MapSnapshot_StringMxValue_ProducesStringCurrentValue()
{
// MxValue can carry string values (e.g. for discrete/string-type tags).
var snap = new ActiveAlarmSnapshot
{
AlarmFullReference = "Mode.Alarm",
SourceObjectReference = "Mode",
AlarmTypeName = "DiscreteAlarm",
CurrentState = ProtoConditionState.Active,
Severity = 500,
CurrentValue = "FAULT".ToMxValue()
};
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
Assert.Equal("FAULT", t.CurrentValue);
Assert.Equal("", t.LimitValue); // not set
}
}