diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index 2772cf1d3b..d498603a28 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -601,6 +601,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, @@ -1107,7 +1137,7 @@ def to_agent_engine( Overrides `worker_pool` / `build_config.worker_pool` from `.agent_engine_config.json` when both are present. """ - app_name = os.path.basename(os.path.normpath(agent_folder)) + app_name = _validate_agent_folder_name(agent_folder) _validate_app_name(app_name) display_name = display_name or app_name parent_folder = os.path.dirname(os.path.normpath(agent_folder)) diff --git a/tests/unittests/cli/utils/test_cli_deploy.py b/tests/unittests/cli/utils/test_cli_deploy.py index eea406f018..918593d65f 100644 --- a/tests/unittests/cli/utils/test_cli_deploy.py +++ b/tests/unittests/cli/utils/test_cli_deploy.py @@ -1483,7 +1483,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" @@ -1509,6 +1508,57 @@ def test_on_rm_error_clears_readonly_and_retries( 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) + + _VALID_WORKER_POOL = ( "projects/my-gcp-project/locations/us-central1/workerPools/my-private-pool" )