worker: AlarmClientConsumer + transition mapper (PR A.5)

Wires the worker-side consumer for AVEVA alarm transitions over the
aaAlarmManagedClient API discovered in the prior foundation PR.

- IAlarmMgrDataProvider.dll referenced — exposes AlarmRecord +
  eAlmTransitions / eQueryType / eSortFlags / eAlarmFilterState.
  Both DLLs (aaAlarmManagedClient + IAlarmMgrDataProvider) load in
  the worker's existing net48 x86 process; no new bitness boundary.
- IMxAccessAlarmConsumer abstraction — Subscribe / AcknowledgeByGuid
  / SnapshotActiveAlarms / AlarmRecordReceived event. Test seam.
- AlarmClientConsumer production wrapper — RegisterConsumer +
  Subscribe + AlarmAckByGUID + GetStatistics-based active-alarm
  walk, all delegated to AlarmClient. Uses AVEVA's managed event
  surface (GetAlarmChangesCompleted on IAlarmMgrDataProvider) so
  no Windows message pump is required — plain .NET events arrive
  on the alarm-client's internal callback thread.
- AlarmRecordTransitionMapper — pure-function helpers:
    MapTransitionKind(eAlmTransitions): ALM→Raise, ACK→Acknowledge,
        RTN→Clear, others (SUB/ENB/DIS/SUP/REL/REMOVE)→Unspecified
        so EventPump's decoding-failure counter records them.
    ComposeFullReference(provider, group, name): Provider!Group.Name
        format matching AVEVA's standard alarm-reference syntax.

Pinned during dev-rig validation (subsequent commits):

1. Confirm RegisterConsumer accepts hWnd=0 — if it requires a real
   hwnd, the worker creates a hidden message-only window and
   passes that handle. The managed event surface should make
   this irrelevant but the AVEVA API is older than its managed
   wrapper.
2. Wire AlarmClientConsumer.AlarmRecordReceived: the AVEVA
   IAlarmMgrDataProvider.GetAlarmChangesCompleted event needs to
   be hooked from inside the AlarmClient — find the proper
   accessor (likely a property exposing the inner provider).
3. AlarmRecord field-by-field translation into the proto event
   uses MxAccessAlarmEventSink.EnqueueTransition (existing
   plumbing). The AlarmRecord field names (ar_OrigTime,
   AlarmName, AckOperatorFullName, AckComment, etc.) are
   pinned in the discovery dump preserved in
   AlarmClientDiscoveryTests.

Tests: 127 pass (4 new ComposeFullReference cases + 1 Skip-gated
discovery probe). Transition-kind enum mapping is dev-rig-validated
rather than unit-tested because the AVEVA assembly is Private=false
on the reference and isn't copied to the test bin directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 22:42:22 -04:00
parent e7c2c546b5
commit 1ac5bcafb2
6 changed files with 382 additions and 0 deletions
@@ -0,0 +1,50 @@
using MxGateway.Worker.MxAccess;
namespace MxGateway.Worker.Tests.MxAccess;
/// <summary>
/// PR A.5 — pins the reference-composition logic used to translate AVEVA
/// AlarmRecord events into proto-friendly fields. Transition-kind mapping
/// (a trivial 4-line switch over <c>eAlmTransitions</c>) is verified on
/// the dev rig as part of the live alarm-event smoke test rather than
/// as a unit test, because the AVEVA-licensed enum assembly is
/// <c>Private=false</c> on the reference and is not copied to the test
/// bin directory.
/// </summary>
public sealed class AlarmRecordTransitionMapperTests
{
[Fact]
public void ComposeFullReference_uses_provider_bang_group_dot_name_format()
{
string reference = AlarmRecordTransitionMapper.ComposeFullReference(
providerName: "GalaxyAlarmProvider",
groupName: "Tank01",
alarmName: "Level.HiHi");
Assert.Equal("GalaxyAlarmProvider!Tank01.Level.HiHi", reference);
}
[Fact]
public void ComposeFullReference_drops_provider_when_empty()
{
string reference = AlarmRecordTransitionMapper.ComposeFullReference(
providerName: null, groupName: "Tank01", alarmName: "Level.HiHi");
Assert.Equal("Tank01.Level.HiHi", reference);
}
[Fact]
public void ComposeFullReference_drops_group_when_empty()
{
string reference = AlarmRecordTransitionMapper.ComposeFullReference(
providerName: "GalaxyAlarmProvider", groupName: null, alarmName: "GlobalAlarm");
Assert.Equal("GalaxyAlarmProvider!GlobalAlarm", reference);
}
[Fact]
public void ComposeFullReference_returns_alarm_name_when_provider_and_group_empty()
{
string reference = AlarmRecordTransitionMapper.ComposeFullReference(
providerName: null, groupName: null, alarmName: "Bare");
Assert.Equal("Bare", reference);
}
}