From 62b22854554ba4a9416451d5f6d59025817250a9 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Thu, 6 Aug 2026 01:15:16 -0700 Subject: [PATCH 1/3] Fix: name the unknown subcommand at the adk root The root command carries an action handler, so commander never reaches its unknownCommand() branch and answers a typo with an argument count instead. Guard the root action on its leftover operands and raise commander's own unknown-command error. The one pre-existing assertion that pinned commander.excessArguments for `adk bogus` encoded the behaviour this change fixes, so it now pins commander.unknownCommand. --- dev/src/cli/cli.ts | 11 +++++++++++ dev/test/cli/cli_test.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/dev/src/cli/cli.ts b/dev/src/cli/cli.ts index fb66c6f96..c46f28ce5 100644 --- a/dev/src/cli/cli.ts +++ b/dev/src/cli/cli.ts @@ -207,6 +207,12 @@ export function createProgram(): Command { program .addOption(new Option('-v, --version', 'Get ADK CLI version')) .action((options: {version?: boolean}) => { + const [unknownCommand] = program.args; + if (unknownCommand !== undefined) { + program.error(`error: unknown command '${unknownCommand}'`, { + code: 'commander.unknownCommand', + }); + } if (options.version) { console.log(version); return; @@ -551,5 +557,10 @@ export function createProgram(): Command { }); }); + // Must stay after every .command() call: commander copies this setting into + // subcommands as they are created, which would make them accept excess + // arguments too. + program.allowExcessArguments(true); + return program; } diff --git a/dev/test/cli/cli_test.ts b/dev/test/cli/cli_test.ts index 7f5588dc3..07813432b 100644 --- a/dev/test/cli/cli_test.ts +++ b/dev/test/cli/cli_test.ts @@ -152,7 +152,7 @@ describe('CLI Entrypoint', () => { expect.fail('expected commander to reject an unknown subcommand'); } - expect(err.code).toBe('commander.excessArguments'); + expect(err.code).toBe('commander.unknownCommand'); expect(err.exitCode).toBe(1); expect(stdout).toBe(''); }); From 5ceda1592105c2aceaa04b7be235a013819322d6 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Thu, 6 Aug 2026 01:15:27 -0700 Subject: [PATCH 2/3] Test: pin the root unknown-subcommand message and the excess-argument guard Cover both branches of the new root guard, and pin that allowExcessArguments(true) stays after the subcommand registrations so the subcommands keep rejecting their own excess arguments. --- dev/test/cli/cli_test.ts | 80 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/dev/test/cli/cli_test.ts b/dev/test/cli/cli_test.ts index 07813432b..8cd3b89e7 100644 --- a/dev/test/cli/cli_test.ts +++ b/dev/test/cli/cli_test.ts @@ -5,7 +5,7 @@ */ import {LogLevel, setLogLevel} from '@google/adk'; -import {CommanderError} from 'commander'; +import {Command, CommanderError} from 'commander'; import {afterEach, beforeEach, describe, expect, it, Mock, vi} from 'vitest'; import {createProgram} from '../../src/cli/cli.js'; import {createAgent} from '../../src/cli/cli_create.js'; @@ -158,6 +158,84 @@ describe('CLI Entrypoint', () => { }); }); + describe('unknown subcommand', () => { + let stdout: string; + let stderr: string; + + const captureOutputAndExit = (cmd: Command) => { + cmd.exitOverride(); + cmd.configureOutput({ + writeOut: (str) => { + stdout += str; + }, + writeErr: (str) => { + stderr += str; + }, + }); + cmd.commands.forEach(captureOutputAndExit); + }; + + beforeEach(() => { + stdout = ''; + stderr = ''; + captureOutputAndExit(program); + }); + + const parseExpectingError = async ( + args: string[], + ): Promise => { + try { + await program.parseAsync(['node', 'cli_entrypoint.js', ...args]); + } catch (e: unknown) { + return e as CommanderError; + } + return undefined; + }; + + it('should name the unrecognised subcommand and skip the root action', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const err = await parseExpectingError(['bogus']); + if (!err) { + expect.fail('expected commander to reject an unknown subcommand'); + } + + expect(stderr).toContain("error: unknown command 'bogus'"); + expect(err.code).toBe('commander.unknownCommand'); + expect(err.exitCode).toBe(1); + expect(logSpy).not.toHaveBeenCalledWith('1.0.0-test'); + }); + + it('should report only the first operand of an unrecognised subcommand', async () => { + const err = await parseExpectingError(['bogus', 'extra']); + if (!err) { + expect.fail('expected commander to reject an unknown subcommand'); + } + + expect(stderr).toContain("error: unknown command 'bogus'"); + expect(stderr).not.toContain('extra'); + expect(err.code).toBe('commander.unknownCommand'); + expect(err.exitCode).toBe(1); + }); + + it('should leave a bare invocation on the non-error path', async () => { + expect(await parseExpectingError([])).toBeUndefined(); + + expect(stderr).toBe(''); + expect(stdout).toContain('Usage: adk'); + }); + + it('should still reject excess arguments on a subcommand', async () => { + const err = await parseExpectingError(['create', 'x', 'y']); + if (!err) { + expect.fail('expected commander to reject excess subcommand arguments'); + } + + expect(err.code).toBe('commander.excessArguments'); + expect(err.exitCode).toBe(1); + }); + }); + describe('command: web', () => { it('should start AdkApiServer with default options', async () => { await parse(['web']); From 857457d9395be8eef8f94b3419aede6f48c83413 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Thu, 6 Aug 2026 01:34:12 -0700 Subject: [PATCH 3/3] Test: narrow the parse helper's catch to CommanderError An unchecked cast reported any unrelated throw as a commander error with an undefined code, which hid the real stack. Unexpected errors now rethrow. --- dev/test/cli/cli_test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/test/cli/cli_test.ts b/dev/test/cli/cli_test.ts index 8cd3b89e7..84d67f3f4 100644 --- a/dev/test/cli/cli_test.ts +++ b/dev/test/cli/cli_test.ts @@ -187,7 +187,10 @@ describe('CLI Entrypoint', () => { try { await program.parseAsync(['node', 'cli_entrypoint.js', ...args]); } catch (e: unknown) { - return e as CommanderError; + if (e instanceof CommanderError) { + return e; + } + throw e; } return undefined; };