test(ablegacy): dead-device gate tolerates a coalesced single-unit pass

Live run against ab_server (slc500/micrologix/plc5 x libplctag/native): with
coalescing on, the blackholed device's 20 tags are one block, i.e. one comms
fault per poll, so its backoff window opens on the second poll rather than the
first. The gate now asserts the actual contract: at most two timeouts across
three polls, the live device Good on every poll, and the third poll fast.

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:48:35 -04:00
parent 61dd498eab
commit 34e455f412
@@ -47,19 +47,23 @@ public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim)
await drv.InitializeAsync("{}", ct);
string[] refs = ["Live", .. dead.Select(d => d.Name)];
var sw = Stopwatch.StartNew();
var first = await drv.ReadAsync(refs, ct);
sw.Stop();
// Two consecutive comms faults mark the device down. With coalescing on, the dead device's 20 tags are ONE
// block (one fault per poll), so the window can open on the second poll rather than the first — either way
// at most two timeouts are paid before the device stops being dialled.
var total = TimeSpan.Zero;
TimeSpan last = default;
for (var poll = 0; poll < 3; poll++)
{
var sw = Stopwatch.StartNew();
var r = await drv.ReadAsync(refs, ct);
sw.Stop();
total += last = sw.Elapsed;
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, $"poll {poll}: the live device must be served in the same poll");
r.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good);
}
first[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "the live device must be served in the same poll");
first.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good);
sw.Elapsed.ShouldBeLessThan(timeout * 3, "a dead device may cost at most two timeouts per backoff window");
sw.Restart();
var second = await drv.ReadAsync(refs, ct);
sw.Stop();
second[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
sw.Elapsed.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled");
total.ShouldBeLessThan(timeout * 2.5, "a dead device may cost at most two timeouts per backoff window");
last.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled");
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
}
}