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
15 changes: 11 additions & 4 deletions src/google/adk/cli/cli_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ def run_cmd(
except ValueError as exc:
raise click.BadParameter(str(exc)) from exc

if not type:
type = _prompt_to_choose_type()

resolved_type = type.lower()
if resolved_type == "code" and not app_name.isidentifier():
raise click.BadParameter(
f"Invalid agent name '{app_name}': code-based agents must be a valid"
" Python identifier (e.g. 'my_agent', not 'my-agent')."
)

agent_folder = os.path.join(os.getcwd(), agent_name)
# check folder doesn't exist or it's empty. Otherwise, throw
if os.path.exists(agent_folder) and os.listdir(agent_folder):
Expand Down Expand Up @@ -256,16 +266,13 @@ def run_cmd(
google_cloud_project = auth_info.project_id
google_cloud_region = auth_info.region

if not type:
type = _prompt_to_choose_type()

_generate_files(
agent_folder,
google_api_key=google_api_key,
google_cloud_project=google_cloud_project,
google_cloud_region=google_cloud_region,
model=model,
type=type.lower(),
type=resolved_type,
)
agents_dir = os.path.dirname(os.path.normpath(agent_name))
click.echo(
Expand Down
117 changes: 117 additions & 0 deletions tests/unittests/cli/utils/test_cli_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,123 @@ def test_run_cmd_invalid_app_name(
)


def test_run_cmd_rejects_non_identifier_agent_name(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Kebab-case should be rejected before any dialogs."""
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))
model_prompt_called = False

def _fail_if_prompted():
nonlocal model_prompt_called
model_prompt_called = True
return "gemini-2.5-flash"

monkeypatch.setattr(cli_create, "_prompt_for_model", _fail_if_prompted)

with pytest.raises(
click.BadParameter, match="must be a valid Python identifier"
):
cli_create.run_cmd(
"my-agent",
model=None,
google_api_key=None,
google_cloud_project=None,
google_cloud_region=None,
type="code",
)
assert not model_prompt_called


@pytest.mark.parametrize(
"valid_name",
[
"my_agent",
"MyAgent",
"myAgent",
"class",
],
)
def test_run_cmd_accepts_valid_identifier_names(
valid_name: str, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""snake_case, PascalCase, camelCase, and keywords should be accepted."""
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))

cli_create.run_cmd(
valid_name,
model="gemini-2.5-flash",
google_api_key="test-key",
google_cloud_project=None,
google_cloud_region=None,
type="code",
)
assert (tmp_path / valid_name / "agent.py").exists()


def test_run_cmd_and_load_keyword_agent(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Creating a keyword-named application succeeds and AgentLoader can load it."""
from google.adk.agents.llm_agent import LlmAgent
from google.adk.cli.utils.agent_loader import AgentLoader

monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))

cli_create.run_cmd(
"class",
model="gemini-2.5-flash",
google_api_key="offline-placeholder",
google_cloud_project=None,
google_cloud_region=None,
type="code",
)
loader = AgentLoader(str(tmp_path))
agent = loader.load_agent("class")
assert isinstance(agent, LlmAgent)
assert agent.name == "root_agent"


def test_run_cmd_allows_kebab_case_for_config_agents(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Config-based agents do not rely on Python imports and allow kebab-case."""
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))

cli_create.run_cmd(
"my-config-agent",
model="gemini-2.5-flash",
google_api_key="test-key",
google_cloud_project=None,
google_cloud_region=None,
type="config",
)
config_file = tmp_path / "my-config-agent" / "root_agent.yaml"
assert config_file.exists()
content = config_file.read_text(encoding="utf-8")
assert "model: gemini-2.5-flash" in content


def test_run_cmd_rejects_kebab_case_when_prompted_as_code(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""If type is not passed, choosing CODE in prompt still validates identifier."""
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))
monkeypatch.setattr(cli_create, "_prompt_to_choose_type", lambda: "CODE")

with pytest.raises(
click.BadParameter, match="must be a valid Python identifier"
):
cli_create.run_cmd(
"kebab-agent",
model="gemini-2.5-flash",
google_api_key="test-key",
google_cloud_project=None,
google_cloud_region=None,
type=None,
)


def test_run_cmd_with_type_config(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
Expand Down
Loading