fix(CLI-37,CLI-38): make status/HRESULT reply validation conformant across all five clients
One cross-client conformance pass; also closes first-cycle CLI-08. CLI-37: an MxStatusProxy entry is a failure iff `category != MX_STATUS_CATEGORY_OK`. The proto contract has always said so — `success` is the raw 16-bit COM member carried verbatim for diagnostics, not a boolean — but four clients branched on `success` alone and .NET required both, so the same gateway reply produced opposite verdicts per language. An absent entry stays success; a present entry with an UNSPECIFIED category is a failure, because the worker always maps a category and an unmapped one is not proven OK. CLI-38: a reply fails on HRESULT iff `hresult` is present and negative, so positive COM success codes such as S_FALSE (1) pass. .NET/Go/Java used `!= 0`, which errored on a parity-preserving S_FALSE that Python and Rust accepted. This makes the existing ClientLibrariesDesign.md claim true rather than rewriting the doc to describe the divergence. Four shared fixtures pin both rules cross-client, and each language suite also carries a table test for the two edges a fixture cannot express (absent entry, UNSPECIFIED category). A Java test fake that built a status with a bare `setSuccess(1)` and no category is fixed — under the category rule that reply was never a success.
This commit is contained in:
+3
-1
@@ -47,7 +47,9 @@ final class MxGatewayErrors {
|
||||
if (reply == null) {
|
||||
return;
|
||||
}
|
||||
if (reply.hasHresult() && reply.getHresult() != 0) {
|
||||
// COM semantics: only a negative HRESULT is a failure. Positive success
|
||||
// codes such as S_FALSE (1) pass.
|
||||
if (reply.hasHresult() && reply.getHresult() < 0) {
|
||||
throw new MxAccessException(operation, reply);
|
||||
}
|
||||
for (var status : reply.getStatusesList()) {
|
||||
|
||||
+17
-7
@@ -8,8 +8,11 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
||||
* Helpers for inspecting {@link MxStatusProxy} values returned by the gateway.
|
||||
*
|
||||
* <p>An {@code MxStatusProxy} mirrors the MXAccess COM {@code MXSTATUS_PROXY}
|
||||
* struct. The success flag uses the MXAccess convention where any non-zero
|
||||
* value indicates success.
|
||||
* struct. Per the wire contract, {@code category} is the authoritative verdict:
|
||||
* an entry succeeds only when its category is
|
||||
* {@code MX_STATUS_CATEGORY_OK}. The {@code success} member carries the raw
|
||||
* 16-bit COM value verbatim for diagnostics and is not a boolean, so it never
|
||||
* decides success or failure.
|
||||
*/
|
||||
public final class MxStatuses {
|
||||
private MxStatuses() {
|
||||
@@ -18,12 +21,17 @@ public final class MxStatuses {
|
||||
/**
|
||||
* Returns whether the supplied status proxy reports success.
|
||||
*
|
||||
* <p>A {@code null} status is success because nothing was reported. A
|
||||
* present entry whose category is {@code MX_STATUS_CATEGORY_UNSPECIFIED}
|
||||
* is a failure: the worker always maps a category, so an unmapped one is
|
||||
* not proven OK.
|
||||
*
|
||||
* @param status the status proxy, may be {@code null}
|
||||
* @return {@code true} if {@code status} is {@code null} or its success
|
||||
* flag is non-zero, {@code false} otherwise
|
||||
* @return {@code true} if {@code status} is {@code null} or its category is
|
||||
* {@code MX_STATUS_CATEGORY_OK}, {@code false} otherwise
|
||||
*/
|
||||
public static boolean succeeded(MxStatusProxy status) {
|
||||
return status == null || status.getSuccess() != 0;
|
||||
return status == null || status.getCategory() == MxStatusCategory.MX_STATUS_CATEGORY_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,9 +52,11 @@ public final class MxStatuses {
|
||||
*/
|
||||
public record MxStatusView(MxStatusProxy raw) {
|
||||
/**
|
||||
* Returns the raw success flag (non-zero indicates success).
|
||||
* Returns the raw {@code success} member exactly as MXAccess reported
|
||||
* it. This is a diagnostic value, not a verdict — use
|
||||
* {@link MxStatuses#succeeded(MxStatusProxy)} to decide success.
|
||||
*
|
||||
* @return the success flag value
|
||||
* @return the raw success member
|
||||
*/
|
||||
public int success() {
|
||||
return raw.getSuccess();
|
||||
|
||||
+7
-4
@@ -701,14 +701,17 @@ final class MxGatewayClientSessionTests {
|
||||
.setSessionId(request.getSessionId())
|
||||
.setKind(request.getCommand().getKind())
|
||||
.setProtocolStatus(ok());
|
||||
// `category` is the authoritative success indicator, so the fake
|
||||
// must set it — a bare non-zero `success` is not a success.
|
||||
var okStatus = mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy.newBuilder()
|
||||
.setSuccess(1)
|
||||
.setCategory(mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory.MX_STATUS_CATEGORY_OK);
|
||||
if (request.getCommand().getKind() == MxCommandKind.MX_COMMAND_KIND_SUSPEND) {
|
||||
reply.setSuspend(mxaccess_gateway.v1.MxaccessGateway.SuspendReply.newBuilder()
|
||||
.setStatus(mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy.newBuilder()
|
||||
.setSuccess(1)));
|
||||
.setStatus(okStatus));
|
||||
} else if (request.getCommand().getKind() == MxCommandKind.MX_COMMAND_KIND_ACTIVATE) {
|
||||
reply.setActivate(mxaccess_gateway.v1.MxaccessGateway.ActivateReply.newBuilder()
|
||||
.setStatus(mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy.newBuilder()
|
||||
.setSuccess(1)));
|
||||
.setStatus(okStatus));
|
||||
}
|
||||
responseObserver.onNext(reply.build());
|
||||
responseObserver.onCompleted();
|
||||
|
||||
+47
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
@@ -20,6 +21,8 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
final class MxGatewayFixtureTests {
|
||||
@Test
|
||||
@@ -89,6 +92,50 @@ final class MxGatewayFixtureTests {
|
||||
throw new AssertionError("expected MxAccessException");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"register.ok.reply.json,false",
|
||||
"write.status-category-error-success-set.reply.json,true",
|
||||
"write.status-category-ok-success-zero.reply.json,false",
|
||||
"write.hresult-s-false.reply.json,false",
|
||||
"write.hresult-e-fail.reply.json,true",
|
||||
})
|
||||
void replyValidationFixturesBranchOnCategoryAndNegativeHresult(String fixture, boolean expectFailure)
|
||||
throws Exception {
|
||||
MxCommandReply.Builder builder = MxCommandReply.newBuilder();
|
||||
JsonFormat.parser().merge(
|
||||
Files.readString(fixtureRoot().resolve("command-replies/" + fixture)),
|
||||
builder);
|
||||
MxCommandReply reply = builder.build();
|
||||
|
||||
if (expectFailure) {
|
||||
assertThrows(MxAccessException.class, () -> MxGatewayErrors.ensureMxAccessSuccess("write", reply));
|
||||
} else {
|
||||
MxGatewayErrors.ensureMxAccessSuccess("write", reply);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"MX_STATUS_CATEGORY_OK,0,true",
|
||||
"MX_STATUS_CATEGORY_OK,1,true",
|
||||
"MX_STATUS_CATEGORY_COMMUNICATION_ERROR,1,false",
|
||||
"MX_STATUS_CATEGORY_UNSPECIFIED,1,false",
|
||||
})
|
||||
void statusEntryVerdictIgnoresTheRawSuccessMember(String category, int success, boolean expectSucceeded) {
|
||||
MxStatusProxy status = MxStatusProxy.newBuilder()
|
||||
.setCategory(MxStatusCategory.valueOf(category))
|
||||
.setSuccess(success)
|
||||
.build();
|
||||
|
||||
assertEquals(expectSucceeded, MxStatuses.succeeded(status));
|
||||
}
|
||||
|
||||
@Test
|
||||
void absentStatusEntryIsSuccess() {
|
||||
assertTrue(MxStatuses.succeeded(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcAuthErrorsAreClassifiedAndRedacted() {
|
||||
RuntimeException authError = MxGatewayErrors.fromGrpc(
|
||||
|
||||
Reference in New Issue
Block a user