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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
/// <summary>Verifies AcknowledgeAlarm rejects empty session_id.</summary>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarm_WithMissingSessionId_ThrowsInvalidArgument()
|
||||
{
|
||||
MxAccessGatewayService service = CreateService(new FakeSessionManager());
|
||||
|
||||
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
||||
async () => await service.AcknowledgeAlarm(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
AlarmFullReference = "Tank01.Level.HiHi",
|
||||
OperatorUser = "alice",
|
||||
},
|
||||
new TestServerCallContext()));
|
||||
|
||||
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeAlarm rejects empty alarm_full_reference.</summary>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument()
|
||||
{
|
||||
MxAccessGatewayService service = CreateService(new FakeSessionManager());
|
||||
|
||||
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
||||
async () => await service.AcknowledgeAlarm(
|
||||
new AcknowledgeAlarmRequest
|
||||
{
|
||||
SessionId = "session-1",
|
||||
OperatorUser = "alice",
|
||||
},
|
||||
new TestServerCallContext()));
|
||||
|
||||
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeAlarm returns OK with a worker-pending diagnostic for valid input.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies QueryActiveAlarms rejects empty session_id.</summary>
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarms_WithMissingSessionId_ThrowsInvalidArgument()
|
||||
{
|
||||
MxAccessGatewayService service = CreateService(new FakeSessionManager());
|
||||
|
||||
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
||||
async () => await service.QueryActiveAlarms(
|
||||
new QueryActiveAlarmsRequest(),
|
||||
new RecordingStreamWriter<ActiveAlarmSnapshot>(),
|
||||
new TestServerCallContext()));
|
||||
|
||||
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies QueryActiveAlarms streams zero snapshots until PR A.2 wires the worker walk.</summary>
|
||||
[Fact]
|
||||
public async Task QueryActiveAlarms_WithValidRequest_StreamsZeroSnapshots()
|
||||
{
|
||||
MxAccessGatewayService service = CreateService(new FakeSessionManager());
|
||||
RecordingStreamWriter<ActiveAlarmSnapshot> sink = new();
|
||||
|
||||
await service.QueryActiveAlarms(
|
||||
new QueryActiveAlarmsRequest
|
||||
{
|
||||
SessionId = "session-1",
|
||||
AlarmFilterPrefix = "Tank01.",
|
||||
},
|
||||
sink,
|
||||
new TestServerCallContext());
|
||||
|
||||
Assert.Empty(sink.Items);
|
||||
}
|
||||
|
||||
/// <summary>Verifies OpenSession advertises the alarm RPC capability strings.</summary>
|
||||
[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<T> : IServerStreamWriter<T>
|
||||
{
|
||||
public List<T> 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 = [];
|
||||
|
||||
Reference in New Issue
Block a user