diff --git a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorTerminalEmissionTests.cs b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorTerminalEmissionTests.cs index 74e0df8..08c8cd1 100644 --- a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorTerminalEmissionTests.cs +++ b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorTerminalEmissionTests.cs @@ -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(), Arg.Any(), Arg.Any()) + .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] public void Terminal_Parked_OnTransientReachingMaxRetries_EmitsEvent_StatusParked() { diff --git a/tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.cs b/tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.cs index 822fe33..5fa79f4 100644 --- a/tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.cs +++ b/tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.cs @@ -26,7 +26,8 @@ public class NotificationForwarderTests : TestKit private static StoreAndForwardMessage BufferedNotification( string id = "msg-1", string listName = "Operators", 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( NotificationId: id, @@ -37,7 +38,8 @@ public class NotificationForwarderTests : TestKit SourceSiteId: string.Empty, SourceInstanceId: originInstance, SourceScript: sourceScript, - SiteEnqueuedAt: DateTimeOffset.UtcNow)); + SiteEnqueuedAt: DateTimeOffset.UtcNow, + OriginExecutionId: originExecutionId)); return new StoreAndForwardMessage { Id = id, @@ -78,6 +80,33 @@ public class NotificationForwarderTests : TestKit 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(); + Assert.Equal(executionId, submit.OriginExecutionId); + centralProbe.Reply(new NotificationSubmitAck(submit.NotificationId, Accepted: true, Error: null)); + + Assert.True(await deliverTask); + } + [Fact] public async Task Deliver_FallsBackToTarget_WhenPayloadListNameIsEmpty() {