Merge branch 'worktree-agent-aa79b4c585b494d98' into fix/flow-debugger-arch-review
This commit is contained in:
@@ -46,8 +46,10 @@ public class FlowGraphBuilderTests
|
||||
executionId: Cascade, parentExecutionId: SiteRun, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 10,
|
||||
extra: new ScriptTraceExtra { TriggerKind = "valuechange", TriggerAttribute = "DelmiaReceiver.Trigger", TriggerValue = "false" }.ToJson());
|
||||
|
||||
// Deliberately shuffled: the builder must order by time.
|
||||
return [wait, cascade, inbound, write2, run, route, write1];
|
||||
// Deliberately shuffled WITHIN each execution: the builder must order by time inside one execution.
|
||||
// The executions themselves are given in emission order (root, site run, cascade) — since arch-review
|
||||
// F5 that is what fixes their relative order, because they do not share a clock.
|
||||
return [inbound, route, wait, run, write2, write1, cascade];
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -365,6 +367,105 @@ public class FlowGraphBuilderTests
|
||||
Assert.Equal(graph.Steps[1].EventId, edge.ToEventId);
|
||||
}
|
||||
|
||||
// ---- Arch-review F5: emission order between executions, timestamps within one ------------
|
||||
|
||||
/// <summary>
|
||||
/// Central and site clocks differ. The four rows are given in EMISSION order (the Live path's
|
||||
/// collector order); the graph must keep the executions in that order whichever way the skew runs.
|
||||
/// </summary>
|
||||
private static List<AuditEvent> SkewedRouteShape(int siteSkewMs)
|
||||
{
|
||||
var inbound = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiInbound, AuditKind.InboundRequest, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0, actor: "k", target: "M", sourceNode: "central-a", executionId: Root, durationMs: 900,
|
||||
requestSummary: "{}", responseSummary: "{\"ok\":true}");
|
||||
var route = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.RouteCall, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(50), target: "M1.S", sourceNode: "central-a", executionId: Root,
|
||||
extra: new ScriptTraceExtra { ChildExecutionId = SiteRun }.ToJson());
|
||||
var run = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(siteSkewMs), actor: "S", target: "S", sourceNode: "node-a",
|
||||
executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 20,
|
||||
extra: new ScriptTraceExtra { TriggerKind = "call" }.ToJson());
|
||||
var write = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(siteSkewMs + 10), actor: "S", target: "A", sourceNode: "node-a",
|
||||
executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 2,
|
||||
extra: new ScriptTraceExtra { Writes = [new AttributeWriteDetail("A", "1", true, null)] }.ToJson());
|
||||
return [inbound, route, run, write];
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteClockAhead_SiteStepsStillFollowTheirRouteCall_AndPrecedeResponse()
|
||||
{
|
||||
var events = SkewedRouteShape(siteSkewMs: 2000); // site 2 s ahead of central
|
||||
var graph = FlowGraphBuilder.Build(events);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
FlowStepKind.InboundRequest, FlowStepKind.RouteCall, FlowStepKind.ScriptRun,
|
||||
FlowStepKind.AttributeWrite, FlowStepKind.Response,
|
||||
},
|
||||
graph.Steps.Select(s => s.Kind));
|
||||
Assert.Equal(events.Select(e => e.EventId), graph.Steps.Take(4).Select(s => s.EventId));
|
||||
// The Response time still comes off the inbound row's own clock (inbound + DurationMs).
|
||||
Assert.Equal(T0.AddMilliseconds(900), graph.Steps[4].OccurredAtUtc.UtcDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteClockBehind_SiteStepsDoNotSortAheadOfTheInboundRequest()
|
||||
{
|
||||
var events = SkewedRouteShape(siteSkewMs: -2000); // site 2 s behind central
|
||||
var graph = FlowGraphBuilder.Build(events);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
FlowStepKind.InboundRequest, FlowStepKind.RouteCall, FlowStepKind.ScriptRun,
|
||||
FlowStepKind.AttributeWrite, FlowStepKind.Response,
|
||||
},
|
||||
graph.Steps.Select(s => s.Kind));
|
||||
Assert.Equal(events.Select(e => e.EventId), graph.Steps.Take(4).Select(s => s.EventId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteClockBehind_TriggerCascade_SpawnsFromTheLastParentStep_NotTheFirst()
|
||||
{
|
||||
// Mixed-version shape: no RouteCall row, so the spawner is found by position, and a site
|
||||
// clock behind central used to push every parent step "after" the child → parentChain[0].
|
||||
var inbound = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiInbound, AuditKind.InboundRequest, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0, actor: "k", target: "M", sourceNode: "central-a", executionId: Root, durationMs: 900);
|
||||
var centralDb = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(10), actor: "api:M", target: "CfgDb", sourceNode: "central-a", executionId: Root);
|
||||
var run = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
occurredAtUtc: T0.AddMilliseconds(-2000), actor: "S", target: "S", sourceNode: "node-a",
|
||||
executionId: SiteRun, parentExecutionId: Root, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 20,
|
||||
extra: new ScriptTraceExtra { TriggerKind = "call" }.ToJson());
|
||||
|
||||
var graph = FlowGraphBuilder.Build([inbound, centralDb, run]);
|
||||
|
||||
var spawn = Assert.Single(graph.Edges, e => e.Spawn);
|
||||
Assert.Equal(centralDb.EventId, spawn.FromEventId); // the LAST parent step, not the inbound row
|
||||
Assert.Equal(run.EventId, spawn.ToEventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FoldedNode_TakesTheEmissionIndexOfItsEarliestEmittedRow()
|
||||
{
|
||||
// The folded step keeps the earliest-by-TIME row's id (the resolve here is emitted first),
|
||||
// so its sequence must be the minimum input index over the rows folded into it.
|
||||
var corr = Guid.NewGuid();
|
||||
var resolve = Outbound(AuditChannel.ApiOutbound, AuditKind.CachedResolve, AuditStatus.Delivered, 4000, corr, "Delmia.Ack", response: "res");
|
||||
var submit = Outbound(AuditChannel.ApiOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted, 35, corr, "Delmia.Ack", request: "req");
|
||||
var run = SiteRunRow(20, Cascade);
|
||||
|
||||
// Emission order interleaves the executions: the folded node's earliest row is index 0, so
|
||||
// its execution leads — even though the id the folded step keeps (the submit) is index 2.
|
||||
var graph = FlowGraphBuilder.Build([resolve, run, submit]);
|
||||
|
||||
var api = Assert.Single(graph.Steps, s => s.Kind == FlowStepKind.ApiCall);
|
||||
Assert.Equal(submit.EventId, api.EventId);
|
||||
Assert.Equal(new[] { FlowStepKind.ApiCall, FlowStepKind.ScriptRun }, graph.Steps.Select(s => s.Kind));
|
||||
}
|
||||
|
||||
// ---- Review findings on Task 2 (2026-09-06) --------------------------------------------
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -124,6 +124,31 @@ public class FlowDebugRunnerTests
|
||||
Assert.Contains(_central, e => e.AsRow().Kind == AuditKind.RouteCall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunMethod_SiteClockBehindCentral_StillReturnsTheSinkEventsInEmissionOrder()
|
||||
{
|
||||
// Arch-review F5: the site's rows carry the SITE clock. A site 2 s behind central used to
|
||||
// re-sort ahead of the always-on central RouteCall row.
|
||||
var method = new ApiMethod("M", "x") { Id = 6, TimeoutSeconds = 5 };
|
||||
_apiRepo.GetApiMethodByIdAsync(6, Arg.Any<CancellationToken>()).Returns(method);
|
||||
_locator.GetSiteIdForInstanceAsync("inst", Arg.Any<CancellationToken>()).Returns("site-a");
|
||||
var child = Guid.NewGuid();
|
||||
var siteRun = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
occurredAtUtc: DateTime.UtcNow.AddSeconds(-2), executionId: child, target: "work");
|
||||
_router.RouteToCallAsync("site-a", Arg.Any<RouteToCallRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => new RouteToCallResponse(((RouteToCallRequest)ci[1]).CorrelationId, true, 7, null, DateTimeOffset.UtcNow, child, [siteRun]));
|
||||
_executor.RegisterHandler("M", ctx => ctx.Route.To("inst").Call("work"));
|
||||
|
||||
var result = await Runner().RunMethodAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunMethodCommand(6, null), Deployer, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Completed, result.Error);
|
||||
Assert.Equal(3, result.Events.Count);
|
||||
Assert.Equal(AuditKind.InboundRequest, result.Events[0].AsRow().Kind);
|
||||
Assert.Equal(AuditKind.RouteCall, result.Events[1].AsRow().Kind);
|
||||
Assert.Same(siteRun, result.Events[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunMethod_UnknownMethod_IsNotFound()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user