diff --git a/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs b/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
index ce4f6e4..85cea1c 100644
--- a/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
@@ -125,7 +125,10 @@ public class ScriptExecutionActor : ReceiveActor
databaseGateway,
storeAndForward,
siteCommunicationActor,
- siteId);
+ siteId,
+ // Notification Outbox (FU3): stamp the executing script onto outbound
+ // notifications using the Site Event Logging "Source" convention.
+ sourceScript: $"ScriptActor:{scriptName}");
var globals = new ScriptGlobals
{
diff --git a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
index 273e59b..ba00063 100644
--- a/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
+++ b/src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
@@ -66,6 +66,15 @@ public class ScriptRuntimeContext
///
private readonly string _siteId;
+ ///
+ /// Notification Outbox (FU3): identifier of the script currently executing in this
+ /// context — stamped onto NotificationSubmit.SourceScript for the central
+ /// audit trail. Uses the Site Event Logging "Source" convention
+ /// ("ScriptActor:<scriptName>"). Null when no single script owns the
+ /// context (e.g. alarm on-trigger paths that do not wire the Notify outbox).
+ ///
+ private readonly string? _sourceScript;
+
public ScriptRuntimeContext(
IActorRef instanceActor,
IActorRef self,
@@ -79,7 +88,8 @@ public class ScriptRuntimeContext
IDatabaseGateway? databaseGateway = null,
StoreAndForwardService? storeAndForward = null,
ICanTell? siteCommunicationActor = null,
- string siteId = "")
+ string siteId = "",
+ string? sourceScript = null)
{
_instanceActor = instanceActor;
_self = self;
@@ -94,6 +104,7 @@ public class ScriptRuntimeContext
_storeAndForward = storeAndForward;
_siteCommunicationActor = siteCommunicationActor;
_siteId = siteId;
+ _sourceScript = sourceScript;
}
///
@@ -209,7 +220,7 @@ public class ScriptRuntimeContext
/// Notify.Status(id) queries the delivery status of that notification.
///
public NotifyHelper Notify => new(
- _storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _askTimeout, _logger);
+ _storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger);
///
/// Helper class for Scripts.CallShared() syntax.
@@ -356,6 +367,7 @@ public class ScriptRuntimeContext
private readonly ICanTell? _siteCommunicationActor;
private readonly string _siteId;
private readonly string _instanceName;
+ private readonly string? _sourceScript;
private readonly TimeSpan _askTimeout;
private readonly ILogger _logger;
@@ -364,6 +376,7 @@ public class ScriptRuntimeContext
ICanTell? siteCommunicationActor,
string siteId,
string instanceName,
+ string? sourceScript,
TimeSpan askTimeout,
ILogger logger)
{
@@ -371,6 +384,7 @@ public class ScriptRuntimeContext
_siteCommunicationActor = siteCommunicationActor;
_siteId = siteId;
_instanceName = instanceName;
+ _sourceScript = sourceScript;
_askTimeout = askTimeout;
_logger = logger;
}
@@ -381,7 +395,7 @@ public class ScriptRuntimeContext
public NotifyTarget To(string listName)
{
return new NotifyTarget(
- listName, _storeAndForward, _siteId, _instanceName, _logger);
+ listName, _storeAndForward, _siteId, _instanceName, _sourceScript, _logger);
}
///
@@ -453,6 +467,7 @@ public class ScriptRuntimeContext
private readonly StoreAndForwardService? _storeAndForward;
private readonly string _siteId;
private readonly string _instanceName;
+ private readonly string? _sourceScript;
private readonly ILogger _logger;
internal NotifyTarget(
@@ -460,12 +475,14 @@ public class ScriptRuntimeContext
StoreAndForwardService? storeAndForward,
string siteId,
string instanceName,
+ string? sourceScript,
ILogger logger)
{
_listName = listName;
_storeAndForward = storeAndForward;
_siteId = siteId;
_instanceName = instanceName;
+ _sourceScript = sourceScript;
_logger = logger;
}
@@ -504,9 +521,10 @@ public class ScriptRuntimeContext
// value is the best-effort site id known to the script runtime.
SourceSiteId: _siteId,
SourceInstanceId: _instanceName,
- // SourceScript: the script runtime does not currently thread the script
- // name down to the Notify helper; left null until that wiring exists.
- SourceScript: null,
+ // SourceScript (FU3): identifier of the script that raised this
+ // notification, threaded down from the script-execution context for the
+ // central audit trail. Null when no single script owns the context.
+ SourceScript: _sourceScript,
SiteEnqueuedAt: DateTimeOffset.UtcNow);
var payloadJson = JsonSerializer.Serialize(payload);
diff --git a/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifyHelperTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifyHelperTests.cs
index 95cf53b..e85e671 100644
--- a/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifyHelperTests.cs
+++ b/tests/ScadaLink.SiteRuntime.Tests/Scripts/NotifyHelperTests.cs
@@ -58,13 +58,16 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
base.Dispose(disposing);
}
- private ScriptRuntimeContext.NotifyHelper CreateHelper(IActorRef siteCommunicationActor)
+ private ScriptRuntimeContext.NotifyHelper CreateHelper(
+ IActorRef siteCommunicationActor,
+ string? sourceScript = null)
{
return new ScriptRuntimeContext.NotifyHelper(
_saf,
siteCommunicationActor,
"site-7",
"Plant.Pump3",
+ sourceScript,
TimeSpan.FromSeconds(3),
NullLogger.Instance);
}
@@ -113,6 +116,36 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
Assert.Equal("Plant.Pump3", payload.SourceInstanceId);
}
+ [Fact]
+ public async Task Send_WhenHelperHasSourceScript_StampsItOnTheNotificationSubmit()
+ {
+ var commProbe = CreateTestProbe();
+ var notify = CreateHelper(commProbe.Ref, sourceScript: "ScriptActor:MonitorSpeed");
+
+ 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(buffered!.PayloadJson);
+ Assert.NotNull(payload);
+ // FU3: the executing script name is threaded down and stamped for the audit trail.
+ Assert.Equal("ScriptActor:MonitorSpeed", payload!.SourceScript);
+ }
+
+ [Fact]
+ public async Task Send_WhenHelperHasNoSourceScript_LeavesSourceScriptNull()
+ {
+ var commProbe = CreateTestProbe();
+ var notify = CreateHelper(commProbe.Ref, sourceScript: null);
+
+ var notificationId = await notify.To("Operators").Send("Pump alarm", "Pump 3 tripped");
+
+ var buffered = await _saf.GetMessageByIdAsync(notificationId);
+ var payload = JsonSerializer.Deserialize(buffered!.PayloadJson);
+ Assert.Null(payload!.SourceScript);
+ }
+
[Fact]
public async Task Status_WhenStillBufferedAtSite_ReportsForwarding()
{