using System.Text.Json; using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; 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 ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport; using ZB.MOM.WW.ScadaBridge.TestSupport; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Review 2026-10-21 01/SR-P5: a deploy's flattened config is deserialized ONCE (in the /// off-thread compile warm) and the parsed object is handed to the gate, the DCL-connection /// ensure and the Instance Actor. These pin the two contracts that refactor must keep: a config /// that does not parse is still a readable gate rejection with nothing applied, and an Instance /// Actor built from the parsed object behaves like one built from the JSON. /// public class DeploymentManagerParseOnceTests : 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 DeploymentManagerParseOnceTests() { _localDb = TestLocalDb.CreateTemp("dm-parse-once"); _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 CreateBootedDeploymentManager() { var health = new DeploymentManagerStartupProbe(); var dm = ActorOf(Props.Create(() => new DeploymentManagerActor( _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 0 }, NullLogger.Instance, dclManager: null, healthCollector: health, serviceProvider: null, loggerFactory: null, configFetcher: null))); AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(25)); return dm; } /// Malformed JSON is rejected by the gate with the deserialization error; nothing is persisted. /// A task that represents the asynchronous operation. [Fact] public async Task Deploy_MalformedConfig_IsRejectedWithTheParseError() { var dm = CreateBootedDeploymentManager(); dm.Tell(new DeployInstanceCommand("dep-1", "Bad1", "r1", "{not json", "tester", DateTimeOffset.UtcNow)); var response = ExpectMsg(TimeSpan.FromSeconds(15)); Assert.Equal(DeploymentStatus.Failed, response.Status); Assert.Contains("failed to deserialize", response.ErrorMessage); Assert.Null(await _storage.GetDeployedConfigAsync("Bad1")); } /// A deploy built from the parsed object serves its configured attributes. /// A task that represents the asynchronous operation. [Fact] public async Task Deploy_ParsedOncePath_CreatesAWorkingInstance() { var dm = CreateBootedDeploymentManager(); var json = JsonSerializer.Serialize(new FlattenedConfiguration { InstanceUniqueName = "Good1", Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "7", DataType = "Int32" }] }); dm.Tell(new DeployInstanceCommand("dep-2", "Good1", "r1", json, "tester", DateTimeOffset.UtcNow)); Assert.Equal(DeploymentStatus.Success, ExpectMsg(TimeSpan.FromSeconds(15)).Status); var instance = await Sys.ActorSelection(dm.Path / "Good1").ResolveOne(TimeSpan.FromSeconds(5)); instance.Tell(new GetAttributeRequest("g", "Good1", "A", DateTimeOffset.UtcNow)); Assert.Equal("7", ExpectMsg().Value?.ToString()); } }