fix(abcip): keep the ALMD last-told state across a re-subscribe so a still-faulted alarm is not re-raised (#641 review)
Now that ALMD transitions route, a re-subscribe (every reconnect, every alarm-set change) started from an empty per-subscription record and re-RAISED every still-faulted alarm, which the host projects as Active+unacked - un-acknowledging a condition the server had just carried across the rebuild with its client ack. The record is now driver-owned and pruned to the new set, mirroring MTConnect's _deliveredConditions. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -206,6 +206,65 @@ public sealed class AbCipAlarmProjectionTests
|
||||
cleared.Kind.ShouldBe(AlarmTransitionKind.Clear);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #641 review — the server carries every surviving native condition's state (a client's
|
||||
/// acknowledgement included) across an address-space rebuild, and the host re-subscribes alarms on every
|
||||
/// reconnect and every alarm-set change. A re-subscribe used to start from an empty "last seen" record, so
|
||||
/// a still-faulted ALMD was re-RAISED, which the host projects as Active+UNacked — un-acknowledging an alarm
|
||||
/// an operator had already acknowledged. The record is the driver's, kept for refs that stay in the set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_resubscribe_does_not_re_raise_a_still_faulted_alarm()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(Options(enabled: true), "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var raises = new List<AlarmEventArgs>();
|
||||
drv.OnAlarmEvent += (_, e) => { if (e.Kind == AlarmTransitionKind.Raise) lock (raises) raises.Add(e); };
|
||||
|
||||
var sub1 = await drv.SubscribeAlarmsAsync(["HighTemp"], CancellationToken.None);
|
||||
await WaitForTagCreation(factory, "HighTemp");
|
||||
factory.Tags["HighTemp"].ValuesByOffset[8] = 500;
|
||||
factory.Tags["HighTemp"].ValuesByOffset[0] = 1;
|
||||
await WaitForEventAsync(raises, _ => true);
|
||||
|
||||
// A reconnect / alarm-set change: the same ref, still faulted.
|
||||
var sub2 = await drv.SubscribeAlarmsAsync(["HighTemp"], CancellationToken.None);
|
||||
await WaitForMoreReadsAsync(factory, "HighTemp", 6);
|
||||
|
||||
lock (raises) raises.Count.ShouldBe(1, "a re-subscribe must not re-raise (and so un-acknowledge) a still-faulted alarm");
|
||||
|
||||
await drv.UnsubscribeAlarmsAsync(sub1, CancellationToken.None);
|
||||
await drv.UnsubscribeAlarmsAsync(sub2, CancellationToken.None);
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #641 review — positive control: a ref that LEFT the subscribed set was materialised fresh
|
||||
/// (inactive) when it came back, so it is raised again.</summary>
|
||||
[Fact]
|
||||
public async Task A_ref_that_left_the_set_and_came_back_is_raised_again()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(Options(enabled: true), "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var raises = new List<AlarmEventArgs>();
|
||||
drv.OnAlarmEvent += (_, e) => { if (e.Kind == AlarmTransitionKind.Raise) lock (raises) raises.Add(e); };
|
||||
|
||||
await drv.SubscribeAlarmsAsync(["HighTemp"], CancellationToken.None);
|
||||
await WaitForTagCreation(factory, "HighTemp");
|
||||
factory.Tags["HighTemp"].ValuesByOffset[8] = 500;
|
||||
factory.Tags["HighTemp"].ValuesByOffset[0] = 1;
|
||||
await WaitForEventAsync(raises, _ => true);
|
||||
|
||||
await drv.SubscribeAlarmsAsync([], CancellationToken.None);
|
||||
await drv.SubscribeAlarmsAsync(["HighTemp"], CancellationToken.None);
|
||||
await WaitForEventAsync(raises, _ => raises.Count >= 2);
|
||||
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unsubscribing stops the alarm poll loop.</summary>
|
||||
[Fact]
|
||||
public async Task Unsubscribe_Stops_The_Poll_Loop()
|
||||
|
||||
Reference in New Issue
Block a user