feat(notif-outbox): carry + persist SourceNode end-to-end via NotificationSubmit

Site: inject INodeIdentityProvider where NotificationSubmit is built; stamp
SourceNode = NodeName at construction.

Central: NotificationOutboxActor.HandleSubmit copies submit.SourceNode onto
the Notification row; the repository INSERT persists it (EF tracked-entity
insert flows it through automatically; raw-SQL extension if not).

After this commit, every Notifications row carries the originating site
node-a/node-b in SourceNode. Existing notifications submitted pre-feature
remain NULL.
This commit is contained in:
Joseph Doherty
2026-05-23 17:28:23 -04:00
parent e6341580b3
commit d1fcab490c
5 changed files with 166 additions and 11 deletions

View File

@@ -62,7 +62,8 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
IActorRef siteCommunicationActor,
string? sourceScript = null,
Guid? executionId = null,
Guid? parentExecutionId = null)
Guid? parentExecutionId = null,
string? sourceNode = null)
{
return new ScriptRuntimeContext.NotifyHelper(
_saf,
@@ -74,7 +75,8 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
NullLogger.Instance,
executionId ?? Guid.NewGuid(),
auditWriter: null,
parentExecutionId: parentExecutionId);
parentExecutionId: parentExecutionId,
sourceNode: sourceNode);
}
[Fact]
@@ -197,6 +199,45 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
Assert.Null(payload!.OriginParentExecutionId);
}
[Fact]
public async Task Send_StampsSourceNode_OnTheNotificationSubmitPayload()
{
// SourceNode-stamping (Task 13): when the helper is wired with the
// local INodeIdentityProvider's NodeName, Notify.Send must stamp it
// onto the NotificationSubmit so it rides inside the serialized S&F
// payload to central, where NotificationOutboxActor persists it on
// the Notifications row.
var commProbe = CreateTestProbe();
var notify = CreateHelper(commProbe.Ref, sourceNode: "node-a");
var notificationId = await notify.To("Operators").Send("Pump alarm", "Pump 3 tripped");
var buffered = await _saf.GetMessageByIdAsync(notificationId);
Assert.NotNull(buffered);
var payload = JsonSerializer.Deserialize<NotificationSubmit>(buffered!.PayloadJson);
Assert.NotNull(payload);
Assert.Equal("node-a", payload!.SourceNode);
}
[Fact]
public async Task Send_NoNodeIdentity_LeavesSourceNodeNull()
{
// Hosts that don't wire INodeIdentityProvider (legacy / tests) pass
// null through. The NotificationSubmit payload's SourceNode stays
// null so the central Notifications row persists NULL rather than
// falling back to a placeholder.
var commProbe = CreateTestProbe();
var notify = CreateHelper(commProbe.Ref, sourceNode: null);
var notificationId = await notify.To("Operators").Send("Pump alarm", "Pump 3 tripped");
var buffered = await _saf.GetMessageByIdAsync(notificationId);
Assert.NotNull(buffered);
var payload = JsonSerializer.Deserialize<NotificationSubmit>(buffered!.PayloadJson);
Assert.NotNull(payload);
Assert.Null(payload!.SourceNode);
}
[Fact]
public async Task Send_WhenHelperHasNoSourceScript_LeavesSourceScriptNull()
{