fix(audit): populate ParentExecutionId on alarm-triggered script runs
M5.4 T4 threaded a `parentExecutionId` parameter through
AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext,
but every call site passed null — so alarm on-trigger runs were silently always
execution-tree roots, contradicting the "tag-cascade coverage is complete"
claim in CLAUDE.md and Component-AuditLog.md.
Source the id where a spawner genuinely exists: a static attribute write issued
by a site script (`Instance.SetAttribute`) or by an inbound API request
(`Route.To(...).SetAttributes(...)`, whose ParentExecutionId was already carried
to the site and then dropped). The id rides site-locally through three additive,
nullable fields — no wire, proto or central schema change:
ScriptRuntimeContext.SetAttribute / RouteToSetAttributesRequest.ParentExecutionId
→ SetStaticAttributeCommand.SourceExecutionId
→ AttributeValueChanged.SourceExecutionId (InstanceActor static-write path)
→ AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext
All four computed trigger types participate. Expression triggers evaluate off
the dispatcher, so the writer of the newest value folded into the snapshot is
captured *with* the snapshot and echoed home on ExpressionEvalResult /
ExpressionEvalFailed — a change arriving mid-flight cannot mis-attribute the
raise.
Deliberately still roots (documented, not deferred): alarms fired by Data
Connection Layer values (external device data has no spawning execution — this
includes the device echo of a script write to a *data-sourced* attribute, so
only static writes cascade), and ScriptActor value-change/conditional/
expression/timer trigger runs (a timer tick has no spawner; a WhileTrue/interval
run has no single identifiable write).
Tests: new SiteRuntime.Tests/Actors/AlarmCascadeParentExecutionTests pins all
three hops — SetAttribute stamps the run's ExecutionId, InstanceActor publishes
it on the change (and publishes null when absent), and ValueMatch/HiLo/
Expression alarms parent the on-trigger run to the writer while a DCL-originated
change leaves it a root.
Docs: CLAUDE.md and Component-AuditLog.md corrected from "complete" to the true
behaviour; Component-SiteRuntime.md gains an "Audit correlation of an on-trigger
run" section with the hop table and the by-design root cases.
This commit is contained in:
@@ -35,8 +35,10 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// The alarm on-trigger plumbing carries a <c>parentExecutionId</c> into the
|
||||
/// script context — null today (the run is a root) but threaded so a future
|
||||
/// firing id can flow.
|
||||
/// script context, and the alarm run is itself a proper execution node whose
|
||||
/// own <c>ExecutionId</c> cascades onward. Which firing execution the alarm run
|
||||
/// is parented TO is covered separately by
|
||||
/// <c>Actors.AlarmCascadeParentExecutionTests</c>.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
@@ -240,11 +242,11 @@ public class ParentExecutionTreeTests : TestKit
|
||||
public void AlarmOnTrigger_NestedCallScript_CarriesAlarmRunsOwnExecutionId_AsParent()
|
||||
{
|
||||
// End-to-end alarm plumbing: when an alarm fires, its on-trigger script
|
||||
// runs in a ScriptRuntimeContext built by AlarmExecutionActor. With no
|
||||
// Guid firing id today the alarm run is a ROOT (its own ParentExecutionId
|
||||
// is null), but it still mints its OWN fresh ExecutionId. A nested
|
||||
// CallScript from that on-trigger script must therefore carry the alarm
|
||||
// run's OWN (non-null) ExecutionId as the child's ParentExecutionId —
|
||||
// runs in a ScriptRuntimeContext built by AlarmExecutionActor. The change
|
||||
// below carries no SourceExecutionId (it stands in for DCL data), so the
|
||||
// alarm run is a ROOT — but it still mints its OWN fresh ExecutionId. A
|
||||
// nested CallScript from that on-trigger script must therefore carry the
|
||||
// alarm run's OWN (non-null) ExecutionId as the child's ParentExecutionId —
|
||||
// proving the alarm context is a proper execution node feeding the
|
||||
// cascade and the parentExecutionId parameter is plumbed end-to-end.
|
||||
var compilationService = new ScriptCompilationService(
|
||||
@@ -280,7 +282,7 @@ public class ParentExecutionTreeTests : TestKit
|
||||
var request = instanceProbe.ExpectMsg<ScriptCallRequest>(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.Equal("Child", request.ScriptName);
|
||||
// The alarm run is a root today (its own parent is null), but its OWN
|
||||
// This alarm run is a root (no writer on the firing change), but its OWN
|
||||
// freshly-minted ExecutionId cascades to the child — so the child's
|
||||
// ParentExecutionId is a real, non-empty value, NOT null.
|
||||
Assert.NotNull(request.ParentExecutionId);
|
||||
|
||||
Reference in New Issue
Block a user