test(auditlog): pin OriginExecutionId preservation in forwarder + Parked NotifyDeliver

This commit is contained in:
Joseph Doherty
2026-05-21 15:42:45 -04:00
parent 85bb61a1f3
commit 6aac4c8ed7
2 changed files with 56 additions and 2 deletions

View File

@@ -212,6 +212,31 @@ public class NotificationOutboxActorTerminalEmissionTests : TestKit
}); });
} }
[Fact]
public void Terminal_Parked_CarriesOriginExecutionId_AsExecutionId()
{
// Audit Log #23: the Parked terminal NotifyDeliver row flows through the
// same BuildNotifyDeliverEvent path as the Delivered row, so it must
// likewise echo the notification's OriginExecutionId — sharing the
// per-run id with the site-emitted NotifySend row.
SetupSmtpRetryPolicy(maxRetries: 5, retryDelay: TimeSpan.FromMinutes(1));
var executionId = Guid.NewGuid();
var notification = MakeNotification(originExecutionId: executionId);
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Permanent("invalid recipient address"));
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
AwaitAssert(() =>
{
var parked = EventsByStatus(AuditStatus.Parked);
Assert.Single(parked);
Assert.Equal(executionId, parked[0].ExecutionId);
});
}
[Fact] [Fact]
public void Terminal_Parked_OnTransientReachingMaxRetries_EmitsEvent_StatusParked() public void Terminal_Parked_OnTransientReachingMaxRetries_EmitsEvent_StatusParked()
{ {

View File

@@ -26,7 +26,8 @@ public class NotificationForwarderTests : TestKit
private static StoreAndForwardMessage BufferedNotification( private static StoreAndForwardMessage BufferedNotification(
string id = "msg-1", string listName = "Operators", string id = "msg-1", string listName = "Operators",
string subject = "Pump alarm", string message = "Pump 3 tripped", string subject = "Pump alarm", string message = "Pump 3 tripped",
string? originInstance = "Plant.Pump3", string? sourceScript = "alarmScript") string? originInstance = "Plant.Pump3", string? sourceScript = "alarmScript",
Guid? originExecutionId = null)
{ {
var payload = JsonSerializer.Serialize(new NotificationSubmit( var payload = JsonSerializer.Serialize(new NotificationSubmit(
NotificationId: id, NotificationId: id,
@@ -37,7 +38,8 @@ public class NotificationForwarderTests : TestKit
SourceSiteId: string.Empty, SourceSiteId: string.Empty,
SourceInstanceId: originInstance, SourceInstanceId: originInstance,
SourceScript: sourceScript, SourceScript: sourceScript,
SiteEnqueuedAt: DateTimeOffset.UtcNow)); SiteEnqueuedAt: DateTimeOffset.UtcNow,
OriginExecutionId: originExecutionId));
return new StoreAndForwardMessage return new StoreAndForwardMessage
{ {
Id = id, Id = id,
@@ -78,6 +80,33 @@ public class NotificationForwarderTests : TestKit
Assert.True(await deliverTask); Assert.True(await deliverTask);
} }
[Fact]
public async Task Deliver_PreservesOriginExecutionId_FromBufferedPayload()
{
// Audit Log #23: the buffered payload's OriginExecutionId is the per-run
// id stamped at Notify.Send time. The forwarder re-stamps only the four
// fields it authoritatively owns (NotificationId, ListName, SourceSiteId,
// SourceInstanceId) via the `with` expression — OriginExecutionId is
// preserved precisely BY being absent from that `with` block. This test
// pins that: if OriginExecutionId is ever added to the `with` expression
// (e.g. reset to null), the forwarded NotificationSubmit would lose the
// per-run id and central could not echo it onto NotifyDeliver rows.
var centralProbe = CreateTestProbe();
var forwarder = new NotificationForwarder(
centralProbe.Ref, "site-7", ForwardTimeout);
var executionId = Guid.NewGuid();
var msg = BufferedNotification(id: "msg-exec", originExecutionId: executionId);
var deliverTask = forwarder.DeliverAsync(msg);
var submit = centralProbe.ExpectMsg<NotificationSubmit>();
Assert.Equal(executionId, submit.OriginExecutionId);
centralProbe.Reply(new NotificationSubmitAck(submit.NotificationId, Accepted: true, Error: null));
Assert.True(await deliverTask);
}
[Fact] [Fact]
public async Task Deliver_FallsBackToTarget_WhenPayloadListNameIsEmpty() public async Task Deliver_FallsBackToTarget_WhenPayloadListNameIsEmpty()
{ {