fix(notifications): park corrupt buffered notification payloads instead of silently discarding as delivered (supersedes StoreAndForward-018)

This commit is contained in:
Joseph Doherty
2026-07-08 20:48:16 -04:00
parent 05a71a0260
commit c3a8576863
3 changed files with 30 additions and 22 deletions
@@ -199,16 +199,13 @@ public class NotificationForwarderTests : TestKit
}
[Fact]
public async Task Deliver_CorruptJsonPayload_ReturnsTrue_AndDoesNotForwardAnything()
public async Task Deliver_CorruptJsonPayload_ReturnsFalse_ParksForForensics_AndDoesNotForward()
{
// Regression test for StoreAndForward-018. The design doc forbids parking
// notifications ("notifications do not park — they are retried at the fixed
// forward interval until central acks"; Component-StoreAndForward.md). The
// previous implementation returned false on a corrupt payload, which the S&F
// engine interprets as a permanent failure and parks the row — contradicting
// the invariant. The fix: discard a corrupt buffered notification by
// returning true (engine clears the buffer via its normal success path),
// with a Warning log line carrying the row id and a payload preview.
// Supersedes StoreAndForward-018 (discard-on-corrupt). Returning false is the
// handler contract's permanent-failure signal: the engine parks the row, which
// preserves the payload for operator forensics. Retrying a corrupt payload is
// pointless; deleting it silently (and reporting Delivered) loses data with no
// parked row, no central Notifications row, and no audit trail.
var centralProbe = CreateTestProbe();
var forwarder = new NotificationForwarder(
centralProbe.Ref, "site-7", ForwardTimeout);
@@ -222,7 +219,7 @@ public class NotificationForwarderTests : TestKit
OriginInstanceName = "Plant.Pump3",
};
Assert.True(await forwarder.DeliverAsync(corrupt));
Assert.False(await forwarder.DeliverAsync(corrupt));
// The corrupt-payload path must NOT round-trip to central — no
// NotificationSubmit / no Ask. ExpectNoMsg confirms nothing was forwarded.
@@ -230,11 +227,11 @@ public class NotificationForwarderTests : TestKit
}
[Fact]
public async Task Deliver_NullDeserializedPayload_ReturnsTrue_AndDoesNotForwardAnything()
public async Task Deliver_NullDeserializedPayload_ReturnsFalse_ParksForForensics_AndDoesNotForward()
{
// The companion case to corrupt JSON: the payload is valid JSON but
// deserialises to null (e.g. "null"). Same treatment per StoreAndForward-018
// — discard rather than park.
// deserialises to null (e.g. "null"). Same treatment — park (return false)
// rather than silently discard-as-delivered. Supersedes StoreAndForward-018.
var centralProbe = CreateTestProbe();
var forwarder = new NotificationForwarder(
centralProbe.Ref, "site-7", ForwardTimeout);
@@ -248,7 +245,7 @@ public class NotificationForwarderTests : TestKit
OriginInstanceName = "Plant.Pump3",
};
Assert.True(await forwarder.DeliverAsync(nullPayload));
Assert.False(await forwarder.DeliverAsync(nullPayload));
centralProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
}