feat(site-runtime): route handlers forward Trace and return child ExecutionId + trace events
Flow Debugger (design 2026-09-05 §5.2/§5.4): RouteInboundApiCall passes Trace on the ScriptCallRequest and maps ScriptCallResult.ExecutionId/TraceEvents onto RouteToCallResponse; RouteInboundApiSetAttributes passes Trace on SetStaticAttributeCommand. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
@@ -2873,7 +2873,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// execution can record its spawner.
|
||||
var scriptCall = new ScriptCallRequest(
|
||||
request.ScriptName, request.Parameters, 0, request.CorrelationId,
|
||||
ParentExecutionId: request.ParentExecutionId);
|
||||
ParentExecutionId: request.ParentExecutionId,
|
||||
// Flow Debugger (design 2026-09-05 §5.2): the trace flag rides beside the parent id.
|
||||
Trace: request.Trace);
|
||||
var sender = Sender;
|
||||
instanceActor.Ask<ScriptCallResult>(scriptCall, TimeSpan.FromSeconds(30))
|
||||
.ContinueWith(t =>
|
||||
@@ -2894,7 +2896,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// the inbound side (which JsonSerializer-serializes it for the
|
||||
// HTTP body / ReturnDefinition validation).
|
||||
NormalizeRoutedReturnValue(result.ReturnValue),
|
||||
result.ErrorMessage, DateTimeOffset.UtcNow);
|
||||
result.ErrorMessage, DateTimeOffset.UtcNow,
|
||||
// Flow Debugger (design §5.4): the site run's id and, for a traced
|
||||
// run, its ScriptTrace events — returned inline so a Live run is
|
||||
// instant. Null from an untraced run.
|
||||
result.ExecutionId, result.TraceEvents);
|
||||
}
|
||||
return new RouteToCallResponse(
|
||||
request.CorrelationId, false, null,
|
||||
@@ -3101,7 +3107,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// carried on the request as ParentExecutionId. Stamping it
|
||||
// here lets an alarm this write trips chain its on-trigger
|
||||
// script run under the inbound execution.
|
||||
SourceExecutionId: request.ParentExecutionId),
|
||||
SourceExecutionId: request.ParentExecutionId,
|
||||
// Flow Debugger (design §5.3): a traced routed write arms the echo stamp.
|
||||
Trace: request.Trace),
|
||||
TimeSpan.FromSeconds(30)))
|
||||
.ToArray();
|
||||
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger design 2026-09-05 §5.2/§5.4: the Deployment Manager's inbound-API route
|
||||
/// handlers forward the <c>Trace</c> flag and map the routed run's <c>ExecutionId</c> and
|
||||
/// inline <c>ScriptTrace</c> events onto the <see cref="RouteToCallResponse"/>. Fixture copied
|
||||
/// from <c>DeploymentManagerActorTests</c> (real Instance/Script actors end-to-end).
|
||||
/// </summary>
|
||||
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<SiteStorageService>.Instance);
|
||||
_storage.InitializeAsync().GetAwaiter().GetResult();
|
||||
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
||||
_sharedScriptLibrary = new SharedScriptLibrary(_compilationService, NullLogger<SharedScriptLibrary>.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<DeploymentManagerActor>.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<IActorRef> DeployedManager(string instance)
|
||||
{
|
||||
var actor = CreateDeploymentManager();
|
||||
await Task.Delay(500);
|
||||
actor.Tell(new DeployInstanceCommand("dep", instance, "sha256:x", ConfigJson(instance), "admin", DateTimeOffset.UtcNow));
|
||||
ExpectMsg<DeploymentStatusResponse>(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<RouteToCallResponse>(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<RouteToCallResponse>(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<string, string> { ["Mode"] = "Hand" }, DateTimeOffset.UtcNow,
|
||||
ParentExecutionId: parent, Trace: true));
|
||||
|
||||
var response = ExpectMsg<RouteToSetAttributesResponse>(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<RouteToGetAttributesResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.Equal("Hand", read.Values["Mode"]?.ToString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user