fix(mtconnect): review rulings for CONDITION alarms (#626)
- Unavailable keeps an ACTIVE condition active: quality-only BadNoCommunication; on data return same statement => quality-only Good, Normal => Clear, different => Retrigger. - A changed alarm reference set (alarm-bearing delta = full rebuild) clears the whole last-told record so every active condition is re-asserted; the same set keeps it. - Quality-only events go through the host's AlarmQualityUpdate path, never a projected snapshot (which could ack a retained unacked condition or rewrite message/severity). - Deterministic tie-break between equal-level conditions (ordinal nativeCode, then type). - Dedup key is level+nativeCode+nativeSeverity+qualifier; a text-only change updates nothing. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -20,6 +20,7 @@ public sealed class MTConnectAlarmSourceTests
|
||||
private const string ConditionRawPath = "Plant/MT/dev1/SystemCond";
|
||||
private const string ConditionValueRawPath = "Plant/MT/dev1/SystemCondText";
|
||||
private const string ConditionId = "dev1_system_cond";
|
||||
private const string OtherConditionRawPath = "Plant/MT/dev1/OtherCond";
|
||||
private const long FixtureInstanceId = 1655000000L;
|
||||
private const long RestartedInstanceId = 1655999999L;
|
||||
|
||||
@@ -39,6 +40,7 @@ public sealed class MTConnectAlarmSourceTests
|
||||
// must not disturb the existing value surface.
|
||||
new RawTagEntry(ConditionRawPath, $$"""{"address":"{{ConditionId}}"}""", false, "dev1"),
|
||||
new RawTagEntry(ConditionValueRawPath, $$"""{"address":"{{ConditionId}}"}""", false, "dev1"),
|
||||
new RawTagEntry(OtherConditionRawPath, """{"address":"dev1_other_cond"}""", false, "dev1"),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -125,6 +127,55 @@ public sealed class MTConnectAlarmSourceTests
|
||||
await driver.ShutdownAsync(Ct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_ack_survives_an_adapter_unavailable_blip()
|
||||
{
|
||||
// A cppagent marks an adapter's items UNAVAILABLE when the adapter socket drops. The Fault must stay
|
||||
// active (no Clear/Raise pair, which would reset the operator's ack); only its quality moves.
|
||||
var (driver, agent, events) = await AlarmDriverAsync();
|
||||
await driver.SubscribeAlarmsAsync([ConditionRawPath], Ct);
|
||||
|
||||
await agent.PumpAsync(Chunk(108, 109, Cond("Fault", "E17", "overtemp"))).WaitAsync(Watchdog, Ct);
|
||||
await agent.PumpAsync(Chunk(109, 110, Cond("UNAVAILABLE"))).WaitAsync(Watchdog, Ct);
|
||||
await agent.PumpAsync(Chunk(110, 111, Cond("Fault", "E17", "overtemp"))).WaitAsync(Watchdog, Ct);
|
||||
|
||||
var seen = events.ToArray();
|
||||
seen.Select(e => e.Kind).ShouldBe(
|
||||
[AlarmTransitionKind.Raise, AlarmTransitionKind.Unspecified, AlarmTransitionKind.Unspecified]);
|
||||
seen[1].IsQualityOnly.ShouldBeTrue();
|
||||
seen[1].QualityStatusCode.ShouldBe(0x80310000u);
|
||||
seen[2].IsQualityOnly.ShouldBeTrue();
|
||||
seen[2].QualityStatusCode.ShouldBe(0x00000000u);
|
||||
|
||||
await driver.ShutdownAsync(Ct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_changed_alarm_set_re_tells_every_condition_and_the_same_set_does_not()
|
||||
{
|
||||
// Review MAJOR 2: a changed set is an alarm-bearing delta ⇒ full rebuild ⇒ every condition node was
|
||||
// recreated inactive, so an active condition still in the set must be re-asserted onto it.
|
||||
var (driver, agent, events) = await AlarmDriverAsync();
|
||||
await driver.SubscribeAlarmsAsync([ConditionRawPath], Ct);
|
||||
await agent.PumpAsync(Chunk(108, 109, Cond("Fault", "E17"))).WaitAsync(Watchdog, Ct);
|
||||
events.ShouldHaveSingleItem().Kind.ShouldBe(AlarmTransitionKind.Raise);
|
||||
|
||||
await driver.SubscribeAlarmsAsync([ConditionRawPath, OtherConditionRawPath], Ct);
|
||||
events.Count.ShouldBe(2, "A is re-raised onto its rebuilt node");
|
||||
events.Last().ConditionId.ShouldBe(ConditionRawPath);
|
||||
events.Last().Kind.ShouldBe(AlarmTransitionKind.Raise);
|
||||
|
||||
await driver.SubscribeAlarmsAsync([OtherConditionRawPath, ConditionRawPath], Ct);
|
||||
|
||||
// Positive control: the next real transition is the very next event.
|
||||
await agent.PumpAsync(Chunk(109, 110, Cond("Normal"))).WaitAsync(Watchdog, Ct);
|
||||
events.Select(e => e.Kind).ShouldBe(
|
||||
[AlarmTransitionKind.Raise, AlarmTransitionKind.Raise, AlarmTransitionKind.Clear],
|
||||
"the same set (a reconnect) must re-raise nothing");
|
||||
|
||||
await driver.ShutdownAsync(Ct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_resubscribe_with_an_unchanged_active_state_raises_nothing()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user