From 9de2c0c43dfe72df32940e218d4911f99cb886ba Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 30 Apr 2026 21:20:57 -0400 Subject: [PATCH] gateway: AcknowledgeAlarm + QueryActiveAlarms handler tests (PR A.4) Nineteenth (final) PR of the alarms-over-gateway epic. Pins the public RPC handler contract added in PR A.3: - AcknowledgeAlarm rejects empty session_id and empty alarm_full_reference with InvalidArgument. - AcknowledgeAlarm with valid input returns OK and a worker-pending diagnostic so clients see a successful round-trip even before A.2's worker dispatch lands. - QueryActiveAlarms rejects empty session_id with InvalidArgument. - QueryActiveAlarms with valid input streams zero snapshots until PR A.2 wires the worker-side QueryActiveAlarmsCommand (filter-prefix passthrough verified at the proto layer). - OpenSession advertises both new RPC capability strings (unary-acknowledge-alarm, server-stream-active-alarms) so client capability negotiation lights up against the contract surface. Closes Track A's gateway-side surface. The remaining worker ConditionRefresh walk + integration parity-rig validation lands during dev-rig hardware validation alongside PR A.2's COM-side alarm subscription pin. Tests: 279 passed (was 273; 6 new). Per-handler integration tests land alongside the dev-rig validation when the worker walks the real MxAccess active-alarm collection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Grpc/MxAccessGatewayServiceTests.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs index e87a143..b3654a1 100644 --- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs +++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs @@ -229,6 +229,124 @@ public sealed class MxAccessGatewayServiceTests Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); } + // ===== PR A.4 — AcknowledgeAlarm + QueryActiveAlarms handler contract ===== + // + // Worker-side dispatch (translating AcknowledgeAlarm to MxAccess Acknowledge, + // walking the active-alarm collection for QueryActiveAlarms) is gated on PR + // A.2's dev-rig validation. These tests pin the public surface so the worker + // wiring lands without changing observable behaviour for clients. + + /// Verifies AcknowledgeAlarm rejects empty session_id. + [Fact] + public async Task AcknowledgeAlarm_WithMissingSessionId_ThrowsInvalidArgument() + { + MxAccessGatewayService service = CreateService(new FakeSessionManager()); + + RpcException exception = await Assert.ThrowsAsync( + async () => await service.AcknowledgeAlarm( + new AcknowledgeAlarmRequest + { + AlarmFullReference = "Tank01.Level.HiHi", + OperatorUser = "alice", + }, + new TestServerCallContext())); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + } + + /// Verifies AcknowledgeAlarm rejects empty alarm_full_reference. + [Fact] + public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument() + { + MxAccessGatewayService service = CreateService(new FakeSessionManager()); + + RpcException exception = await Assert.ThrowsAsync( + async () => await service.AcknowledgeAlarm( + new AcknowledgeAlarmRequest + { + SessionId = "session-1", + OperatorUser = "alice", + }, + new TestServerCallContext())); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + } + + /// Verifies AcknowledgeAlarm returns OK with a worker-pending diagnostic for valid input. + [Fact] + public async Task AcknowledgeAlarm_WithValidRequest_ReturnsOkWithWorkerPendingDiagnostic() + { + MxAccessGatewayService service = CreateService(new FakeSessionManager()); + + AcknowledgeAlarmReply reply = await service.AcknowledgeAlarm( + new AcknowledgeAlarmRequest + { + SessionId = "session-1", + ClientCorrelationId = "corr-1", + AlarmFullReference = "Tank01.Level.HiHi", + Comment = "investigating", + OperatorUser = "alice", + }, + new TestServerCallContext()); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal("session-1", reply.SessionId); + Assert.Equal("corr-1", reply.CorrelationId); + Assert.Contains("worker", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase); + } + + /// Verifies QueryActiveAlarms rejects empty session_id. + [Fact] + public async Task QueryActiveAlarms_WithMissingSessionId_ThrowsInvalidArgument() + { + MxAccessGatewayService service = CreateService(new FakeSessionManager()); + + RpcException exception = await Assert.ThrowsAsync( + async () => await service.QueryActiveAlarms( + new QueryActiveAlarmsRequest(), + new RecordingStreamWriter(), + new TestServerCallContext())); + + Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); + } + + /// Verifies QueryActiveAlarms streams zero snapshots until PR A.2 wires the worker walk. + [Fact] + public async Task QueryActiveAlarms_WithValidRequest_StreamsZeroSnapshots() + { + MxAccessGatewayService service = CreateService(new FakeSessionManager()); + RecordingStreamWriter sink = new(); + + await service.QueryActiveAlarms( + new QueryActiveAlarmsRequest + { + SessionId = "session-1", + AlarmFilterPrefix = "Tank01.", + }, + sink, + new TestServerCallContext()); + + Assert.Empty(sink.Items); + } + + /// Verifies OpenSession advertises the alarm RPC capability strings. + [Fact] + public async Task OpenSession_AdvertisesAlarmRpcCapabilities() + { + FakeSessionManager sessionManager = new(); + GatewayRequestIdentityAccessor identityAccessor = new(); + MxAccessGatewayService service = CreateService(sessionManager, identityAccessor); + + using IDisposable identityScope = identityAccessor.Push(CreateIdentity()); + + OpenSessionReply reply = await service.OpenSession( + new OpenSessionRequest(), + new TestServerCallContext()); + + Assert.Contains("unary-acknowledge-alarm", reply.Capabilities); + Assert.Contains("server-stream-active-alarms", reply.Capabilities); + } + private static MxAccessGatewayService CreateService( FakeSessionManager sessionManager, IGatewayRequestIdentityAccessor? identityAccessor = null, @@ -549,6 +667,18 @@ public sealed class MxAccessGatewayServiceTests } } + private sealed class RecordingStreamWriter : IServerStreamWriter + { + public List Items { get; } = new(); + public WriteOptions? WriteOptions { get; set; } + + public Task WriteAsync(T message) + { + Items.Add(message); + return Task.CompletedTask; + } + } + private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext { private readonly Metadata requestHeaders = [];