diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index fea6145e87..b4592f401b 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -17,6 +17,7 @@ import importlib import json import os +import re import shutil import stat import subprocess @@ -483,6 +484,36 @@ def _validate_gcloud_extra_args( ) +def _validate_agent_folder_name(agent_folder: str) -> str: + """Validates that the agent directory name is a valid Python identifier. + + Agent Engine requires the agent folder to be importable as a Python module. + Directory names containing hyphens (-) or other non-identifier characters + lead to syntax and import errors at deployment runtime. + + Args: + agent_folder: Path to the agent directory. + + Returns: + The validated folder name. + + Raises: + click.ClickException: If the folder name is not a valid Python identifier. + """ + folder_name = os.path.basename(os.path.normpath(agent_folder)) + if not folder_name.isidentifier(): + suggested = re.sub(r'\W|^(?=\d)', '_', folder_name) + if not suggested.isidentifier(): + suggested = f'_{suggested}' + raise click.ClickException( + f"Agent directory name '{folder_name}' is not a valid Python" + " identifier (cannot contain dashes '-' or special characters)." + " Agent Engine requires the directory name to be a valid Python" + f" module name. Please rename your agent folder (e.g., '{suggested}')." + ) + return folder_name + + def _validate_agent_import( agent_src_path: str, adk_app_object: str, @@ -980,7 +1011,7 @@ def to_agent_engine( extra_packages (list[str]): Optional. Additional local file or directory paths to stage alongside the agent and make importable in the image. """ - app_name = os.path.basename(agent_folder) + app_name = _validate_agent_folder_name(agent_folder) display_name = display_name or app_name parent_folder = os.path.dirname(agent_folder) if adk_app_object: diff --git a/tests/unittests/cli/utils/test_cli_deploy.py b/tests/unittests/cli/utils/test_cli_deploy.py index 60ed5af930..aa7f0ca088 100644 --- a/tests/unittests/cli/utils/test_cli_deploy.py +++ b/tests/unittests/cli/utils/test_cli_deploy.py @@ -1195,7 +1195,6 @@ def test_removes_directory_tree(self, tmp_path: Path) -> None: def test_removes_readonly_files(self, tmp_path: Path) -> None: """It should remove a tree containing read-only files.""" - import os import stat d = tmp_path / "ro_dir" @@ -1219,3 +1218,55 @@ def test_on_rm_error_clears_readonly_and_retries( cli_deploy._on_rm_error(os.remove, str(ro_file), None) assert not ro_file.exists() + + +# _validate_agent_folder_name tests + + +class TestValidateAgentFolderName: + """Tests for the _validate_agent_folder_name helper.""" + + def test_valid_identifier_succeeds(self) -> None: + """A valid identifier folder name should succeed and return basename.""" + assert ( + cli_deploy._validate_agent_folder_name("/path/to/my_agent") + == "my_agent" + ) + assert cli_deploy._validate_agent_folder_name("agent123") == "agent123" + assert ( + cli_deploy._validate_agent_folder_name("/path/to/my_agent/") + == "my_agent" + ) + + def test_invalid_folder_name_with_dashes_raises(self) -> None: + """A folder name with dashes should raise ClickException with suggestions.""" + with pytest.raises(click.ClickException) as exc_info: + cli_deploy._validate_agent_folder_name("my-agent-app") + assert "not a valid Python identifier" in str(exc_info.value) + assert "my_agent_app" in str(exc_info.value) + + def test_invalid_folder_name_starting_with_number_raises(self) -> None: + """A folder name starting with a number should raise ClickException.""" + with pytest.raises(click.ClickException) as exc_info: + cli_deploy._validate_agent_folder_name("123agent") + assert "not a valid Python identifier" in str(exc_info.value) + assert "_123agent" in str(exc_info.value) + + def test_to_agent_engine_raises_on_dashed_folder_name( + self, tmp_path: Path + ) -> None: + """to_agent_engine should fail early when agent directory contains dashes.""" + dashed_agent = tmp_path / "dashed-agent-name" + dashed_agent.mkdir() + (dashed_agent / "agent.py").write_text("root_agent = 'test'\n") + (dashed_agent / "__init__.py").touch() + + with pytest.raises(click.ClickException) as exc_info: + cli_deploy.to_agent_engine( + agent_folder=str(dashed_agent), + project="test-project", + region="us-central1", + ) + assert "not a valid Python identifier" in str(exc_info.value) + assert "dashed_agent_name" in str(exc_info.value) +