diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
index 0689f0dd..503d6b41 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
@@ -361,6 +361,73 @@ public sealed class WonderwareHistorianClientTests
outcomes[0].ShouldBe(HistorianWriteOutcome.Ack);
}
+ ///
+ /// When PerEventStatus is present but its length does not equal the batch size,
+ /// the client must ignore it and fall back to the legacy PerEventOk path to
+ /// avoid mis-indexing into the status array. Here a 2-event batch receives
+ /// PerEventStatus=[1] (length 1) but PerEventOk=[true, false]; the
+ /// outcomes must reflect the PerEventOk values ([Ack, RetryPlease]), not the status
+ /// byte (which would have produced [RetryPlease] had it been used).
+ ///
+ [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
+ }
+
+ ///
+ /// Status byte 1 (the only value that is neither 0 nor 2) must map to
+ /// via the default arm of the
+ /// PerEventStatus switch. A single-event batch with PerEventStatus=[1]
+ /// (length matches batch) must yield [RetryPlease].
+ ///
+ [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);
+ }
+
///
/// 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).