using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// PLAN-03 Task 18 (P2): the Instance Actor routes each attribute change only to the /// children interested in it. A value-change / conditional script (and a monitored-attribute /// alarm) subscribes to a single named attribute; an Expression-triggered script/alarm hears /// every change (it keeps a full attribute snapshot). These tests pin the routing-map /// construction directly — the behavioral gate inside each child stays as defense in depth. /// public class InstanceActorChildRoutingTests : TestKit, IDisposable { private readonly SiteStorageService _storage; private readonly ScriptCompilationService _compilationService; private readonly SharedScriptLibrary _sharedScriptLibrary; private readonly SiteRuntimeOptions _options; private readonly string _dbFile; public InstanceActorChildRoutingTests() { _dbFile = Path.Combine(Path.GetTempPath(), $"instance-routing-test-{Guid.NewGuid():N}.db"); _storage = new SiteStorageService( $"Data Source={_dbFile}", NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService( NullLogger.Instance); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); _options = new SiteRuntimeOptions { MaxScriptCallDepth = 10, ScriptExecutionTimeoutSeconds = 30 }; } void IDisposable.Dispose() { Shutdown(); try { File.Delete(_dbFile); } catch { /* cleanup */ } } private static FlattenedConfiguration ScriptsAB_and_Expression(string instanceName) => new() { InstanceUniqueName = instanceName, Attributes = [ new ResolvedAttribute { CanonicalName = "A", Value = "1", DataType = "Double" }, new ResolvedAttribute { CanonicalName = "B", Value = "2", DataType = "Double" } ], Scripts = [ new ResolvedScript { CanonicalName = "S_A", Code = "return 1;", TriggerType = "ValueChange", TriggerConfiguration = "{\"attributeName\":\"A\"}" }, new ResolvedScript { CanonicalName = "S_B", Code = "return 2;", TriggerType = "ValueChange", TriggerConfiguration = "{\"attributeName\":\"B\"}" }, new ResolvedScript { CanonicalName = "S_expr", Code = "return 3;", TriggerType = "Expression", TriggerConfiguration = "{\"expression\":\"true\"}" } ] }; private InstanceActor BuildInstance(FlattenedConfiguration config) { var testRef = ActorOfAsTestActorRef( Props.Create(() => new InstanceActor( config.InstanceUniqueName, JsonSerializer.Serialize(config), _storage, _compilationService, _sharedScriptLibrary, null, _options, NullLogger.Instance)), "instance-" + Guid.NewGuid().ToString("N")); return testRef.UnderlyingActor; } [Fact] public void RoutingMaps_AreBuiltFromTriggerConfigs() { var actor = BuildInstance(ScriptsAB_and_Expression("RoutePump")); // Two value-change scripts route to their own attribute; the expression script // routes to the all-changes bucket. Assert.Single(actor.ChildrenByMonitoredAttribute["A"]); Assert.Single(actor.ChildrenByMonitoredAttribute["B"]); Assert.Single(actor.AllChangeChildren); } [Fact] public void MonitoredAttributeAlarm_RoutesToItsAttribute_ExpressionAlarm_RoutesToAllChanges() { var config = new FlattenedConfiguration { InstanceUniqueName = "AlarmRoutePump", Attributes = [ new ResolvedAttribute { CanonicalName = "Temp", Value = "10", DataType = "Double" } ], Alarms = [ new ResolvedAlarm { CanonicalName = "HiTemp", TriggerType = "RangeViolation", TriggerConfiguration = "{\"attributeName\":\"Temp\",\"min\":0,\"max\":100}", PriorityLevel = 1 }, new ResolvedAlarm { CanonicalName = "ExprAlarm", TriggerType = "Expression", TriggerConfiguration = "{\"expression\":\"true\"}", PriorityLevel = 1 } ] }; var actor = BuildInstance(config); Assert.Single(actor.ChildrenByMonitoredAttribute["Temp"]); Assert.Single(actor.AllChangeChildren); // the expression alarm } }