using ZB.MOM.WW.Audit; 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; /// /// Flow Debugger outbound-calls plan (2026-09-06): the Commons vocabulary — External lane, /// the three outbound kinds, Pending/Parked outcomes, the lifecycle row, and additive edge/step shapes. /// public class FlowStepProjectorTests { private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); /// Verifies that the vocabulary has an External lane, three outbound kinds, and Pending and Parked outcomes. [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().Contains("External")); Assert.True(Enum.IsDefined(FlowStepOutcome.Pending)); Assert.True(Enum.IsDefined(FlowStepOutcome.Parked)); } /// Verifies that a flow step can be constructed without a lifecycle and defaults it to null. [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); } /// Verifies that the Async and Spawn flags of a flow edge default to false. [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); } // ---- Outbound rows project onto the External lane --------------------------------- /// Verifies that a delivered synchronous API call projects to an Ok API-call step on the External lane with a sync subtitle. [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); } /// Verifies that a cached submit on the database outbound channel projects to a pending database-call step with a cached subtitle. [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); } /// Verifies that a database write with a rows-affected extra has a subtitle combining the operation and the row count. [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); } /// Verifies that a submitted notification send projects to a pending notify step. [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); } /// Verifies that each outbound audit status maps to the expected flow step outcome. /// The outbound audit status. /// The expected flow step outcome. [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); } /// Verifies that script-trace outcomes are unchanged, so a failed status stays failed and other statuses are Ok. [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); } /// Verifies that building a database subtitle never throws on odd extra values and yields the expected text. /// The extra payload of the audit row. /// The expected subtitle. [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); } /// Verifies that projection carries the audit row's correlation id onto the step. [Fact] public void Project_CarriesTheAuditRowsCorrelationId() { var corr = Guid.NewGuid(); var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.CachedSubmit, AuditStatus.Submitted, occurredAtUtc: T0.UtcDateTime, target: "Delmia.Ack", executionId: Guid.NewGuid(), correlationId: corr, sourceSiteId: "site-a"); Assert.Equal(corr, FlowStepProjector.Project(evt).CorrelationId); } /// Verifies that a null correlation id on the audit row leaves the step's correlation id null. [Fact] public void Project_NullCorrelation_LeavesCorrelationIdNull() { var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered, occurredAtUtc: T0.UtcDateTime, target: "Delmia.Ack", executionId: Guid.NewGuid(), sourceSiteId: "site-a"); Assert.Null(FlowStepProjector.Project(evt).CorrelationId); } }