Exercise the alarm subcommands in the client e2e matrix

Add an opt-in alarm phase (-VerifyAlarms) to run-client-e2e-tests.ps1:
each of the five client CLIs runs stream-alarms (asserting at least one
AlarmFeedMessage) and acknowledge-alarm against the gateway's central
alarm monitor. Both RPCs are session-less. -AlarmReference and
-AlarmStreamMax tune the phase; GatewayTesting.md documents it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-21 19:47:20 -04:00
parent 120cd0b1b6
commit 3e22285f09
2 changed files with 152 additions and 1 deletions
+14
View File
@@ -293,6 +293,18 @@ path and writes a JSON report under `artifacts/e2e/`:
write command is rejected — e.g. against a gateway whose worker predates
write support (`MxAccessCommandExecutor` returning `InvalidRequest` for
`Write`/`Write2`/`WriteSecured`/`WriteSecured2`).
8. **Alarm feed + acknowledge***opt-in (`-VerifyAlarms`).* Runs after the
stream phase. Exercises the two session-less alarm subcommands against the
gateway's central alarm monitor: `stream-alarms` reads a bounded slice of
the feed (`-AlarmStreamMax`, default 1 — the feed's first message always
arrives immediately, whereas later ones depend on live transitions) and
asserts at least one `AlarmFeedMessage`; `acknowledge-alarm` acknowledges
`-AlarmReference` (default `Galaxy!TestArea.TestMachine_001.TestAlarm001`)
and asserts the RPC round-trips. The native ack outcome is not asserted —
it depends on whether that alarm is currently active.
It is opt-in because it depends on the gateway's central alarm monitor
being enabled (`MxGateway:Alarms:Enabled`) and a live alarm provider.
Each client CLI is driven through one long-lived `batch` process. Every CLI
exposes a `batch` subcommand: a process that reads one command line from stdin,
@@ -329,6 +341,8 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipB
# Write round-trip (opt-in): point at a writable scalar attribute and its
# value type.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyWrite -WriteAttribute TestChangingInt -WriteType int32
# Alarm feed + acknowledge (opt-in): needs MxGateway:Alarms:Enabled on the gateway.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyAlarms -AlarmReference "Galaxy!TestArea.TestMachine_001.TestAlarm001"
# Auth rejection: also assert an insufficient-scope key is denied.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -RejectScopeApiKeyEnv MXGATEWAY_READONLY_API_KEY
# Run all five clients concurrently as isolated child processes.
+138 -1
View File
@@ -7,7 +7,9 @@ Drives the .NET, Go, Rust, Python, and Java client CLIs against a running
gateway + worker. For each language the script exercises session open/close,
register, bulk subscribe/unsubscribe, per-tag add-item/advise, event
streaming, a write round-trip with value assertion, error-path (parity)
checks, and API-key auth rejection.
checks, and API-key auth rejection. With -VerifyAlarms it also exercises the
session-less stream-alarms and acknowledge-alarm subcommands against the
gateway's central alarm monitor.
Each client CLI is driven through one long-lived `batch` process: the harness
writes one command line to its stdin and reads the JSON result back, so the
@@ -60,6 +62,18 @@ param(
[string]$WriteType = "int32",
[int]$WriteValueBase = 424200,
[int]$WriteEchoMaxEvents = 200,
# Alarm feed + acknowledge coverage. Opt-in because it depends on the
# gateway's central alarm monitor being enabled (MxGateway:Alarms:Enabled)
# and a live alarm provider: stream-alarms reads the monitor's snapshot and
# acknowledge-alarm acknowledges -AlarmReference. Both RPCs are session-less
# — they exercise the gateway's always-on monitor, not a client session.
[switch]$VerifyAlarms,
[string]$AlarmReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
# Messages to read from the central alarm feed. 1 is enough to confirm the
# subcommand round-trips: the feed's first message (an active-alarm
# snapshot, or snapshot-complete when no alarms are active) always arrives
# immediately, whereas later messages depend on live alarm transitions.
[int]$AlarmStreamMax = 1,
# Error-path (parity) checks.
[switch]$SkipParity,
# API-key auth rejection checks.
@@ -118,6 +132,10 @@ if ($WriteEchoMaxEvents -lt 1) {
throw "WriteEchoMaxEvents must be greater than zero."
}
if ($AlarmStreamMax -lt 1) {
throw "AlarmStreamMax must be greater than zero."
}
foreach ($client in $Clients) {
if ($validClients -notcontains $client) {
throw "Unsupported client '$client'. Supported clients: $($validClients -join ', ')."
@@ -327,6 +345,25 @@ function Get-StreamEvents {
}
}
# Counts the messages in a stream-alarms reply. The CLIs shape the aggregate
# JSON differently: .NET nests them under `alarms`, Rust under `messages` with
# a `messageCount`, Python under `messages`; Go and Java emit one AlarmFeedMessage
# object per line (Read-JsonObject collapses NDJSON into a bare array).
function Get-AlarmMessageCount {
param(
[string]$Client,
[object]$Json
)
switch ($Client) {
"dotnet" { return @($Json.alarms).Count }
"go" { return @($Json).Count }
"rust" { return [int]$Json.messageCount }
"python" { return @($Json.messages).Count }
"java" { return @($Json).Count }
}
}
function Get-PropertyValue {
param(
[object]$Object,
@@ -564,6 +601,13 @@ function Get-ClientCommand {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents")
} elseif ($Operation -eq "stream-alarms") {
$arguments += @("--max-events", "$streamMaxEvents")
if ($Values.ContainsKey("filterPrefix")) { $arguments += @("--filter-prefix", $Values.filterPrefix) }
} elseif ($Operation -eq "acknowledge-alarm") {
$arguments += @("--reference", $Values.alarmReference)
if ($Values.ContainsKey("comment")) { $arguments += @("--comment", $Values.comment) }
if ($Values.ContainsKey("operator")) { $arguments += @("--operator", $Values.operator) }
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
@@ -600,6 +644,13 @@ function Get-ClientCommand {
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handle", "$($Values.itemHandle)", "-type", $Values.valueType, "-value", $Values.value)
} elseif ($Operation -eq "stream-events") {
$arguments += @("-session-id", $Values.sessionId, "-limit", "$streamMaxEvents")
} elseif ($Operation -eq "stream-alarms") {
$arguments += @("-limit", "$streamMaxEvents")
if ($Values.ContainsKey("filterPrefix")) { $arguments += @("-filter-prefix", $Values.filterPrefix) }
} elseif ($Operation -eq "acknowledge-alarm") {
$arguments += @("-reference", $Values.alarmReference)
if ($Values.ContainsKey("comment")) { $arguments += @("-comment", $Values.comment) }
if ($Values.ContainsKey("operator")) { $arguments += @("-operator", $Values.operator) }
} elseif ($Operation -eq "close-session") {
$arguments += @("-session-id", $Values.sessionId)
}
@@ -637,6 +688,13 @@ function Get-ClientCommand {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--value-type", $Values.valueType, "--value", $Values.value)
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents")
} elseif ($Operation -eq "stream-alarms") {
$arguments += @("--max-events", "$streamMaxEvents")
if ($Values.ContainsKey("filterPrefix")) { $arguments += @("--filter-prefix", $Values.filterPrefix) }
} elseif ($Operation -eq "acknowledge-alarm") {
$arguments += @("--reference", $Values.alarmReference)
if ($Values.ContainsKey("comment")) { $arguments += @("--comment", $Values.comment) }
if ($Values.ContainsKey("operator")) { $arguments += @("--operator", $Values.operator) }
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
@@ -673,6 +731,13 @@ function Get-ClientCommand {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents", "--timeout", "$pythonStreamTimeout")
} elseif ($Operation -eq "stream-alarms") {
$arguments += @("--max-messages", "$streamMaxEvents", "--timeout", "$pythonStreamTimeout")
if ($Values.ContainsKey("filterPrefix")) { $arguments += @("--filter-prefix", $Values.filterPrefix) }
} elseif ($Operation -eq "acknowledge-alarm") {
$arguments += @("--reference", $Values.alarmReference)
if ($Values.ContainsKey("comment")) { $arguments += @("--comment", $Values.comment) }
if ($Values.ContainsKey("operator")) { $arguments += @("--operator", $Values.operator) }
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
@@ -712,6 +777,13 @@ function Get-ClientCommand {
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
} elseif ($Operation -eq "stream-events") {
$cliArgs += @("--session-id", $Values.sessionId, "--limit", "$streamMaxEvents")
} elseif ($Operation -eq "stream-alarms") {
$cliArgs += @("--limit", "$streamMaxEvents")
if ($Values.ContainsKey("filterPrefix")) { $cliArgs += @("--filter-prefix", $Values.filterPrefix) }
} elseif ($Operation -eq "acknowledge-alarm") {
$cliArgs += @("--reference", $Values.alarmReference)
if ($Values.ContainsKey("comment")) { $cliArgs += @("--comment", $Values.comment) }
if ($Values.ContainsKey("operator")) { $cliArgs += @("--operator", $Values.operator) }
} elseif ($Operation -eq "close-session") {
$cliArgs += @("--session-id", $Values.sessionId)
}
@@ -801,6 +873,36 @@ function Get-DryRunReply {
default { return [pscustomobject]@{ events = $events } }
}
}
"stream-alarms" {
# Synthesize an active-alarm snapshot followed by the
# snapshot-complete sentinel. The reply is shaped per client:
# Go and Java emit one message object per line (Read-JsonObject
# collapses NDJSON to a bare array), Rust aggregates under
# `messages` with a `messageCount`, Python under `messages`, and
# .NET under `alarms`.
$activeAlarm = [pscustomobject]@{
activeAlarm = [pscustomobject]@{
alarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001"
currentState = "ALARM_CONDITION_STATE_ACTIVE"
severity = 500
}
}
$snapshotComplete = [pscustomobject]@{ snapshotComplete = $true }
$messages = @($activeAlarm, $snapshotComplete)
switch ($Client) {
"go" { return ,$messages }
"java" { return ,$messages }
"rust" { return [pscustomobject]@{ messageCount = $messages.Count; messages = $messages } }
"dotnet" { return [pscustomobject]@{ alarms = $messages } }
default { return [pscustomobject]@{ messages = $messages } }
}
}
"acknowledge-alarm" {
return [pscustomobject]@{
rawReply = [pscustomobject]@{ hresult = 0; diagnosticMessage = "dry-run ack" }
reply = [pscustomobject]@{ hresult = 0 }
}
}
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
}
}
@@ -1053,6 +1155,7 @@ function Invoke-ClientFlow {
addedItems = @()
eventCount = 0
write = $null
alarms = $null
parity = @()
auth = @()
closed = $false
@@ -1285,6 +1388,35 @@ function Invoke-ClientFlow {
}
}
# --- Alarm feed + acknowledge -------------------------------------
# Session-less RPCs against the gateway's always-on central alarm
# monitor. Opt-in (-VerifyAlarms) because it needs the monitor enabled
# (MxGateway:Alarms:Enabled) and a live alarm provider.
if ($VerifyAlarms) {
$alarmStreamJson = Invoke-ClientOperation -Client $Client -Operation "stream-alarms" -Values @{
maxEvents = $AlarmStreamMax
}
$alarmMessageCount = Get-AlarmMessageCount -Client $Client -Json $alarmStreamJson
if ($alarmMessageCount -lt 1) {
throw "The $Client stream-alarms command returned no alarm-feed messages."
}
# The acknowledge round-trips against the central monitor; the
# native ack outcome depends on whether the referenced alarm is
# currently active, so only the RPC's success is asserted here.
Invoke-ClientOperation -Client $Client -Operation "acknowledge-alarm" -Values @{
alarmReference = $AlarmReference
comment = "e2e-matrix"
operator = "mxgw-e2e"
} | Out-Null
$clientResult.alarms = [ordered]@{
streamMessageCount = $alarmMessageCount
acknowledgeReference = $AlarmReference
acknowledged = $true
}
}
# --- Error-path (parity) checks -----------------------------------
# MXAccess parity: an invalid item handle and an unknown session must
# both be rejected rather than silently succeeding.
@@ -1391,6 +1523,8 @@ function Get-ChildArgumentList {
"-WriteType", $WriteType,
"-WriteValueBase", "$WriteValueBase",
"-WriteEchoMaxEvents", "$WriteEchoMaxEvents",
"-AlarmReference", $AlarmReference,
"-AlarmStreamMax", "$AlarmStreamMax",
"-ReportPath", $ChildReportPath,
"-EmitReport"
)
@@ -1400,6 +1534,7 @@ function Get-ChildArgumentList {
if ($SkipStream) { $childArgs += "-SkipStream" }
if ($SkipBulk) { $childArgs += "-SkipBulk" }
if ($VerifyWrite) { $childArgs += "-VerifyWrite" }
if ($VerifyAlarms) { $childArgs += "-VerifyAlarms" }
if ($SkipParity) { $childArgs += "-SkipParity" }
if ($SkipAuth) { $childArgs += "-SkipAuth" }
if ($DryRun) { $childArgs += "-DryRun" }
@@ -1479,6 +1614,7 @@ if ($Parallel -and $Clients.Count -gt 1) {
skipStream = [bool]$SkipStream
skipBulk = [bool]$SkipBulk
verifyWrite = [bool]$VerifyWrite
verifyAlarms = [bool]$VerifyAlarms
skipParity = [bool]$SkipParity
skipAuth = [bool]$SkipAuth
writeAttribute = $WriteAttribute
@@ -1540,6 +1676,7 @@ $run = [ordered]@{
skipStream = [bool]$SkipStream
skipBulk = [bool]$SkipBulk
verifyWrite = [bool]$VerifyWrite
verifyAlarms = [bool]$VerifyAlarms
skipParity = [bool]$SkipParity
skipAuth = [bool]$SkipAuth
writeAttribute = $WriteAttribute