using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Contracts; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Tests.Contracts; public sealed class ProtobufContractRoundTripTests { /// Verifies that gateway descriptor contains expected public service methods. [Fact] public void GatewayDescriptor_ContainsInitialPublicServiceMethods() { var service = Assert.Single( MxaccessGatewayReflection.Descriptor.Services, descriptor => descriptor.Name == "MxAccessGateway"); Assert.Contains(service.Methods, method => method.Name == "OpenSession"); Assert.Contains(service.Methods, method => method.Name == "CloseSession"); Assert.Contains(service.Methods, method => method.Name == "Invoke"); Assert.Contains(service.Methods, method => method.Name == "StreamEvents"); Assert.Contains(service.Methods, method => method.Name == "AcknowledgeAlarm"); Assert.Contains(service.Methods, method => method.Name == "StreamAlarms"); } /// Verifies that worker envelope descriptor contains required correlation fields. [Fact] public void WorkerEnvelopeDescriptor_ContainsRequiredCorrelationFields() { var fields = WorkerEnvelope.Descriptor.Fields.InDeclarationOrder(); Assert.Contains(fields, field => field.Name == "protocol_version"); Assert.Contains(fields, field => field.Name == "session_id"); Assert.Contains(fields, field => field.Name == "sequence"); Assert.Contains(fields, field => field.Name == "correlation_id"); } /// Verifies that command request round-trips through serialization. [Fact] public void CommandRequest_RoundTripsMethodSpecificPayload() { var original = new MxCommandRequest { SessionId = "session-1", ClientCorrelationId = "client-correlation-1", Command = new MxCommand { Kind = MxCommandKind.Register, Register = new RegisterCommand { ClientName = "mxaccessgw-test-client", }, }, }; var parsed = MxCommandRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.Register, parsed.Command.PayloadCase); } /// Verifies that command reply round-trips with return values and statuses. [Fact] public void CommandReply_RoundTripsHResultReturnValueOutParamsAndStatuses() { var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-1", Kind = MxCommandKind.AddItem, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, }, Hresult = 0, ReturnValue = new MxValue { DataType = MxDataType.Integer, Int32Value = 1234, VariantType = "VT_I4", }, AddItem = new AddItemReply { ItemHandle = 1234, }, }; original.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingLmx, Detail = 0, }); var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.HasHresult); Assert.Equal(MxCommandReply.PayloadOneofCase.AddItem, parsed.PayloadCase); Assert.Single(parsed.Statuses); } /// Verifies that event round-trips with value, status, and sequence. [Fact] public void Event_RoundTripsValueStatusSequenceAndBufferedBody() { var timestamp = Timestamp.FromDateTime(new DateTime(2026, 4, 26, 20, 0, 0, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnBufferedDataChange, SessionId = "session-1", ServerHandle = 10, ItemHandle = 20, Value = new MxValue { DataType = MxDataType.Float, ArrayValue = new MxArray { ElementDataType = MxDataType.Float, FloatValues = new FloatArray { Values = { 1.5f, 2.5f }, }, Dimensions = { 2 }, VariantType = "VT_ARRAY|VT_R4", }, }, Quality = 192, SourceTimestamp = timestamp, WorkerSequence = 42, WorkerTimestamp = timestamp, GatewayReceiveTimestamp = timestamp, OnBufferedDataChange = new OnBufferedDataChangeEvent { DataType = MxDataType.Float, QualityValues = new MxArray { ElementDataType = MxDataType.Integer, Int32Values = new Int32Array { Values = { 192, 192 }, }, Dimensions = { 2 }, }, TimestampValues = new MxArray { ElementDataType = MxDataType.Time, TimestampValues = new TimestampArray { Values = { timestamp, timestamp }, }, Dimensions = { 2 }, }, }, }; original.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingNmx, Detail = 0, }); var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, parsed.BodyCase); Assert.Single(parsed.Statuses); } /// Verifies that worker envelope round-trips through serialization preserving protocol and command fields. [Fact] public void WorkerEnvelope_RoundTripsProtocolFieldsAndCommandBody() { var original = new WorkerEnvelope { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, SessionId = "session-1", Sequence = 7, CorrelationId = "gateway-correlation-1", WorkerCommand = new WorkerCommand { EnqueueTimestamp = Timestamp.FromDateTime( new DateTime(2026, 4, 26, 20, 5, 0, DateTimeKind.Utc)), Command = new MxCommand { Kind = MxCommandKind.Advise, Advise = new AdviseCommand { ServerHandle = 10, ItemHandle = 20, }, }, }, }; var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, parsed.BodyCase); Assert.Equal(MxCommand.PayloadOneofCase.Advise, parsed.WorkerCommand.Command.PayloadCase); } /// Verifies that an OnAlarmTransition event round-trips with full payload. [Fact] public void Event_RoundTripsOnAlarmTransitionWithFullPayload() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnAlarmTransition, SessionId = "session-1", WorkerSequence = 99, WorkerTimestamp = ack, GatewayReceiveTimestamp = ack, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Acknowledge, Severity = 750, OriginalRaiseTimestamp = raise, TransitionTimestamp = ack, OperatorUser = "operator1", OperatorComment = "investigating", Category = "Process", Description = "Tank 01 high-high level", CurrentValue = new MxValue { DataType = MxDataType.Float, FloatValue = 95.4f, VariantType = "VT_R4", }, LimitValue = new MxValue { DataType = MxDataType.Float, FloatValue = 90.0f, VariantType = "VT_R4", }, }, }; var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, parsed.BodyCase); Assert.Equal(AlarmTransitionKind.Acknowledge, parsed.OnAlarmTransition.TransitionKind); Assert.Equal(raise, parsed.OnAlarmTransition.OriginalRaiseTimestamp); Assert.Equal("operator1", parsed.OnAlarmTransition.OperatorUser); } /// Verifies that an OnAlarmTransition event round-trips with only the required fields populated. [Fact] public void Event_RoundTripsOnAlarmTransitionWithOptionalFieldsEmpty() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnAlarmTransition, SessionId = "session-1", WorkerSequence = 100, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Raise, Severity = 750, TransitionTimestamp = raise, }, }; var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorUser); Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorComment); Assert.Null(parsed.OnAlarmTransition.OriginalRaiseTimestamp); Assert.Null(parsed.OnAlarmTransition.CurrentValue); } /// Verifies that an MxEvent body oneof rejects multiple bodies — last write wins per proto3 semantics. [Fact] public void Event_OneofGuard_LastBodyWins() { var ev = new MxEvent { Family = MxEventFamily.OnAlarmTransition, OnDataChange = new OnDataChangeEvent(), OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "X", TransitionKind = AlarmTransitionKind.Raise, }, }; Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, ev.BodyCase); Assert.Null(ev.OnDataChange); } /// Verifies that AcknowledgeAlarmRequest round-trips through serialization. [Fact] public void AcknowledgeAlarmRequest_RoundTripsAllFields() { var original = new AcknowledgeAlarmRequest { ClientCorrelationId = "client-correlation-7", AlarmFullReference = "Tank01.Level.HiHi", Comment = "shift handover", OperatorUser = "operator2", }; var parsed = AcknowledgeAlarmRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); } /// Verifies that AcknowledgeAlarmReply round-trips with status, hresult, and diagnostics. [Fact] public void AcknowledgeAlarmReply_RoundTripsStatusAndHresult() { var original = new AcknowledgeAlarmReply { CorrelationId = "gateway-correlation-7", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingLmx, }, DiagnosticMessage = "ack accepted", }; var parsed = AcknowledgeAlarmReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.HasHresult); } /// /// Pins the documented command/reply payload-reuse contract: an /// ACKNOWLEDGE_ALARM_BY_NAME command's reply intentionally has no /// by-name-specific payload case and instead reuses the /// acknowledge_alarm () /// case. A future change that adds a separate by-name reply case — or /// drops the reuse — breaks this test. See Contracts-002 and /// docs/AlarmClientDiscovery.md section 4. /// [Fact] public void MxCommandReply_AcknowledgeAlarmByName_ReusesAcknowledgeAlarmPayloadCase() { // The reply oneof must NOT have a by-name-specific case. If a future // edit adds one, this assertion fails and forces the doc/test contract // to be revisited deliberately. foreach (MxCommandReply.PayloadOneofCase value in System.Enum.GetValues()) { Assert.NotEqual("AcknowledgeAlarmByName", value.ToString()); } var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-7", Kind = MxCommandKind.AcknowledgeAlarmByName, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, // By-name ack reuses the acknowledge_alarm payload case; see the // worker's MxAccessCommandExecutor.ExecuteAcknowledgeAlarmByName. AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0, }, }; var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); // Kind distinguishes the by-name ack; the payload case is shared. Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, parsed.Kind); Assert.Equal(MxCommandReply.PayloadOneofCase.AcknowledgeAlarm, parsed.PayloadCase); Assert.Equal(0, parsed.AcknowledgeAlarm.NativeStatus); // The by-name command has its own command payload case — the asymmetry // with the reply oneof is the documented contract under test. Assert.Contains( MxCommand.PayloadOneofCase.AcknowledgeAlarmByNameCommand, System.Enum.GetValues()); } /// Verifies that ActiveAlarmSnapshot round-trips with current state and operator metadata. [Fact] public void ActiveAlarmSnapshot_RoundTripsAllFields() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)); var original = new ActiveAlarmSnapshot { AlarmFullReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", Severity = 750, OriginalRaiseTimestamp = raise, CurrentState = AlarmConditionState.ActiveAcked, Category = "Process", Description = "Tank 01 high-high level", LastTransitionTimestamp = ack, OperatorUser = "operator2", OperatorComment = "investigating", }; var parsed = ActiveAlarmSnapshot.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(AlarmConditionState.ActiveAcked, parsed.CurrentState); } /// Verifies that StreamAlarmsRequest round-trips with and without a filter prefix. [Fact] public void StreamAlarmsRequest_RoundTripsWithAndWithoutFilter() { var withoutFilter = new StreamAlarmsRequest { ClientCorrelationId = "client-correlation-8", }; var withFilter = new StreamAlarmsRequest { ClientCorrelationId = "client-correlation-9", AlarmFilterPrefix = "Tank01.", }; Assert.Equal(withoutFilter, StreamAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray())); Assert.Equal(withFilter, StreamAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray())); } /// /// Verifies that QueryActiveAlarmsRequest pins the additive-only field numbering /// (session_id = 1, client_correlation_id = 2, alarm_filter_prefix = 3) /// advertised in its proto comment, that the message round-trips with the optional /// alarm_filter_prefix populated (the filter semantic the public RPC comment /// documents), and that QueryActiveAlarms remains on the public service surface. /// [Fact] public void QueryActiveAlarmsRequest_PinsFieldNumbersAndRoundTripsPrefixFilter() { var service = Assert.Single( MxaccessGatewayReflection.Descriptor.Services, descriptor => descriptor.Name == "MxAccessGateway"); Assert.Contains(service.Methods, method => method.Name == "QueryActiveAlarms"); var fields = QueryActiveAlarmsRequest.Descriptor.Fields; Assert.Equal(1, fields[QueryActiveAlarmsRequest.SessionIdFieldNumber].FieldNumber); Assert.Equal(2, fields[QueryActiveAlarmsRequest.ClientCorrelationIdFieldNumber].FieldNumber); Assert.Equal(3, fields[QueryActiveAlarmsRequest.AlarmFilterPrefixFieldNumber].FieldNumber); Assert.Equal("session_id", fields[QueryActiveAlarmsRequest.SessionIdFieldNumber].Name); Assert.Equal("client_correlation_id", fields[QueryActiveAlarmsRequest.ClientCorrelationIdFieldNumber].Name); Assert.Equal("alarm_filter_prefix", fields[QueryActiveAlarmsRequest.AlarmFilterPrefixFieldNumber].Name); var withoutFilter = new QueryActiveAlarmsRequest { ClientCorrelationId = "client-correlation-10", }; var withFilter = new QueryActiveAlarmsRequest { ClientCorrelationId = "client-correlation-11", AlarmFilterPrefix = "Tank01.", }; Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray())); var parsedWithFilter = QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray()); Assert.Equal(withFilter, parsedWithFilter); Assert.Equal("Tank01.", parsedWithFilter.AlarmFilterPrefix); } /// Verifies that an MxValue carrying a raw_value bytes payload round-trips. [Fact] public void MxValue_RoundTripsRawValueBytesPayload() { var original = new MxValue { DataType = MxDataType.Unknown, VariantType = "VT_UNKNOWN", RawDataType = 99, RawDiagnostic = "uninterpreted COM variant", RawValue = ByteString.CopyFrom(0x01, 0x02, 0xFE, 0xFF), }; var parsed = MxValue.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxValue.KindOneofCase.RawValue, parsed.KindCase); Assert.Equal(new byte[] { 0x01, 0x02, 0xFE, 0xFF }, parsed.RawValue.ToByteArray()); } /// Verifies that an MxArray carrying a RawArray of byte blobs round-trips. [Fact] public void MxArray_RoundTripsRawArrayPayload() { var original = new MxArray { ElementDataType = MxDataType.Unknown, VariantType = "VT_ARRAY|VT_UNKNOWN", RawElementDataType = 99, RawDiagnostic = "uninterpreted SAFEARRAY", Dimensions = { 2 }, RawValues = new RawArray { Values = { ByteString.CopyFrom(0xAA, 0xBB), ByteString.CopyFrom(0xCC, 0xDD), }, }, }; var parsed = MxArray.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxArray.ValuesOneofCase.RawValues, parsed.ValuesCase); Assert.Equal(2, parsed.RawValues.Values.Count); } /// Verifies that a BulkSubscribeReply with per-item SubscribeResults round-trips. [Fact] public void BulkSubscribeReply_RoundTripsSubscribeResults() { var original = new BulkSubscribeReply { Results = { new SubscribeResult { ServerHandle = 10, TagAddress = "Provider!Tank01.Level", ItemHandle = 21, WasSuccessful = true, }, new SubscribeResult { ServerHandle = 10, TagAddress = "Provider!Bad.Tag", WasSuccessful = false, ErrorMessage = "item not found", }, }, }; var parsed = BulkSubscribeReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(2, parsed.Results.Count); Assert.True(parsed.Results[0].WasSuccessful); Assert.False(parsed.Results[1].WasSuccessful); } /// Verifies that a bulk-subscribe command and its BulkSubscribeReply payload round-trip. [Fact] public void MxCommandReply_RoundTripsBulkSubscribePayload() { var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-bulk", Kind = MxCommandKind.SubscribeBulk, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, SubscribeBulk = new BulkSubscribeReply { Results = { new SubscribeResult { ServerHandle = 5, TagAddress = "Provider!Tank01.Level", ItemHandle = 7, WasSuccessful = true, }, }, }, }; var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommandReply.PayloadOneofCase.SubscribeBulk, parsed.PayloadCase); Assert.Single(parsed.SubscribeBulk.Results); } /// Verifies that a WorkerEnvelope carrying a WorkerFault body round-trips. [Fact] public void WorkerEnvelope_RoundTripsWorkerFaultBody() { var original = new WorkerEnvelope { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, SessionId = "session-1", Sequence = 11, CorrelationId = "gateway-correlation-fault", WorkerFault = new WorkerFault { Category = WorkerFaultCategory.MxaccessCommandFailed, CommandMethod = "Register", Hresult = unchecked((int)0x80004005), ExceptionType = "System.Runtime.InteropServices.COMException", DiagnosticMessage = "MXAccess COM call failed.", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.ProtocolViolation }, }, }; var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerFault, parsed.BodyCase); Assert.True(parsed.WorkerFault.HasHresult); } /// Verifies that a WorkerEnvelope carrying a WorkerHeartbeat body round-trips. [Fact] public void WorkerEnvelope_RoundTripsWorkerHeartbeatBody() { var activity = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 9, 0, 0, DateTimeKind.Utc)); var original = new WorkerEnvelope { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, SessionId = "session-1", Sequence = 12, CorrelationId = "gateway-correlation-heartbeat", WorkerHeartbeat = new WorkerHeartbeat { WorkerProcessId = 4242, State = WorkerState.Ready, LastStaActivityTimestamp = activity, PendingCommandCount = 3, OutboundEventQueueDepth = 7, LastEventSequence = 1234, CurrentCommandCorrelationId = "in-flight-1", }, }; var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHeartbeat, parsed.BodyCase); Assert.Equal(WorkerState.Ready, parsed.WorkerHeartbeat.State); } /// Verifies that the Galaxy Repository service descriptor exposes its browse RPCs. [Fact] public void GalaxyRepositoryDescriptor_ContainsBrowseServiceMethods() { var service = Assert.Single( GalaxyRepositoryReflection.Descriptor.Services, descriptor => descriptor.Name == "GalaxyRepository"); Assert.Contains(service.Methods, method => method.Name == "TestConnection"); Assert.Contains(service.Methods, method => method.Name == "GetLastDeployTime"); Assert.Contains(service.Methods, method => method.Name == "DiscoverHierarchy"); Assert.Contains(service.Methods, method => method.Name == "WatchDeployEvents"); Assert.Contains(service.Methods, method => method.Name == "BrowseChildren"); } /// /// Verifies that a DiscoverHierarchyRequest round-trips through every /// root oneof arm and its proto wrapper-typed max_depth field. /// /// The oneof arm selector (0=RootGobjectId, 1=RootTagName, 2=RootContainedPath). [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] public void DiscoverHierarchyRequest_RoundTripsRootOneofAndWrapperFields(int rootArm) { var original = new DiscoverHierarchyRequest { PageSize = 100, PageToken = "page-2", MaxDepth = 5, CategoryIds = { 3, 9 }, TemplateChainContains = { "Analog", "Pump" }, TagNameGlob = "Tank*", IncludeAttributes = true, AlarmBearingOnly = true, HistorizedOnly = false, }; switch (rootArm) { case 0: original.RootGobjectId = 4711; break; case 1: original.RootTagName = "Tank01"; break; default: original.RootContainedPath = "Area1.Tank01"; break; } var parsed = DiscoverHierarchyRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(original.RootCase, parsed.RootCase); Assert.NotEqual(DiscoverHierarchyRequest.RootOneofCase.None, parsed.RootCase); Assert.NotNull(parsed.MaxDepth); Assert.Equal(5, parsed.MaxDepth!.Value); Assert.True(parsed.HasIncludeAttributes); Assert.True(parsed.IncludeAttributes); } /// /// Verifies that a DiscoverHierarchyReply round-trips with nested /// GalaxyObject and GalaxyAttribute graphs. /// [Fact] public void DiscoverHierarchyReply_RoundTripsObjectAndAttributeGraph() { var original = new DiscoverHierarchyReply { NextPageToken = "page-3", TotalObjectCount = 2, Objects = { new GalaxyObject { GobjectId = 4711, TagName = "Tank01", ContainedName = "Tank01", BrowseName = "Tank 01", ParentGobjectId = 12, IsArea = false, CategoryId = 3, HostedByGobjectId = 8, TemplateChain = { "$AnalogDevice", "$Tank" }, Attributes = { new GalaxyAttribute { AttributeName = "Level", FullTagReference = "Galaxy!Tank01.Level", MxDataType = 3, DataTypeName = "Float", IsArray = false, ArrayDimension = 0, ArrayDimensionPresent = false, MxAttributeCategory = 1, SecurityClassification = 0, IsHistorized = true, IsAlarm = true, }, }, }, new GalaxyObject { GobjectId = 12, TagName = "Area1", IsArea = true, }, }, }; var parsed = DiscoverHierarchyReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(2, parsed.Objects.Count); Assert.Single(parsed.Objects[0].Attributes); Assert.True(parsed.Objects[0].Attributes[0].IsAlarm); } /// /// Verifies that a BrowseChildrenRequest round-trips through every /// parent oneof arm with the full filter set populated. /// /// The oneof arm selector (0=ParentGobjectId, 1=ParentTagName, 2=ParentContainedPath). [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] public void BrowseChildrenRequest_RoundTripsParentOneofAndFilters(int parentArm) { var original = new BrowseChildrenRequest { PageSize = 200, PageToken = "opaque-2", CategoryIds = { 3, 9 }, TemplateChainContains = { "Analog", "Pump" }, TagNameGlob = "Tank*", IncludeAttributes = true, AlarmBearingOnly = true, HistorizedOnly = false, }; switch (parentArm) { case 0: original.ParentGobjectId = 4711; break; case 1: original.ParentTagName = "Tank01"; break; default: original.ParentContainedPath = "Area1.Tank01"; break; } var parsed = BrowseChildrenRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(original.ParentCase, parsed.ParentCase); Assert.NotEqual(BrowseChildrenRequest.ParentOneofCase.None, parsed.ParentCase); Assert.True(parsed.HasIncludeAttributes); Assert.True(parsed.IncludeAttributes); } /// /// Verifies that a BrowseChildrenReply round-trips its children list, /// the parallel-indexed child_has_children array, and the /// cache sequence used to bind page tokens. /// [Fact] public void BrowseChildrenReply_RoundTripsChildrenAndHasChildrenParallelArrays() { var original = new BrowseChildrenReply { NextPageToken = "opaque-3", TotalChildCount = 2, CacheSequence = 42UL, Children = { new GalaxyObject { GobjectId = 4711, TagName = "Tank01", ContainedName = "Tank01", BrowseName = "Tank 01", ParentGobjectId = 12, IsArea = false, CategoryId = 3, HostedByGobjectId = 8, TemplateChain = { "$AnalogDevice", "$Tank" }, Attributes = { new GalaxyAttribute { AttributeName = "Level", FullTagReference = "Galaxy!Tank01.Level", MxDataType = 3, DataTypeName = "Float", IsArray = false, ArrayDimension = 0, ArrayDimensionPresent = false, MxAttributeCategory = 1, SecurityClassification = 0, IsHistorized = true, IsAlarm = true, }, }, }, new GalaxyObject { GobjectId = 12, TagName = "Area1", IsArea = true, }, }, ChildHasChildren = { true, false }, }; var parsed = BrowseChildrenReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(2, parsed.Children.Count); Assert.Equal(2, parsed.ChildHasChildren.Count); Assert.True(parsed.ChildHasChildren[0]); Assert.False(parsed.ChildHasChildren[1]); Assert.Equal(42UL, parsed.CacheSequence); } /// Verifies that a DeployEvent round-trips with its timestamp and counters. [Fact] public void DeployEvent_RoundTripsTimestampAndCounters() { var observed = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 30, 0, DateTimeKind.Utc)); var deploy = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 0, 0, DateTimeKind.Utc)); var original = new DeployEvent { Sequence = 17, ObservedAt = observed, TimeOfLastDeploy = deploy, TimeOfLastDeployPresent = true, ObjectCount = 240, AttributeCount = 3600, }; var parsed = DeployEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.TimeOfLastDeployPresent); Assert.Equal(deploy, parsed.TimeOfLastDeploy); } /// Verifies that GetLastDeployTimeReply and TestConnectionReply round-trip. [Fact] public void GalaxyConnectionReplies_RoundTrip() { var deploy = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 0, 0, DateTimeKind.Utc)); var lastDeploy = new GetLastDeployTimeReply { Present = true, TimeOfLastDeploy = deploy, }; var testConnection = new TestConnectionReply { Ok = true }; Assert.Equal(lastDeploy, GetLastDeployTimeReply.Parser.ParseFrom(lastDeploy.ToByteArray())); Assert.Equal(testConnection, TestConnectionReply.Parser.ParseFrom(testConnection.ToByteArray())); } /// /// Verifies that a carrying multiple /// items round-trips, including the /// per-entry value and user_id fields. /// [Fact] public void WriteBulkCommand_RoundTripsEntries() { var original = new MxCommand { Kind = MxCommandKind.WriteBulk, WriteBulk = new WriteBulkCommand { ServerHandle = 10, Entries = { new WriteBulkEntry { ItemHandle = 21, UserId = 7, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 1.25f, VariantType = "VT_R4", }, }, new WriteBulkEntry { ItemHandle = 22, Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 42, VariantType = "VT_I4", }, }, }, }, }; var parsed = MxCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.WriteBulk, parsed.PayloadCase); Assert.Equal(2, parsed.WriteBulk.Entries.Count); Assert.Equal(7, parsed.WriteBulk.Entries[0].UserId); } /// /// Verifies that a round-trips, including /// the per-entry timestamp_value field that distinguishes Write2 /// from Write. /// [Fact] public void Write2BulkCommand_RoundTripsEntriesWithTimestampValue() { var timestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 19, 11, 0, 0, DateTimeKind.Utc)); var original = new MxCommand { Kind = MxCommandKind.Write2Bulk, Write2Bulk = new Write2BulkCommand { ServerHandle = 10, Entries = { new Write2BulkEntry { ItemHandle = 21, UserId = 7, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 99.9f, VariantType = "VT_R4", }, TimestampValue = new MxValue { DataType = MxDataType.Time, TimestampValue = timestamp, VariantType = "VT_DATE", }, }, }, }, }; var parsed = MxCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.Write2Bulk, parsed.PayloadCase); Assert.NotNull(parsed.Write2Bulk.Entries[0].TimestampValue); } /// /// Verifies that a round-trips, /// pinning the credential-bearing entry shape /// (current_user_id, verifier_user_id, value). /// See Contracts-011 for the credential-sensitivity comment on /// WriteSecuredBulkEntry.value. /// [Fact] public void WriteSecuredBulkCommand_RoundTripsCredentialBearingEntries() { var original = new MxCommand { Kind = MxCommandKind.WriteSecuredBulk, WriteSecuredBulk = new WriteSecuredBulkCommand { ServerHandle = 10, Entries = { new WriteSecuredBulkEntry { ItemHandle = 21, CurrentUserId = 100, VerifierUserId = 200, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 75.0f, VariantType = "VT_R4", }, }, }, }, }; var parsed = MxCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.WriteSecuredBulk, parsed.PayloadCase); Assert.Equal(100, parsed.WriteSecuredBulk.Entries[0].CurrentUserId); Assert.Equal(200, parsed.WriteSecuredBulk.Entries[0].VerifierUserId); } /// /// Verifies that a round-trips, /// including both the credential-sensitive value and the /// timestamp_value per entry. /// [Fact] public void WriteSecured2BulkCommand_RoundTripsCredentialBearingEntriesWithTimestamp() { var timestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 19, 11, 30, 0, DateTimeKind.Utc)); var original = new MxCommand { Kind = MxCommandKind.WriteSecured2Bulk, WriteSecured2Bulk = new WriteSecured2BulkCommand { ServerHandle = 10, Entries = { new WriteSecured2BulkEntry { ItemHandle = 21, CurrentUserId = 100, VerifierUserId = 200, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 50.0f, VariantType = "VT_R4", }, TimestampValue = new MxValue { DataType = MxDataType.Time, TimestampValue = timestamp, VariantType = "VT_DATE", }, }, }, }, }; var parsed = MxCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.WriteSecured2Bulk, parsed.PayloadCase); Assert.NotNull(parsed.WriteSecured2Bulk.Entries[0].TimestampValue); } /// /// Verifies that a round-trips, including /// the tag_addresses list and the timeout_ms field that /// distinguishes the cached-vs-snapshot lifecycle. /// [Fact] public void ReadBulkCommand_RoundTripsTagAddressesAndTimeout() { var original = new MxCommand { Kind = MxCommandKind.ReadBulk, ReadBulk = new ReadBulkCommand { ServerHandle = 10, TagAddresses = { "Provider!Tank01.Level", "Provider!Tank02.Level" }, TimeoutMs = 2500, }, }; var parsed = MxCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.ReadBulk, parsed.PayloadCase); Assert.Equal(2, parsed.ReadBulk.TagAddresses.Count); Assert.Equal(2500u, parsed.ReadBulk.TimeoutMs); } /// /// Verifies that a carrying mixed-outcome /// entries round-trips and that the /// proto3 optional int32 hresult presence flag survives both the /// "hresult set" and "hresult unset" cases. /// [Fact] public void BulkWriteReply_RoundTripsResultsWithOptionalHresultPresence() { var original = new BulkWriteReply { Results = { new BulkWriteResult { ServerHandle = 10, ItemHandle = 21, WasSuccessful = true, Hresult = 0, Statuses = { new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingLmx, }, }, }, new BulkWriteResult { ServerHandle = 10, ItemHandle = 22, WasSuccessful = false, Hresult = unchecked((int)0x80004005), ErrorMessage = "item not advised", }, new BulkWriteResult { ServerHandle = 10, ItemHandle = 23, WasSuccessful = false, // Hresult deliberately UNSET — exercises the proto3 // `optional int32` HasField() = false arm. ErrorMessage = "tag rejected by allowlist", }, }, }; var parsed = BulkWriteReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(3, parsed.Results.Count); Assert.True(parsed.Results[0].HasHresult); Assert.True(parsed.Results[1].HasHresult); Assert.False(parsed.Results[2].HasHresult); Assert.True(parsed.Results[0].WasSuccessful); Assert.False(parsed.Results[2].WasSuccessful); Assert.Single(parsed.Results[0].Statuses); } /// /// Verifies that a carrying both cached /// (was_cached = true) and uncached (was_cached = false) /// entries round-trips. Pins the /// deliberate absence of hresult on /// — failures are carried as was_successful = false plus /// error_message only. /// [Fact] public void BulkReadReply_RoundTripsCachedAndSnapshotResults() { var sourceTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc)); var original = new BulkReadReply { Results = { new BulkReadResult { ServerHandle = 10, TagAddress = "Provider!Tank01.Level", ItemHandle = 21, WasSuccessful = true, WasCached = true, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 42.5f, VariantType = "VT_R4", }, Quality = 192, SourceTimestamp = sourceTimestamp, Statuses = { new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingNmx, }, }, }, new BulkReadResult { ServerHandle = 10, TagAddress = "Provider!Tank02.Level", ItemHandle = 22, WasSuccessful = true, WasCached = false, Value = new MxValue { DataType = MxDataType.Integer, Int32Value = 0, VariantType = "VT_I4", }, SourceTimestamp = sourceTimestamp, }, new BulkReadResult { ServerHandle = 10, TagAddress = "Provider!Bad.Tag", WasSuccessful = false, WasCached = false, ErrorMessage = "snapshot timed out before first OnDataChange", }, }, }; var parsed = BulkReadReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(3, parsed.Results.Count); Assert.True(parsed.Results[0].WasCached); Assert.False(parsed.Results[1].WasCached); Assert.False(parsed.Results[2].WasSuccessful); Assert.Equal("snapshot timed out before first OnDataChange", parsed.Results[2].ErrorMessage); // BulkReadResult has no `hresult` field — pin that contract. Assert.DoesNotContain( BulkReadResult.Descriptor.Fields.InDeclarationOrder(), field => field.Name == "hresult"); } /// /// Verifies that an with each of the new /// bulk write/read payload oneof cases round-trips and that /// resolves to the /// expected value. Pins every new oneof case added by the bulk /// write/read extension. /// /// The command kind to test. /// The expected payload oneof case. [Theory] [InlineData(MxCommandKind.WriteBulk, MxCommandReply.PayloadOneofCase.WriteBulk)] [InlineData(MxCommandKind.Write2Bulk, MxCommandReply.PayloadOneofCase.Write2Bulk)] [InlineData(MxCommandKind.WriteSecuredBulk, MxCommandReply.PayloadOneofCase.WriteSecuredBulk)] [InlineData(MxCommandKind.WriteSecured2Bulk, MxCommandReply.PayloadOneofCase.WriteSecured2Bulk)] public void MxCommandReply_RoundTripsBulkWritePayloadCases( MxCommandKind kind, MxCommandReply.PayloadOneofCase expectedPayloadCase) { var reply = new BulkWriteReply { Results = { new BulkWriteResult { ServerHandle = 5, ItemHandle = 7, WasSuccessful = true, Hresult = 0, }, }, }; var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-bulk-write", Kind = kind, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, }; switch (expectedPayloadCase) { case MxCommandReply.PayloadOneofCase.WriteBulk: original.WriteBulk = reply; break; case MxCommandReply.PayloadOneofCase.Write2Bulk: original.Write2Bulk = reply; break; case MxCommandReply.PayloadOneofCase.WriteSecuredBulk: original.WriteSecuredBulk = reply; break; case MxCommandReply.PayloadOneofCase.WriteSecured2Bulk: original.WriteSecured2Bulk = reply; break; default: throw new System.ArgumentOutOfRangeException(nameof(expectedPayloadCase)); } var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(expectedPayloadCase, parsed.PayloadCase); Assert.Equal(kind, parsed.Kind); } /// /// Verifies that an carrying the /// provider_status payload case round-trips and resolves to /// . /// [Fact] public void Feed_RoundTripsProviderStatus() { var since = Timestamp.FromDateTime(new DateTime(2026, 6, 13, 9, 0, 0, DateTimeKind.Utc)); var original = new AlarmFeedMessage { ProviderStatus = new AlarmProviderStatus { Mode = AlarmProviderMode.Subtag, Degraded = true, Reason = "wnwrap poll failed 3x (HRESULT 0x80004005)", Since = since, }, }; var parsed = AlarmFeedMessage.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(AlarmFeedMessage.PayloadOneofCase.ProviderStatus, parsed.PayloadCase); Assert.True(parsed.ProviderStatus.Degraded); Assert.Equal(AlarmProviderMode.Subtag, parsed.ProviderStatus.Mode); } /// /// Verifies that an carrying the /// new degraded and source_provider provenance fields /// round-trips with their values preserved. /// [Fact] public void Transition_RoundTripsDegradedProvenance() { var t = new OnAlarmTransitionEvent { AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi", TransitionKind = AlarmTransitionKind.Raise, Degraded = true, SourceProvider = AlarmProviderMode.Subtag, }; var parsed = OnAlarmTransitionEvent.Parser.ParseFrom(t.ToByteArray()); Assert.True(parsed.Degraded); Assert.Equal(AlarmProviderMode.Subtag, parsed.SourceProvider); } /// /// Verifies that an with kind /// and a populated /// payload round-trips and resolves to /// . /// [Fact] public void MxCommandReply_RoundTripsReadBulkPayload() { var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-read-bulk", Kind = MxCommandKind.ReadBulk, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, ReadBulk = new BulkReadReply { Results = { new BulkReadResult { ServerHandle = 5, TagAddress = "Provider!Tank01.Level", ItemHandle = 7, WasSuccessful = true, WasCached = true, Value = new MxValue { DataType = MxDataType.Float, FloatValue = 12.5f, VariantType = "VT_R4", }, }, }, }, }; var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommandReply.PayloadOneofCase.ReadBulk, parsed.PayloadCase); Assert.Single(parsed.ReadBulk.Results); Assert.True(parsed.ReadBulk.Results[0].WasCached); } /// /// Verifies that an carrying the /// alarm-provider provenance fields degraded (14) and /// source_provider (15) round-trips with their values preserved, /// pinning the wire shape of the byte-identical provenance fields that /// also appear on . /// [Fact] public void ActiveAlarmSnapshot_RoundTripsDegradedProvenance() { var raise = Timestamp.FromDateTime(new DateTime(2026, 6, 13, 12, 0, 0, DateTimeKind.Utc)); var original = new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", Severity = 750, OriginalRaiseTimestamp = raise, CurrentState = AlarmConditionState.Active, Degraded = true, SourceProvider = AlarmProviderMode.Subtag, }; var parsed = ActiveAlarmSnapshot.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.Degraded); Assert.Equal(AlarmProviderMode.Subtag, parsed.SourceProvider); } /// /// Verifies that a populating the /// alarm-provider fallback extensions — forced_mode (2), a /// watch_list entry with all six /// string fields (3), and a failover /// (4) — round-trips end to end, /// pinning the wire shape that the forced-subtag-mode fix depends on. /// [Fact] public void SubscribeAlarmsCommand_RoundTripsForcedModeWatchListAndFailover() { var original = new SubscribeAlarmsCommand { SubscriptionExpression = @"\\node\Galaxy!Area", ForcedMode = AlarmProviderMode.Subtag, WatchList = { new AlarmSubtagTarget { AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi", SourceObjectReference = "Tank01", ActiveSubtag = "Tank01.Level.HiHi.InAlarm", AckedSubtag = "Tank01.Level.HiHi.Acked", AckCommentSubtag = "Tank01.Level.HiHi.AckMsg", PrioritySubtag = "Tank01.Level.HiHi.Priority", }, }, Failover = new AlarmFailoverConfig { ConsecutiveFailureThreshold = 3, FailbackProbeIntervalSeconds = 10, FailbackStableProbes = 5, }, }; var parsed = SubscribeAlarmsCommand.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(AlarmProviderMode.Subtag, parsed.ForcedMode); var target = Assert.Single(parsed.WatchList); Assert.Equal("Galaxy!Area.Tank01.Level.HiHi", target.AlarmFullReference); Assert.Equal("Tank01", target.SourceObjectReference); Assert.Equal("Tank01.Level.HiHi.InAlarm", target.ActiveSubtag); Assert.Equal("Tank01.Level.HiHi.Acked", target.AckedSubtag); Assert.Equal("Tank01.Level.HiHi.AckMsg", target.AckCommentSubtag); Assert.Equal("Tank01.Level.HiHi.Priority", target.PrioritySubtag); Assert.Equal(3, parsed.Failover.ConsecutiveFailureThreshold); Assert.Equal(10, parsed.Failover.FailbackProbeIntervalSeconds); Assert.Equal(5, parsed.Failover.FailbackStableProbes); } /// /// Verifies that an carrying an /// body (the /// MxEvent.body oneof tag 25 paired with /// , family 6) /// round-trips and resolves to /// . /// [Fact] public void MxEvent_RoundTripsOnAlarmProviderModeChangedBody() { var at = Timestamp.FromDateTime(new DateTime(2026, 6, 13, 9, 30, 0, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnAlarmProviderModeChanged, SessionId = "session-1", WorkerSequence = 42, OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent { Mode = AlarmProviderMode.Subtag, Reason = "wnwrap poll failed 3x", Hresult = unchecked((int)0x80004005), At = at, }, }; var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxEvent.BodyOneofCase.OnAlarmProviderModeChanged, parsed.BodyCase); Assert.Equal(MxEventFamily.OnAlarmProviderModeChanged, parsed.Family); Assert.Equal(AlarmProviderMode.Subtag, parsed.OnAlarmProviderModeChanged.Mode); Assert.Equal(unchecked((int)0x80004005), parsed.OnAlarmProviderModeChanged.Hresult); } }