fix(clients): resolve 2026-06-18 array-write review findings

- Client.Dotnet-030: add advise-supervisory to IsKnownGatewayCommand (was dead/unreachable, exit 2)
- Client.Go-035/036/037: usage+README list advise-supervisory; add session-id guard test; fix write2 README wording
- Client.Python-037/038: drop regressed 'scaffold' from pyproject; add advise-supervisory CLI tests
- Client.Rust-039/040: document write_array_elements/advise-supervisory in design doc; pin outer MxValue data_type==0
- Client.Java-049/050/051: sync CLIENT_VERSION to 0.1.2; add advise-supervisory test; guard negative uint32 inputs (pending windev gradle verification)

Client READMEs also updated for Server-057 add-family normalization wording.
.NET/Go/Python/Rust verified green locally; Java pending windev.
This commit is contained in:
Joseph Doherty
2026-06-18 10:58:33 -04:00
parent 85ef453d0d
commit 6c853b43af
22 changed files with 404 additions and 43 deletions
+4 -3
View File
@@ -144,8 +144,9 @@ array before forwarding to the worker. Indices not listed in the map are
written as the element type's default — this is a **reset**, not a preserve;
current values at those positions are discarded. `totalLength` is required and
must match the declared length of the array attribute. Bare-name array items
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form at `AddItem` so
the array attribute accepts the write.
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form across the whole
add family — `AddItem`, `AddItem2`, `AddItemBulk`, and `AddBufferedItem` — so the
array attribute accepts the write.
## Galaxy Repository Browse
@@ -396,7 +397,7 @@ repositories {
}
dependencies {
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.1'
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.2'
}
````
@@ -56,7 +56,7 @@ final class MxGatewayCliTests {
assertEquals(0, run.exitCode());
assertEquals("", run.errors());
assertTrue(run.output().contains("mxgateway-java 0.1.1"));
assertTrue(run.output().contains("mxgateway-java 0.1.2"));
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
assertTrue(run.output().contains("workerProtocolVersion=1"));
}
@@ -86,7 +86,7 @@ final class MxGatewayCliTests {
CliRun run = execute(new FakeClientFactory(), "version", "--json");
assertEquals(0, run.exitCode());
assertTrue(run.output().contains("\"clientVersion\":\"0.1.1\""));
assertTrue(run.output().contains("\"clientVersion\":\"0.1.2\""));
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
}
@@ -148,6 +148,26 @@ final class MxGatewayCliTests {
assertTrue(run.output().contains("\"itemHandle\":7"));
}
@Test
void adviseSupervisoryCommandCallsAdviseSupervisoryRaw() {
// Client.Java-050: dedicated test for advise-supervisory, using a
// separate adviseSupervisoryCalled flag so it cannot be masked by the
// plain advise path that shares adviseCalled.
FakeClientFactory factory = new FakeClientFactory();
CliRun run = execute(
factory,
"advise-supervisory",
"--session-id", "session-cli",
"--server-handle", "12",
"--item-handle", "34",
"--json");
assertEquals(0, run.exitCode());
assertTrue(factory.client.session.adviseSupervisoryCalled);
assertFalse(factory.client.session.adviseCalled, "plain advise must not be called");
assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_ADVISE_SUPERVISORY\""));
}
// ---- ping subcommand (D4) ----
@Test
@@ -1235,6 +1255,7 @@ final class MxGatewayCliTests {
private boolean registerCalled;
private boolean addItemCalled;
private boolean adviseCalled;
private boolean adviseSupervisoryCalled;
private MxValue lastWriteValue;
private String lastPingMessage;
private long lastReadBulkTimeoutMs;
@@ -1304,7 +1325,7 @@ final class MxGatewayCliTests {
@Override
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
adviseCalled = true;
adviseSupervisoryCalled = true;
return MxCommandReply.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
.setProtocolStatus(ok())
@@ -9,7 +9,7 @@ package com.zb.mom.ww.mxgateway.client;
public final class MxGatewayClientVersion {
private static final int GATEWAY_PROTOCOL_VERSION = 3;
private static final int WORKER_PROTOCOL_VERSION = 1;
private static final String CLIENT_VERSION = "0.1.1";
private static final String CLIENT_VERSION = "0.1.2";
private MxGatewayClientVersion() {
}
@@ -623,13 +623,22 @@ public final class MxGatewaySession implements AutoCloseable {
* supplied indices must be within {@code [0, totalLength)}. Elements are iterated in
* ascending index order so the produced command is deterministic.
*
* <p>Because the proto fields {@code MxSparseArray.total_length} and
* {@code MxSparseElement.index} are {@code uint32}, passing a negative Java {@code int}
* would silently sign-extend to a large unsigned value on the wire. This method
* therefore rejects negative {@code totalLength} and negative element indices with
* {@link IllegalArgumentException} rather than allowing a hard-to-diagnose gateway error.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to write
* @param elementDataType the {@link MxDataType} of the array's elements
* @param totalLength the total length of the expanded array
* @param elements the indices to write mapped to their scalar values; unmentioned
* indices are reset to the element type default
* @param totalLength the total length of the expanded array; must be &gt; 0
* @param elements the indices to write mapped to their scalar values; each index must
* be in {@code [0, totalLength)}; unmentioned indices are reset to the element
* type default
* @param userId the MXAccess user id used for security checks
* @throws IllegalArgumentException if {@code totalLength} is not positive, or if any
* element index is negative or &ge; {@code totalLength}
* @throws MxGatewayException on transport or protocol failure
*/
public void writeArrayElements(
@@ -641,6 +650,16 @@ public final class MxGatewaySession implements AutoCloseable {
int userId) {
Objects.requireNonNull(elementDataType, "elementDataType");
Objects.requireNonNull(elements, "elements");
if (totalLength <= 0) {
throw new IllegalArgumentException("totalLength must be > 0, got " + totalLength);
}
for (Map.Entry<Integer, MxValue> entry : elements.entrySet()) {
int idx = entry.getKey();
if (idx < 0 || idx >= totalLength) {
throw new IllegalArgumentException(
"element index " + idx + " is out of range [0, " + totalLength + ")");
}
}
MxSparseArray.Builder sparse = MxSparseArray.newBuilder()
.setElementDataType(elementDataType)
.setTotalLength(totalLength);
@@ -451,6 +451,64 @@ final class MxGatewayClientSessionTests {
}
}
@Test
void writeArrayElementsRejectsNonPositiveTotalLength() throws Exception {
// Client.Java-051: negative/zero totalLength silently sign-extends to a
// large uint32 on the wire; the client must reject it with
// IllegalArgumentException before building the proto message (before any
// network call is issued).
try (InProcessGateway gateway = InProcessGateway.start(
new TestGatewayService() {}, new AtomicReference<>());
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
MxGatewaySession session = MxGatewaySession.forSessionId(client, "guard-session");
assertThrows(
IllegalArgumentException.class,
() -> session.writeArrayElements(
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, -1, Map.of(), 0),
"negative totalLength must throw");
assertThrows(
IllegalArgumentException.class,
() -> session.writeArrayElements(
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 0, Map.of(), 0),
"zero totalLength must throw");
}
}
@Test
void writeArrayElementsRejectsOutOfRangeIndex() throws Exception {
// Client.Java-051: a negative index silently sign-extends to a large
// uint32 on the wire; an index >= totalLength exceeds the declared
// array bounds. Both must be caught before the network call.
try (InProcessGateway gateway = InProcessGateway.start(
new TestGatewayService() {}, new AtomicReference<>());
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
MxGatewaySession session = MxGatewaySession.forSessionId(client, "guard-session");
assertThrows(
IllegalArgumentException.class,
() -> session.writeArrayElements(
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
Map.of(-1, MxValues.int32Value(7)), 0),
"negative index must throw");
assertThrows(
IllegalArgumentException.class,
() -> session.writeArrayElements(
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
Map.of(5, MxValues.int32Value(7)), 0),
"index equal to totalLength must throw");
assertThrows(
IllegalArgumentException.class,
() -> session.writeArrayElements(
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
Map.of(10, MxValues.int32Value(7)), 0),
"index above totalLength must throw");
}
}
private static ProtocolStatus ok() {
return ProtocolStatus.newBuilder()
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)