The .NET and Java stream-events commands handed the raw ReplayGap sentinel
MxEvent to their protobuf JSON formatters (Java text mode printed
'0 MX_EVENT_FAMILY_UNSPECIFIED'), while the Go/Python/Rust CLIs already emit
the typed row (CLI-35/36). Both now branch on the sentinel: Java text mode
prints 'REPLAY_GAP requested_after=<n> oldest_available=<n>' and JSON mode a
hand-built {"replayGap":{...}} line via nextItem()/isReplayGap(); the .NET
CLI emits the same hand-built row in jsonl/text (its text mode is
JSON-per-line) and inside the --json events array. Rows are hand-built so
the cursors are JSON numbers like the other three CLIs, not the proto3 JSON
mapping's quoted uint64 strings — the CrossLanguageSmokeMatrix divergence
table collapses to a single converged contract.
Tests: .NET MxGatewayClientCliTests 35/35 (new RendersReplayGapAsTypedRow
covers jsonl + aggregate); Java gradle test 52/52 (new
streamEventsRendersReplayGapAsTypedRow covers --json + text over the
in-process harness). No generated-file churn.
9.3 KiB
Cross-Language Smoke Matrix
The cross-language smoke matrix defines the documented commands used to compare official clients against the same live gateway flow. It is a repository validation fixture and command reference; normal unit tests validate the matrix shape without connecting to a gateway.
The matrix lives in
clients/proto/fixtures/smoke/cross-language-smoke-matrix.json.
Scope
The matrix covers the supported client languages:
- .NET
- Go
- Rust
- Python
- Java
Each client entry defines commands for the same required operation sequence:
open-sessionregisteradd-itemadvisestream-eventsclose-session
The optional write command is documented separately because writing changes
provider state and should only run when the operator supplies a safe test value.
When stream-events is resumed with an after_worker_sequence cursor that
predates the oldest event still in the gateway's replay ring, the gateway emits a
single ReplayGap sentinel at the head of the stream. Every client surfaces this
as a distinct, typed, non-terminal signal (see each client README); the resume
contract is after_worker_sequence = oldest_available_sequence - 1, and it holds
even when the ring has been emptied entirely by age eviction — the gateway then
reports the next deliverable sequence rather than 0 (see Sessions).
The default smoke sequence opens a fresh stream (no cursor) and does not exercise
the gap path; a resume-with-gap fixture case is tracked separately (TST-24).
All five CLIs consume the typed gap and emit a dedicated row rather than a degenerate event row (the .NET and Java halves were the last to convert — NEXT-02):
| CLI | Text mode | JSON mode |
|---|---|---|
mxgw-rs (Rust, canonical) |
REPLAY_GAP requested_after=<n> oldest_available=<n> |
{"replayGap": {"requestedAfterSequence": <n>, "oldestAvailableSequence": <n>}} as one entry of the events array |
mxgw-go (Go) |
REPLAY_GAP requested_after=<n> oldest_available=<n> |
one {"replayGap": {"requestedAfterSequence": <n>, "oldestAvailableSequence": <n>}} line, counted toward -limit like any other row |
mxgw-py (Python) |
same JSON dump as --json |
{"replayGap": {"requestedAfterSequence": <n>, "oldestAvailableSequence": <n>}} as one entry of the events array |
mxgw-dotnet (.NET) |
one {"replayGap": {...}} line (its "text" mode is JSON-per-line) |
the same row — per line with --jsonl, as one entry of the events array with --json |
mxgw-java (Java) |
REPLAY_GAP requested_after=<n> oldest_available=<n> |
one {"replayGap": {"requestedAfterSequence": <n>, "oldestAvailableSequence": <n>}} line |
All five emit the same two key names and, deliberately, the same JSON value
types: the cursors are JSON numbers (7), not strings. That is why every
CLI types the row by hand instead of marshalling ReplayGap through its
protobuf JSON formatter — the proto3 JSON mapping renders 64-bit integers as
strings ("7"). Normal event rows still come from the protobuf formatters, so
a matrix runner must still compare parsed values, not raw bytes, when it mixes
gap rows with event rows.
Two further formatting differences among the three canonical CLIs, none of them
semantic: Python sorts object keys and uses ", " / ": " separators
(json.dumps(..., sort_keys=True)), while Rust and Go emit compact,
declaration-ordered JSON; and the row sits alone on its own line for Go and for
Rust's --jsonl, but inside an events array for Python and for Rust's
aggregate --json.
Integration Gate
Cross-language smoke execution is opt-in. Runners should skip the matrix unless this variable is set:
$env:MXGATEWAY_INTEGRATION = "1"
The shared inputs are:
| Variable | Default | Purpose |
|---|---|---|
MXGATEWAY_ENDPOINT |
localhost:5000 |
Gateway endpoint used by client CLIs. |
MXGATEWAY_API_KEY |
Empty | API key source for authenticated gateway deployments. |
MXGATEWAY_TEST_ITEM |
TestChildObject.TestInt |
MXAccess item used by add-item. |
MXGATEWAY_TEST_WRITE_VALUE |
Empty | Enables the optional write step when set by a runner. |
The commands in the matrix use MXGATEWAY_API_KEY through each CLI's
api-key-env flag. They must not embed bearer tokens or raw API keys.
Credential contract for authenticate-user
Every CLI resolves the MXAccess verify-user credential the same way, so one exported variable drives the same operator workflow in all five languages:
| Variable | Default | Purpose |
|---|---|---|
MXGATEWAY_VERIFY_PASSWORD |
Empty | Verify-user credential read by authenticate-user when --password is omitted. |
- Flags are
--password(Go:-password) for an explicit value and--password-env(Go:-password-env) for the name of the environment variable, defaulting toMXGATEWAY_VERIFY_PASSWORD. - Resolution order is flag, then environment variable. Prefer the variable: the flag puts the secret in shell history and the process table.
- A resolved credential that is missing or empty is a usage error. The CLI fails fast before dialing rather than authenticating with an empty password, and the error names only the flag and the variable — never the value. Nothing echoes the credential to stdout, stderr, or logs.
This is CLI argument validation, not an MXAccess parity exception: the client libraries still transmit whatever credential they are given. Only the operator tools refuse to fabricate an empty one.
The .NET CLI accepted --verify-user-password, --verify-user-password-env, and
MXGATEWAY_VERIFY_USER_PASSWORD before this contract was unified. Those names
remain as deprecated aliases for one release; new scripts must use the canonical
names above. The full .NET resolution order is --password,
--verify-user-password, the variable named by --password-env (or the
deprecated --verify-user-password-env, default MXGATEWAY_VERIFY_PASSWORD),
then MXGATEWAY_VERIFY_USER_PASSWORD.
TLS variant
The matrix runs over plaintext (h2c) by default. A TLS variant exists but stays
a manual/opt-in run, consistent with the gate above, because it needs the gateway
started with an HTTPS endpoint (an https:// MXGATEWAY_ENDPOINT) and each CLI
switched to its TLS flag (--tls / -tls / --plaintext=false /
plaintext=False). The clients are lenient by default and accept the gateway's
auto-generated self-signed certificate without extra trust setup, except the Rust
CLI, which is pin-only and needs --ca-file or --require-certificate-validation
(and Python uses trust-on-first-use). See
Gateway Configuration — Automatic self-signed certificate
and each client README for the per-client TLS flags.
JSON Comparison
Every command in the matrix requests JSON output. A runner can compare the normalized smoke record across languages with these fields:
- language,
- operation,
- session id,
- server handle,
- item handle,
- event count,
- event family,
- worker sequence,
- protocol status,
- HRESULT,
- status arrays,
- close status.
Failure output must include the client language, endpoint, and redacted auth
context. Auth context identifies the source, such as MXGATEWAY_API_KEY, but
does not include the secret value.
Bundled Smoke Commands
Each client also exposes a bundled smoke command. Those commands are useful
for quick local checks, but the full cross-language matrix uses explicit
operation commands because not every bundled smoke command streams events yet.
The explicit sequence remains the parity baseline for issue-level validation.
Per-CLI Subcommand Coverage
The matrix sequence itself is available everywhere, but the later single-item session commands were not added to every CLI at the same time. A runner that reaches beyond the required sequence must branch on language, so the current deltas are specified here rather than left to be discovered:
| Subcommand | .NET | Rust | Go | Python | Java |
|---|---|---|---|---|---|
unregister |
yes | yes | no | no | no |
add-buffered-item |
yes | no | no | no | no |
set-buffered-update-interval |
yes | no | no | no | no |
suspend |
yes | no | no | no | no |
activate |
yes | no | no | no | no |
write-secured |
yes | yes | yes | yes | yes |
write-secured2 |
yes | no | no | no | no |
authenticate-user |
yes | yes | yes | yes | yes |
archestra-user-to-id |
yes | no | no | no | no |
Only .NET exposes all nine. Rust adds unregister and the credential pair; Go,
Python, and Java expose the credential pair only. Every gap is CLI surface only —
all five libraries implement all nine typed helpers, so a gap is a missing
operator command, never a missing capability. Levelling the CLIs is separate
feature work and is not tracked as a defect here.
Validation
Run the matrix shape tests after changing the smoke matrix:
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter FullyQualifiedName~CrossLanguageSmokeMatrixTests
Live execution remains a separate opt-in step because it depends on a running gateway, the installed MXAccess worker path, and provider state.