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
@@ -409,6 +409,37 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
CancellationToken cancellationToken = default)
{
WorkerEnvelope commandEnvelope = await ReadCommandAsync(cancellationToken).ConfigureAwait(false);
return await RespondToControlCommandAsync(commandEnvelope, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Accepts an already-read command envelope and, if it is one of the five control
/// command kinds (Ping, GetSessionState, GetWorkerInfo, DrainEvents, ShutdownWorker),
/// writes a canned reply that mirrors the real worker's reply shape. For ShutdownWorker
/// the method additionally sends a <see cref="WorkerShutdownAck"/> after the OK reply.
/// Use this overload when the caller has already consumed the envelope from the pipe
/// (e.g., to inspect the kind before routing) to avoid re-reading.
/// </summary>
/// <param name="commandEnvelope">The already-read command envelope to respond to.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>The command envelope that was handled.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="commandEnvelope"/> does not contain a <c>WorkerCommand</c>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the command kind is not one of the five control command kinds.
/// </exception>
public async Task<WorkerEnvelope> RespondToControlCommandAsync(
WorkerEnvelope commandEnvelope,
CancellationToken cancellationToken = default)
{
if (commandEnvelope.BodyCase != WorkerEnvelope.BodyOneofCase.WorkerCommand)
{
throw new ArgumentException(
$"Expected WorkerCommand envelope but received {commandEnvelope.BodyCase}.",
nameof(commandEnvelope));
}
MxCommand command = commandEnvelope.WorkerCommand.Command;
switch (command.Kind)