feat(flow-debugger): outbound calls on the External lane — CLI projection, specs, rig demo + verification step I

CLI flow-debug table now projects through FlowGraphBuilder (one row per step,
outbound lifecycles folded, new 'lifecycle' column; per-event fallback kept).
Specs: Component-CentralUI (four lanes, node anatomy, pending refresh),
Component-AuditLog (central method-script Database.* now audited; replay reads
the three outbound channels), Component-InboundAPI (DbOutbound row per helper
call), Component-ScriptAnalysis (REQ-SA-7 kinds + Async edges), design doc
v1.1 status line, CLAUDE.md bullet, deferred-work register row 111 (Retry/
Discard + ?correlationId= deep links deferred). Rig: seed-flow-outbound-demo.sh
(RigRest/RigDb/FlowDemo + OutboundDemo script + FlowOutboundDemo method/key)
and verify-delmia-notifier.sh step I (live rows inline, replay until terminal).
Rig verification result follows in its own commit.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 02:10:50 -04:00
parent c3b4068de2
commit 3c9f9da818
14 changed files with 330 additions and 26 deletions
@@ -4,6 +4,7 @@ using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
@@ -181,4 +182,58 @@ public class FlowDebugCommandsTests
Assert.Equal("""{"error":"x"}""", FlowDebugTableProjection.ProjectEvents("""{"error":"x"}"""));
Assert.Equal("[1]", FlowDebugTableProjection.ProjectEvents("[1]"));
}
[Fact]
public void Projection_FoldsAnOutboundLifecycle_IntoOneExternalLaneRow()
{
var root = Guid.NewGuid();
var child = Guid.NewGuid();
var corr = Guid.NewGuid();
var t0 = new DateTime(2026, 9, 6, 9, 0, 0, 0, DateTimeKind.Utc);
var events = new List<AuditEvent>
{
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.RouteCall, AuditStatus.Delivered,
occurredAtUtc: t0, target: "DelmiaSim01.OutboundDemo", executionId: root, durationMs: 300,
extra: new ScriptTraceExtra { ChildExecutionId = child }.ToJson()),
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
occurredAtUtc: t0.AddMilliseconds(10), target: "OutboundDemo", executionId: child, parentExecutionId: root,
sourceSiteId: "site-a", sourceInstanceId: "DelmiaSim01", durationMs: 250),
ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered,
occurredAtUtc: t0.AddMilliseconds(20), target: "RigRest.Echo", executionId: child, parentExecutionId: root,
sourceSiteId: "site-a", sourceInstanceId: "DelmiaSim01", durationMs: 40),
ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted,
occurredAtUtc: t0.AddMilliseconds(70), target: "RigDb", correlationId: corr, executionId: child, parentExecutionId: root,
sourceSiteId: "site-a", sourceInstanceId: "DelmiaSim01"),
ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWriteCached, AuditStatus.Attempted,
occurredAtUtc: t0.AddMilliseconds(90), target: "RigDb", correlationId: corr, executionId: child, parentExecutionId: root,
sourceSiteId: "site-a", sourceInstanceId: "DelmiaSim01", durationMs: 15),
ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.CachedResolve, AuditStatus.Delivered,
occurredAtUtc: t0.AddMilliseconds(120), target: "RigDb", correlationId: corr, executionId: child, parentExecutionId: root,
sourceSiteId: "site-a", sourceInstanceId: "DelmiaSim01"),
};
var result = new FlowDebugRunResult(root, true, null, null, events, false, new DateTimeOffset(t0), new DateTimeOffset(t0.AddSeconds(1)));
var projected = FlowDebugTableProjection.ProjectEvents(JsonSerializer.Serialize(result, CamelCase));
using var doc = JsonDocument.Parse(projected);
var rows = doc.RootElement.EnumerateArray().ToList();
Assert.Equal(4, rows.Count); // route, run, sync api call, ONE folded db write
Assert.Equal("ApiCall", rows[2].GetProperty("kind").GetString());
Assert.Equal("External", rows[2].GetProperty("lane").GetString());
Assert.Equal("RigRest.Echo", rows[2].GetProperty("target").GetString());
Assert.Equal("1 row", rows[2].GetProperty("lifecycle").GetString());
Assert.Equal(40, rows[2].GetProperty("durationMs").GetInt32());
Assert.Equal("DbCall", rows[3].GetProperty("kind").GetString());
Assert.Equal("External", rows[3].GetProperty("lane").GetString());
Assert.Equal("RigDb", rows[3].GetProperty("target").GetString());
Assert.Equal("3 rows", rows[3].GetProperty("lifecycle").GetString());
Assert.Equal("Delivered", rows[3].GetProperty("status").GetString());
Assert.Equal(50, rows[3].GetProperty("durationMs").GetInt32());
Assert.Equal("09:00:00.070", rows[3].GetProperty("occurred").GetString());
Assert.Equal("DelmiaSim01.OutboundDemo", rows[1].GetProperty("target").GetString());
Assert.Equal(JsonValueKind.Null, rows[1].GetProperty("lifecycle").ValueKind);
}
}