Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dev/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,12 @@ export function createProgram(): Command {
}
});

// The intermediate groups `deploy` and `integration` deliberately have no
// action handler: naming a group without a subcommand is a usage error, so
// commander prints the group help to stderr and exits 1. An action handler
// also switches off commander's unknown-subcommand check, and `deploy`
// allows excess arguments, so `adk deploy cloud-run` would print help and
// exit 0 instead of reporting the typo.
const DEPLOY_COMMAND = program
.command('deploy')
.description('Deploy agent')
Expand Down
93 changes: 93 additions & 0 deletions dev/test/cli/cli_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {LogLevel, setLogLevel} from '@google/adk';
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';
Expand Down Expand Up @@ -431,4 +432,96 @@ describe('CLI Entrypoint', () => {
});
});
});

describe('bare intermediate command groups', () => {
let stdout: string;
let stderr: string;

beforeEach(() => {
stdout = '';
stderr = '';
// `command()` snapshots the parent's exit callback and output config when
// the child is created, so the outer `program.exitOverride()` covers the
// root only. Without this walk a group reaches `process.exit(1)` and
// kills the worker.
const applyTestIo = (cmd: Command) => {
cmd.exitOverride();
cmd.configureOutput({
writeOut: (str) => {
stdout += str;
},
writeErr: (str) => {
stderr += str;
},
});
cmd.commands.forEach(applyTestIo);
};
applyTestIo(program);
});

const parseExpectingError = async (
args: string[],
): Promise<CommanderError> => {
try {
await program.parseAsync(['node', 'cli_entrypoint.js', ...args]);
} catch (e: unknown) {
if (e instanceof CommanderError) {
return e;
}
throw e;
}
return expect.fail(`expected 'adk ${args.join(' ')}' to throw`);
};

it('writes deploy help to stderr and exits 1 when no subcommand is given', async () => {
const error = await parseExpectingError(['deploy']);

expect(error.code).toBe('commander.help');
expect(error.exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain('deploy [options] [command]');
expect(stderr).toContain('Deploy agent');
for (const child of ['cloud_run', 'agent_engine', 'reasoning_engine']) {
expect(stderr).toContain(child);
}
});

it('writes integration help to stderr and exits 1 when no subcommand is given', async () => {
const error = await parseExpectingError(['integration']);

expect(error.code).toBe('commander.help');
expect(error.exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain('integration [options] [command]');
expect(stderr).toContain('Run ADK integration and conformance tests');
expect(stderr).toContain('conformance');
});

it('rejects an unknown deploy subcommand instead of falling back to help', async () => {
const error = await parseExpectingError(['deploy', 'bogus']);

expect(error.code).toBe('commander.unknownCommand');
expect(error.exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain("unknown command 'bogus'");
});

it('rejects an unknown integration subcommand', async () => {
const error = await parseExpectingError(['integration', 'bogus']);

expect(error.code).toBe('commander.unknownCommand');
expect(error.exitCode).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain("unknown command 'bogus'");
});

it('writes deploy help to stdout and exits 0 for an explicit --help', async () => {
const error = await parseExpectingError(['deploy', '--help']);

expect(error.code).toBe('commander.helpDisplayed');
expect(error.exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain('deploy [options] [command]');
});
});
});
Loading