feat(java): add ping + galaxy-browse CLI subcommands and galaxy command aliases

- D4: add 'ping' subcommand (MX_COMMAND_KIND_PING / PingCommand{message}),
  accepting --session-id and optional --message (default "ping"); prints the
  worker's echoed diagnostic message.
- D8-java: add 'galaxy-browse' subcommand over browse()/LazyBrowseNode.expand()
  and raw BrowseChildren paging for --parent. JSON node shape matches the
  cross-client surface (flattened object fields + hasChildrenHint + nested
  children array).
- D9-java: make galaxy-test-connection / galaxy-last-deploy the primary names,
  keeping galaxy-test / galaxy-deploy-time as deprecated picocli aliases.
- Tests for ping, galaxy-browse JSON hasChildrenHint key, and alias resolution.
- README updated for the new/renamed subcommands.
This commit is contained in:
Joseph Doherty
2026-06-15 10:58:04 -04:00
parent bb5139fec2
commit 0d5b488c11
3 changed files with 463 additions and 12 deletions
+23 -6
View File
@@ -115,17 +115,33 @@ try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
messages directly so callers can read all fields (including the nested
`GalaxyAttribute` list) without an extra DTO layer.
The CLI exposes matching subcommands: `galaxy-test`, `galaxy-deploy-time`,
`galaxy-discover`, and `galaxy-watch`. They take the same `--endpoint`,
`--api-key-env`, `--plaintext`, `--ca-file`, `--server-name-override`,
`--timeout`, and `--json` options as the gateway commands.
The CLI exposes matching subcommands: `galaxy-test-connection`,
`galaxy-last-deploy`, `galaxy-discover`, `galaxy-browse`, and `galaxy-watch`.
The short names `galaxy-test` and `galaxy-deploy-time` remain as deprecated
aliases for `galaxy-test-connection` and `galaxy-last-deploy` so existing
scripts keep working. They take the same `--endpoint`, `--api-key-env`,
`--plaintext`, `--ca-file`, `--server-name-override`, `--timeout`, and `--json`
options as the gateway commands.
```powershell
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-test-connection --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-last-deploy --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
```
`galaxy-browse` walks the hierarchy via `BrowseChildren`. Without `--parent` it
returns the root nodes and eagerly expands `--depth` further levels; with
`--parent <gobject-id>` it returns exactly one level of children for that
parent. The filter flags (`--category-ids`, `--template-contains`,
`--tag-name-glob`, `--alarm-bearing-only`, `--historized-only`,
`--include-attributes`) match `galaxy-discover`. The `--json` node shape is the
cross-client browse surface: the flattened object fields plus a
`hasChildrenHint` flag and a nested `children` array.
```powershell
gradle :zb-mom-ww-mxgateway-cli:run --args="galaxy-browse --depth 1 --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
```
### Browsing lazily
For UI trees or OPC UA bridges, use `browseChildrenRaw` to walk one level at a
@@ -239,6 +255,7 @@ Run the CLI through Gradle:
```powershell
gradle :zb-mom-ww-mxgateway-cli:run --args="version --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="ping --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --message hello --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
gradle :zb-mom-ww-mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
@@ -1,7 +1,9 @@
package com.zb.mom.ww.mxgateway.cli;
import com.zb.mom.ww.mxgateway.client.BrowseChildrenOptions;
import com.zb.mom.ww.mxgateway.client.DeployEventStream;
import com.zb.mom.ww.mxgateway.client.GalaxyRepositoryClient;
import com.zb.mom.ww.mxgateway.client.LazyBrowseNode;
import com.zb.mom.ww.mxgateway.client.MxEventStream;
import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
import com.zb.mom.ww.mxgateway.client.MxGatewayClient;
@@ -10,6 +12,8 @@ import com.zb.mom.ww.mxgateway.client.MxGatewayClientVersion;
import com.zb.mom.ww.mxgateway.client.MxGatewaySecrets;
import com.zb.mom.ww.mxgateway.client.MxGatewaySession;
import com.zb.mom.ww.mxgateway.client.MxValues;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyAttribute;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
@@ -26,8 +30,10 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
@@ -42,11 +48,14 @@ import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommand;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OnAlarmTransitionEvent;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.PingCommand;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
import mxaccess_gateway.v1.MxaccessGateway.Write2BulkEntry;
@@ -126,6 +135,7 @@ public final class MxGatewayCli implements Callable<Integer> {
commandLine.addSubcommand("version", new VersionCommand());
commandLine.addSubcommand("open-session", new OpenSessionCommand(clientFactory));
commandLine.addSubcommand("close-session", new CloseSessionCommand(clientFactory));
commandLine.addSubcommand("ping", new PingCommandLine(clientFactory));
commandLine.addSubcommand("register", new RegisterCommand(clientFactory));
commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory));
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
@@ -142,9 +152,10 @@ public final class MxGatewayCli implements Callable<Integer> {
commandLine.addSubcommand("stream-alarms", new StreamAlarmsCommand(clientFactory));
commandLine.addSubcommand("acknowledge-alarm", new AcknowledgeAlarmCommand(clientFactory));
commandLine.addSubcommand("smoke", new SmokeCommand(clientFactory));
commandLine.addSubcommand("galaxy-test", new GalaxyTestConnectionCommand());
commandLine.addSubcommand("galaxy-deploy-time", new GalaxyDeployTimeCommand());
commandLine.addSubcommand("galaxy-test-connection", new GalaxyTestConnectionCommand());
commandLine.addSubcommand("galaxy-last-deploy", new GalaxyDeployTimeCommand());
commandLine.addSubcommand("galaxy-discover", new GalaxyDiscoverCommand());
commandLine.addSubcommand("galaxy-browse", new GalaxyBrowseCommand());
commandLine.addSubcommand("galaxy-watch", new GalaxyWatchCommand());
commandLine.addSubcommand("batch", new BatchCommand(clientFactory));
return commandLine;
@@ -359,7 +370,10 @@ public final class MxGatewayCli implements Callable<Integer> {
}
}
@Command(name = "galaxy-test", description = "Calls GalaxyRepository.TestConnection.")
@Command(
name = "galaxy-test-connection",
aliases = {"galaxy-test"},
description = "Calls GalaxyRepository.TestConnection.")
static final class GalaxyTestConnectionCommand extends GalaxyCommand {
@Override
public Integer call() {
@@ -368,7 +382,7 @@ public final class MxGatewayCli implements Callable<Integer> {
PrintWriter out = common.spec.commandLine().getOut();
if (json) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("command", "galaxy-test");
output.put("command", "galaxy-test-connection");
output.put("options", common.redactedJsonMap());
output.put("ok", ok);
out.println(jsonObject(output));
@@ -380,7 +394,10 @@ public final class MxGatewayCli implements Callable<Integer> {
}
}
@Command(name = "galaxy-deploy-time", description = "Calls GalaxyRepository.GetLastDeployTime.")
@Command(
name = "galaxy-last-deploy",
aliases = {"galaxy-deploy-time"},
description = "Calls GalaxyRepository.GetLastDeployTime.")
static final class GalaxyDeployTimeCommand extends GalaxyCommand {
@Override
public Integer call() {
@@ -389,7 +406,7 @@ public final class MxGatewayCli implements Callable<Integer> {
PrintWriter out = common.spec.commandLine().getOut();
if (json) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("command", "galaxy-deploy-time");
output.put("command", "galaxy-last-deploy");
output.put("options", common.redactedJsonMap());
output.put("present", result.isPresent());
output.put("timeOfLastDeploy", result.map(Instant::toString).orElse(""));
@@ -429,6 +446,260 @@ public final class MxGatewayCli implements Callable<Integer> {
}
}
/**
* Page size used for the raw {@code BrowseChildren} paging loop driven by
* the {@code --parent} one-level path. Mirrors {@code BROWSE_CHILDREN_PAGE_SIZE}
* in the client library's lazy-browse helper and the other clients' CLI page
* size so paging behaviour is consistent across languages.
*/
private static final int BROWSE_CHILDREN_CLI_PAGE_SIZE = 500;
@Command(
name = "galaxy-browse",
description = "Browses the Galaxy hierarchy via GalaxyRepository.BrowseChildren.")
static final class GalaxyBrowseCommand extends GalaxyCommand {
@Option(
names = "--parent",
defaultValue = "-1",
description = "Parent gobject id to browse one level of children for; omit to walk the roots.")
int parent;
@Option(
names = "--depth",
defaultValue = "0",
description = "When walking roots, eagerly expand this many further levels before printing.")
int depth;
@Option(names = "--category-ids", description = "Comma-separated category ids to include.")
String categoryIds;
@Option(names = "--template-contains", description = "Comma-separated template names each child's chain must contain.")
String templateContains;
@Option(names = "--tag-name-glob", description = "SQL-LIKE-style glob applied to tag_name.")
String tagNameGlob;
@Option(names = "--alarm-bearing-only", description = "Restrict to alarm-bearing objects.")
boolean alarmBearingOnly;
@Option(names = "--historized-only", description = "Restrict to objects with at least one historized attribute.")
boolean historizedOnly;
@Option(names = "--include-attributes", description = "Request attribute population on each returned object.")
boolean includeAttributes;
@Override
public Integer call() {
if (depth < 0) {
throw new IllegalArgumentException("--depth must be non-negative");
}
BrowseChildrenOptions options = buildOptions();
PrintWriter out = common.spec.commandLine().getOut();
PrintWriter err = common.spec.commandLine().getErr();
try (GalaxyRepositoryClient client = connect()) {
if (parent >= 0) {
if (depth > 0) {
err.println("warning: --depth is ignored when --parent is specified.");
}
List<BrowseChild> children = browseOneLevel(client, parent, options);
if (json) {
List<Map<String, Object>> nodes = new ArrayList<>(children.size());
for (BrowseChild child : children) {
nodes.add(browseNodeMap(child.object(), child.hasChildrenHint(), List.of()));
}
Map<String, Object> output = new LinkedHashMap<>();
output.put("command", "galaxy-browse");
output.put("options", common.redactedJsonMap());
output.put("parentId", parent);
output.put("nodes", nodes);
out.println(jsonObject(output));
} else {
out.println(children.size());
for (BrowseChild child : children) {
printBrowseChild(out, child);
}
}
return 0;
}
List<LazyBrowseNode> roots = client.browse(options);
for (LazyBrowseNode root : roots) {
expandToDepth(root, depth);
}
if (json) {
List<Map<String, Object>> nodes = new ArrayList<>(roots.size());
for (LazyBrowseNode root : roots) {
nodes.add(lazyNodeMap(root));
}
Map<String, Object> output = new LinkedHashMap<>();
output.put("command", "galaxy-browse");
output.put("options", common.redactedJsonMap());
output.put("nodes", nodes);
out.println(jsonObject(output));
} else {
out.println(roots.size());
for (LazyBrowseNode root : roots) {
printLazyNode(out, root, 0);
}
}
}
return 0;
}
private BrowseChildrenOptions buildOptions() {
return BrowseChildrenOptions.builder()
.categoryIds(parseOptionalIntList(categoryIds))
.templateChainContains(parseOptionalStringList(templateContains))
.tagNameGlob(tagNameGlob == null ? "" : tagNameGlob)
// Tri-state: only override the server default when the flag is present.
.includeAttributes(includeAttributes ? Boolean.TRUE : null)
.alarmBearingOnly(alarmBearingOnly)
.historizedOnly(historizedOnly)
.build();
}
}
/** One raw {@code BrowseChildren} child paired with its server-supplied has-children hint. */
private record BrowseChild(GalaxyObject object, boolean hasChildrenHint) {
}
/**
* Drives the raw {@code BrowseChildren} paging loop for a single parent and
* returns the flattened one-level child list. Used by the {@code --parent}
* path, which surfaces a single level rather than the lazy root-tree walk.
*/
private static List<BrowseChild> browseOneLevel(
GalaxyRepositoryClient client, int parentGobjectId, BrowseChildrenOptions options) {
List<BrowseChild> children = new ArrayList<>();
Set<String> seenPageTokens = new HashSet<>();
String pageToken = "";
while (true) {
BrowseChildrenRequest.Builder builder = BrowseChildrenRequest.newBuilder()
.setPageSize(BROWSE_CHILDREN_CLI_PAGE_SIZE)
.setPageToken(pageToken)
.setParentGobjectId(parentGobjectId)
.setAlarmBearingOnly(options.isAlarmBearingOnly())
.setHistorizedOnly(options.isHistorizedOnly());
if (!options.getCategoryIds().isEmpty()) {
builder.addAllCategoryIds(options.getCategoryIds());
}
if (!options.getTemplateChainContains().isEmpty()) {
builder.addAllTemplateChainContains(options.getTemplateChainContains());
}
if (!options.getTagNameGlob().isEmpty()) {
builder.setTagNameGlob(options.getTagNameGlob());
}
if (options.getIncludeAttributes() != null) {
builder.setIncludeAttributes(options.getIncludeAttributes());
}
BrowseChildrenReply reply = client.browseChildrenRaw(builder.build());
for (int i = 0; i < reply.getChildrenCount(); i++) {
boolean hint = i < reply.getChildHasChildrenCount() && reply.getChildHasChildren(i);
children.add(new BrowseChild(reply.getChildren(i), hint));
}
pageToken = reply.getNextPageToken();
if (pageToken == null || pageToken.isEmpty()) {
return children;
}
if (!seenPageTokens.add(pageToken)) {
throw new IllegalStateException(
"galaxy browse children returned repeated page token: " + pageToken);
}
}
}
/**
* Recursively expands a {@link LazyBrowseNode} up to {@code depth} further
* levels. A {@code depth} of 0 leaves the node unexpanded so callers print
* only the requested level. Nodes the server reports as childless are not
* expanded.
*/
private static void expandToDepth(LazyBrowseNode node, int depth) {
if (depth <= 0) {
return;
}
if (node.hasChildrenHint()) {
node.expand();
}
for (LazyBrowseNode child : node.getChildren()) {
expandToDepth(child, depth - 1);
}
}
/**
* Renders one {@link LazyBrowseNode} (and any already-expanded descendants)
* as a JSON map. Mirrors the {@code galaxy-discover} object shape with an
* added {@code hasChildrenHint} flag and a nested {@code children} array,
* matching the cross-client browse JSON surface.
*/
private static Map<String, Object> lazyNodeMap(LazyBrowseNode node) {
List<Map<String, Object>> children = new ArrayList<>();
if (node.isExpanded()) {
for (LazyBrowseNode child : node.getChildren()) {
children.add(lazyNodeMap(child));
}
}
return browseNodeMap(node.getObject(), node.hasChildrenHint(), children);
}
/**
* Builds the per-node browse JSON map: the flattened Galaxy object fields,
* the {@code hasChildrenHint} flag, and a nested {@code children} array.
* The {@code hasChildrenHint} key is the cross-client standard (Rust /
* Python / .NET / Go all use the same key and node shape).
*/
static Map<String, Object> browseNodeMap(
GalaxyObject object, boolean hasChildrenHint, List<Map<String, Object>> children) {
Map<String, Object> values = galaxyObjectMap(object);
values.put("hasChildrenHint", hasChildrenHint);
values.put("children", children);
return values;
}
private static void printLazyNode(PrintWriter out, LazyBrowseNode node, int level) {
GalaxyObject obj = node.getObject();
out.printf(
"%s%d\t%s\t%s\t(attrs=%d, hasChildrenHint=%b)%n",
" ".repeat(level),
obj.getGobjectId(),
obj.getTagName(),
obj.getBrowseName(),
obj.getAttributesCount(),
node.hasChildrenHint());
if (node.isExpanded()) {
for (LazyBrowseNode child : node.getChildren()) {
printLazyNode(out, child, level + 1);
}
}
}
private static void printBrowseChild(PrintWriter out, BrowseChild child) {
GalaxyObject obj = child.object();
out.printf(
"%d\t%s\t%s\t(attrs=%d, hasChildrenHint=%b)%n",
obj.getGobjectId(),
obj.getTagName(),
obj.getBrowseName(),
obj.getAttributesCount(),
child.hasChildrenHint());
}
private static List<Integer> parseOptionalIntList(String value) {
if (value == null || value.isBlank()) {
return List.of();
}
return parseIntList(value);
}
private static List<String> parseOptionalStringList(String value) {
if (value == null || value.isBlank()) {
return List.of();
}
return parseStringList(value);
}
@Command(
name = "galaxy-watch",
description = "Streams GalaxyRepository.WatchDeployEvents until cancelled.")
@@ -622,6 +893,31 @@ public final class MxGatewayCli implements Callable<Integer> {
}
}
@Command(name = "ping", description = "Sends a diagnostic ping command to the session worker.")
static final class PingCommandLine extends GatewayCommand {
@Option(names = "--session-id", required = true, description = "Gateway session id.")
String sessionId;
@Option(names = "--message", defaultValue = "ping", description = "Message echoed back in the reply.")
String message;
PingCommandLine(MxGatewayCliClientFactory clientFactory) {
super(clientFactory);
}
@Override
public Integer call() {
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
MxCommandReply reply = client.session(sessionId).pingRaw(message);
// The worker echoes the message in the diagnostic message field;
// there is no dedicated ping reply payload, so the plain-text path
// surfaces that field.
writeOutput("ping", common, json, reply, reply::getDiagnosticMessage);
}
return 0;
}
}
@Command(name = "register", description = "Invokes MXAccess Register.")
static final class RegisterCommand extends GatewayCommand {
@Option(names = "--session-id", required = true, description = "Gateway session id.")
@@ -1438,6 +1734,8 @@ public final class MxGatewayCli implements Callable<Integer> {
}
interface MxGatewayCliSession {
MxCommandReply pingRaw(String message);
int register(String clientName);
MxCommandReply registerRaw(String clientName);
@@ -1523,6 +1821,14 @@ public final class MxGatewayCli implements Callable<Integer> {
}
record GrpcMxGatewayCliSession(MxGatewaySession session) implements MxGatewayCliSession {
@Override
public MxCommandReply pingRaw(String message) {
return session.invokeCommand(MxCommand.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_PING)
.setPing(PingCommand.newBuilder().setMessage(message))
.build());
}
@Override
public int register(String clientName) {
return session.register(clientName);
@@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
import com.zb.mom.ww.mxgateway.client.MxGatewayClientOptions;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -15,6 +16,7 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
@@ -124,6 +126,119 @@ final class MxGatewayCliTests {
assertTrue(run.output().contains("\"itemHandle\":7"));
}
// ---- ping subcommand (D4) ----
@Test
void pingCommandForwardsMessageAndPrintsEcho() {
FakeClientFactory factory = new FakeClientFactory();
CliRun run = execute(
factory, "ping", "--session-id", "session-cli", "--message", "hello-mxgw");
assertEquals(0, run.exitCode());
assertEquals("hello-mxgw", factory.client.session.lastPingMessage);
// The worker echoes the message in the diagnostic message field; the
// plain-text path surfaces exactly that echoed value.
assertEquals("hello-mxgw", run.output().trim());
}
@Test
void pingCommandDefaultsMessageToPing() {
FakeClientFactory factory = new FakeClientFactory();
CliRun run = execute(factory, "ping", "--session-id", "session-cli");
assertEquals(0, run.exitCode());
assertEquals("ping", factory.client.session.lastPingMessage);
}
@Test
void pingCommandJsonIncludesPingKindAndDiagnosticMessage() {
FakeClientFactory factory = new FakeClientFactory();
CliRun run = execute(
factory, "ping", "--session-id", "session-cli", "--message", "diag-1", "--json");
assertEquals(0, run.exitCode());
String out = run.output();
assertTrue(out.contains("\"command\":\"ping\""), out);
assertTrue(out.contains("\"kind\":\"MX_COMMAND_KIND_PING\""), out);
assertTrue(out.contains("diag-1"), out);
}
// ---- galaxy-browse subcommand (D8-java) ----
@Test
void galaxyBrowseNodeJsonUsesHasChildrenHintKeyAndFlattensObjectFields() {
GalaxyObject object = GalaxyObject.newBuilder()
.setGobjectId(101)
.setTagName("Area001")
.setBrowseName("Area001")
.build();
Map<String, Object> leaf = MxGatewayCli.browseNodeMap(
GalaxyObject.newBuilder().setGobjectId(202).setTagName("Pump001").build(),
false,
List.of());
Map<String, Object> node = MxGatewayCli.browseNodeMap(object, true, List.of(leaf));
// Cross-client JSON parity: the per-node "has children" flag MUST use the
// key hasChildrenHint (Rust / Python / .NET / Go all standardized on it).
assertTrue(node.containsKey("hasChildrenHint"), node.toString());
assertEquals(Boolean.TRUE, node.get("hasChildrenHint"));
// Object fields are flattened directly into the node (matching the
// galaxy-discover object shape), not nested under an "object" key.
assertFalse(node.containsKey("object"), node.toString());
assertEquals(101L, ((Number) node.get("gobjectId")).longValue());
assertEquals("Area001", node.get("tagName"));
// Nested children array carries the same node shape recursively.
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
assertEquals(1, children.size());
assertTrue(children.get(0).containsKey("hasChildrenHint"));
assertEquals(Boolean.FALSE, children.get(0).get("hasChildrenHint"));
}
@Test
void galaxyBrowseInvocationsParseCleanly() {
// galaxy-browse connects via GalaxyRepositoryClient.connect (a static),
// so the full surface is exercised only by the cross-language matrix
// against a live gateway. Here we assert the option surface parses.
assertReadmeExampleParses(new String[] {"galaxy-browse", "--json"});
assertReadmeExampleParses(new String[] {"galaxy-browse", "--parent", "42", "--json"});
assertReadmeExampleParses(new String[] {
"galaxy-browse",
"--depth", "2",
"--category-ids", "1,2",
"--template-contains", "$Pump",
"--tag-name-glob", "Area%",
"--alarm-bearing-only",
"--historized-only",
"--include-attributes",
"--json"
});
}
// ---- galaxy command-name aliases (D9-java) ----
@Test
void galaxyTestConnectionCanonicalAndDeprecatedAliasResolve() {
picocli.CommandLine commandLine = MxGatewayCli.commandLine(new FakeClientFactory());
// Both the canonical dash-separated name and the deprecated short alias
// must resolve to the same subcommand so existing scripts keep working.
assertTrue(commandLine.getSubcommands().containsKey("galaxy-test-connection"));
assertTrue(commandLine.getSubcommands().containsKey("galaxy-test"));
assertEquals(
commandLine.getSubcommands().get("galaxy-test-connection"),
commandLine.getSubcommands().get("galaxy-test"));
}
@Test
void galaxyLastDeployCanonicalAndDeprecatedAliasResolve() {
picocli.CommandLine commandLine = MxGatewayCli.commandLine(new FakeClientFactory());
assertTrue(commandLine.getSubcommands().containsKey("galaxy-last-deploy"));
assertTrue(commandLine.getSubcommands().containsKey("galaxy-deploy-time"));
assertEquals(
commandLine.getSubcommands().get("galaxy-last-deploy"),
commandLine.getSubcommands().get("galaxy-deploy-time"));
}
@Test
void subscribeBulkCommandPrintsResults() {
CliRun run = execute(
@@ -652,6 +767,19 @@ final class MxGatewayCliTests {
private boolean addItemCalled;
private boolean adviseCalled;
private MxValue lastWriteValue;
private String lastPingMessage;
@Override
public MxCommandReply pingRaw(String message) {
lastPingMessage = message;
// The worker echoes the request message in the diagnostic message
// field; there is no dedicated ping reply payload.
return MxCommandReply.newBuilder()
.setKind(MxCommandKind.MX_COMMAND_KIND_PING)
.setProtocolStatus(ok())
.setDiagnosticMessage(message)
.build();
}
@Override
public int register(String clientName) {