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.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; 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.TestSupport; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Flow Debugger design 2026-09-05 §5.2/§5.4: the Deployment Manager's inbound-API route /// handlers forward the Trace flag and map the routed run's ExecutionId and /// inline ScriptTrace events onto the . Fixture copied /// from DeploymentManagerActorTests (real Instance/Script actors end-to-end). /// public class DeploymentManagerRouteTraceTests : TestKit, IDisposable { private readonly SiteStorageService _storage; private readonly ScriptCompilationService _compilationService; private readonly SharedScriptLibrary _sharedScriptLibrary; private readonly TestLocalDb _localDb; public DeploymentManagerRouteTraceTests() { _localDb = TestLocalDb.CreateTemp("dm-route-trace-test"); _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 CreateDeploymentManager() => ActorOf(Props.Create(() => new DeploymentManagerActor( _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions(), NullLogger.Instance, dclManager: null, healthCollector: null, serviceProvider: null, loggerFactory: null, configFetcher: null))); private static string ConfigJson(string instance) => JsonSerializer.Serialize(new FlattenedConfiguration { InstanceUniqueName = instance, Attributes = [new ResolvedAttribute { CanonicalName = "Mode", Value = "Auto", DataType = "String" }], Scripts = [new ResolvedScript { CanonicalName = "DoWork", Code = "await Instance.SetAttribute(\"Mode\", \"Manual\"); return 7;" }], }); private async Task DeployedManager(string instance) { var actor = CreateDeploymentManager(); await Task.Delay(500); actor.Tell(new DeployInstanceCommand("dep", instance, "sha256:x", ConfigJson(instance), "admin", DateTimeOffset.UtcNow)); ExpectMsg(TimeSpan.FromSeconds(5)); await Task.Delay(1000); return actor; } [Fact] public async Task RouteCall_WithTrace_ReturnsChildExecutionId_AndInlineTraceEvents() { var actor = await DeployedManager("TracedPump"); var parent = Guid.NewGuid(); actor.Tell(new RouteToCallRequest("rc-1", "TracedPump", "DoWork", null, DateTimeOffset.UtcNow, ParentExecutionId: parent, Trace: true)); var response = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.True(response.Success, response.ErrorMessage); Assert.Equal(7, Convert.ToInt32(response.ReturnValue)); Assert.NotNull(response.ChildExecutionId); Assert.NotNull(response.TraceEvents); var rows = response.TraceEvents!.Select(e => e.AsRow()).ToList(); // The script wrote an attribute (AttributeWrite) and the run itself (ScriptRun) — both // stamped with the routed run's id and parented to the inbound request. Assert.Contains(rows, r => r.Kind == Commons.Types.Enums.AuditKind.AttributeWrite); var run = Assert.Single(rows, r => r.Kind == Commons.Types.Enums.AuditKind.ScriptRun); Assert.Equal(response.ChildExecutionId, run.ExecutionId); Assert.Equal(parent, run.ParentExecutionId); Assert.All(rows, r => Assert.Equal(response.ChildExecutionId, r.ExecutionId)); } [Fact] public async Task RouteCall_WithoutTrace_ReturnsExecutionId_ButNoTraceEvents() { var actor = await DeployedManager("PlainPump"); actor.Tell(new RouteToCallRequest("rc-2", "PlainPump", "DoWork", null, DateTimeOffset.UtcNow)); var response = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.True(response.Success, response.ErrorMessage); Assert.NotNull(response.ChildExecutionId); Assert.Null(response.TraceEvents); } [Fact] public async Task RouteSetAttributes_ForwardsTraceFlag_OnTheCommand() { var actor = await DeployedManager("SetPump"); var parent = Guid.NewGuid(); actor.Tell(new RouteToSetAttributesRequest("rs-1", "SetPump", new Dictionary { ["Mode"] = "Hand" }, DateTimeOffset.UtcNow, ParentExecutionId: parent, Trace: true)); var response = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.True(response.Success, response.ErrorMessage); // The write landed with the trace stamp: reading it back through a debug snapshot is // heavier than needed — the InstanceActorTraceArmTests pin the publish; here we assert // the routed write succeeded end-to-end with the additive flag set, i.e. the command // shape the handler builds is accepted by the Instance Actor. actor.Tell(new RouteToGetAttributesRequest("rg-1", "SetPump", ["Mode"], DateTimeOffset.UtcNow)); var read = ExpectMsg(TimeSpan.FromSeconds(10)); Assert.Equal("Hand", read.Values["Mode"]?.ToString()); } }