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 step-logs plan (2026-09-09) Task 1: the Commons log-line vocabulary — /// exception detail on a step (D3), and the ScriptTrace.ScriptLog row projecting /// onto a rather than a node (D1). /// public class FlowStepProjectorLogLineTests { [Fact] public void Project_CarriesErrorDetail() { var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Failed, target: "S", executionId: Guid.NewGuid(), errorMessage: "boom", errorDetail: "System.InvalidOperationException: boom\n at X"); var step = FlowStepProjector.Project(evt); Assert.Equal("boom", step.ErrorMessage); Assert.StartsWith("System.InvalidOperationException", step.ErrorDetail); } [Fact] public void Project_ErrorDetail_IsNullWhenAbsent() { var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered, target: "S"); Assert.Null(FlowStepProjector.Project(evt).ErrorDetail); } [Fact] public void ScriptLog_ProjectsToUnknownKind_NeverItsOwnStepKind() { // Mixed-version rule (design §4): an old central renders a ScriptLog row as an Unknown node // rather than dropping it. No FlowStepKind is added for it. Assert.Equal(FlowStepKind.Unknown, FlowStepProjector.ResolveKind(nameof(AuditChannel.ScriptTrace), nameof(AuditKind.ScriptLog))); Assert.False(Enum.GetNames().Contains("ScriptLog")); } [Fact] public void IsLogLine_TrueOnlyForScriptTraceScriptLog() { var log = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered, target: "S"); var run = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered, target: "S"); Assert.True(FlowStepProjector.IsLogLine(log)); Assert.False(FlowStepProjector.IsLogLine(run)); } [Fact] public void IsLogLine_FalseForOtherChannels() { var outbound = ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered, target: "S"); Assert.False(FlowStepProjector.IsLogLine(outbound)); } [Fact] public void ToLogLine_ReadsLevelSourceAndException() { var exec = Guid.NewGuid(); var extra = new ScriptTraceExtra { LogLevel = "warn", LogSource = "runtime", StepEventId = Guid.NewGuid(), Seq = 3 }; var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered, target: "S", executionId: exec, requestSummary: "acked Uncertain", errorMessage: "inner", errorDetail: "System.Exception: inner", extra: extra.ToJson()); var line = FlowStepProjector.ToLogLine(evt); Assert.Equal(evt.EventId, line.EventId); Assert.Equal(exec, line.ExecutionId); Assert.Equal(evt.OccurredAtUtc, line.OccurredAtUtc); Assert.Equal("warn", line.Level); Assert.Equal("runtime", line.Source); Assert.Equal("acked Uncertain", line.Message); Assert.Equal("inner", line.ExceptionMessage); Assert.Equal("System.Exception: inner", line.ExceptionDetail); Assert.Equal(extra.StepEventId, line.StepEventId); Assert.Equal(3, line.Seq); } [Fact] public void ToLogLine_DefaultsLevelAndSourceWhenExtraMissing() { var evt = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered, target: "S"); var line = FlowStepProjector.ToLogLine(evt); Assert.Equal("info", line.Level); Assert.Equal("script", line.Source); Assert.Equal(string.Empty, line.Message); Assert.Null(line.ExceptionMessage); Assert.Null(line.ExceptionDetail); Assert.Null(line.StepEventId); Assert.Null(line.Seq); } [Fact] public void ScriptTraceExtra_LogMembers_RoundTripThroughJson() { var extra = new ScriptTraceExtra { LogLevel = "error", LogSource = "script", StepEventId = Guid.NewGuid(), Seq = 12 }; var parsed = ScriptTraceExtra.TryParse(extra.ToJson()); Assert.NotNull(parsed); Assert.Equal("error", parsed!.LogLevel); Assert.Equal("script", parsed.LogSource); Assert.Equal(extra.StepEventId, parsed.StepEventId); Assert.Equal(12, parsed.Seq); } [Fact] public void FlowStep_LogsAndErrorDetail_DefaultToNull() { var step = new FlowStep(Guid.NewGuid(), null, null, FlowStepKind.ScriptRun, FlowLane.Site, "S", null, DateTimeOffset.UnixEpoch, null, FlowStepOutcome.Ok, null, null, null, null, null, null, null, null); Assert.Null(step.ErrorDetail); Assert.Null(step.Logs); var line = new FlowLogLine(Guid.NewGuid(), null, DateTimeOffset.UnixEpoch, "info", "script", "m", null, null, null, null); Assert.Single((step with { Logs = [line] }).Logs!); } [Fact] public void FlowGraph_LogTruncationMembers_DefaultToOff() { var graph = new FlowGraph([], [], false, false); Assert.False(graph.LogsTruncated); Assert.Equal(0, graph.UnattachedLogLines); Assert.False(FlowGraph.Empty.LogsTruncated); Assert.Equal(0, FlowGraph.Empty.UnattachedLogLines); } }