contracts: round-trip degraded provenance/watch-list/mode-changed; proto doc (Contracts-018,019)

This commit is contained in:
Joseph Doherty
2026-06-15 02:46:06 -04:00
parent 56dd56954b
commit ddf2d84fbc
4 changed files with 221 additions and 6 deletions
+69 -2
View File
@@ -4,8 +4,8 @@
|---|---| |---|---|
| Module | `src/ZB.MOM.WW.MxGateway.Contracts` | | Module | `src/ZB.MOM.WW.MxGateway.Contracts` |
| Reviewer | Claude Code | | Reviewer | Claude Code |
| Review date | 2026-05-24 | | Review date | 2026-06-15 |
| Commit reviewed | `42b0037` | | Commit reviewed | `410acc9` |
| Status | Re-reviewed | | Status | Re-reviewed |
| Open findings | 0 | | Open findings | 0 |
@@ -50,6 +50,43 @@ Python and Go descriptors. No fields renumbered or repurposed.
| 9 | Testing coverage | No issues found — `ProtobufContractRoundTripTests` and `GatewayContractInfoTests` continue to pin the protocol version; new `QueryActiveAlarmsRequest` lacks a round-trip test but the RPC type is generated and exercised end-to-end by the gRPC client tests in each language. | | 9 | Testing coverage | No issues found — `ProtobufContractRoundTripTests` and `GatewayContractInfoTests` continue to pin the protocol version; new `QueryActiveAlarmsRequest` lacks a round-trip test but the RPC type is generated and exercised end-to-end by the gRPC client tests in each language. |
| 10 | Documentation & comments | Issues found: Contracts-017 (the `rpc QueryActiveAlarms` comment block does not mention the `alarm_filter_prefix` request field). | | 10 | Documentation & comments | Issues found: Contracts-017 (the `rpc QueryActiveAlarms` comment block does not mention the `alarm_filter_prefix` request field). |
#### 2026-06-15 re-review (commit 410acc9)
Re-review pass at `410acc9` scoped to the contract changes since `42b0037`
(`git diff 42b0037..HEAD -- src/ZB.MOM.WW.MxGateway.Contracts/`). The window
contains two unrelated additive contract features. The brief targets the
**alarm-provider fallback** surface in `mxaccess_gateway.proto`: the new
`AlarmProviderMode` enum (`UNSPECIFIED=0`/`ALARMMGR=1`/`SUBTAG=2`), the
`AlarmSubtagTarget` watch-list message, `AlarmFailoverConfig`, the three new
`SubscribeAlarmsCommand` fields (`forced_mode=2`, `watch_list=3`, `failover=4`),
the `OnAlarmProviderModeChangedEvent` (`MxEvent.body` oneof tag 25,
`MxEventFamily=6`), the `degraded=14`/`source_provider=15` provenance fields on
`OnAlarmTransitionEvent` **and** `ActiveAlarmSnapshot`, and the
`AlarmFeedMessage.provider_status=4` oneof case carrying `AlarmProviderStatus`.
The same window also adds the Galaxy `BrowseChildren` lazy-browse RPC
(`galaxy_repository.proto`) and three XML doc comments on `GatewayContractInfo`
constants — both outside the brief's alarm focus but checked for additive-only
hygiene (clean). `Generated/*.cs` is build output and was not reviewed as
hand-written. `mxaccess_worker.proto` is unchanged (the alarm additions live in
the gateway proto the worker imports — matches the design doc's Superseded note).
Verified against `docs/plans/2026-06-13-alarm-subtag-fallback-design.md`,
`docs/plans/2026-06-15-forced-subtag-mode-fix.md`, and the worker/gateway source
(`AlarmDispatcher.cs:213`, `MxAccessEventMapper.cs:151`, `GatewayAlarmMonitor.cs`).
| # | Category | Result |
|---|---|---|
| 1 | Correctness & logic bugs | No issues found. Field semantics are correct against source: `AlarmProviderStatus.degraded`/`OnAlarmTransitionEvent.degraded` track `mode == SUBTAG` (worker `AlarmDispatcher.cs:213` sets `SourceProvider = Degraded ? Subtag : Alarmmgr`; gateway `GatewayAlarmMonitor._providerDegraded = toMode == Subtag`). `OnAlarmProviderModeChangedEvent.hresult` "0 on failback" matches the Auto-mode failover/failback path that emits it; forced mode is seeded gateway-side and emits no worker event, so the comment is not contradicted. |
| 2 | mxaccessgw conventions | No issues found. The subtag fallback synthesizes events **inside the worker** and marks every synthesized transition `degraded`, satisfying the CLAUDE.md "gateway forwards only worker-emitted events; synthesizing is an explicit opt-in non-parity mode" rule. `snake_case` fields, `PascalCase` messages, the `ALARM_PROVIDER_MODE_`/`MX_EVENT_FAMILY_` enum-prefix discipline, and the top-of-file wire-compatibility policy block (Contracts-005) are all honoured. Generated code regenerated, not hand-edited. |
| 3 | Concurrency & thread safety | N/A — pure contract definitions plus a static constants class. |
| 4 | Error handling & resilience | No issues found. The degraded/provider-status surface lets clients distinguish the lower-fidelity subtag feed from the authoritative alarmmgr feed; `AlarmProviderStatus` is emitted on stream open and every switch so late joiners learn the mode. |
| 5 | Security | No issues found — none of the new fields carry credentials or secrets. `AlarmSubtagTarget` carries only item-address strings. |
| 6 | Performance & resource management | No issues found. `repeated AlarmSubtagTarget watch_list` is sent once at subscribe time, not per-event; provenance fields are scalars. No hot-path bloat. |
| 7 | Design-document adherence | No drift. The shipped contract matches `docs/plans/2026-06-13-alarm-subtag-fallback-design.md` (including its Superseded notes: additions in the gateway proto, not the worker proto). |
| 8 | Code organization & conventions | No issues found. Every addition uses a new, contiguous field number — `SubscribeAlarmsCommand` 2-4, `MxEvent.body` 25, `MxEventFamily` 6, `OnAlarmTransitionEvent`/`ActiveAlarmSnapshot` 14-15, `AlarmFeedMessage.payload` 4 — with no reuse, renumbering, or type narrowing of any existing field. Enum zero-values are `UNSPECIFIED`. Additive-only invariant preserved. |
| 9 | Testing coverage | Issues found: Contracts-018 — `ProtobufContractRoundTripTests` covers the new `AlarmProviderStatus` (via `AlarmFeedMessage`) and the `OnAlarmTransitionEvent` `degraded`/`source_provider` fields, but has no round-trip coverage for the `ActiveAlarmSnapshot` provenance fields, the `SubscribeAlarmsCommand` extensions (`forced_mode`/`watch_list`/`failover`), or `OnAlarmProviderModeChangedEvent`. |
| 10 | Documentation & comments | Issues found: Contracts-019 — the `ActiveAlarmSnapshot.degraded`/`source_provider` fields carry no in-proto comment while the byte-identical fields on `OnAlarmTransitionEvent` are documented; and the `AlarmProviderMode` enum doc explains `UNSPECIFIED` only for the `forced_mode` use, not for the provenance (`source_provider`) reuse. |
## Findings ## Findings
### Contracts-001 ### Contracts-001
@@ -341,3 +378,33 @@ additive-only with no reuse, renumbering, or type narrowing.
Re-review: no new findings. Open finding count remains 0. All seventeen Re-review: no new findings. Open finding count remains 0. All seventeen
recorded Contracts findings (Contracts-001..017) remain closed recorded Contracts findings (Contracts-001..017) remain closed
(Resolved / Won't Fix). (Resolved / Won't Fix).
### Contracts-018
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | `src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs:396` (`ActiveAlarmSnapshot_RoundTripsAllFields`) |
| Status | Resolved |
**Description:** The alarm-provider fallback feature added several new wire fields to `mxaccess_gateway.proto`. `ProtobufContractRoundTripTests` was extended with `AlarmFeedMessage_RoundTripsProviderStatus` (covers `AlarmProviderStatus` + the `provider_status` oneof case) and `Transition_RoundTripsDegradedProvenance` (covers `OnAlarmTransitionEvent.degraded`/`source_provider`), but three pieces of the new contract surface have no round-trip coverage: (a) `ActiveAlarmSnapshot.degraded` (14) / `source_provider` (15) — `ActiveAlarmSnapshot_RoundTripsAllFields` stops at `OperatorComment` (field 11) and never sets or asserts the two new provenance fields, so a future renumber/type change to them would not be caught; (b) the `SubscribeAlarmsCommand` extensions `forced_mode` (2), `watch_list` (3, `repeated AlarmSubtagTarget`), and `failover` (4, `AlarmFailoverConfig`) — no test exercises these, and the live `forced_mode` enum-drop concern that prompted the `2026-06-15-forced-subtag-mode-fix` investigation is exactly the kind of wire shape prior contract tests have been written to pin; (c) `OnAlarmProviderModeChangedEvent` (the `MxEvent.body` oneof tag 25 / `MxEventFamily=6` worker→gateway event). This is the same class of gap previously flagged for the bulk family (Contracts-007 / Contracts-010): new wire shapes shipped without round-trip pinning.
**Recommendation:** Extend `ActiveAlarmSnapshot_RoundTripsAllFields` (or add a focused test) to set and assert `degraded = true` + `source_provider = AlarmProviderMode.Subtag`; add a round-trip test for `SubscribeAlarmsCommand` populating `forced_mode`, a `watch_list` entry (all six `AlarmSubtagTarget` string fields), and a `failover` `AlarmFailoverConfig`; and add a round-trip / `MxEvent` oneof-case test for `OnAlarmProviderModeChangedEvent` pinning `MxEvent.BodyCase == OnAlarmProviderModeChanged` for `MxEventFamily.OnAlarmProviderModeChanged`.
**Resolution:** _(2026-06-15)_ Verified the three coverage gaps against the proto — `ActiveAlarmSnapshot.degraded`/`source_provider` (14/15), `SubscribeAlarmsCommand.forced_mode`/`watch_list`/`failover` (2/3/4), and the `MxEvent.body` oneof tag 25 / `MxEventFamily=6` `OnAlarmProviderModeChangedEvent` were all unpinned. Added three focused round-trip tests to `ProtobufContractRoundTripTests`: `ActiveAlarmSnapshot_RoundTripsDegradedProvenance` (sets/asserts `degraded = true` + `source_provider = AlarmProviderMode.Subtag`), `SubscribeAlarmsCommand_RoundTripsForcedModeWatchListAndFailover` (populates `forced_mode`, a `watch_list` entry with all six `AlarmSubtagTarget` string fields, and a `failover` `AlarmFailoverConfig`), and `MxEvent_RoundTripsOnAlarmProviderModeChangedBody` (pins `MxEvent.BodyCase == OnAlarmProviderModeChanged` + `Family == OnAlarmProviderModeChanged`). All fields round-trip — no contract bug found. The full `ProtobufContractRoundTrip` filter is 49/49 green.
### Contracts-019
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:850-851` (`ActiveAlarmSnapshot`), `:318-324` (`AlarmProviderMode`) |
| Status | Resolved |
**Description:** Two in-proto documentation gaps on the new alarm-provider surface. (1) `OnAlarmTransitionEvent.degraded` (line 805-808) and `source_provider` (809-810) carry clear comments ("True when this transition came from the subtag-monitoring fallback … synthesized from data changes, reduced fidelity"; "Which provider produced this transition."), but the byte-identical `ActiveAlarmSnapshot.degraded` (850) and `source_provider` (851) are declared bare with no comment. The two messages model the same provenance concept and a reader of `ActiveAlarmSnapshot` alone gets no signal that a non-`UNSPECIFIED` `source_provider` plus `degraded = true` means the snapshot came from the lower-fidelity subtag source. (2) The `AlarmProviderMode` enum comment (318-319) documents the zero value only for one use site — "UNSPECIFIED on a SubscribeAlarmsCommand means auto: alarmmgr primary with subtag fallback" — but the same enum is reused as a provenance field on `OnAlarmTransitionEvent.source_provider`, `ActiveAlarmSnapshot.source_provider`, `OnAlarmProviderModeChangedEvent.mode`, and `AlarmProviderStatus.mode`. The worker always sets `source_provider` to `ALARMMGR` or `SUBTAG` (never `UNSPECIFIED`; `MxAccessEventMapper.cs:151` defaults to `Alarmmgr`, `AlarmDispatcher.cs:213` picks `Subtag`/`Alarmmgr`), so `UNSPECIFIED` as a provenance value has no defined meaning and the comment does not say so. The ProtobufStyleGuide rule "comment fields carrying MXAccess parity / non-obvious semantics" applies — this is a non-parity provenance marker.
**Recommendation:** (1) Add comments to `ActiveAlarmSnapshot.degraded` / `source_provider` mirroring the wording already on `OnAlarmTransitionEvent` (or a one-line cross-reference). (2) Extend the `AlarmProviderMode` enum comment to note that as a `source_provider` / `mode` provenance value the field is always `ALARMMGR` or `SUBTAG` on the wire and `UNSPECIFIED` should be treated as "unknown / not yet determined", so the zero value is unambiguous at every use site. Comment-only changes; no wire-format impact.
**Resolution:** _(2026-06-15)_ Confirmed both gaps in `mxaccess_gateway.proto`: `ActiveAlarmSnapshot.degraded`/`source_provider` (14/15) were bare while the byte-identical `OnAlarmTransitionEvent` fields were documented, and the `AlarmProviderMode` enum comment only explained `UNSPECIFIED` for the `forced_mode` use. (1) Added comments to `ActiveAlarmSnapshot.degraded`/`source_provider` mirroring the `OnAlarmTransitionEvent` wording (subtag-fallback / reduced-fidelity, always ALARMMGR or SUBTAG, never UNSPECIFIED). (2) Extended the `AlarmProviderMode` enum comment to distinguish its two use sites: as `forced_mode`, `UNSPECIFIED` = auto; as a provenance value (`OnAlarmTransitionEvent.source_provider`, `ActiveAlarmSnapshot.source_provider`, `OnAlarmProviderModeChangedEvent.mode`, `AlarmProviderStatus.mode`) the worker always emits ALARMMGR/SUBTAG and `UNSPECIFIED` should be read as "unknown / not yet determined". Comment-only changes; no wire-format impact. NOTE: on this dev box the `csharp` protoc generator DOES emit proto leading comments into `Generated/MxaccessGateway.cs` `<summary>` XML doc (contrary to the brief's assumption), so the build regenerated `Generated/MxaccessGateway.cs` with the new doc comments only — diff is `///`-comment lines exclusively, zero code/wire/type changes. `dotnet build -f net10.0` succeeds with 0 warnings / 0 errors.
@@ -668,8 +668,15 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
} }
/// <summary> /// <summary>
/// Provider selection / current provider for the alarm feed. UNSPECIFIED on a /// Provider selection / current provider for the alarm feed. The zero value
/// SubscribeAlarmsCommand means auto: alarmmgr primary with subtag fallback. /// has two distinct meanings depending on the use site:
/// - As SubscribeAlarmsCommand.forced_mode, UNSPECIFIED means auto: alarmmgr
/// primary with subtag fallback.
/// - As a provenance value (OnAlarmTransitionEvent.source_provider,
/// ActiveAlarmSnapshot.source_provider, OnAlarmProviderModeChangedEvent.mode,
/// AlarmProviderStatus.mode), the worker always emits ALARMMGR or SUBTAG and
/// never UNSPECIFIED; clients should treat a UNSPECIFIED provenance value as
/// "unknown / not yet determined".
/// </summary> /// </summary>
public enum AlarmProviderMode { public enum AlarmProviderMode {
[pbr::OriginalName("ALARM_PROVIDER_MODE_UNSPECIFIED")] Unspecified = 0, [pbr::OriginalName("ALARM_PROVIDER_MODE_UNSPECIFIED")] Unspecified = 0,
@@ -26528,6 +26535,12 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
/// <summary>Field number for the "degraded" field.</summary> /// <summary>Field number for the "degraded" field.</summary>
public const int DegradedFieldNumber = 14; public const int DegradedFieldNumber = 14;
private bool degraded_; private bool degraded_;
/// <summary>
/// True when this snapshot came from the subtag-monitoring fallback rather
/// than the native alarmmgr provider — synthesized from data changes, reduced
/// fidelity (synthetic GUID, no native raise time). Mirrors
/// OnAlarmTransitionEvent.degraded.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Degraded { public bool Degraded {
@@ -26540,6 +26553,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
/// <summary>Field number for the "source_provider" field.</summary> /// <summary>Field number for the "source_provider" field.</summary>
public const int SourceProviderFieldNumber = 15; public const int SourceProviderFieldNumber = 15;
private global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode sourceProvider_ = global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode.Unspecified; private global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode sourceProvider_ = global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode.Unspecified;
/// <summary>
/// Which provider produced this snapshot. Mirrors
/// OnAlarmTransitionEvent.source_provider; always ALARMMGR or SUBTAG on the
/// wire (never UNSPECIFIED).
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode SourceProvider { public global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode SourceProvider {
@@ -315,8 +315,15 @@ message SubscribeBulkCommand {
repeated string tag_addresses = 2; repeated string tag_addresses = 2;
} }
// Provider selection / current provider for the alarm feed. UNSPECIFIED on a // Provider selection / current provider for the alarm feed. The zero value
// SubscribeAlarmsCommand means auto: alarmmgr primary with subtag fallback. // has two distinct meanings depending on the use site:
// - As SubscribeAlarmsCommand.forced_mode, UNSPECIFIED means auto: alarmmgr
// primary with subtag fallback.
// - As a provenance value (OnAlarmTransitionEvent.source_provider,
// ActiveAlarmSnapshot.source_provider, OnAlarmProviderModeChangedEvent.mode,
// AlarmProviderStatus.mode), the worker always emits ALARMMGR or SUBTAG and
// never UNSPECIFIED; clients should treat a UNSPECIFIED provenance value as
// "unknown / not yet determined".
enum AlarmProviderMode { enum AlarmProviderMode {
ALARM_PROVIDER_MODE_UNSPECIFIED = 0; ALARM_PROVIDER_MODE_UNSPECIFIED = 0;
ALARM_PROVIDER_MODE_ALARMMGR = 1; ALARM_PROVIDER_MODE_ALARMMGR = 1;
@@ -847,7 +854,14 @@ message ActiveAlarmSnapshot {
string operator_comment = 11; string operator_comment = 11;
MxValue current_value = 12; MxValue current_value = 12;
MxValue limit_value = 13; MxValue limit_value = 13;
// True when this snapshot came from the subtag-monitoring fallback rather
// than the native alarmmgr provider — synthesized from data changes, reduced
// fidelity (synthetic GUID, no native raise time). Mirrors
// OnAlarmTransitionEvent.degraded.
bool degraded = 14; bool degraded = 14;
// Which provider produced this snapshot. Mirrors
// OnAlarmTransitionEvent.source_provider; always ALARMMGR or SUBTAG on the
// wire (never UNSPECIFIED).
AlarmProviderMode source_provider = 15; AlarmProviderMode source_provider = 15;
} }
@@ -1427,4 +1427,120 @@ public sealed class ProtobufContractRoundTripTests
Assert.Single(parsed.ReadBulk.Results); Assert.Single(parsed.ReadBulk.Results);
Assert.True(parsed.ReadBulk.Results[0].WasCached); 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);
}
} }