# 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: 1. `open-session` 2. `register` 3. `add-item` 4. `advise` 5. `stream-events` 6. `close-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](Sessions.md)). 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). The CLIs differ in how they *print* that library-level signal. Three of them consume the typed gap and emit a dedicated row rather than a degenerate event row; the other two hand the raw sentinel `MxEvent` straight to the formatter, so they print the sentinel itself, whose `replayGap` field carries the same cursors: | CLI | Text mode | JSON mode | |-----|-----------|-----------| | `mxgw-rs` (Rust, canonical) | `REPLAY_GAP requested_after= oldest_available=` | `{"replayGap": {"requestedAfterSequence": , "oldestAvailableSequence": }}` as one entry of the `events` array | | `mxgw-go` (Go) | `REPLAY_GAP requested_after= oldest_available=` | one `{"replayGap": {"requestedAfterSequence": , "oldestAvailableSequence": }}` line, counted toward `-limit` like any other row | | `mxgw-py` (Python) | same JSON dump as `--json` | `{"replayGap": {"requestedAfterSequence": , "oldestAvailableSequence": }}` as one entry of the `events` array | | `mxgw-dotnet` (.NET) | the raw sentinel `MxEvent` as protobuf JSON, including its `replayGap` field | same, as one entry of the `events` array | | `mxgw-java` (Java) | the sentinel's `worker_sequence` and `family` (`0 MX_EVENT_FAMILY_UNSPECIFIED`) | the raw sentinel `MxEvent` as protobuf JSON, including its `replayGap` field | Rust, Go, and Python emit the same two key names and, deliberately, the same JSON value **types**: the cursors are JSON numbers (`7`), not strings. That is why the Go CLI types the row by hand instead of marshalling `ReplayGap` with `protojson` — the proto3 JSON mapping renders 64-bit integers as strings (`"7"`), which is also why the .NET and Java rows, which pass the sentinel through a protobuf JSON formatter, carry **quoted** cursors. A matrix runner must therefore compare parsed values, not raw bytes, and must not assume the same value type across all five CLIs. 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: ```powershell $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 to `MXGATEWAY_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](./GatewayConfiguration.md#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: ```bash 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. ## Related Documentation - [Gateway Testing](./GatewayTesting.md) - [Client Libraries Detailed Design](./ClientLibrariesDesign.md) - [Client Proto Generation](./ClientProtoGeneration.md)