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; /// /// Step-logs design 2026-09-09 D2: a ScriptTrace.ScriptLog row is never a node — it folds onto the /// step its StepEventId names, else onto its execution's anchor (ScriptRun / /// InboundRequest / RouteCall), else its execution's first non-Response step, else it is /// dropped and counted in . /// public class FlowGraphBuilderLogLineTests { private static readonly DateTime T0 = new(2026, 9, 9, 12, 0, 0, DateTimeKind.Utc); private static AuditEvent Run(Guid execution, Guid? parent = null, int ms = 0) => ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "SomeScript", sourceNode: "node-a", executionId: execution, parentExecutionId: parent, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 100, extra: new ScriptTraceExtra { TriggerKind = "call" }.ToJson()); private static AuditEvent Write(Guid execution, Guid? eventId = null, int ms = 10) => ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered, eventId: eventId, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "M1.Flag", sourceNode: "node-a", executionId: execution, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 2, extra: new ScriptTraceExtra { Writes = [new AttributeWriteDetail("M1.Flag", "true", true, null)] }.ToJson()); private static AuditEvent Inbound(Guid execution, int ms = 0) => ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiInbound, AuditKind.InboundRequest, AuditStatus.Delivered, occurredAtUtc: T0.AddMilliseconds(ms), actor: "some-key", target: "SomeMethod", sourceNode: "central-a", executionId: execution, durationMs: 50, requestSummary: "{}", responseSummary: "{\"ok\":true}"); private static AuditEvent RouteCallRoot(Guid execution, int ms = 0) => ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.RouteCall, AuditStatus.Delivered, occurredAtUtc: T0.AddMilliseconds(ms), actor: "flow-debugger", target: "M1.SomeScript", sourceNode: "central-a", executionId: execution, durationMs: 120); private static AuditEvent ApiOut(Guid execution, Guid eventId, int ms = 20) => ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered, eventId: eventId, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "ERP/orders", sourceNode: "node-a", executionId: execution, sourceSiteId: "site-a", durationMs: 30); private static AuditEvent Line(Guid execution, Guid? step = null, int? seq = null, string level = "info", int ms = 5, string message = "hello") => ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "SomeScript", sourceNode: "node-a", executionId: execution, sourceSiteId: "site-a", sourceInstanceId: "M1", requestSummary: message, extra: new ScriptTraceExtra { LogLevel = level, LogSource = "script", StepEventId = step, Seq = seq }.ToJson()); private static FlowStep StepOf(FlowGraph graph, FlowStepKind kind) => Assert.Single(graph.Steps, s => s.Kind == kind); [Fact] public void LogLine_WithStepEventId_AttachesToThatStep() { var execution = Guid.NewGuid(); var writeId = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution), Write(execution, writeId), Line(execution, step: writeId)]); var write = StepOf(graph, FlowStepKind.AttributeWrite); var line = Assert.Single(write.Logs!); Assert.Equal("hello", line.Message); Assert.Null(StepOf(graph, FlowStepKind.ScriptRun).Logs); } [Fact] public void LogLine_WithoutStepId_AttachesToTheExecutionAnchor() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution), Write(execution), Line(execution)]); Assert.Single(StepOf(graph, FlowStepKind.ScriptRun).Logs!); Assert.Null(StepOf(graph, FlowStepKind.AttributeWrite).Logs); } [Fact] public void CentralLine_AttachesToInboundRow_NotResponse() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Inbound(execution), Line(execution)]); Assert.Single(StepOf(graph, FlowStepKind.InboundRequest).Logs!); Assert.Null(StepOf(graph, FlowStepKind.Response).Logs); } [Fact] public void ScriptEntryLine_AttachesToRouteCallRoot() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([RouteCallRoot(execution), Line(execution)]); Assert.Single(StepOf(graph, FlowStepKind.RouteCall).Logs!); } [Fact] public void LogLine_UnknownStepId_FallsBackToAnchor() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution), Line(execution, step: Guid.NewGuid())]); Assert.Single(StepOf(graph, FlowStepKind.ScriptRun).Logs!); Assert.Equal(0, graph.UnattachedLogLines); } [Fact] public void LogLine_NoAnchor_FallsBackToFirstStep() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Write(execution), Line(execution)]); Assert.Single(StepOf(graph, FlowStepKind.AttributeWrite).Logs!); Assert.Equal(0, graph.UnattachedLogLines); } [Fact] public void LogLine_NoStepInExecution_IsCountedUnattached() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution), Line(Guid.NewGuid())]); Assert.Equal(1, graph.UnattachedLogLines); Assert.All(graph.Steps, s => Assert.True(s.Logs is null or { Count: 0 })); } [Fact] public void Build_LinesOnly_ReturnsEmptyGraphWithUnattachedCount() { var graph = FlowGraphBuilder.Build([Line(Guid.NewGuid()), Line(Guid.NewGuid())], logsTruncated: true); Assert.Empty(graph.Steps); Assert.Empty(graph.Edges); Assert.Equal(2, graph.UnattachedLogLines); Assert.True(graph.LogsTruncated); } [Fact] public void Lines_OrderBySeqWithinSameMillisecond_NullSeqLast() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build( [ Run(execution), Line(execution, seq: 3, ms: 5, message: "three"), Line(execution, seq: 1, ms: 5, message: "one"), Line(execution, seq: null, ms: 5, message: "null"), Line(execution, seq: 2, ms: 5, message: "two"), ]); var logs = StepOf(graph, FlowStepKind.ScriptRun).Logs!; Assert.Equal(new int?[] { 1, 2, 3, null }, logs.Select(l => l.Seq).ToArray()); Assert.Equal(new[] { "one", "two", "three", "null" }, logs.Select(l => l.Message).ToArray()); } [Fact] public void LogLines_NeverBecomeSteps() { var execution = Guid.NewGuid(); var events = new List { Run(execution) }; for (var i = 0; i < 5; i++) events.Add(Line(execution, seq: i, ms: 1 + i, message: $"line {i}")); var lineIds = events.Skip(1).Select(e => e.EventId).ToHashSet(); var graph = FlowGraphBuilder.Build(events); var step = Assert.Single(graph.Steps); Assert.Equal(FlowStepKind.ScriptRun, step.Kind); Assert.Equal(5, step.Logs!.Count); Assert.DoesNotContain(graph.Edges, e => lineIds.Contains(e.FromEventId) || lineIds.Contains(e.ToEventId)); } [Fact] public void Build_PassesLogsTruncatedThrough() { var execution = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution)], truncated: false, logsTruncated: true); Assert.True(graph.LogsTruncated); Assert.False(graph.Truncated); } [Fact] public void LogLine_OnFoldedOutboundStep_AttachesByFirstRowId() { var execution = Guid.NewGuid(); var callId = Guid.NewGuid(); var graph = FlowGraphBuilder.Build([Run(execution), ApiOut(execution, callId), Line(execution, step: callId)]); var call = StepOf(graph, FlowStepKind.ApiCall); Assert.Equal(callId, call.EventId); Assert.Single(call.Logs!); Assert.Null(StepOf(graph, FlowStepKind.ScriptRun).Logs); } }