using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// /// Flow Debugger (design 2026-09-05 §5.2/§5.4): the site_command.proto trace fields — the /// trace flag on both route requests, and child_execution_id + /// trace_events on the call response — round-trip through /// , and an old-shape DTO (no fields set) maps to the /// untraced defaults. /// public class SiteCommandDtoMapperTraceTests { private static readonly DateTimeOffset T = new(2026, 9, 5, 12, 0, 0, TimeSpan.Zero); [Fact] public void RouteToCallResponse_RoundTrips_ChildExecutionId_AndTraceEvents() { var child = Guid.NewGuid(); var run = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered, occurredAtUtc: T.UtcDateTime, actor: "ScriptActor:S", target: "S", sourceNode: "node-b", executionId: child, parentExecutionId: Guid.NewGuid(), sourceSiteId: "site-a", durationMs: 40, requestSummary: "{\"x\":1}", responseSummary: "\"ok\"", extra: new ScriptTraceExtra { TriggerKind = "call" }.ToJson()); var write = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered, occurredAtUtc: T.UtcDateTime.AddMilliseconds(5), actor: "ScriptActor:S", target: "A", executionId: child, extra: new ScriptTraceExtra { Writes = [new AttributeWriteDetail("A", "1", true, null)] }.ToJson()); var original = new RouteToCallResponse("c", true, 7L, null, T, child, [run, write]); var restored = SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(original)); Assert.Equal(child, restored.ChildExecutionId); Assert.NotNull(restored.TraceEvents); Assert.Equal(2, restored.TraceEvents!.Count); Assert.Equal(run.Action, restored.TraceEvents[0].Action); Assert.Equal(run.DetailsJson, restored.TraceEvents[0].DetailsJson); Assert.Equal(write.EventId, restored.TraceEvents[1].EventId); Assert.Equal(original, restored with { TraceEvents = original.TraceEvents }); Assert.Equal(run, restored.TraceEvents[0]); } [Fact] public void OldShapeDtos_MapToUntracedDefaults() { // A DTO from a site/central that predates the fields: proto3 defaults everywhere. var call = SiteCommandDtoMapper.FromProto(new RouteToCallRequestDto { CorrelationId = "c", InstanceUniqueName = "i", ScriptName = "s", Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(T), }); Assert.False(call.Trace); var set = SiteCommandDtoMapper.FromProto(new RouteToSetAttributesRequestDto { CorrelationId = "c", InstanceUniqueName = "i", Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(T), }); Assert.False(set.Trace); var reply = SiteCommandDtoMapper.FromProto(new RouteToCallResponseDto { CorrelationId = "c", Success = true, Timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(T), }); Assert.Null(reply.ChildExecutionId); Assert.Null(reply.TraceEvents); } [Fact] public void TraceFlag_PropagatesOnBothRequests() { var call = new RouteToCallRequest("c", "i", "s", null, T, Guid.NewGuid(), Trace: true); Assert.True(SiteCommandDtoMapper.ToProto(call).Trace); Assert.True(SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(call)).Trace); var set = new RouteToSetAttributesRequest("c", "i", new Dictionary { ["a"] = "1" }, T, Guid.NewGuid(), Trace: true); Assert.True(SiteCommandDtoMapper.ToProto(set).Trace); Assert.True(SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(set)).Trace); } [Fact] public void EmptyTraceEventsList_MapsToNull_NotEmpty() { // An untraced run sends none; the CLR side distinguishes "none" (null) so the UI can // label the site lane — an empty list would read as "traced, did nothing". var original = new RouteToCallResponse("c", true, null, null, T, Guid.NewGuid(), Array.Empty()); var restored = SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(original)); Assert.Null(restored.TraceEvents); Assert.Equal(original.ChildExecutionId, restored.ChildExecutionId); } }