fix(site-runtime): seed republished overrides into Script Actors without firing scripts; alarms still evaluate (review 2026-10-21 01/SR-U1, decision M1)

HandleOverridesLoaded now sends children a site-local AttributeValueSeed
(stream still gets the AttributeValueChanged). ScriptActor updates its
snapshot, Conditional truth value and Expression last result (seed-only eval,
never fires); AlarmActor evaluates it as a normal change.
This commit is contained in:
Joseph Doherty
2026-10-22 09:03:10 -04:00
parent 999653a6b9
commit 3c11510c52
6 changed files with 174 additions and 14 deletions
@@ -172,4 +172,85 @@ public class InstanceActorOverrideRepublishTests : TestKit, IDisposable
Assert.False(response.TimedOut);
Assert.Contains("instance stopped", response.ErrorMessage);
}
private static ResolvedScript FiringScript(string name, string triggerType, object triggerConfig, string flag) => new()
{
CanonicalName = name,
TriggerType = triggerType,
TriggerConfiguration = JsonSerializer.Serialize(triggerConfig),
Code = $"await Instance.SetAttribute(\"{flag}\", \"1\"); return null;"
};
private string AttributeOf(IActorRef instance, string instanceName, string attribute)
{
instance.Tell(new GetAttributeRequest("g", instanceName, attribute, DateTimeOffset.UtcNow));
return ExpectMsg<GetAttributeResponse>(TimeSpan.FromSeconds(15)).Value?.ToString() ?? "";
}
/// <summary>
/// User decision M1: an override seeded at actor start updates trigger state but does NOT run
/// ValueChange / Conditional / Expression scripts; an Expression ALARM still raises; and a
/// later real change fires all three scripts.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SeededOverride_RaisesAlarm_ButFiresNoScript_UntilARealChange()
{
const string name = "PumpSeed";
await _storage.SetStaticOverrideAsync(name, "Level", "50");
const string over20 = "int.Parse(Attributes[\"Level\"].ToString()) > 20";
var config = new FlattenedConfiguration
{
InstanceUniqueName = name,
Attributes =
[
new ResolvedAttribute { CanonicalName = "Level", Value = "10", DataType = "Int32" },
new ResolvedAttribute { CanonicalName = "FiredVC", Value = "0", DataType = "String" },
new ResolvedAttribute { CanonicalName = "FiredCond", Value = "0", DataType = "String" },
new ResolvedAttribute { CanonicalName = "FiredExpr", Value = "0", DataType = "String" }
],
Scripts =
[
FiringScript("VC", "ValueChange", new { attributeName = "Level" }, "FiredVC"),
FiringScript("Cond", "Conditional",
new { attributeName = "Level", @operator = ">", threshold = 20 }, "FiredCond"),
FiringScript("Expr", "Expression", new { expression = over20 }, "FiredExpr")
],
Alarms =
[
new ResolvedAlarm
{
CanonicalName = "LevelHigh", TriggerType = "Expression", PriorityLevel = 500,
TriggerConfiguration = JsonSerializer.Serialize(new { expression = over20 })
}
]
};
var instance = StartInstance(name, config);
// The alarm raises from the seeded value alone — no other write.
AwaitAssert(() => Assert.Equal(AlarmState.Active, AlarmStateOf(instance, name, "LevelHigh")),
TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(100));
Assert.Equal("50", AttributeOf(instance, name, "Level"));
await Task.Delay(TimeSpan.FromSeconds(1.5));
Assert.Equal("0", AttributeOf(instance, name, "FiredVC"));
Assert.Equal("0", AttributeOf(instance, name, "FiredCond"));
Assert.Equal("0", AttributeOf(instance, name, "FiredExpr"));
// Real changes fire them: down (Expression edge resets) then up again.
instance.Tell(new SetStaticAttributeCommand("w1", name, "Level", "5", DateTimeOffset.UtcNow));
ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(15));
await Task.Delay(500);
instance.Tell(new SetStaticAttributeCommand("w2", name, "Level", "60", DateTimeOffset.UtcNow));
ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(15));
AwaitAssert(() =>
{
Assert.Equal("1", AttributeOf(instance, name, "FiredVC"));
Assert.Equal("1", AttributeOf(instance, name, "FiredCond"));
Assert.Equal("1", AttributeOf(instance, name, "FiredExpr"));
}, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(200));
}
}