test+docs(mtconnect): wire test for the Unavailable blip + ack survival; docs for the review rulings (#626)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+32
-6
@@ -27,7 +27,7 @@ public sealed class MTConnectConditionAlarmWireTests
|
||||
private const string DataItemId = "dev1_system_cond";
|
||||
|
||||
[Fact]
|
||||
public async Task A_condition_fault_and_its_clear_reach_the_raw_device_folder_as_part9_events()
|
||||
public async Task A_condition_fault_an_unavailable_blip_and_its_clear_reach_the_raw_device_folder()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var rig = await StartAsync("MTConnectConditionWire", encrypted: false, ct);
|
||||
@@ -53,12 +53,19 @@ public sealed class MTConnectConditionAlarmWireTests
|
||||
"mt-wire",
|
||||
_ => agent);
|
||||
|
||||
// What DriverHostActor.ForwardNativeAlarm does for a routed transition, minus the routing.
|
||||
// What DriverHostActor.ForwardNativeAlarm does for a routed transition, minus the routing: a
|
||||
// quality-only event takes the Quality-only node path, anything else is a projected snapshot.
|
||||
var projector = new NativeAlarmProjector();
|
||||
var routed = new List<string>();
|
||||
driver.OnAlarmEvent += (_, e) =>
|
||||
{
|
||||
lock (routed) routed.Add(e.ConditionId);
|
||||
if (e is { IsQualityOnly: true, QualityStatusCode: { } quality })
|
||||
{
|
||||
rig.Sink.WriteAlarmQuality(e.ConditionId, quality, e.SourceTimestampUtc, AddressSpaceRealm.Raw);
|
||||
return;
|
||||
}
|
||||
|
||||
rig.Sink.WriteAlarmCondition(e.ConditionId, projector.Project(e.ConditionId, e), e.SourceTimestampUtc, AddressSpaceRealm.Raw);
|
||||
};
|
||||
|
||||
@@ -69,14 +76,33 @@ public sealed class MTConnectConditionAlarmWireTests
|
||||
const string FaultMessage = "Spindle overtemperature [nativeCode=E17]";
|
||||
await RecordedEvents.WaitUntilAsync(() => events.Has(folderItem, FaultMessage));
|
||||
|
||||
agent.Push(Condition("Normal", null, null, 109));
|
||||
var fault = events.ConditionEvents(folderItem, FaultMessage).ShouldHaveSingleItem();
|
||||
fault[RecordedEvents.AckedField].Value.ShouldBe(false, "a fresh fault is unacknowledged");
|
||||
|
||||
// An operator acknowledges it (applied server-side, as the Part 9 method does), then a deploy clears
|
||||
// the host's projector prior state.
|
||||
rig.Sink.WriteAlarmCondition(RawAlarmPath,
|
||||
new AlarmConditionSnapshot(Active: true, Acknowledged: true, Confirmed: true, Enabled: true,
|
||||
AlarmShelvingKind.Unshelved, 800, FaultMessage),
|
||||
DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
await RecordedEvents.WaitUntilAsync(() => events.ConditionEvents(folderItem, FaultMessage).Count == 2);
|
||||
projector = new NativeAlarmProjector();
|
||||
|
||||
// The adapter blips: UNAVAILABLE, then the same Fault. Each is a quality-only annotation that must
|
||||
// leave the condition active, ACKNOWLEDGED, and carrying its message (review MAJOR 1 / MINOR 1).
|
||||
agent.Push(Condition("UNAVAILABLE", null, null, 109));
|
||||
agent.Push(Condition("Fault", "E17", "Spindle overtemperature", 110));
|
||||
await RecordedEvents.WaitUntilAsync(() => events.ConditionEvents(folderItem, FaultMessage).Count == 4);
|
||||
foreach (var blip in events.ConditionEvents(folderItem, FaultMessage).Skip(2))
|
||||
{
|
||||
blip[RecordedEvents.AckedField].Value.ShouldBe(true, "an Unavailable blip must not reset the ack");
|
||||
}
|
||||
|
||||
agent.Push(Condition("Normal", null, null, 111));
|
||||
await RecordedEvents.WaitUntilAsync(() => events.Has(folderItem, "SYSTEM Normal"));
|
||||
|
||||
lock (routed) routed.ShouldAllBe(id => id == RawAlarmPath, "ConditionId must be the alarm tag's RawPath");
|
||||
|
||||
var fault = events.ConditionEvents(folderItem, FaultMessage).ShouldHaveSingleItem();
|
||||
fault[RecordedEvents.AckedField].Value.ShouldBe(false, "a fresh fault is unacknowledged");
|
||||
|
||||
await driver.ShutdownAsync(ct);
|
||||
}
|
||||
finally
|
||||
|
||||
Reference in New Issue
Block a user