fix(commons): FlowGraphBuilder review fixes — leading async steps link from the spawner, robust async rule, lifecycle tie-break, total DbSubtitle
Review of Task 2 (2026-09-06 outbound-calls plan): (1) an outbound step that precedes the first chain step of its execution — the default Replay shape when a run's ScriptTrace rows are absent — was orphaned or given a solid spawner edge; leading async steps now hang off the spawner on Async edges and the first non-async step gets the normal link. (2) IsAsync keyed on the first row being CachedSubmit; it now mirrors the projector's sync rule (first row not ApiCall/DbWrite) so a group missing its submit row still branches. (3) Same- tick rows tie-broke by GUID; new LifecycleRank (submit < attempt < resolve, non-terminal < terminal) makes first/latest attribution order-independent. (4) DbSubtitle used GetInt32 (FormatException on out-of-range/fractional) — now TryGetInt32, never throws. Also: External-lane edges never count as central↔site crossings; DurationMs clamped; FlowStep doc updated. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -363,4 +363,102 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(graph.Steps[0].EventId, edge.FromEventId);
|
||||
Assert.Equal(graph.Steps[1].EventId, edge.ToEventId);
|
||||
}
|
||||
|
||||
// ---- Review findings on Task 2 (2026-09-06) --------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void LeadingAsyncSteps_HangOffTheSpawner_NeverOrphaned()
|
||||
{
|
||||
// A routed run whose ScriptTrace rows are absent (TraceAllCalls off / older site): only its outbound rows survive.
|
||||
var route = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.RouteCall, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(10), target: "M1.S", sourceNode: "central-a", executionId: Root,
|
||||
extra: new ScriptTraceExtra { ChildExecutionId = SiteRun }.ToJson());
|
||||
var cached = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted,
|
||||
occurredAtUtc: T0.AddMilliseconds(20), target: "Delmia.Ack", correlationId: Guid.NewGuid(), executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a");
|
||||
var notify = ScadaBridgeAuditEventFactory.Create(AuditChannel.Notification, AuditKind.NotifySend, AuditStatus.Submitted,
|
||||
occurredAtUtc: T0.AddMilliseconds(30), target: "Planners", correlationId: Guid.NewGuid(), executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a");
|
||||
var sync = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(40), target: "MesDb", executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a");
|
||||
|
||||
var graph = FlowGraphBuilder.Build([route, cached, notify, sync]);
|
||||
|
||||
var routeStep = graph.Steps[0];
|
||||
var reached = graph.Edges.Select(e => e.ToEventId).ToHashSet();
|
||||
Assert.All(graph.Steps.Skip(1), s => Assert.Contains(s.EventId, reached));
|
||||
Assert.Equal(2, graph.Edges.Count(e => e.Async && e.FromEventId == routeStep.EventId));
|
||||
var syncEdge = Assert.Single(graph.Edges, e => e.ToEventId == graph.Steps[3].EventId);
|
||||
Assert.Equal(routeStep.EventId, syncEdge.FromEventId);
|
||||
Assert.False(syncEdge.Async);
|
||||
Assert.False(syncEdge.Spawn);
|
||||
Assert.Equal(3, graph.Edges.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyNotifyRows_InAnExecution_AllLinkFromTheSpawner()
|
||||
{
|
||||
var run = SiteRunRow(20);
|
||||
var n1 = ScadaBridgeAuditEventFactory.Create(AuditChannel.Notification, AuditKind.NotifySend, AuditStatus.Submitted,
|
||||
occurredAtUtc: T0.AddMilliseconds(30), target: "A", correlationId: Guid.NewGuid(), executionId: Cascade, parentExecutionId: SiteRun, sourceSiteId: "site-a");
|
||||
var n2 = ScadaBridgeAuditEventFactory.Create(AuditChannel.Notification, AuditKind.NotifySend, AuditStatus.Submitted,
|
||||
occurredAtUtc: T0.AddMilliseconds(31), target: "B", correlationId: Guid.NewGuid(), executionId: Cascade, parentExecutionId: SiteRun, sourceSiteId: "site-a");
|
||||
|
||||
var graph = FlowGraphBuilder.Build([run, n1, n2]);
|
||||
|
||||
Assert.Equal(2, graph.Edges.Count);
|
||||
Assert.All(graph.Edges, e => { Assert.True(e.Async); Assert.True(e.Spawn); Assert.Equal(graph.Steps[0].EventId, e.FromEventId); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fold_IsOrderIndependent_AndTiesBreakByLifecycleRank()
|
||||
{
|
||||
var corr = Guid.NewGuid();
|
||||
var submit = Outbound(AuditChannel.ApiOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted, 35, corr, "Delmia.Ack", request: "req");
|
||||
var resolve = Outbound(AuditChannel.ApiOutbound, AuditKind.CachedResolve, AuditStatus.Delivered, 35, corr, "Delmia.Ack", response: "res");
|
||||
|
||||
var a = FlowGraphBuilder.Build([SiteRunRow(20), resolve, submit]);
|
||||
var b = FlowGraphBuilder.Build([SiteRunRow(20), submit, resolve]);
|
||||
|
||||
var sa = Assert.Single(a.Steps, s => s.Kind == FlowStepKind.ApiCall);
|
||||
var sb = Assert.Single(b.Steps, s => s.Kind == FlowStepKind.ApiCall);
|
||||
Assert.Equal(submit.EventId, sa.EventId);
|
||||
Assert.Equal(sa.EventId, sb.EventId);
|
||||
Assert.Equal("req", sa.InputJson);
|
||||
Assert.Equal("res", sa.OutputJson);
|
||||
Assert.Equal(FlowStepOutcome.Ok, sa.Outcome);
|
||||
Assert.Equal(0, sa.DurationMs);
|
||||
Assert.True(FlowGraphBuilder.IsAsync(sa));
|
||||
Assert.Equal(sa.Lifecycle!.Select(r => r.Kind), sb.Lifecycle!.Select(r => r.Kind));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CachedStep_WhoseSubmitRowIsMissing_IsStillAsync()
|
||||
{
|
||||
var corr = Guid.NewGuid();
|
||||
var graph = FlowGraphBuilder.Build([
|
||||
SiteRunRow(20),
|
||||
Outbound(AuditChannel.ApiOutbound, AuditKind.ApiCallCached, AuditStatus.Attempted, 3000, corr, "Delmia.Ack"),
|
||||
Outbound(AuditChannel.ApiOutbound, AuditKind.CachedResolve, AuditStatus.Delivered, 4000, corr, "Delmia.Ack"),
|
||||
]);
|
||||
var api = Assert.Single(graph.Steps, s => s.Kind == FlowStepKind.ApiCall);
|
||||
Assert.True(FlowGraphBuilder.IsAsync(api));
|
||||
Assert.Single(graph.Edges, e => e.Async && e.ToEventId == api.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalLaneEdges_NeverCountAsClusterBoundaryCrossings()
|
||||
{
|
||||
var inbound = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiInbound, AuditKind.InboundRequest, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0, target: "M", sourceNode: "central-a", executionId: Root, durationMs: 50, responseSummary: "{}");
|
||||
var centralDb = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(5), actor: "api:M", target: "CfgDb", sourceNode: "central-a", executionId: Root,
|
||||
extra: "{\"op\":\"read\",\"rowsReturned\":2}");
|
||||
|
||||
var graph = FlowGraphBuilder.Build([inbound, centralDb]);
|
||||
|
||||
Assert.DoesNotContain(graph.Edges, e => e.CrossesBoundary);
|
||||
Assert.Equal("read · 2 rows", graph.Steps[1].Subtitle);
|
||||
// Response links from the DB call — the inbound execution's last sync step.
|
||||
var into = Assert.Single(graph.Edges, e => e.ToEventId == graph.Steps[2].EventId);
|
||||
Assert.Equal(graph.Steps[1].EventId, into.FromEventId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,4 +148,17 @@ public class FlowStepProjectorTests
|
||||
Assert.Equal(FlowStepOutcome.Ok, FlowStepProjector.Project(ok).Outcome);
|
||||
Assert.Equal(FlowStepOutcome.Failed, FlowStepProjector.Project(failed).Outcome);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"op\":\"write\",\"rowsAffected\":2147483648}", "write")]
|
||||
[InlineData("{\"op\":\"write\",\"rowsAffected\":1.5}", "write")]
|
||||
[InlineData("{\"op\":\"write\",\"rowsAffected\":-1}", "write")]
|
||||
[InlineData("{\"op\":7}", "Delivered")]
|
||||
[InlineData("not json", "Delivered")]
|
||||
public void DbSubtitle_NeverThrows_OnOddExtra(string extra, string expected)
|
||||
{
|
||||
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.UtcDateTime, target: "MesDb", extra: extra);
|
||||
Assert.Equal(expected, FlowStepProjector.Project(evt).Subtitle);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user