contracts: round-trip degraded provenance/watch-list/mode-changed; proto doc (Contracts-018,019)
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user