Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
T

1547 lines
59 KiB
C#

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
{
/// <summary>Verifies that gateway descriptor contains expected public service methods.</summary>
[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");
}
/// <summary>Verifies that worker envelope descriptor contains required correlation fields.</summary>
[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");
}
/// <summary>Verifies that command request round-trips through serialization.</summary>
[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);
}
/// <summary>Verifies that command reply round-trips with return values and statuses.</summary>
[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);
}
/// <summary>Verifies that event round-trips with value, status, and sequence.</summary>
[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);
}
/// <summary>Verifies that worker envelope round-trips through serialization preserving protocol and command fields.</summary>
[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);
}
/// <summary>Verifies that an OnAlarmTransition event round-trips with full payload.</summary>
[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);
}
/// <summary>Verifies that an OnAlarmTransition event round-trips with only the required fields populated.</summary>
[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);
}
/// <summary>Verifies that an MxEvent body oneof rejects multiple bodies — last write wins per proto3 semantics.</summary>
[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);
}
/// <summary>Verifies that AcknowledgeAlarmRequest round-trips through serialization.</summary>
[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);
}
/// <summary>Verifies that AcknowledgeAlarmReply round-trips with status, hresult, and diagnostics.</summary>
[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);
}
/// <summary>
/// Pins the documented command/reply payload-reuse contract: an
/// <c>ACKNOWLEDGE_ALARM_BY_NAME</c> command's reply intentionally has no
/// by-name-specific payload case and instead reuses the
/// <c>acknowledge_alarm</c> (<see cref="AcknowledgeAlarmReplyPayload"/>)
/// 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.
/// </summary>
[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<MxCommandReply.PayloadOneofCase>())
{
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<MxCommand.PayloadOneofCase>());
}
/// <summary>Verifies that ActiveAlarmSnapshot round-trips with current state and operator metadata.</summary>
[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);
}
/// <summary>Verifies that StreamAlarmsRequest round-trips with and without a filter prefix.</summary>
[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()));
}
/// <summary>
/// Verifies that <c>QueryActiveAlarmsRequest</c> pins the additive-only field numbering
/// (<c>session_id = 1</c>, <c>client_correlation_id = 2</c>, <c>alarm_filter_prefix = 3</c>)
/// advertised in its proto comment, that the message round-trips with the optional
/// <c>alarm_filter_prefix</c> populated (the filter semantic the public RPC comment
/// documents), and that <c>QueryActiveAlarms</c> remains on the public service surface.
/// </summary>
[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);
}
/// <summary>Verifies that an MxValue carrying a raw_value bytes payload round-trips.</summary>
[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());
}
/// <summary>Verifies that an MxArray carrying a RawArray of byte blobs round-trips.</summary>
[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);
}
/// <summary>Verifies that a BulkSubscribeReply with per-item SubscribeResults round-trips.</summary>
[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);
}
/// <summary>Verifies that a bulk-subscribe command and its BulkSubscribeReply payload round-trip.</summary>
[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);
}
/// <summary>Verifies that a WorkerEnvelope carrying a WorkerFault body round-trips.</summary>
[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);
}
/// <summary>Verifies that a WorkerEnvelope carrying a WorkerHeartbeat body round-trips.</summary>
[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);
}
/// <summary>Verifies that the Galaxy Repository service descriptor exposes its browse RPCs.</summary>
[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");
}
/// <summary>
/// Verifies that a DiscoverHierarchyRequest round-trips through every
/// <c>root</c> oneof arm and its proto wrapper-typed <c>max_depth</c> field.
/// </summary>
/// <param name="rootArm">The oneof arm selector (0=RootGobjectId, 1=RootTagName, 2=RootContainedPath).</param>
[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);
}
/// <summary>
/// Verifies that a DiscoverHierarchyReply round-trips with nested
/// GalaxyObject and GalaxyAttribute graphs.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a BrowseChildrenRequest round-trips through every
/// <c>parent</c> oneof arm with the full filter set populated.
/// </summary>
/// <param name="parentArm">The oneof arm selector (0=ParentGobjectId, 1=ParentTagName, 2=ParentContainedPath).</param>
[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);
}
/// <summary>
/// Verifies that a BrowseChildrenReply round-trips its children list,
/// the parallel-indexed <c>child_has_children</c> array, and the
/// cache sequence used to bind page tokens.
/// </summary>
[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);
}
/// <summary>Verifies that a DeployEvent round-trips with its timestamp and counters.</summary>
[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);
}
/// <summary>Verifies that GetLastDeployTimeReply and TestConnectionReply round-trip.</summary>
[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()));
}
/// <summary>
/// Verifies that a <see cref="WriteBulkCommand"/> carrying multiple
/// <see cref="WriteBulkEntry"/> items round-trips, including the
/// per-entry <c>value</c> and <c>user_id</c> fields.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="Write2BulkCommand"/> round-trips, including
/// the per-entry <c>timestamp_value</c> field that distinguishes Write2
/// from Write.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="WriteSecuredBulkCommand"/> round-trips,
/// pinning the credential-bearing entry shape
/// (<c>current_user_id</c>, <c>verifier_user_id</c>, <c>value</c>).
/// See Contracts-011 for the credential-sensitivity comment on
/// <c>WriteSecuredBulkEntry.value</c>.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="WriteSecured2BulkCommand"/> round-trips,
/// including both the credential-sensitive <c>value</c> and the
/// <c>timestamp_value</c> per entry.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="ReadBulkCommand"/> round-trips, including
/// the <c>tag_addresses</c> list and the <c>timeout_ms</c> field that
/// distinguishes the cached-vs-snapshot lifecycle.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="BulkWriteReply"/> carrying mixed-outcome
/// <see cref="BulkWriteResult"/> entries round-trips and that the
/// proto3 <c>optional int32 hresult</c> presence flag survives both the
/// "hresult set" and "hresult unset" cases.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="BulkReadReply"/> carrying both cached
/// (<c>was_cached = true</c>) and uncached (<c>was_cached = false</c>)
/// <see cref="BulkReadResult"/> entries round-trips. Pins the
/// deliberate absence of <c>hresult</c> on <see cref="BulkReadResult"/>
/// — failures are carried as <c>was_successful = false</c> plus
/// <c>error_message</c> only.
/// </summary>
[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");
}
/// <summary>
/// Verifies that an <see cref="MxCommandReply"/> with each of the new
/// bulk write/read payload oneof cases round-trips and that
/// <see cref="MxCommandReply.PayloadOneofCase"/> resolves to the
/// expected value. Pins every new oneof case added by the bulk
/// write/read extension.
/// </summary>
/// <param name="kind">The command kind to test.</param>
/// <param name="expectedPayloadCase">The expected payload oneof case.</param>
[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);
}
/// <summary>
/// Verifies that an <see cref="AlarmFeedMessage"/> carrying the
/// <c>provider_status</c> payload case round-trips and resolves to
/// <see cref="AlarmFeedMessage.PayloadOneofCase.ProviderStatus"/>.
/// </summary>
[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);
}
/// <summary>
/// Verifies that an <see cref="OnAlarmTransitionEvent"/> carrying the
/// new <c>degraded</c> and <c>source_provider</c> provenance fields
/// round-trips with their values preserved.
/// </summary>
[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);
}
/// <summary>
/// Verifies that an <see cref="MxCommandReply"/> with kind
/// <see cref="MxCommandKind.ReadBulk"/> and a populated
/// <see cref="BulkReadReply"/> payload round-trips and resolves to
/// <see cref="MxCommandReply.PayloadOneofCase.ReadBulk"/>.
/// </summary>
[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);
}
/// <summary>
/// Verifies that an <see cref="ActiveAlarmSnapshot"/> carrying the
/// alarm-provider provenance fields <c>degraded</c> (14) and
/// <c>source_provider</c> (15) round-trips with their values preserved,
/// pinning the wire shape of the byte-identical provenance fields that
/// also appear on <see cref="OnAlarmTransitionEvent"/>.
/// </summary>
[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);
}
/// <summary>
/// Verifies that a <see cref="SubscribeAlarmsCommand"/> populating the
/// alarm-provider fallback extensions — <c>forced_mode</c> (2), a
/// <c>watch_list</c> entry with all six <see cref="AlarmSubtagTarget"/>
/// string fields (3), and a <c>failover</c>
/// <see cref="AlarmFailoverConfig"/> (4) — round-trips end to end,
/// pinning the wire shape that the forced-subtag-mode fix depends on.
/// </summary>
[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);
}
/// <summary>
/// Verifies that an <see cref="MxEvent"/> carrying an
/// <see cref="OnAlarmProviderModeChangedEvent"/> body (the
/// <c>MxEvent.body</c> oneof tag 25 paired with
/// <see cref="MxEventFamily.OnAlarmProviderModeChanged"/>, family 6)
/// round-trips and resolves to
/// <see cref="MxEvent.BodyOneofCase.OnAlarmProviderModeChanged"/>.
/// </summary>
[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);
}
}