865c4be651
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
165 lines
7.6 KiB
C#
165 lines
7.6 KiB
C#
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.Commons.Tests.Debugging;
|
|
|
|
/// <summary>
|
|
/// Flow Debugger outbound-calls plan (2026-09-06) Task 0: the Commons vocabulary — External lane,
|
|
/// the three outbound kinds, Pending/Parked outcomes, the lifecycle row, and additive edge/step shapes.
|
|
/// </summary>
|
|
public class FlowStepProjectorTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
[Fact]
|
|
public void Vocabulary_HasExternalLane_ThreeOutboundKinds_AndPendingParkedOutcomes()
|
|
{
|
|
Assert.Equal(3, (int)FlowLane.External);
|
|
Assert.True(Enum.IsDefined(FlowStepKind.ApiCall));
|
|
Assert.True(Enum.IsDefined(FlowStepKind.DbCall));
|
|
Assert.True(Enum.IsDefined(FlowStepKind.Notify));
|
|
Assert.True(Enum.IsDefined(FlowStepKind.Unknown));
|
|
Assert.False(Enum.GetNames<FlowStepKind>().Contains("External"));
|
|
Assert.True(Enum.IsDefined(FlowStepOutcome.Pending));
|
|
Assert.True(Enum.IsDefined(FlowStepOutcome.Parked));
|
|
}
|
|
|
|
[Fact]
|
|
public void FlowStep_WithoutLifecycle_StillConstructs_AndDefaultsToNull()
|
|
{
|
|
var step = new FlowStep(Guid.NewGuid(), null, null, FlowStepKind.ScriptRun, FlowLane.Site, "S", null,
|
|
T0, null, FlowStepOutcome.Ok, null, null, null, null, null, null, null, null);
|
|
Assert.Null(step.Lifecycle);
|
|
|
|
var row = new FlowLifecycleRow(Guid.NewGuid(), "CachedSubmit", "Submitted", T0, "node-a", null, null);
|
|
var withLifecycle = step with { Lifecycle = [row] };
|
|
Assert.Single(withLifecycle.Lifecycle!);
|
|
Assert.Equal("Submitted", withLifecycle.Lifecycle![0].Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void FlowEdge_AsyncAndSpawn_DefaultFalse()
|
|
{
|
|
var a = Guid.NewGuid();
|
|
var b = Guid.NewGuid();
|
|
var edge = new FlowEdge(a, b, false, false);
|
|
Assert.False(edge.Async);
|
|
Assert.False(edge.Spawn);
|
|
Assert.True((edge with { Async = true }).Async);
|
|
Assert.True(new FlowEdge(a, b, false, false, Async: false, Spawn: true).Spawn);
|
|
}
|
|
|
|
// ---- Task 1: outbound rows project onto the External lane ---------------------------------
|
|
|
|
[Fact]
|
|
public void SyncApiCall_Delivered_IsApiCall_External_Ok_SyncSubtitle()
|
|
{
|
|
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.UtcDateTime, actor: "AckRecipe", target: "Delmia.AckRecipe", sourceNode: "node-a",
|
|
executionId: Guid.NewGuid(), sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 212,
|
|
requestSummary: "{\"id\":1}", responseSummary: "{\"ok\":true}");
|
|
|
|
var step = FlowStepProjector.Project(evt);
|
|
|
|
Assert.Equal(FlowStepKind.ApiCall, step.Kind);
|
|
Assert.Equal(FlowLane.External, step.Lane);
|
|
Assert.Equal(FlowStepOutcome.Ok, step.Outcome);
|
|
Assert.Equal("Delmia.AckRecipe", step.Title);
|
|
Assert.Equal("Delivered · sync", step.Subtitle);
|
|
Assert.Equal("{\"id\":1}", step.InputJson);
|
|
Assert.Equal("{\"ok\":true}", step.OutputJson);
|
|
Assert.Equal(212, step.DurationMs);
|
|
}
|
|
|
|
[Fact]
|
|
public void CachedSubmit_InDbOutbound_IsDbCall_Pending_CachedSubtitle()
|
|
{
|
|
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted,
|
|
occurredAtUtc: T0.UtcDateTime, target: "MesDb", correlationId: Guid.NewGuid(), executionId: Guid.NewGuid());
|
|
|
|
var step = FlowStepProjector.Project(evt);
|
|
|
|
Assert.Equal(FlowStepKind.DbCall, step.Kind);
|
|
Assert.Equal(FlowLane.External, step.Lane);
|
|
Assert.Equal(FlowStepOutcome.Pending, step.Outcome);
|
|
Assert.Equal("Submitted · cached", step.Subtitle);
|
|
}
|
|
|
|
[Fact]
|
|
public void DbWrite_WithRowsAffectedExtra_SubtitleIsOpAndRows()
|
|
{
|
|
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.UtcDateTime, target: "MesDb", executionId: Guid.NewGuid(),
|
|
extra: "{\"op\":\"write\",\"rowsAffected\":3}");
|
|
|
|
var step = FlowStepProjector.Project(evt);
|
|
|
|
Assert.Equal(FlowStepKind.DbCall, step.Kind);
|
|
Assert.Equal(FlowStepOutcome.Ok, step.Outcome);
|
|
Assert.Equal("write · 3 rows", step.Subtitle);
|
|
|
|
var read = ScadaBridgeAuditEventFactory.Create(AuditChannel.DbOutbound, AuditKind.DbWrite, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.UtcDateTime, target: "MesDb", extra: "{\"op\":\"read\",\"rowsReturned\":1}");
|
|
Assert.Equal("read · 1 row", FlowStepProjector.Project(read).Subtitle);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotifySend_Submitted_IsNotify_Pending()
|
|
{
|
|
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.Notification, AuditKind.NotifySend, AuditStatus.Submitted,
|
|
occurredAtUtc: T0.UtcDateTime, target: "Planners", correlationId: Guid.NewGuid(), executionId: Guid.NewGuid());
|
|
|
|
var step = FlowStepProjector.Project(evt);
|
|
|
|
Assert.Equal(FlowStepKind.Notify, step.Kind);
|
|
Assert.Equal(FlowLane.External, step.Lane);
|
|
Assert.Equal(FlowStepOutcome.Pending, step.Outcome);
|
|
Assert.Equal("Planners", step.Title);
|
|
Assert.Equal("Submitted", step.Subtitle);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(AuditStatus.Parked, FlowStepOutcome.Parked)]
|
|
[InlineData(AuditStatus.Discarded, FlowStepOutcome.Parked)]
|
|
[InlineData(AuditStatus.Failed, FlowStepOutcome.Failed)]
|
|
[InlineData(AuditStatus.Forwarded, FlowStepOutcome.Pending)]
|
|
[InlineData(AuditStatus.Attempted, FlowStepOutcome.Pending)]
|
|
[InlineData(AuditStatus.Delivered, FlowStepOutcome.Ok)]
|
|
public void OutboundStatus_MapsToOutcome(AuditStatus status, FlowStepOutcome expected)
|
|
{
|
|
var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.CachedResolve, status,
|
|
occurredAtUtc: T0.UtcDateTime, target: "Delmia.AckRecipe", correlationId: Guid.NewGuid(),
|
|
errorMessage: status == AuditStatus.Failed ? "boom" : null);
|
|
|
|
var step = FlowStepProjector.Project(evt);
|
|
|
|
Assert.Equal(expected, step.Outcome);
|
|
Assert.Equal($"{status} · cached", step.Subtitle);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScriptTraceOutcome_Unchanged_FailedStaysFailed_OtherStatusesOk()
|
|
{
|
|
var ok = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.UtcDateTime, target: "A");
|
|
var failed = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Failed,
|
|
occurredAtUtc: T0.UtcDateTime, target: "A", errorMessage: "rejected");
|
|
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);
|
|
}
|
|
}
|