fix(java): picocli ParameterException for browse --depth; warn on --parent 0

Replaces the raw IllegalArgumentException thrown by GalaxyBrowseCommand for
--depth < 0 with a CommandLine.ParameterException so picocli surfaces a clean
single-line error instead of an unhandled stack trace. Adds an upper bound of
50 (matching the Python client) so --depth > 50 is also rejected cleanly.

Emits a stderr warning when --parent 0 is supplied explicitly, matching
Go/Rust client behaviour, because gobject id 0 is the server's root-walk
sentinel and passing it via --parent is almost always a mistake.

Adds three new tests: negative depth, depth > 50, and the --parent 0 warning path.
This commit is contained in:
Joseph Doherty
2026-06-15 11:08:07 -04:00
parent 0d5b488c11
commit b298ca74be
5 changed files with 104 additions and 77 deletions
@@ -156,7 +156,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
Assert.Equal(ProtocolStatusCode.Ok, infoReply.ProtocolStatus.Code);
Assert.Equal(MxCommandKind.GetWorkerInfo, infoReply.Kind);
Assert.NotNull(infoReply.WorkerInfo);
Assert.Equal(ControlCommandFakeWorkerProcessLauncher.ProcessId, infoReply.WorkerInfo.WorkerProcessId);
Assert.Equal(FakeWorkerHarness.DefaultWorkerProcessId, infoReply.WorkerInfo.WorkerProcessId);
Assert.False(string.IsNullOrEmpty(infoReply.WorkerInfo.MxaccessProgid));
// DrainEvents — the scripted worker returns an empty drain reply.
@@ -522,9 +522,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
or MxCommandKind.GetWorkerInfo or MxCommandKind.DrainEvents
or MxCommandKind.ShutdownWorker)
{
// Re-enter the harness to process the already-read envelope
// by replaying it through the control-command responder path.
await RespondToKnownControlCommandAsync(harness, envelope, cancellationToken)
await harness.RespondToControlCommandAsync(envelope, cancellationToken)
.ConfigureAwait(false);
_commandHandled.Release();
continue;
@@ -535,70 +533,6 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
$"ControlCommandFakeWorkerProcessLauncher received unexpected envelope {envelope.BodyCase}.");
}
}
private static async Task RespondToKnownControlCommandAsync(
FakeWorkerHarness harness,
WorkerEnvelope commandEnvelope,
CancellationToken cancellationToken)
{
MxCommand command = commandEnvelope.WorkerCommand.Command;
switch (command.Kind)
{
case MxCommandKind.Ping:
await harness.ReplyToCommandAsync(
commandEnvelope,
configureReply: reply =>
{
string? message = command.Ping?.Message;
if (!string.IsNullOrEmpty(message))
{
reply.DiagnosticMessage = message;
}
},
cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case MxCommandKind.GetSessionState:
await harness.ReplyToCommandAsync(
commandEnvelope,
configureReply: reply => reply.SessionState = new SessionStateReply
{
State = SessionState.Ready,
},
cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case MxCommandKind.GetWorkerInfo:
await harness.ReplyToCommandAsync(
commandEnvelope,
configureReply: reply => reply.WorkerInfo = new WorkerInfoReply
{
WorkerProcessId = ControlCommandFakeWorkerProcessLauncher.ProcessId,
WorkerVersion = "fake-control-worker",
MxaccessProgid = "LMXProxy.LMXProxyServer.1",
MxaccessClsid = "{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}",
},
cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case MxCommandKind.DrainEvents:
await harness.ReplyToCommandAsync(
commandEnvelope,
configureReply: reply => reply.DrainEvents = new DrainEventsReply(),
cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case MxCommandKind.ShutdownWorker:
await harness.ReplyToCommandAsync(commandEnvelope, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await harness.SendShutdownAckAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
break;
default:
throw new InvalidOperationException(
$"Unexpected control command kind {command.Kind} in ControlCommandFakeWorkerProcessLauncher.");
}
}
}
private sealed class FakeWorkerProcess(int processId) : IWorkerProcess