feat(historian): emit PermanentFail for poison alarm events via additive PerEventStatus sidecar IPC field
This commit is contained in:
+89
@@ -302,6 +302,95 @@ public sealed class WonderwareHistorianClientTests
|
||||
outcomes[1].ShouldBe(HistorianWriteOutcome.RetryPlease);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The granular PerEventStatus wire field maps directly: 0→Ack, 1→Retry, 2→PermanentFail.
|
||||
/// A poison event the sidecar marks Permanent (status 2) must dead-letter via
|
||||
/// <see cref="HistorianWriteOutcome.PermanentFail"/> rather than retrying.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_PerEventStatusPermanent_MapsToPermanentFail()
|
||||
{
|
||||
await using var server = new FakeSidecarServer(Secret)
|
||||
{
|
||||
OnWriteAlarmEvents = _ => new WriteAlarmEventsReply
|
||||
{
|
||||
Success = true,
|
||||
PerEventStatus = [2], // Permanent
|
||||
},
|
||||
};
|
||||
await server.StartAsync();
|
||||
|
||||
await using var client = TcpClientFor(server);
|
||||
var batch = new[]
|
||||
{
|
||||
new AlarmHistorianEvent("ev-poison", "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.PermanentFail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PerEventStatus = 0 maps to <see cref="HistorianWriteOutcome.Ack"/>; the granular path
|
||||
/// takes precedence over the legacy PerEventOk bool when both are present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_PerEventStatusAck_MapsToAck()
|
||||
{
|
||||
await using var server = new FakeSidecarServer(Secret)
|
||||
{
|
||||
OnWriteAlarmEvents = _ => new WriteAlarmEventsReply
|
||||
{
|
||||
Success = true,
|
||||
PerEventStatus = [0], // Ack
|
||||
},
|
||||
};
|
||||
await server.StartAsync();
|
||||
|
||||
await using var client = TcpClientFor(server);
|
||||
var batch = new[]
|
||||
{
|
||||
new AlarmHistorianEvent("ev-ok", "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.Ack);
|
||||
}
|
||||
|
||||
/// <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).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_EmptyPerEventStatus_FallsBackToLegacyPerEventOk()
|
||||
{
|
||||
await using var server = new FakeSidecarServer(Secret)
|
||||
{
|
||||
OnWriteAlarmEvents = _ => new WriteAlarmEventsReply
|
||||
{
|
||||
Success = true,
|
||||
PerEventStatus = [], // older sidecar — no granular status
|
||||
PerEventOk = [false],
|
||||
},
|
||||
};
|
||||
await server.StartAsync();
|
||||
|
||||
await using var client = TcpClientFor(server);
|
||||
var batch = new[]
|
||||
{
|
||||
new AlarmHistorianEvent("ev-legacy", "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>Verifies that Hello handshake throws UnauthorizedAccessException on secret mismatch.</summary>
|
||||
[Fact]
|
||||
public async Task Hello_BadSecret_ThrowsUnauthorizedAccess()
|
||||
|
||||
Reference in New Issue
Block a user