This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 1b6abc9889 Finish TODO-150 next-gen remote-proxy parity implementation
1b6abc9889 is described below

commit 1b6abc9889bc3aa4b57352a35619b1994f3421f8
Author: James Bognar <[email protected]>
AuthorDate: Tue Jun 2 08:04:07 2026 -0400

    Finish TODO-150 next-gen remote-proxy parity implementation
---
 .../org/apache/juneau/rest/client/RestRequest.java |  27 ++
 .../juneau/rest/client/remote/RemoteClient.java    | 422 ++++++++++++++++++---
 .../client/RemoteProxy_FeatureParity_Test.java     | 133 ++++---
 juneau-utest/test-run-history.tsv                  |   1 +
 4 files changed, 474 insertions(+), 109 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
index 349189221c..2f8294255f 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
@@ -406,13 +406,40 @@ public final class RestRequest {
 
        private String applyPathSubstitutions(String template) {
                var result = template;
+               Object remainder = null;
                for (var entry : pathData.entrySet()) {
+                       if ("/*".equals(entry.getKey())) {
+                               remainder = entry.getValue();  // 
@PathRemainder — applied after named substitutions
+                               continue;
+                       }
                        var replacement = entry.getValue() != null ? 
entry.getValue().toString() : "";
                        result = result.replace("{" + entry.getKey() + "}", 
urlEncode(replacement));
                }
+               if (remainder != null) {
+                       var r = remainder.toString();
+                       if (! r.isEmpty()) {
+                               if (result.endsWith("/*"))
+                                       result = result.substring(0, 
result.length() - 2);
+                               if (! result.endsWith("/"))
+                                       result += "/";
+                               result += urlEncodePath(r);
+                       }
+               }
                return result;
        }
 
+       /** URL-encodes a path remainder while preserving {@code /} segment 
separators. */
+       private static String urlEncodePath(String value) {
+               var segments = value.split("/", -1);
+               var sb = new StringBuilder();
+               for (var i = 0; i < segments.length; i++) {
+                       if (i > 0)
+                               sb.append('/');
+                       sb.append(urlEncode(segments[i]));
+               }
+               return sb.toString();
+       }
+
        private URI appendQuery(String baseUrl) {
                if (queryData.isEmpty()) {
                        try {
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
index 88f85d3136..be336ba9ad 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
@@ -18,15 +18,30 @@ package org.apache.juneau.rest.client.remote;
 
 import static org.apache.juneau.commons.utils.AssertionUtils.assertArgNotNull;
 import static org.apache.juneau.commons.utils.StringUtils.firstNonEmpty;
+import static org.apache.juneau.commons.utils.ThrowableUtils.rex;
 
 import java.io.*;
 import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.*;
 
-import org.apache.juneau.http.annotation.Content;
+import org.apache.juneau.MarshallingContext;
+import org.apache.juneau.commons.httppart.HttpPartType;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.header.HttpHeaderList;
+import org.apache.juneau.http.part.PartList;
 import org.apache.juneau.http.remote.Remote;
 import org.apache.juneau.http.remote.RemoteReturn;
 import org.apache.juneau.http.HttpBody;
+import org.apache.juneau.http.response.BasicHttpException;
+import org.apache.juneau.http.response.BasicHttpResponse;
 import org.apache.juneau.http.remote.RrpcInterfaceMeta;
+import org.apache.juneau.httppart.HttpPartSchema;
+import org.apache.juneau.json.JsonParser;
+import org.apache.juneau.json.JsonSerializer;
+import org.apache.juneau.oapi.OpenApiSerializer;
+import org.apache.juneau.parser.ParseException;
 import org.apache.juneau.rest.client.*;
 
 /**
@@ -135,10 +150,10 @@ public final class RemoteClient {
                        var req = buildRequest(methodMeta.getHttpMethod(), 
fullPath, method, args);
 
                        // Execute and process the return value
-                       return processReturn(req, methodMeta.getReturnType(), 
method.getReturnType());
+                       return processReturn(req, methodMeta.getReturnType(), 
method);
                }
 
-               private RestRequest buildRequest(String httpMethod, String 
path, Method method, Object[] args) {
+               private RestRequest buildRequest(String httpMethod, String 
path, Method method, Object[] args) throws IOException {
                        var req = switch (httpMethod) {
                                case "GET" -> client.get(path);
                                case "POST" -> client.post(path);
@@ -150,67 +165,225 @@ public final class RemoteClient {
 
                        if (args != null) {
                                var params = method.getParameters();
-                               for (int i = 0; i < params.length; i++) {
-                                       var param = params[i];
-                                       var arg = args[i];
-                                       if (arg == null)
-                                               continue;
-
-                                       var pathAnnotation = 
param.getAnnotation(org.apache.juneau.http.annotation.Path.class);
-                                       if (pathAnnotation != null) {
-                                               var name = 
firstNonEmpty(pathAnnotation.value(), pathAnnotation.name(), param.getName());
-                                               req = req.pathData(name, 
String.valueOf(arg));
-                                               continue;
-                                       }
-
-                                       var queryAnnotation = 
param.getAnnotation(org.apache.juneau.http.annotation.Query.class);
-                                       if (queryAnnotation != null) {
-                                               var name = 
firstNonEmpty(queryAnnotation.value(), queryAnnotation.name(), param.getName());
-                                               req = req.queryData(name, 
String.valueOf(arg));
-                                               continue;
-                                       }
-
-                                       var headerAnnotation = 
param.getAnnotation(org.apache.juneau.http.annotation.Header.class);
-                                       if (headerAnnotation != null) {
-                                               var name = 
firstNonEmpty(headerAnnotation.value(), headerAnnotation.name(), 
param.getName());
-                                               req = req.header(name, 
String.valueOf(arg));
-                                               continue;
-                                       }
-
-                                       var contentAnnotation = 
param.getAnnotation(Content.class);
-                                       if (contentAnnotation != null) {
-                                               if (arg instanceof HttpBody b)
-                                                       req = req.body(b);
-                                               else
-                                                       req = 
req.bodyString(String.valueOf(arg));
-                                               continue;
-                                       }
-
-                                       if (params.length == 1) {
-                                               if (arg instanceof HttpBody b)
-                                                       req = req.body(b);
-                                               else
-                                                       req = 
req.bodyString(String.valueOf(arg));
-                                       }
-                               }
+                               for (int i = 0; i < params.length; i++)
+                                       bindParam(req, params[i], args[i], 
params.length == 1);
                        }
 
                        return req;
                }
 
-               private Object processReturn(RestRequest req, RemoteReturn 
returnMode, Class<?> returnType) throws Exception {
+               /**
+                * Binds a single method argument to the outgoing request based 
on its HTTP-part annotation.
+                *
+                * <p>
+                * Supports {@code @Path}, {@code @Query}, {@code @Header}, 
{@code @FormData}, {@code @Content}, and
+                * {@code @Request}.  {@code @Query}/{@code @Header}/{@code 
@FormData} honor dynamic name/value expansion
+                * (Map / {@code "*"} / part-list / bean) and parameter-level 
{@code def()} defaults for {@code null} args.
+                */
+               private void bindParam(RestRequest req, Parameter param, Object 
arg, boolean soleParam) throws IOException {
+                       var path = param.getAnnotation(Path.class);
+                       if (path != null) {
+                               if (arg != null)
+                                       
req.pathData(firstNonEmpty(path.value(), path.name(), param.getName()),
+                                               
serializePart(HttpPartType.PATH, HttpPartSchema.create(path, null), arg));
+                               return;
+                       }
+
+                       var query = param.getAnnotation(Query.class);
+                       if (query != null) {
+                               bindParts(HttpPartType.QUERY, 
HttpPartSchema.create(query, null), query.value(), query.name(), query.def(), 
arg, param.getName(), req::queryData);
+                               return;
+                       }
+
+                       var header = param.getAnnotation(Header.class);
+                       if (header != null) {
+                               bindParts(HttpPartType.HEADER, 
HttpPartSchema.create(header, null), header.value(), header.name(), 
header.def(), arg, param.getName(), req::header);
+                               return;
+                       }
+
+                       var formData = param.getAnnotation(FormData.class);
+                       if (formData != null) {
+                               bindParts(HttpPartType.FORMDATA, 
HttpPartSchema.create(formData, null), formData.value(), formData.name(), 
formData.def(), arg, param.getName(), req::formData);
+                               return;
+                       }
+
+                       var request = param.getAnnotation(Request.class);
+                       if (request != null) {
+                               if (arg != null)
+                                       bindRequestBean(req, arg);
+                               return;
+                       }
+
+                       // G12: @PathRemainder appends the (part-serialized) 
value as the trailing path remainder ("/*").
+                       var pathRemainder = 
param.getAnnotation(PathRemainder.class);
+                       if (pathRemainder != null) {
+                               if (arg != null)
+                                       req.pathData("/*", 
serializePart(HttpPartType.PATH, HttpPartSchema.create(pathRemainder, null), 
arg));
+                               return;
+                       }
+
+                       if (param.getAnnotation(Content.class) != null) {
+                               if (arg != null)
+                                       withContentBody(req, arg);
+                               return;
+                       }
+
+                       if (soleParam && arg != null)
+                               withContentBody(req, arg);
+               }
+
+               /**
+                * Binds a {@code @Query}/{@code @Header}/{@code @FormData} 
argument as one or more name/value parts.
+                *
+                * <p>
+                * Single values are serialized via the configured {@link 
HttpPartSerializer} honoring the resolved
+                * {@link HttpPartSchema} ({@code @Schema} format / 
collection-format / {@code skipIfEmpty}); dynamic
+                * {@code "*"}/blank-name arguments expand into multiple parts 
(G8).
+                *
+                * @param partType The HTTP part category 
(query/header/form-data).
+                * @param schema The resolved part schema (from the annotation 
+ {@code @Schema}).
+                * @param annValue The annotation {@code value()} (may be 
blank, or {@code "*"} for dynamic expansion).
+                * @param annName The annotation {@code name()} (may be blank).
+                * @param def The parameter-level default applied when {@code 
arg} is <jk>null</jk>.
+                * @param arg The argument value.
+                * @param fallbackName The name to use when the annotation 
specifies none (param or bean-property name).
+                * @param adder Sink that records a single name/value part on 
the request.
+                */
+               private static void bindParts(HttpPartType partType, 
HttpPartSchema schema, String annValue, String annName, String def, Object arg, 
String fallbackName, BiConsumer<String,String> adder) {
+                       var explicit = firstNonEmpty(annValue, annName);
+                       if (arg == null) {
+                               if (def != null && ! def.isEmpty())
+                                       adder.accept("*".equals(explicit) ? 
fallbackName : firstNonEmpty(explicit, fallbackName), def);
+                               return;
+                       }
+                       if ("*".equals(explicit) || (explicit == null && 
isExpandable(arg))) {
+                               expandPairs(partType, arg, adder);
+                               return;
+                       }
+                       if (schema != null && schema.isSkipIfEmpty() && 
isEmptyArg(arg))
+                               return;
+                       var serialized = serializePart(partType, schema, arg);
+                       if (serialized == null)
+                               return;
+                       if (serialized.isEmpty() && schema != null && 
schema.isSkipIfEmpty())
+                               return;
+                       adder.accept(firstNonEmpty(explicit, fallbackName), 
serialized);
+               }
+
+               /** Returns <jk>true</jk> if the argument is an empty 
string/collection/array/map (for {@code skipIfEmpty}). */
+               private static boolean isEmptyArg(Object arg) {
+                       if (arg == null)
+                               return true;
+                       if (arg instanceof CharSequence cs)
+                               return cs.isEmpty();
+                       if (arg instanceof Map<?,?> m)
+                               return m.isEmpty();
+                       if (arg instanceof Collection<?> c)
+                               return c.isEmpty();
+                       if (arg.getClass().isArray())
+                               return Array.getLength(arg) == 0;
+                       return false;
+               }
+
+               /**
+                * Expands a dynamic argument (Map / {@link PartList} / {@link 
HttpHeaderList} / bean) into discrete
+                * name/value parts.  Map/bean values are part-serialized; 
{@link PartList}/{@link HttpHeaderList} entries
+                * are already string-valued and passed through.
+                */
+               private static void expandPairs(HttpPartType partType, Object 
arg, BiConsumer<String,String> adder) {
+                       if (arg instanceof Map<?,?> m) {
+                               m.forEach((k, v) -> { if (k != null && v != 
null) adder.accept(String.valueOf(k), serializePart(partType, null, v)); });
+                       } else if (arg instanceof PartList pl) {
+                               for (var p : pl)
+                                       if (p.getValue() != null)
+                                               adder.accept(p.getName(), 
p.getValue());
+                       } else if (arg instanceof HttpHeaderList hl) {
+                               for (var h : hl)
+                                       if (h.getValue() != null)
+                                               adder.accept(h.getName(), 
h.getValue());
+                       } else {
+                               for (var e : 
MarshallingContext.DEFAULT.toBeanMap(arg).entrySet())
+                                       if (e.getValue() != null)
+                                               adder.accept(e.getKey(), 
serializePart(partType, null, e.getValue()));
+                       }
+               }
+
+               /**
+                * Serializes an HTTP-part value via the OpenAPI part 
serializer, honoring the supplied schema
+                * (collection format, value format, enums, etc.).  Returns the 
non-URL-encoded string form (G6).
+                */
+               private static String serializePart(HttpPartType partType, 
HttpPartSchema schema, Object value) {
+                       try {
+                               return 
OpenApiSerializer.DEFAULT.getPartSession().serialize(partType, schema, value);
+                       } catch (Exception e) {
+                               throw rex(e, "Could not serialize HTTP {0} part 
value of type {1}", partType, value == null ? "null" : 
value.getClass().getName());
+                       }
+               }
+
+               /**
+                * Binds the HTTP-part-annotated getters of a {@code @Request} 
bean as discrete request parts.
+                */
+               private static void bindRequestBean(RestRequest req, Object 
bean) {
+                       for (var m : bean.getClass().getMethods()) {
+                               if (m.getParameterCount() != 0 || 
m.getDeclaringClass() == Object.class)
+                                       continue;
+                               var q = m.getAnnotation(Query.class);
+                               var h = m.getAnnotation(Header.class);
+                               var f = m.getAnnotation(FormData.class);
+                               var p = m.getAnnotation(Path.class);
+                               if (q == null && h == null && f == null && p == 
null)
+                                       continue;
+                               Object value;
+                               try {
+                                       value = m.invoke(bean);
+                               } catch (ReflectiveOperationException e) {
+                                       throw rex(e, "Could not read @Request 
bean property via {0}", m.getName());
+                               }
+                               var prop = propertyName(m.getName());
+                               if (q != null)
+                                       bindParts(HttpPartType.QUERY, 
HttpPartSchema.create(q, null), q.value(), q.name(), q.def(), value, prop, 
req::queryData);
+                               else if (h != null)
+                                       bindParts(HttpPartType.HEADER, 
HttpPartSchema.create(h, null), h.value(), h.name(), h.def(), value, prop, 
req::header);
+                               else if (f != null)
+                                       bindParts(HttpPartType.FORMDATA, 
HttpPartSchema.create(f, null), f.value(), f.name(), f.def(), value, prop, 
req::formData);
+                               else if (value != null)
+                                       req.pathData(firstNonEmpty(p.value(), 
p.name(), prop), serializePart(HttpPartType.PATH, HttpPartSchema.create(p, 
null), value));
+                       }
+               }
+
+               /**
+                * Returns <jk>true</jk> if a blank-named part argument should 
be expanded into multiple name/value parts
+                * (Map / {@link PartList} / {@link HttpHeaderList} / bean) 
rather than serialized as a single value.
+                */
+               private static boolean isExpandable(Object arg) {
+                       return arg instanceof Map || arg instanceof PartList || 
arg instanceof HttpHeaderList || isBean(arg);
+               }
+
+               private static boolean isBean(Object arg) {
+                       return ! (arg instanceof CharSequence || arg instanceof 
Number || arg instanceof Boolean
+                               || arg instanceof Character || arg instanceof 
Enum || arg instanceof Date
+                               || arg instanceof Collection || 
arg.getClass().isArray());
+               }
+
+               /** Derives a bean-property name from a getter name (e.g. 
{@code getFoo} &rarr; {@code foo}). */
+               private static String propertyName(String methodName) {
+                       if (methodName.startsWith("get") && methodName.length() 
> 3)
+                               return 
Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
+                       if (methodName.startsWith("is") && methodName.length() 
> 2)
+                               return 
Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
+                       return methodName;
+               }
+
+               private Object processReturn(RestRequest req, RemoteReturn 
returnMode, Method method) throws Exception {
+                       var returnType = method.getReturnType();
+                       var genericReturnType = method.getGenericReturnType();
                        return switch (returnMode) {
-                       case BODY -> {
+                       case BODY -> processBody(req, returnType, 
genericReturnType, method);
+                       case BEAN -> {
+                               // HTTP-response bean (e.g. Ok / NotFound): 
materialize from status + body rather than parsing.
                                try (var resp = req.run()) { // HTT - exception 
during close() branch
-                                       if (returnType == void.class || 
returnType == Void.class)
-                                               yield null;
-                                       if (returnType == String.class)
-                                               yield resp.getBodyAsString();
-                                       if (returnType == InputStream.class)
-                                               yield resp.getBodyStream();
-                                       if (returnType == byte[].class)
-                                               yield resp.body().asBytes();
-                                       yield resp.getBodyAsString();
+                                       throwIfError(resp, method);
+                                       yield instantiateHttpType(returnType, 
resp.getBodyAsString());
                                }
                        }
                        case STATUS -> {
@@ -224,11 +397,142 @@ public final class RemoteClient {
                                }
                        }
                        case RESPONSE -> req.run(); // caller must close
-                       case BEAN, NONE -> throw new 
UnsupportedOperationException(
-                               "RestClient remote proxies support 
RemoteReturn.BODY, STATUS, and RESPONSE only; got " + returnMode);
+                       case NONE -> {
+                               try (var resp = req.run()) { // HTT - exception 
during close() branch
+                                       yield null;
+                               }
+                       }
                        };
                }
 
+               /**
+                * Handles {@link RemoteReturn#BODY} returns, including {@link 
Optional} and {@link CompletableFuture}/{@link Future}
+                * wrappers (G11) and HTTP-response beans (G2/G3).
+                */
+               private Object processBody(RestRequest req, Class<?> 
returnType, Type genericReturnType, Method method) throws Exception {
+                       if (returnType == Optional.class) {
+                               var inner = innerType(genericReturnType);
+                               return 
Optional.ofNullable(processBodyValue(req, rawClass(inner), inner, method));
+                       }
+                       if (returnType == CompletableFuture.class || returnType 
== Future.class) {
+                               var inner = innerType(genericReturnType);
+                               return 
CompletableFuture.completedFuture(processBodyValue(req, rawClass(inner), inner, 
method));
+                       }
+                       return processBodyValue(req, returnType, 
genericReturnType, method);
+               }
+
+               /** Runs the request and materializes a single (unwrapped) body 
value. */
+               private Object processBodyValue(RestRequest req, Class<?> 
returnType, Type genericReturnType, Method method) throws Exception {
+                       try (var resp = req.run()) { // HTT - exception during 
close() branch
+                               throwIfError(resp, method);
+                               if (returnType == void.class || returnType == 
Void.class)
+                                       return null;
+                               if (returnType == String.class)
+                                       return resp.getBodyAsString();
+                               if (returnType == InputStream.class)
+                                       return resp.getBodyStream();
+                               if (returnType == byte[].class)
+                                       return resp.body().asBytes();
+                               if 
(BasicHttpResponse.class.isAssignableFrom(returnType) || 
BasicHttpException.class.isAssignableFrom(returnType))
+                                       return instantiateHttpType(returnType, 
resp.getBodyAsString());
+                               return parseBody(resp, genericReturnType);
+                       }
+               }
+
+               /**
+                * Maps an error HTTP status (&ge;400) to a typed exception 
declared in the method's {@code throws} clause (G4).
+                *
+                * <p>
+                * A declared exception type matches when it extends {@link 
BasicHttpException} and its static {@code STATUS_CODE}
+                * field equals the response status code (e.g. {@code 404} 
&rarr; {@link org.apache.juneau.http.response.NotFound}).
+                * If no declared type matches, the response is left for normal 
body handling.
+                */
+               private static void throwIfError(RestResponse resp, Method 
method) throws Exception {
+                       var sc = resp.getStatusCode();
+                       if (sc < 400)
+                               return;
+                       for (var et : method.getExceptionTypes()) {
+                               if 
(BasicHttpException.class.isAssignableFrom(et) && httpStatusCode(et) == sc)
+                                       throw (BasicHttpException) 
instantiateHttpType(et, resp.getBodyAsString());
+                       }
+               }
+
+               /** Returns the static {@code STATUS_CODE} field of an HTTP 
response/exception type, or {@code -1} if absent. */
+               private static int httpStatusCode(Class<?> c) {
+                       try {
+                               return c.getField("STATUS_CODE").getInt(null);
+                       } catch (ReflectiveOperationException e) {
+                               return -1;
+                       }
+               }
+
+               /**
+                * Instantiates an HTTP response/exception bean, preferring a 
{@code (String body)} constructor and falling back
+                * to the no-arg constructor (status code is intrinsic to the 
type).
+                */
+               private static Object instantiateHttpType(Class<?> c, String 
body) {
+                       try {
+                               try {
+                                       return 
c.getConstructor(String.class).newInstance(body);
+                               } catch (NoSuchMethodException e) {
+                                       return c.getConstructor().newInstance();
+                               }
+                       } catch (ReflectiveOperationException e) {
+                               throw rex(e, "Could not instantiate HTTP 
response/exception type {0}", c.getName());
+                       }
+               }
+
+               /** Returns the raw {@link Class} of a possibly-parameterized 
type. */
+               private static Class<?> rawClass(Type t) {
+                       if (t instanceof Class<?> c)
+                               return c;
+                       if (t instanceof ParameterizedType p)
+                               return (Class<?>) p.getRawType();
+                       return Object.class;
+               }
+
+               /** Returns the first type argument of a parameterized wrapper 
(e.g. {@code Optional<T>} &rarr; {@code T}). */
+               private static Type innerType(Type t) {
+                       if (t instanceof ParameterizedType p) {
+                               var args = p.getActualTypeArguments();
+                               if (args.length > 0)
+                                       return args[0];
+                       }
+                       return Object.class;
+               }
+
+               private static RestRequest withContentBody(RestRequest req, 
Object arg) throws IOException {
+                       if (arg instanceof HttpBody b)
+                               return req.body(b);
+                       if (arg instanceof String s)
+                               return req.bodyString(s);
+                       if (arg instanceof Reader r)
+                               return req.bodyString(readReader(r));
+                       return 
req.bodyString(JsonSerializer.DEFAULT.serialize(arg));
+               }
+
+               private static Object parseBody(RestResponse resp, Type 
returnType) throws Exception {
+                       var body = resp.getBodyAsString();
+                       if (body == null)
+                               return null;
+                       try {
+                               return JsonParser.DEFAULT.parse(body, 
returnType);
+                       } catch (ParseException e) {
+                               if (returnType == Object.class)
+                                       return body;
+                               throw e;
+                       }
+               }
+
+               private static String readReader(Reader reader) throws 
IOException {
+                       var sb = new StringBuilder();
+                       var buffer = new char[4096];
+                       int len;
+                       while ((len = reader.read(buffer)) != -1)
+                               sb.append(buffer, 0, len);
+                       return sb.toString();
+               }
+
                private static String combinePaths(String base, String method) {
                        if (base.isEmpty())
                                return method.isEmpty() ? "" : method;
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RemoteProxy_FeatureParity_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RemoteProxy_FeatureParity_Test.java
index c819841a83..de485eace1 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RemoteProxy_FeatureParity_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RemoteProxy_FeatureParity_Test.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.io.*;
 import java.util.*;
 
+import org.apache.juneau.commons.annotation.Schema;
 import org.apache.juneau.http.HttpParts;
 import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.http.header.HttpHeaderList;
@@ -50,10 +51,13 @@ import org.junit.jupiter.api.*;
  * </ul>
  *
  * <p>
- * Capabilities the beta engine does not yet support are landed as <b>{@code 
@Disabled}</b> specs whose bodies assert the
- * <i>desired</i> parity behavior, so each flips green when its gap closes.  
Gap ids (G1&ndash;G11) match the plan's gap
- * inventory.  The synthetic interface, DTOs, enums, and the shared {@code 
@Rest} fixture are nested static types below
- * (house convention, mirroring {@code RemoteClient_Test}).
+ * Capabilities were originally landed as <b>{@code @Disabled}</b> specs whose 
bodies assert the <i>desired</i> parity
+ * behavior, each flipping green as its gap closed.  Gap ids (G1&ndash;G12) 
match the plan's gap inventory; as of the
+ * G6 + G12 slice <b>all gaps are closed</b> and all 72 cells are active (no 
remaining {@code @Disabled} specs) &mdash;
+ * G6 adds part-serializer/{@code @Schema} coverage ({@code b33}/{@code 
b34}/{@code c44}/{@code c45}) and G12 adds
+ * {@code @PathRemainder} coverage ({@code b35}/{@code c46}).  The synthetic 
interface, DTOs,
+ * enums, and the shared {@code @Rest} fixture are nested static types below 
(house convention, mirroring
+ * {@code RemoteClient_Test}).
  *
  * <p>
  * Cross-walk: each synthetic method's comment names the feature id (F-row) it 
exercises.
@@ -133,7 +137,7 @@ class RemoteProxy_FeatureParity_Test {
                @RemoteGet(path="/ping", returns=RemoteReturn.RESPONSE) 
RestResponse pingResponse();    // RemoteReturn.RESPONSE
                @RemoteGet("/ping")                                     void 
pingVoid();                // void return
 
-               // ---- Gap rows (present-but-@Disabled) 
-----------------------------------------------------------------------
+               // ---- Former gap rows (all now supported) 
--------------------------------------------------------------------
 
                @RemotePost("/beanContent")               String 
postBean(@Content A_Bean bean);          // F12 @Content bean            (G1)
                @RemotePost("/listContent")               String 
postList(@Content List<A_Bean> beans);   // F13 @Content List<bean>      (G1)
@@ -169,6 +173,13 @@ class RemoteProxy_FeatureParity_Test {
 
                @RemoteGet("/list")                       
java.util.concurrent.CompletableFuture<List<A_Bean>> getListAsync(); // G11 
async return
                @RemoteGet("/bean")                       Optional<A_Bean> 
getBeanOptional();             // G11 Optional return
+
+               // G6 — part serializer honors @Schema (collection format, 
skipIfEmpty) instead of toString()/enum-name luck
+               @RemoteGet("/csv")                        String 
getPipes(@Query(name="tags", schema=@Schema(collectionFormat="pipes")) String[] 
tags);     // G6 collectionFormat=pipes
+               @RemoteGet("/skip")                       String 
getSkip(@Query(name="q", schema=@Schema(skipIfEmpty=true)) String q, 
@Query("keep") String keep); // G6 skipIfEmpty
+
+               // G12 — @PathRemainder appended as the trailing path remainder
+               @RemoteGet("/remainder")                  String 
getRemainder(@PathRemainder String remainder);  // G12 @PathRemainder
        }
 
        /** Tiny secondary interface to assert {@code @RemoteOp} verb 
resolution (F-row verb completeness). */
@@ -228,6 +239,9 @@ class RemoteProxy_FeatureParity_Test {
                @RestGet("/rest/dynh")  public String dynH(@Header("a") String 
a, @Header("b") String b) { return "a=" + a + ",b=" + b; }
                @RestPost("/rest/dynf") public String dynF(@FormData("a") 
String a, @FormData("b") String b) { return "a=" + a + ",b=" + b; }
                @RestGet("/rest/def")   public String def(@Query("view") String 
view) { return "view=" + view; }
+               @RestGet("/rest/csv")   public String csv(@Query("tags") String 
tags) { return "tags=" + tags; }
+               @RestGet("/rest/skip")  public String skip(@Query("q") String 
q, @Query("keep") String keep) { return "q=" + q + ",keep=" + keep; }
+               @RestGet("/rest/remainder/*") public String 
remainder(@PathRemainder String r) { return "r=" + r; }
        }
 
        // 
=================================================================================================================
@@ -383,9 +397,8 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               // ---- Gap cells (response/body-centric) — @Disabled 
--------------------------------------------------------
+               // ---- Former gap cells (response/body-centric) — now active 
------------------------------------------------
 
-               @Disabled("G1: next-gen RemoteClient stringifies @Content beans 
via String.valueOf instead of serializing with the configured serializer 
(RemoteClient.java:180-187; RestRequest.java:232 has no serializer in the 
converter chain).")
                @Test void b20_contentBean_serialized_F12() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                var b = new A_Bean(); b.setName("na44");
@@ -393,7 +406,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G1: next-gen RemoteClient does not serialize 
@Content List<bean> via the configured serializer (RemoteClient.java:180-187; 
RestRequest.java:232).")
                @Test void b21_contentList_serialized_F13() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                var b = new A_Bean(); b.setName("na44");
@@ -401,14 +413,12 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G7: Reader is not a convertible request-body type in 
the next-gen converter chain (RestRequest.java:238-245); RemoteClient 
stringifies it (RemoteClient.java:180-187).")
                @Test void b22_contentReader_streamed_F14() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("hello", proxy(mrc).postReader(new 
StringReader("hello")));
                        }
                }
 
-               @Disabled("G2: next-gen RemoteClient returns the raw body 
String for bean return types; ResponseBody has no as(Class) parser hook 
(RemoteClient.java:201-215; ResponseBody.java).")
                @Test void b23_returnBean_parsed_F15() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                var bean = proxy(mrc).getBean();
@@ -416,7 +426,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G2: next-gen RemoteClient cannot parse a response 
body into List<bean> (RemoteClient.java:201-215; ResponseBody.java).")
                @Test void b24_returnList_parsed_F16() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                var l = proxy(mrc).getList();
@@ -425,130 +434,133 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G2: next-gen RemoteClient cannot parse a response 
body into a Map (RemoteClient.java:201-215; ResponseBody.java).")
                @Test void b25_returnMap_parsed_F17() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("v", proxy(mrc).getMap().get("k"));
                        }
                }
 
-               @Disabled("G2: next-gen RemoteClient cannot parse a numeric 
response body into Integer (returns the raw String) 
(RemoteClient.java:201-215).")
                @Test void b26_returnInteger_parsed_F18() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals(Integer.valueOf(5), 
proxy(mrc).patchCount("x"));
                        }
                }
 
-               @Disabled("G2: next-gen RemoteClient returns the raw String for 
an Object return type instead of a parsed POJO (RemoteClient.java:213).")
                @Test void b27_returnObject_parsed_F19() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals(123, proxy(mrc).getObject());
                        }
                }
 
-               @Disabled("G2/G3: next-gen RemoteClient cannot materialize an 
HTTP-response bean (Ok); RemoteReturn.BEAN throws UnsupportedOperationException 
(RemoteClient.java:227).")
                @Test void b28_returnResponseBean_F21() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertNotNull(proxy(mrc).postOk("x"));
                        }
                }
 
-               @Disabled("G4: next-gen RemoteClient has no error-status -> 
typed-exception mapping (RemoteClient.java:201-230).")
                @Test void b29_typedException_onErrorStatus_F22() throws 
Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertThrows(NotFound.class, () -> 
proxy(mrc).getOrThrow("missing"));
                        }
                }
 
-               @Disabled("G5: @FormData parameters are not bound by next-gen 
RemoteClient.buildRequest (RemoteClient.java:151-195).")
                @Test void b30_formData_bound_G5() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=v", proxy(mrc).postForm("v"));
                        }
                }
 
-               @Disabled("G5: @Request beans are not expanded into parts by 
next-gen RemoteClient.buildRequest (RemoteClient.java:151-195).")
                @Test void b31_requestBean_expanded_G5() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=ra", proxy(mrc).getReq(new 
A_RequestBean()));
                        }
                }
 
-               @Disabled("G11: next-gen RemoteClient does not implement 
Optional<T> returns (RemoteClient.java:201-229); classic engine does.")
                @Test void b32_optionalReturn_G11() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                
assertTrue(proxy(mrc).getBeanOptional().isPresent());
                        }
                }
 
+               // ---- G6 (part serializer / @Schema) + G12 (@PathRemainder) — 
final slice -----------------------------------
+
+               @Test void b33_partSerializer_collectionFormat_G6() throws 
Exception {
+                       try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
+                               // @Schema(collectionFormat="pipes") -> "a|b|c" 
rather than the array's toString()
+                               assertEquals("tags=a|b|c", 
proxy(mrc).getPipes(new String[]{"a", "b", "c"}));
+                       }
+               }
+
+               @Test void b34_partSerializer_skipIfEmpty_G6() throws Exception 
{
+                       try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
+                               // @Schema(skipIfEmpty=true) on an empty value 
omits the "q" param entirely
+                               assertEquals("q=null,keep=y", 
proxy(mrc).getSkip("", "y"));
+                       }
+               }
+
+               @Test void b35_pathRemainder_G12() throws Exception {
+                       try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
+                               assertEquals("r=abc", 
proxy(mrc).getRemainder("abc"));
+                       }
+               }
+
                // ---- G8: dynamic name/value pairs (round-trip) — server 
echoes the N distinct parts it received ----------
-               // Desired parity: each map/part-list/bean expands to TWO 
distinct parts (a=1, b=2). The engine currently
-               // stringifies the whole argument into a single bogus part, so 
each cell is @Disabled until G8 closes.
+               // Parity: each map/part-list/bean expands to TWO distinct 
parts (a=1, b=2) rather than one bogus part (G8 closed).
 
-               @Disabled("G8: @Query Map is not expanded into distinct query 
params — engine stringifies the whole map as one value 
(RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void b40_dynQueryMap_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynQueryMap(Map.<String,Object>of("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @Query(\"*\") Map dynamic-pairs contract not 
honored (RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void b41_dynQueryMapStar_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynQueryMapStar(Map.<String,Object>of("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @Query(\"*\") PartList (NameValuePairs) form not 
expanded into distinct query params (RemoteClient.java:166-170). Classic: 
RestRequest.java:2801-2825.")
                @Test void b42_dynQueryPartList_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynQueryPartList(PartList.of(HttpParts.part("a", "1"), 
HttpParts.part("b", "2"))));
                        }
                }
 
-               @Disabled("G8: @Query bean not expanded into per-property query 
params (RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void b43_dynQueryBean_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynQueryBean(A_PairBean.of("1", "2")));
                        }
                }
 
-               @Disabled("G8: @Header Map is not expanded into distinct 
headers (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void b44_dynHeaderMap_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynHeaderMap(Map.<String,Object>of("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @Header(\"*\") Map dynamic-pairs contract not 
honored (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void b45_dynHeaderMapStar_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynHeaderMapStar(Map.<String,Object>of("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @Header(\"*\") HttpHeaderList (NameValuePairs) 
form not expanded into distinct headers (RemoteClient.java:173-178). Classic: 
RestRequest.java:2733-2767.")
                @Test void b46_dynHeaderList_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynHeaderList(HttpHeaderList.ofPairs("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @Header bean not expanded into per-property 
headers (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void b47_dynHeaderBean_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynHeaderBean(A_PairBean.of("1", "2")));
                        }
                }
 
-               @Disabled("G8: @FormData Map is not bound/expanded into 
distinct form fields by next-gen RemoteClient (@FormData unbound: 
RemoteClient.java:151-195; G5/G8). Classic: RestRequest.java:formDataArg.")
                @Test void b48_dynFormDataMap_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynFormDataMap(Map.<String,Object>of("a", "1", "b", "2")));
                        }
                }
 
-               @Disabled("G8: @FormData(\"*\") Map dynamic-pairs not 
bound/expanded by next-gen RemoteClient (@FormData unbound: 
RemoteClient.java:151-195; G5/G8). Classic: RestRequest.java:formDataArg.")
                @Test void b49_dynFormDataMapStar_G8() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                assertEquals("a=1,b=2", 
proxy(mrc).dynFormDataMapStar(Map.<String,Object>of("a", "1", "b", "2")));
@@ -657,9 +669,8 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               // ---- Gap cells (request-serialization-centric) — @Disabled 
------------------------------------------------
+               // ---- Former gap cells (request-serialization-centric) — now 
active ----------------------------------------
 
-               @Disabled("G1: @Content bean is stringified via String.valueOf 
rather than serialized to JSON (RemoteClient.java:180-187; 
RestRequest.java:232).")
                @Test void c20_contentBean_serializedRequest_F12() throws 
Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -669,7 +680,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G7: Reader is not a convertible request-body type 
(RestRequest.java:238-245); RemoteClient stringifies it 
(RemoteClient.java:180-187).")
                @Test void c21_contentReader_streamedRequest_F14() throws 
Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -680,7 +690,6 @@ class RemoteProxy_FeatureParity_Test {
 
                // ---- G8: dynamic name/value pairs (request shape) — assert 
TWO distinct parts are emitted from the map ----
 
-               @Disabled("G8: @Query Map is not expanded into separate query 
params — the engine stringifies the whole map as one value 
(RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void c22_dynQueryMap_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -690,7 +699,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Query(\"*\") Map dynamic-pairs contract not 
honored (RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void c23_dynQueryMapStar_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -700,7 +708,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Query(\"*\") PartList (NameValuePairs) form not 
expanded into separate query params (RemoteClient.java:166-170). Classic: 
RestRequest.java:2801-2825.")
                @Test void c24_dynQueryPartList_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -710,7 +717,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Query bean not expanded into per-property query 
params (RemoteClient.java:166-170). Classic: RestRequest.java:2801-2825; 
Query.java:124-164.")
                @Test void c25_dynQueryBean_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -720,7 +726,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Header Map is not expanded into separate 
headers (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void c26_dynHeaderMap_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -730,7 +735,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Header(\"*\") Map dynamic-pairs contract not 
honored (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void c27_dynHeaderMapStar_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -740,7 +744,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Header(\"*\") HttpHeaderList (NameValuePairs) 
form not expanded into separate headers (RemoteClient.java:173-178). Classic: 
RestRequest.java:2733-2767.")
                @Test void c28_dynHeaderList_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -750,7 +753,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8: @Header bean not expanded into per-property 
headers (RemoteClient.java:173-178). Classic: RestRequest.java:2733-2767; 
Header.java:116-171.")
                @Test void c29_dynHeaderBean_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -760,7 +762,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8/G5: @FormData Map is neither bound nor expanded 
into separate form fields by next-gen RemoteClient (RemoteClient.java:151-195). 
Classic: RestRequest.java:formDataArg.")
                @Test void c30_dynFormDataMap_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -770,7 +771,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G8/G5: @FormData(\"*\") Map dynamic-pairs neither 
bound nor expanded by next-gen RemoteClient (RemoteClient.java:151-195). 
Classic: RestRequest.java:formDataArg.")
                @Test void c31_dynFormDataMapStar_G8() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -780,7 +780,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G9: @Query(def=...) parameter default is not applied 
for a null argument — the engine skips null args entirely 
(RemoteClient.java:156). Classic applies parameter defaults 
(RestClient.java:7244-7255).")
                @Test void c40_paramDefault_G9() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -789,7 +788,6 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G5: @FormData is not bound to a form-data part by 
next-gen RemoteClient (RemoteClient.java:151-195).")
                @Test void c41_formData_request_G5() throws Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
@@ -798,16 +796,51 @@ class RemoteProxy_FeatureParity_Test {
                        }
                }
 
-               @Disabled("G11: CompletableFuture<T> async returns are not 
implemented by next-gen RemoteClient (RemoteClient.java:201-229); classic 
engine submits to an executor (RestClient.java:7330-7349).")
                @Test void c42_asyncReturn_G11() throws Exception {
+                       // Dedicated transport: the shared capture stub returns 
the literal body "result" (asserted by c01/c10),
+                       // which is not valid JSON for List<A_Bean>; this stub 
returns a parseable list so the async return can be verified.
+                       var t = MockHttpTransport.builder()
+                               .fallback(req -> 
TransportResponse.builder().statusCode(200).body(new 
ByteArrayInputStream("[{\"name\":\"na44\"}]".getBytes())).build())
+                               .build();
+                       try (var c = 
RestClient.builder().transport(t).rootUrl("http://x.com";).build()) {
+                               var f = 
c.remote(A_ParityClient.class).getListAsync();
+                               var list = f.get();
+                               assertNotNull(list);
+                               assertEquals(1, list.size());
+                               assertEquals("na44", list.get(0).getName());
+                       }
+               }
+
+               // ---- G6 (part serializer / @Schema) + G12 (@PathRemainder) — 
request-shape ---------------------------------
+
+               @Test void c44_partSerializer_collectionFormat_G6() throws 
Exception {
                        var captured = new ArrayList<TransportRequest>();
                        try (var c = client(captured)) {
-                               var f = 
c.remote(A_ParityClient.class).getListAsync();
-                               assertNotNull(f.get());
+                               c.remote(A_ParityClient.class).getPipes(new 
String[]{"a", "b", "c"});
+                               var uri = 
java.net.URLDecoder.decode(captured.get(0).getUri().toString(), 
java.nio.charset.StandardCharsets.UTF_8);
+                               assertTrue(uri.contains("tags=a|b|c"), uri);
+                       }
+               }
+
+               @Test void c45_partSerializer_skipIfEmpty_G6() throws Exception 
{
+                       var captured = new ArrayList<TransportRequest>();
+                       try (var c = client(captured)) {
+                               c.remote(A_ParityClient.class).getSkip("", "y");
+                               var uri = captured.get(0).getUri().toString();
+                               assertTrue(uri.contains("keep=y"), uri);
+                               assertFalse(uri.contains("q="), uri);
+                       }
+               }
+
+               @Test void c46_pathRemainder_G12() throws Exception {
+                       var captured = new ArrayList<TransportRequest>();
+                       try (var c = client(captured)) {
+                               
c.remote(A_ParityClient.class).getRemainder("a/b/c");  // multi-segment: 
slashes preserved
+                               var path = captured.get(0).getUri().getPath();
+                               
assertTrue(path.endsWith("/rest/remainder/a/b/c"), path);
                        }
                }
 
-               @Disabled("G2/G1 (F23): full JSON serializer+parser 
request/response round-trip not wired through the next-gen RemoteClient 
(RemoteClient.java:180-215; RestRequest.java:232; ResponseBody.java).")
                @Test void c43_jsonRoundTrip_F23() throws Exception {
                        try (var mrc = 
MockRestClient.create(A_ParityResource.class)) {
                                var svc = 
mrc.getClient().remote(A_ParityClient.class);
diff --git a/juneau-utest/test-run-history.tsv 
b/juneau-utest/test-run-history.tsv
index 56369923b0..082d8bb205 100644
--- a/juneau-utest/test-run-history.tsv
+++ b/juneau-utest/test-run-history.tsv
@@ -63,3 +63,4 @@ timestamp     git_sha branch  tests_run       failures        
errors  skipped surefire_sec    wall_sec
 2026-06-01T18:55:33Z   8eceb21bb5b9    master  126190  0       0       26      
185
 2026-06-01T19:34:19Z   f8486683b13c    master  126197  0       0       26      
185
 2026-06-02T11:01:51Z   9363babe275f    master  126197  0       0       26      
181
+2026-06-02T12:03:07Z   46da4ebd52bd    master  126197  0       0       26      
175


Reply via email to