using System.Text.Json; using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Messages; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; using ZB.MOM.WW.ScadaBridge.TestSupport; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Review 2026-10-21 01/SR-P4: Route.To().GetAttributes()/SetAttributes() used one Ask /// per attribute. The Instance Actor now answers a whole batch in one message while keeping the /// per-attribute outcome a single read/write would report. /// public class InstanceActorAttributeBatchTests : TestKit, IDisposable { private readonly SiteStorageService _storage; private readonly ScriptCompilationService _compilationService; private readonly SharedScriptLibrary _sharedScriptLibrary; private readonly TestLocalDb _localDb; /// Creates a temporary LocalDb-backed site store per test. public InstanceActorAttributeBatchTests() { _localDb = TestLocalDb.CreateTemp("instance-attr-batch"); _storage = new SiteStorageService(_localDb.Db, NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService(NullLogger.Instance); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); } /// void IDisposable.Dispose() { Shutdown(); var path = _localDb.Path; _localDb.Dispose(); TestLocalDb.DeleteFiles(path); } private IActorRef StartInstance(IActorRef? dcl = null) { var config = new FlattenedConfiguration { InstanceUniqueName = "Batch1", Attributes = [ new ResolvedAttribute { CanonicalName = "Mode", Value = "Manual", DataType = "String" }, new ResolvedAttribute { CanonicalName = "Count", Value = "3", DataType = "Int32" }, new ResolvedAttribute { CanonicalName = "Speed", Value = "0", DataType = "Int32", DataSourceReference = "ns=2;s=Speed", BoundDataConnectionName = "conn-1" } ] }; var json = JsonSerializer.Serialize(config); return ActorOf(Props.Create(() => new InstanceActor( "Batch1", json, _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions(), NullLogger.Instance, dcl))); } private static SetStaticAttributeCommand Write(string attribute, string value) => new("corr", "Batch1", attribute, value, DateTimeOffset.UtcNow); /// One reply, index-aligned, with found/quality per attribute. [Fact] public void GetAttributes_AnswersEveryNameInOneReply() { var instance = StartInstance(); instance.Tell(new InstanceGetAttributesRequest("corr", "Batch1", ["Count", "Missing", "Mode", "Speed"])); var reply = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.Equal(["Count", "Missing", "Mode", "Speed"], reply.Results.Select(r => r.AttributeName)); Assert.Equal("3", reply.Results[0].Value?.ToString()); Assert.False(reply.Results[1].Found); Assert.Equal("Manual", reply.Results[2].Value?.ToString()); Assert.Equal("Uncertain", reply.Results[3].Quality); ExpectNoMsg(TimeSpan.FromMilliseconds(200)); } /// Static writes apply; an unknown attribute fails on its own row only. [Fact] public void SetAttributes_StaticAndUnknown_ReportsPerAttributeOutcome() { var instance = StartInstance(); instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Mode", "Auto"), Write("Nope", "1")])); var reply = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.True(reply.Results[0].Success); Assert.False(reply.Results[1].Success); Assert.Contains("Unknown attribute 'Nope'", reply.Results[1].ErrorMessage); instance.Tell(new GetAttributeRequest("g", "Batch1", "Mode", DateTimeOffset.UtcNow)); Assert.Equal("Auto", ExpectMsg().Value?.ToString()); } /// /// A data-sourced write inside the batch goes to the DCL; the batch reply waits for the /// device answer and carries its failure on that attribute's row. /// [Fact] public void SetAttributes_DataSourcedWrite_WaitsForTheDclAndCarriesItsOutcome() { var dcl = CreateTestProbe(); var instance = StartInstance(dcl.Ref); dcl.ExpectMsg(TimeSpan.FromSeconds(10)); instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Count", "9"), Write("Speed", "42")])); var write = dcl.ExpectMsg(TimeSpan.FromSeconds(10)); Assert.Equal("ns=2;s=Speed", write.TagPath); ExpectNoMsg(TimeSpan.FromMilliseconds(300)); // the reply waits for the device dcl.Reply(new WriteTagResponse(write.CorrelationId, false, "device rejected", DateTimeOffset.UtcNow)); var reply = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.True(reply.Results[0].Success); Assert.False(reply.Results[1].Success); Assert.Equal("Speed", reply.Results[1].AttributeName); Assert.Equal("device rejected", reply.Results[1].ErrorMessage); } }