# Alarm Probe Findings `WnWrapAlarmConsumer` rested on two assumptions that no unit test can settle, because both are properties of AVEVA's alarm provider rather than of our code. The second is now settled and one half of the first is; the questions are stated here as they were asked: 1. **GUID identity.** The snapshot diff in `ComputeTransitions` keys on the alarm record's `GUID`. If wnwrap mints a fresh GUID when an alarm changes state, a single `UNACK_ALM → ACK_ALM` transition reads as one alarm disappearing and a different one appearing — a spurious clear plus a spurious raise on every acknowledge. 2. **`ALARM_RECORDS/@COUNT` semantics.** `IsTruncatedFetch` treats a reply holding exactly `maxAlmCnt` records as truncated, because `GetXmlCurrentAlarms2` exposes no explicit "more available" flag. If the reply's `COUNT` attribute carries the *total* active count rather than the records-in-reply count, truncation detection can become exact instead of conservative, and the bounded staleness `ApplySnapshotUpdate` accepts goes away. This document records what live probe runs against the dev rig (`DESKTOP-6JL3KKO`, 2026-08-17 and 2026-08-18) could and could not establish, so the next attempt starts from the blocker rather than rediscovering it. ## Outcome | Question | Status | |---|---| | GUID stable across polls and `ALM → RTN` | Answered — yes (2026-05-01 capture in `AlarmClientDiscovery.md`, reconfirmed 2026-08-18) | | GUID stable across clear-then-re-raise | Answered 2026-08-18 — **no**, a re-raise mints a new GUID | | GUID stable across `UNACK → ACK` | **Open** — the rig cannot be driven into an acknowledged state at all | | `COUNT` = total active vs records-in-reply under a capped fetch | Answered 2026-08-18 — **records in the reply** | The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The 2026-08-18 run cleared that blocker with `AuthenticateUser` + `WriteSecured` and answered two of the three questions; the acknowledge leg is now blocked on something narrower and different, described in "Second attempt". ## First attempt (2026-08-17): plain `Write` ### Why The Rig Could Not Raise An Alarm The rig is otherwise healthy, which is what makes the blocker specific rather than a general "nothing works": - `aaEngine`, `alarmmgr`, `NmxSvc`, and `wnwrapServerEx` are all running. - `TestArea` (area of `TestMachine_001`…`_003`) and the objects themselves are deployed (`deployed_version` non-null in the `ZB` Galaxy Repository) and on scan — the probe's advised `ScanState` subtags report true, and every advised alarm attribute delivers an initial value, so the MXAccess read path is live. - The wnwrap consumer subscribes cleanly: `InitializeConsumer`, `RegisterConsumer`, `Subscribe(\\DESKTOP-6JL3KKO\Galaxy!TestArea)`, and `SetXmlAlarmQuery` all return 0, and `GetXmlCurrentAlarms2` returns well-formed XML on every poll. What fails is the *write* that would set the alarm condition. Every `Write` to the alarm UDAs completes with a security failure: ``` WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=] ``` The status comes back from the responding automation object, not from the proxy, so the request reaches the engine and the engine refuses it. The advised value confirms the refusal is total rather than transient: neither the alarm UDA nor its `.InAlarm` /`.Acked` subtags report any change after a write attempt, across six write attempts in one session (raise, clear, re-raise, cleanup). The attributes carry a security classification that a plain `Write` cannot satisfy. The 2026-05-01 capture that answered the `ALM → RTN` leg did not hit this, because the alarm condition was driven from *inside* the engine by a System Platform script rather than from an external MXAccess client. That script is not running now, and the values sat idle for the whole probe session. ### Unblocking Any one of these makes both questions answerable, in rough order of cost. The second one is what the 2026-08-18 run did, and it worked: - Re-enable the System Platform script that flips `TestMachine_001.TestAlarm001` (referenced throughout `AlarmClientDiscovery.md`). It writes from inside the engine, so the attribute's security classification does not apply. - Drive the write through `AuthenticateUser` + `WriteSecured` with a Galaxy account permitted on that classification. The worker already implements both verbs; the probe used plain `Write`, which is the wrong verb for a secured attribute. - Reclassify the test UDAs to free access in the IDE and redeploy `TestMachine_001`…`_003`. Three separate objects are wired to the same alarm UDA name, so once writes land, a `maxAlmCnt` of 1 or 2 forces truncation against three active alarms and answers the `COUNT` question in the same run. ### Evidence Snapshot payload, identical at every cap (1, 2, and 1024) and at every poll across the ~100-second session: ```xml ``` Two things follow from the empty case alone. `COUNT` is present on the root element in every reply, so the attribute exists as a candidate signal rather than something wnwrap omits. And `COUNT` agrees with the element count here — but trivially, since both are zero, which is exactly the case that cannot discriminate the two hypotheses. The probe used for the run was a throwaway file in the windev CI clone (`C:\build\mxaccessgw-ci`), deleted afterwards; the clone is back to a clean tree at `origin/main`. Nothing in this repository changed to run it. The reusable, Skip-gated harness it was modelled on is `src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs`. ## Second attempt (2026-08-18): secured-write verb The findings above named the fix in their own "Unblocking" list, and it holds: `AuthenticateUser` + `WriteSecured` raises the alarms that plain `Write` could not touch. The rig's alarm attributes are not unreachable — they are secured, and the 2026-08-17 probe used the wrong verb. ### Method Three throwaway probes in the windev CI clone (`C:\build\mxaccessgw-ci`, detached at `ab3ff16`), each an x86 net48 xUnit fact running on a dedicated STA with a `PeekMessage`/`DispatchMessage` pump so MXAccess events deliver: 1. secured write + both GUID legs + the capped-fetch question, 2. the acknowledge surface, exhaustively, 3. the acknowledge question re-asked through an independent observer. All three used `LMXProxyServerClass` directly for the write path and a `wwAlarmConsumerClass` pair — a reader with `SetXmlAlarmQuery` applied and an ack-only consumer without it — mirroring `WnWrapAlarmConsumer`'s own two-consumer arrangement. The subscription was `\\DESKTOP-6JL3KKO\Galaxy!TestArea`, matching the 2026-08-17 run. `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER` / `_PASSWORD` are set at neither machine nor user scope on the box, so the probes used the default rig identity, `Administrator` with an empty password. All three files were deleted afterwards and the clone is a clean tree. ### The secured write lands Same session, same item, one after the other — the control reproduces 2026-08-17 exactly and the secured verb succeeds: ``` Write 'TestMachine_001.TestAlarm001' <- True WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=] observed 'TestMachine_001.TestAlarm001' = False AuthenticateUser -> userId=1 WriteSecured 'TestMachine_001.TestAlarm001' <- True currentUserId=1 verifierUserId=0 DATA-CHANGE hItem=1 value=True quality=192 WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=-1 category=Ok detectedBy=RespondingAutomationObject detail=0 text=] observed 'TestMachine_001.TestAlarm001' = True ``` `AuthenticateUser("Administrator", "")` resolves to user id 1, matching the value `WorkerLiveMxAccessSmokeTests` records. No verifier is needed: `verifierUserId=0` is accepted. All six subsequent raise/clear writes across the three objects behaved the same way, so the path is reliable rather than a one-off. ### Q2 — `ALARM_RECORDS/@COUNT` reports the reply, not the total With all three `TestMachine_00{1,2,3}.TestAlarm001` alarms active, the same subscription fetched at three caps back to back: ``` FETCH three-active cap=1024 ALARM_RECORDS/@COUNT=3 elementCount=3 FETCH three-active cap=1 ALARM_RECORDS/@COUNT=1 elementCount=1 FETCH three-active cap=2 ALARM_RECORDS/@COUNT=2 elementCount=2 ``` `@COUNT` tracked the cap, not the population. It equals the element count in every reply, including the two that are provably truncated — the galaxy held three active alarms while `@COUNT` read 1 and 2. The attribute therefore carries no "more available" information, and the hypothesis that it might report the total is refuted rather than merely untested. `IsTruncatedFetch` stays exactly as written, and this is now settled rather than deferred: there is no exact truncation signal to switch to. Tightening it is **not** a future task. The conservative rule keeps its original justification — at the cap, treating a complete fetch as truncated costs one poll of staleness, while treating a truncated fetch as complete broadcasts clears for every alarm past the cap. ### Q1 — clear-then-re-raise mints a new GUID `TestMachine_001.TestAlarm001` was raised, cleared, and re-raised through `WriteSecured`, with a full snapshot fetch after each leg: ``` after-raise TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_ALM VALUE=true after-clear-001 TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_RTN VALUE=false after-reraise TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC29CA51E STATE=UNACK_ALM VALUE=true ``` Two facts, one confirming and one new. The `ALM → RTN` leg holds its GUID, reconfirming the 2026-05-01 capture against a write-driven rather than script-driven transition. The re-raise does not: the returned record keeps the old GUID and the new active condition arrives as a separate record under a fresh one. That is the correct reading for `ComputeTransitions` rather than a problem for it. The old GUID leaving the active set is a real Clear and the new GUID is a real Raise, because they are two alarm instances. The diff needed no change. ### Q1 — the acknowledge leg is still unobserved, for a new reason The blocker moved. The rig can now be driven into an alarm; it cannot be driven out of `UNACK_ALM`. Every acknowledge surface `wwAlarmConsumerClass` exposes was tried against a freshly raised alarm, each followed by 16 seconds of polling: | Attempt | Result | |---|---| | `AlarmAckByName` 6-arg on the ack-only consumer, node = machine | `rc=0`, state unchanged | | same, node empty | `rc=0`, state unchanged | | same, name as `Galaxy!TestArea.TestMachine_001.TestAlarm001` | `rc=0`, state unchanged | | same, operator name `Administrator` | `rc=0`, state unchanged | | `AlarmAckByName` 6-arg on the reader consumer (`SetXmlAlarmQuery` applied) | `rc=-55`, state unchanged | | `AlarmAckByName` 8-arg (v2) on the ack-only consumer | `rc=0`, state unchanged | The `rc=-55` on the `SetXmlAlarmQuery`-applied consumer reproduces the 2026-05-01 finding that motivated the two-consumer split, so the ack-only consumer was correctly provisioned; the `rc=0` returns are the ones that go nowhere. A third probe removed the last doubt by watching the alarm extension's own `.Acked` attribute over MXAccess — an observer entirely independent of the wnwrap snapshot: ``` VALUES after-raise: 'TestMachine_001.TestAlarm001.Acked' = False AlarmAckByName -> rc=0 VALUES after-wnwrap-ack: 'TestMachine_001.TestAlarm001.Acked' = False XML after-wnwrap-ack: STATE=UNACK_ALM OPRNAME= OPRNODE= ``` Nothing moves: not the extension attribute, not the snapshot `STATE`, not `OPERATOR_NAME`. `rc=0` from wnwrap means the call was accepted, not that an acknowledgement was applied. Acknowledging by writing the extension attribute directly is not an alternative — the attribute is not writable, and says so with an operational rather than a security failure, for both verbs: ``` WriteSecured 'TestMachine_001.TestAlarm001.Acked' <- true WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007] Write 'TestMachine_001.TestAlarm001.Acked' <- true WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007] ``` `detail=1007` from the responding automation object, unchanged by authentication, is a read-only attribute rather than a permission refusal — the `1008` `SecurityError` the alarm UDA itself returns is what a permission refusal looks like on this rig. So the ack has no MXAccess-side entry point and the wnwrap-side entry point is inert. #### Remaining unblock paths for the acknowledge leg - Acknowledge from the System Platform side — the IDE's alarm client, InTouch, or an ArchestrA graphic bound to the alarm — and watch the snapshot from a running probe. This proves whether wnwrap's `STATE` ever reports `ACK_ALM` at all, which is the actual question; the ack API being inert may be a wnwrap defect layered on top of a snapshot that would report the state correctly. - Check whether `alarmmgr` on this rig is configured with an alarm-acknowledgement security requirement that the wnwrap consumer, which passes an operator *name* string and no authenticated identity, cannot meet. If so, ack over wnwrap is not merely untested here but unavailable by configuration, and the gateway's `AcknowledgeByName` path needs the same treatment on any customer galaxy configured that way. - If neither lands, the acknowledge leg stays assumed. It is worth restating that this is a documentation gap, not a correctness one: a re-minted GUID on acknowledge would produce a spurious Clear plus a spurious Raise, which is the same shape the now-observed re-raise behaviour produces and which `ComputeTransitions` already handles as two instances. ### Rig state left behind The three `TestMachine_00{1,2,3}.TestAlarm001` UDAs are back to `false` and their `.InAlarm` subtags read `false`, but each leaves a `UNACK_RTN` record in the wnwrap snapshot, since nothing can acknowledge them away. `SnapshotActiveAlarms` counts only `UNACK_ALM` and `ACK_ALM` as active, so these are inert for the gateway; they will clear on the next `alarmmgr` restart.