using Google.Protobuf; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc; /// /// Round-trip golden tests for — the DTO bridge /// for the seven CentralControlService RPCs (Phase 1A of the ClusterClient→gRPC /// migration). /// /// /// /// Every message gets a pair: a FULLY-POPULATED case that proves no field is dropped, /// and a MINIMAL case that proves every nullable/optional/empty-collection member comes /// back as null-or-empty rather than as a zero-valued stand-in. The minimal cases are /// the ones that matter: a per-field unit test happily passes while a whole optional /// branch is silently never written, which is exactly the class of bug the round-trip /// guard in PLAN-05 T8 caught five times over. /// /// /// When a round-trip here fails, the mapper is wrong — not the test. /// /// public class CentralControlDtoMapperTests { private static readonly DateTimeOffset SampleInstant = new(2026, 7, 22, 9, 30, 15, 250, TimeSpan.Zero); // ----------------------------------------------------------------------- // NotificationSubmit / Ack // ----------------------------------------------------------------------- [Fact] public void NotificationSubmit_RoundTrip_FullyPopulated_PreservesEveryField() { var original = new NotificationSubmit( NotificationId: Guid.NewGuid().ToString(), ListName: "plant-ops", Subject: "Tank 4 overfill", Body: "Level exceeded 95% for 5 minutes.", SourceSiteId: "site-a", SourceInstanceId: "Tank04", SourceScript: "OnLevelHigh", SiteEnqueuedAt: SampleInstant, OriginExecutionId: Guid.NewGuid(), OriginParentExecutionId: Guid.NewGuid(), SourceNode: "node-b"); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); // Every member is a scalar, so record value-equality is a true deep compare. Assert.Equal(original, roundTripped); } [Fact] public void NotificationSubmit_RoundTrip_AllOptionalsNull_StayNull() { var original = new NotificationSubmit( NotificationId: Guid.NewGuid().ToString(), ListName: "plant-ops", Subject: "s", Body: "b", SourceSiteId: "site-a", SourceInstanceId: null, SourceScript: null, SiteEnqueuedAt: SampleInstant, OriginExecutionId: null, OriginParentExecutionId: null, SourceNode: null); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(original, roundTripped); Assert.Null(roundTripped.SourceInstanceId); Assert.Null(roundTripped.SourceScript); Assert.Null(roundTripped.OriginExecutionId); Assert.Null(roundTripped.OriginParentExecutionId); Assert.Null(roundTripped.SourceNode); } [Fact] public void NotificationSubmit_NonUtcOffset_NormalizesToTheSameInstant() { // Documented contract: a protobuf Timestamp is an instant, so the offset // component of a DateTimeOffset does not survive. Every producer stamps UTC // (Notify.Send uses DateTimeOffset.UtcNow), so the instant is what matters. var melbourne = new DateTimeOffset(2026, 7, 22, 19, 30, 15, TimeSpan.FromHours(10)); var original = new NotificationSubmit( "id", "list", "s", "b", "site-a", null, null, melbourne); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(melbourne.UtcDateTime, roundTripped.SiteEnqueuedAt.UtcDateTime); Assert.Equal(TimeSpan.Zero, roundTripped.SiteEnqueuedAt.Offset); } [Fact] public void NotificationSubmitAck_RoundTrip_Accepted_And_Rejected() { var accepted = new NotificationSubmitAck("n1", Accepted: true, Error: null); var rejected = new NotificationSubmitAck("n2", Accepted: false, Error: "list not found"); Assert.Equal(accepted, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(accepted)))); Assert.Equal(rejected, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(rejected)))); Assert.Null(CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(accepted))).Error); } // ----------------------------------------------------------------------- // NotificationStatusQuery / Response // ----------------------------------------------------------------------- [Fact] public void NotificationStatusQuery_RoundTrip_PreservesEveryField() { var original = new NotificationStatusQuery("corr-1", Guid.NewGuid().ToString()); Assert.Equal(original, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original)))); } [Fact] public void NotificationStatusResponse_RoundTrip_FullyPopulated_PreservesEveryField() { var original = new NotificationStatusResponse( CorrelationId: "corr-1", Found: true, Status: "Delivered", RetryCount: 3, LastError: "smtp 421", DeliveredAt: SampleInstant); Assert.Equal(original, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original)))); } [Fact] public void NotificationStatusResponse_RoundTrip_NotFound_LeavesOptionalsNull() { var original = new NotificationStatusResponse( CorrelationId: "corr-1", Found: false, Status: "Unknown", RetryCount: 0, LastError: null, DeliveredAt: null); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(original, roundTripped); Assert.Null(roundTripped.LastError); Assert.Null(roundTripped.DeliveredAt); } // ----------------------------------------------------------------------- // Audit ingest envelopes (rows delegate to AuditEventDtoMapper / SiteCallDtoMapper) // ----------------------------------------------------------------------- [Fact] public void IngestAuditEventsCommand_RoundTrip_PreservesOrderAndRows() { var first = NewAuditEvent(sourceScript: "OnDemand"); var second = NewAuditEvent(sourceScript: "OnTrigger"); var original = new IngestAuditEventsCommand([first, second]); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(2, roundTripped.Events.Count); Assert.Equal(first.AsRow().EventId, roundTripped.Events[0].AsRow().EventId); Assert.Equal("OnDemand", roundTripped.Events[0].AsRow().SourceScript); Assert.Equal(second.AsRow().EventId, roundTripped.Events[1].AsRow().EventId); Assert.Equal("OnTrigger", roundTripped.Events[1].AsRow().SourceScript); } [Fact] public void IngestAuditEventsCommand_RoundTrip_EmptyBatch_StaysEmpty() { var roundTripped = CentralControlDtoMapper.FromDto( Wire(CentralControlDtoMapper.ToDto(new IngestAuditEventsCommand([])))); Assert.Empty(roundTripped.Events); } [Fact] public void IngestCachedTelemetryCommand_RoundTrip_PreservesBothHalvesOfEachPacket() { var audit = NewAuditEvent(sourceScript: "OnDemand"); var siteCall = NewSiteCall(); var original = new IngestCachedTelemetryCommand([new CachedTelemetryEntry(audit, siteCall)]); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); var entry = Assert.Single(roundTripped.Entries); Assert.Equal(audit.AsRow().EventId, entry.Audit.AsRow().EventId); // IngestedAtUtc is central-set inside the dual-write transaction and is // deliberately off the wire, so it is the one member excluded from the compare. Assert.Equal(siteCall with { IngestedAtUtc = default }, entry.SiteCall with { IngestedAtUtc = default }); } [Fact] public void IngestCachedTelemetryCommand_RoundTrip_NullableSiteCallFields_StayNull() { var siteCall = NewSiteCall() with { SourceNode = null, LastError = null, HttpStatus = null, TerminalAtUtc = null, }; var original = new IngestCachedTelemetryCommand( [new CachedTelemetryEntry(NewAuditEvent(), siteCall)]); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); var entry = Assert.Single(roundTripped.Entries); Assert.Null(entry.SiteCall.SourceNode); Assert.Null(entry.SiteCall.LastError); Assert.Null(entry.SiteCall.HttpStatus); Assert.Null(entry.SiteCall.TerminalAtUtc); } [Fact] public void IngestCachedTelemetryCommand_RoundTrip_EmptyBatch_StaysEmpty() { var roundTripped = CentralControlDtoMapper.FromDto( Wire(CentralControlDtoMapper.ToDto(new IngestCachedTelemetryCommand([])))); Assert.Empty(roundTripped.Entries); } [Fact] public void IngestAck_RoundTrip_PreservesIdsAndOrder() { var ids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; var roundTripped = CentralControlDtoMapper.FromIngestAck( Wire(CentralControlDtoMapper.ToIngestAck(ids))); Assert.Equal(ids, roundTripped); } [Fact] public void IngestAck_RoundTrip_NothingAccepted_StaysEmpty() { // An empty ack is meaningful — "zero rows persisted" leaves the site's rows // Pending — so it must not be indistinguishable from a dropped field. Assert.Empty(CentralControlDtoMapper.FromIngestAck( Wire(CentralControlDtoMapper.ToIngestAck([])))); } // ----------------------------------------------------------------------- // Startup reconciliation // ----------------------------------------------------------------------- [Fact] public void ReconcileSiteRequest_RoundTrip_PreservesInventoryMap() { var original = new ReconcileSiteRequest( SiteIdentifier: "site-a", NodeId: "node-a", LocalNameToRevisionHash: new Dictionary { ["Plant.Tank04"] = "hash-1", ["Plant.Pump01"] = "hash-2", }); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(original.SiteIdentifier, roundTripped.SiteIdentifier); Assert.Equal(original.NodeId, roundTripped.NodeId); Assert.Equal( original.LocalNameToRevisionHash.OrderBy(kv => kv.Key), roundTripped.LocalNameToRevisionHash.OrderBy(kv => kv.Key)); } [Fact] public void ReconcileSiteRequest_RoundTrip_EmptyInventory_StaysEmpty() { // A node with nothing deployed is the normal first-boot case, and central // must see an empty inventory rather than a missing one. var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto( new ReconcileSiteRequest("site-a", "node-a", new Dictionary())))); Assert.Empty(roundTripped.LocalNameToRevisionHash); } [Fact] public void ReconcileSiteResponse_RoundTrip_PreservesGapOrphansAndUrl() { var original = new ReconcileSiteResponse( Gap: [ new ReconcileGapItem("Plant.Tank04", "dep-1", "hash-1", IsEnabled: true, "token-1"), new ReconcileGapItem("Plant.Pump01", "dep-2", "hash-2", IsEnabled: false, "token-2"), ], OrphanNames: ["Plant.Retired01", "Plant.Retired02"], CentralFetchBaseUrl: "http://scadabridge-central-a:5000"); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(original.Gap, roundTripped.Gap); Assert.Equal(original.OrphanNames, roundTripped.OrphanNames); Assert.Equal(original.CentralFetchBaseUrl, roundTripped.CentralFetchBaseUrl); } [Fact] public void ReconcileSiteResponse_RoundTrip_NoGap_StaysEmpty() { var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto( new ReconcileSiteResponse([], [], "http://central:5000")))); Assert.Empty(roundTripped.Gap); Assert.Empty(roundTripped.OrphanNames); } // ----------------------------------------------------------------------- // Site health // ----------------------------------------------------------------------- [Fact] public void SiteHealthReport_RoundTrip_FullyPopulated_PreservesEveryField() { var original = FullyPopulatedHealthReport(); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); AssertHealthReportsEqual(original, roundTripped); } [Fact] public void SiteHealthReport_RoundTrip_MinimalReport_LeavesEveryOptionalNullOrEmpty() { // The shape a producer emits before any reporter has run: no endpoints map, no // tag-quality map, no cluster-node list, no audit backlog, no nullable gauges. var original = new SiteHealthReport( SiteId: "site-a", SequenceNumber: 1, ReportTimestamp: SampleInstant, DataConnectionStatuses: new Dictionary(), TagResolutionCounts: new Dictionary(), ScriptErrorCount: 0, AlarmEvaluationErrorCount: 0, StoreAndForwardBufferDepths: new Dictionary(), DeadLetterCount: 0, DeployedInstanceCount: 0, EnabledInstanceCount: 0, DisabledInstanceCount: 0); var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); AssertHealthReportsEqual(original, roundTripped); Assert.Empty(roundTripped.DataConnectionStatuses); Assert.Empty(roundTripped.TagResolutionCounts); Assert.Empty(roundTripped.StoreAndForwardBufferDepths); Assert.Null(roundTripped.DataConnectionEndpoints); Assert.Null(roundTripped.DataConnectionTagQuality); Assert.Null(roundTripped.ClusterNodes); Assert.Null(roundTripped.SiteAuditBacklog); Assert.Null(roundTripped.OldestParkedMessageAgeSeconds); Assert.Null(roundTripped.ScriptOldestBusyAgeSeconds); Assert.Null(roundTripped.LocalDbReplicationConnected); Assert.Null(roundTripped.LocalDbOplogBacklog); } [Fact] public void SiteHealthReport_RoundTrip_EmptyNullableCollections_StayEmptyNotNull() { // The distinction the three wrapper messages exist for. Null means "this node // does not report the signal"; empty means "it reports it, and there is // nothing in it". proto3 cannot express presence on repeated/map fields, so // collapsing one into the other here would be invisible until an operator // misread the health page. var original = FullyPopulatedHealthReport() with { DataConnectionEndpoints = new Dictionary(), DataConnectionTagQuality = new Dictionary(), ClusterNodes = [], }; var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.NotNull(roundTripped.DataConnectionEndpoints); Assert.Empty(roundTripped.DataConnectionEndpoints); Assert.NotNull(roundTripped.DataConnectionTagQuality); Assert.Empty(roundTripped.DataConnectionTagQuality); Assert.NotNull(roundTripped.ClusterNodes); Assert.Empty(roundTripped.ClusterNodes); } [Fact] public void SiteHealthReport_RoundTrip_FalseAndZeroGauges_StayFalseAndZero() { // The mirror of the null case: LocalDbReplicationConnected=false ("configured // but disconnected") and LocalDbOplogBacklog=0 ("healthy, nothing queued") are // real values that must not decay into null on the wire. var original = FullyPopulatedHealthReport() with { LocalDbReplicationConnected = false, LocalDbOplogBacklog = 0, OldestParkedMessageAgeSeconds = 0d, ScriptOldestBusyAgeSeconds = 0d, }; var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.False(roundTripped.LocalDbReplicationConnected); Assert.Equal(0L, roundTripped.LocalDbOplogBacklog); Assert.Equal(0d, roundTripped.OldestParkedMessageAgeSeconds); Assert.Equal(0d, roundTripped.ScriptOldestBusyAgeSeconds); } [Fact] public void SiteHealthReport_RoundTrip_BacklogWithEmptyQueue_KeepsNullOldestPending() { var original = FullyPopulatedHealthReport() with { SiteAuditBacklog = new SiteAuditBacklogSnapshot(0, null, 4096), }; var roundTripped = CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original))); Assert.Equal(new SiteAuditBacklogSnapshot(0, null, 4096), roundTripped.SiteAuditBacklog); } [Fact] public void SiteHealthReportAck_RoundTrip_Accepted_And_Rejected() { var accepted = new SiteHealthReportAck("site-a", 42, Accepted: true); var rejected = new SiteHealthReportAck("site-a", 43, Accepted: false, Error: "aggregator down"); Assert.Equal(accepted, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(accepted)))); Assert.Equal(rejected, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(rejected)))); Assert.Null(CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(accepted))).Error); } // ----------------------------------------------------------------------- // Heartbeat + ConnectionHealth enum // ----------------------------------------------------------------------- [Theory] [InlineData(true)] [InlineData(false)] public void HeartbeatMessage_RoundTrip_PreservesEveryField(bool isActive) { var original = new HeartbeatMessage("site-a", "scadabridge-site-a-node-b", isActive, SampleInstant); Assert.Equal(original, CentralControlDtoMapper.FromDto(Wire(CentralControlDtoMapper.ToDto(original)))); } [Theory] [InlineData(ConnectionHealth.Connected)] [InlineData(ConnectionHealth.Disconnected)] [InlineData(ConnectionHealth.Connecting)] [InlineData(ConnectionHealth.Error)] public void ConnectionHealth_RoundTrip_EveryValueSurvives(ConnectionHealth health) { Assert.Equal(health, CentralControlDtoMapper.FromDto(CentralControlDtoMapper.ToDto(health))); } [Fact] public void ConnectionHealth_Unspecified_DecodesToErrorNotConnected() { // Fail-safe direction: a wire value this build does not recognise must never // render as "healthy" on the central health page. Assert.Equal( ConnectionHealth.Error, CentralControlDtoMapper.FromDto(ConnectionHealthEnum.ConnectionHealthUnspecified)); } [Fact] public void ConnectionHealth_EveryEnumMemberIsMapped() { // Guards the ArgumentOutOfRangeException path: adding a ConnectionHealth value // without extending the mapper should fail here, not silently on a rig. foreach (var health in Enum.GetValues()) { var wire = CentralControlDtoMapper.ToDto(health); Assert.NotEqual(ConnectionHealthEnum.ConnectionHealthUnspecified, wire); } } // ----------------------------------------------------------------------- // Fixtures // ----------------------------------------------------------------------- private static SiteHealthReport FullyPopulatedHealthReport() => new( SiteId: "site-a", SequenceNumber: 987654321L, ReportTimestamp: SampleInstant, DataConnectionStatuses: new Dictionary { ["opc-main"] = ConnectionHealth.Connected, ["mx-gateway"] = ConnectionHealth.Error, ["opc-backup"] = ConnectionHealth.Connecting, ["legacy"] = ConnectionHealth.Disconnected, }, TagResolutionCounts: new Dictionary { ["opc-main"] = new(TotalSubscribed: 120, SuccessfullyResolved: 118), }, ScriptErrorCount: 4, AlarmEvaluationErrorCount: 2, StoreAndForwardBufferDepths: new Dictionary { ["erp"] = 17, ["mes"] = 0, }, DeadLetterCount: 5, DeployedInstanceCount: 30, EnabledInstanceCount: 28, DisabledInstanceCount: 2, NodeRole: "Active", NodeHostname: "scadabridge-site-a-node-a", DataConnectionEndpoints: new Dictionary { ["opc-main"] = "opc.tcp://opcua:4840", }, DataConnectionTagQuality: new Dictionary { ["opc-main"] = new(Good: 100, Bad: 3, Uncertain: 15), }, ParkedMessageCount: 6, ClusterNodes: [ new NodeStatus("scadabridge-site-a-node-a", IsOnline: true, "Active"), new NodeStatus("scadabridge-site-a-node-b", IsOnline: false, "Standby"), ], SiteAuditWriteFailures: 7, AuditRedactionFailure: 8, SiteAuditBacklog: new SiteAuditBacklogSnapshot( PendingCount: 42, OldestPendingUtc: new DateTime(2026, 7, 22, 8, 0, 0, DateTimeKind.Utc), OnDiskBytes: 1_234_567L), SiteEventLogWriteFailures: 9L, OldestParkedMessageAgeSeconds: 3600.5d) { ScriptQueueDepth = 11, ScriptBusyThreads = 3, ScriptOldestBusyAgeSeconds = 12.25d, LocalDbReplicationConnected = true, LocalDbOplogBacklog = 250L, }; /// /// Compares two reports member by member. is a record, /// but its dictionary/list members compare by reference under record equality, so /// Assert.Equal(a, b) would pass trivially for the scalars and never look /// inside the collections — the exact blind spot these goldens exist to close. /// private static void AssertHealthReportsEqual(SiteHealthReport expected, SiteHealthReport actual) { Assert.Equal(expected.SiteId, actual.SiteId); Assert.Equal(expected.SequenceNumber, actual.SequenceNumber); Assert.Equal(expected.ReportTimestamp, actual.ReportTimestamp); Assert.Equal( expected.DataConnectionStatuses.OrderBy(kv => kv.Key), actual.DataConnectionStatuses.OrderBy(kv => kv.Key)); Assert.Equal( expected.TagResolutionCounts.OrderBy(kv => kv.Key), actual.TagResolutionCounts.OrderBy(kv => kv.Key)); Assert.Equal(expected.ScriptErrorCount, actual.ScriptErrorCount); Assert.Equal(expected.AlarmEvaluationErrorCount, actual.AlarmEvaluationErrorCount); Assert.Equal( expected.StoreAndForwardBufferDepths.OrderBy(kv => kv.Key), actual.StoreAndForwardBufferDepths.OrderBy(kv => kv.Key)); Assert.Equal(expected.DeadLetterCount, actual.DeadLetterCount); Assert.Equal(expected.DeployedInstanceCount, actual.DeployedInstanceCount); Assert.Equal(expected.EnabledInstanceCount, actual.EnabledInstanceCount); Assert.Equal(expected.DisabledInstanceCount, actual.DisabledInstanceCount); Assert.Equal(expected.NodeRole, actual.NodeRole); Assert.Equal(expected.NodeHostname, actual.NodeHostname); Assert.Equal( expected.DataConnectionEndpoints?.OrderBy(kv => kv.Key), actual.DataConnectionEndpoints?.OrderBy(kv => kv.Key)); Assert.Equal( expected.DataConnectionTagQuality?.OrderBy(kv => kv.Key), actual.DataConnectionTagQuality?.OrderBy(kv => kv.Key)); Assert.Equal(expected.ParkedMessageCount, actual.ParkedMessageCount); Assert.Equal(expected.ClusterNodes, actual.ClusterNodes); Assert.Equal(expected.SiteAuditWriteFailures, actual.SiteAuditWriteFailures); Assert.Equal(expected.AuditRedactionFailure, actual.AuditRedactionFailure); Assert.Equal(expected.SiteAuditBacklog, actual.SiteAuditBacklog); Assert.Equal(expected.SiteEventLogWriteFailures, actual.SiteEventLogWriteFailures); Assert.Equal(expected.OldestParkedMessageAgeSeconds, actual.OldestParkedMessageAgeSeconds); Assert.Equal(expected.ScriptQueueDepth, actual.ScriptQueueDepth); Assert.Equal(expected.ScriptBusyThreads, actual.ScriptBusyThreads); Assert.Equal(expected.ScriptOldestBusyAgeSeconds, actual.ScriptOldestBusyAgeSeconds); Assert.Equal(expected.LocalDbReplicationConnected, actual.LocalDbReplicationConnected); Assert.Equal(expected.LocalDbOplogBacklog, actual.LocalDbOplogBacklog); } /// /// Serializes a wire message and parses it back, so every round-trip below crosses a /// real protobuf encode/decode rather than only exercising the mapper's object graph. /// This is what proves the three collection wrapper messages keep their presence /// when empty — an empty message encodes as a tag with zero-length payload, and a /// mapper that used a bare repeated/map field instead would decode an /// empty collection back as null with no test-visible difference at the object level. /// /// Generated protobuf message type. /// The message to send through an encode/decode cycle. /// An independent instance parsed from 's bytes. private static T Wire(T message) where T : IMessage, new() => new MessageParser(() => new T()).ParseFrom(message.ToByteArray()); private static ZB.MOM.WW.Audit.AuditEvent NewAuditEvent(string? sourceScript = null) => ScadaBridgeAuditEventFactory.Create( channel: AuditChannel.ApiOutbound, kind: AuditKind.ApiCallCached, status: AuditStatus.Forwarded, eventId: Guid.NewGuid(), occurredAtUtc: new DateTime(2026, 7, 22, 9, 0, 0, DateTimeKind.Utc), target: "ERP.GetOrder", sourceSiteId: "site-a", sourceNode: "node-a", sourceScript: sourceScript); private static SiteCall NewSiteCall() => new() { TrackedOperationId = TrackedOperationId.New(), Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-a", SourceNode = "node-a", Status = "Delivered", RetryCount = 2, LastError = "transient 503", HttpStatus = 200, CreatedAtUtc = new DateTime(2026, 7, 22, 8, 0, 0, DateTimeKind.Utc), UpdatedAtUtc = new DateTime(2026, 7, 22, 8, 5, 0, DateTimeKind.Utc), TerminalAtUtc = new DateTime(2026, 7, 22, 8, 10, 0, DateTimeKind.Utc), IngestedAtUtc = new DateTime(2026, 7, 22, 8, 10, 1, DateTimeKind.Utc), }; }