test(historian): cover PerEventStatus length-mismatch fallback + Retry status mapping

This commit is contained in:
Joseph Doherty
2026-06-18 12:37:31 -04:00
parent e5f568d01f
commit 70aad3ef48
@@ -361,6 +361,73 @@ public sealed class WonderwareHistorianClientTests
outcomes[0].ShouldBe(HistorianWriteOutcome.Ack);
}
/// <summary>
/// When <c>PerEventStatus</c> is present but its length does not equal the batch size,
/// the client must ignore it and fall back to the legacy <c>PerEventOk</c> path to
/// avoid mis-indexing into the status array. Here a 2-event batch receives
/// <c>PerEventStatus=[1]</c> (length 1) but <c>PerEventOk=[true, false]</c>; the
/// outcomes must reflect the PerEventOk values ([Ack, RetryPlease]), not the status
/// byte (which would have produced [RetryPlease] had it been used).
/// </summary>
[Fact]
public async Task WriteBatchAsync_PerEventStatusLengthMismatch_FallsBackToPerEventOk()
{
await using var server = new FakeSidecarServer(Secret)
{
OnWriteAlarmEvents = _ => new WriteAlarmEventsReply
{
Success = true,
PerEventStatus = [1], // length 1 ≠ batch count 2 → must be ignored
PerEventOk = [true, false], // legacy fallback: true→Ack, false→RetryPlease
},
};
await server.StartAsync();
await using var client = TcpClientFor(server);
var batch = new[]
{
new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow),
new AlarmHistorianEvent("ev-2", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Acknowledged", "msg", "u", null, DateTime.UtcNow),
};
var outcomes = await client.WriteBatchAsync(batch, CancellationToken.None);
outcomes.Count.ShouldBe(2);
outcomes[0].ShouldBe(HistorianWriteOutcome.Ack); // PerEventOk[0] = true
outcomes[1].ShouldBe(HistorianWriteOutcome.RetryPlease); // PerEventOk[1] = false
}
/// <summary>
/// Status byte 1 (the only value that is neither 0 nor 2) must map to
/// <see cref="HistorianWriteOutcome.RetryPlease"/> via the default arm of the
/// <c>PerEventStatus</c> switch. A single-event batch with <c>PerEventStatus=[1]</c>
/// (length matches batch) must yield <c>[RetryPlease]</c>.
/// </summary>
[Fact]
public async Task WriteBatchAsync_PerEventStatusRetry_MapsToRetryPlease()
{
await using var server = new FakeSidecarServer(Secret)
{
OnWriteAlarmEvents = _ => new WriteAlarmEventsReply
{
Success = true,
PerEventStatus = [1], // status 1 → RetryPlease
},
};
await server.StartAsync();
await using var client = TcpClientFor(server);
var batch = new[]
{
new AlarmHistorianEvent("ev-retry", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow),
};
var outcomes = await client.WriteBatchAsync(batch, CancellationToken.None);
outcomes.Count.ShouldBe(1);
outcomes[0].ShouldBe(HistorianWriteOutcome.RetryPlease);
}
/// <summary>
/// Rolling-deploy back-compat: an older sidecar that sends an empty PerEventStatus but a
/// populated PerEventOk must still classify via the legacy bool path (false→RetryPlease).