chore: reformat the codes using autoformat.sh

PiperOrigin-RevId: 762004002
This commit is contained in:
Xiang (Sean) Zhou
2025-05-22 09:43:03 -07:00
committed by Copybara-Service
parent a2263b1808
commit ff8a3c9b43
23 changed files with 496 additions and 447 deletions

View File

@@ -16,182 +16,195 @@
from __future__ import annotations
import click
import json
import pytest
from pathlib import Path
import sys
import types
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple
import click
import google.adk.cli.cli as cli
import pytest
from pathlib import Path
from typing import Any, Dict, List, Tuple
# Helpers
class _Recorder:
"""Callable that records every invocation."""
"""Callable that records every invocation."""
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __call__(self, *args: Any, **kwargs: Any) -> None:
self.calls.append((args, kwargs))
def __call__(self, *args: Any, **kwargs: Any) -> None:
self.calls.append((args, kwargs))
# Fixtures
@pytest.fixture(autouse=True)
def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None:
"""Silence click output in every test."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
"""Silence click output in every test."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
@pytest.fixture(autouse=True)
def _patch_types_and_runner(monkeypatch: pytest.MonkeyPatch) -> None:
"""Replace google.genai.types and Runner with lightweight fakes."""
"""Replace google.genai.types and Runner with lightweight fakes."""
# Dummy Part / Content
class _Part:
def __init__(self, text: str | None = "") -> None:
self.text = text
# Dummy Part / Content
class _Part:
class _Content:
def __init__(self, role: str, parts: List[_Part]) -> None:
self.role = role
self.parts = parts
def __init__(self, text: str | None = "") -> None:
self.text = text
monkeypatch.setattr(cli.types, "Part", _Part)
monkeypatch.setattr(cli.types, "Content", _Content)
class _Content:
# Fake Runner yielding a single assistant echo
class _FakeRunner:
def __init__(self, *a: Any, **k: Any) -> None: ...
def __init__(self, role: str, parts: List[_Part]) -> None:
self.role = role
self.parts = parts
async def run_async(self, *a: Any, **k: Any):
message = a[2] if len(a) >= 3 else k["new_message"]
text = message.parts[0].text if message.parts else ""
response = _Content("assistant", [_Part(f"echo:{text}")])
yield types.SimpleNamespace(author="assistant", content=response)
monkeypatch.setattr(cli.types, "Part", _Part)
monkeypatch.setattr(cli.types, "Content", _Content)
monkeypatch.setattr(cli, "Runner", _FakeRunner)
# Fake Runner yielding a single assistant echo
class _FakeRunner:
def __init__(self, *a: Any, **k: Any) -> None:
...
async def run_async(self, *a: Any, **k: Any):
message = a[2] if len(a) >= 3 else k["new_message"]
text = message.parts[0].text if message.parts else ""
response = _Content("assistant", [_Part(f"echo:{text}")])
yield types.SimpleNamespace(author="assistant", content=response)
monkeypatch.setattr(cli, "Runner", _FakeRunner)
@pytest.fixture()
def fake_agent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Create a minimal importable agent package and patch importlib."""
"""Create a minimal importable agent package and patch importlib."""
parent_dir = tmp_path / "agents"
parent_dir.mkdir()
agent_dir = parent_dir / "fake_agent"
agent_dir.mkdir()
# __init__.py exposes root_agent with .name
(agent_dir / "__init__.py").write_text(
"from types import SimpleNamespace\n"
"root_agent = SimpleNamespace(name='fake_root')\n"
)
parent_dir = tmp_path / "agents"
parent_dir.mkdir()
agent_dir = parent_dir / "fake_agent"
agent_dir.mkdir()
# __init__.py exposes root_agent with .name
(agent_dir / "__init__.py").write_text(
"from types import SimpleNamespace\n"
"root_agent = SimpleNamespace(name='fake_root')\n"
)
# Ensure importable via sys.path
sys.path.insert(0, str(parent_dir))
# Ensure importable via sys.path
sys.path.insert(0, str(parent_dir))
import importlib
import importlib
module = importlib.import_module("fake_agent")
fake_module = types.SimpleNamespace(agent=module)
module = importlib.import_module("fake_agent")
fake_module = types.SimpleNamespace(agent=module)
monkeypatch.setattr(importlib, "import_module", lambda n: fake_module)
monkeypatch.setattr(cli.envs, "load_dotenv_for_agent", lambda *a, **k: None)
monkeypatch.setattr(importlib, "import_module", lambda n: fake_module)
monkeypatch.setattr(cli.envs, "load_dotenv_for_agent", lambda *a, **k: None)
yield parent_dir, "fake_agent"
yield parent_dir, "fake_agent"
# Cleanup
sys.path.remove(str(parent_dir))
del sys.modules["fake_agent"]
# Cleanup
sys.path.remove(str(parent_dir))
del sys.modules["fake_agent"]
# _run_input_file
@pytest.mark.asyncio
async def test_run_input_file_outputs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""run_input_file should echo user & assistant messages and return a populated session."""
recorder: List[str] = []
async def test_run_input_file_outputs(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""run_input_file should echo user & assistant messages and return a populated session."""
recorder: List[str] = []
def _echo(msg: str) -> None:
recorder.append(msg)
def _echo(msg: str) -> None:
recorder.append(msg)
monkeypatch.setattr(click, "echo", _echo)
monkeypatch.setattr(click, "echo", _echo)
input_json = {
"state": {"foo": "bar"},
"queries": ["hello world"],
}
input_path = tmp_path / "input.json"
input_path.write_text(json.dumps(input_json))
input_json = {
"state": {"foo": "bar"},
"queries": ["hello world"],
}
input_path = tmp_path / "input.json"
input_path.write_text(json.dumps(input_json))
artifact_service = cli.InMemoryArtifactService()
session_service = cli.InMemorySessionService()
dummy_root = types.SimpleNamespace(name="root")
artifact_service = cli.InMemoryArtifactService()
session_service = cli.InMemorySessionService()
dummy_root = types.SimpleNamespace(name="root")
session = await cli.run_input_file(
app_name="app",
user_id="user",
root_agent=dummy_root,
artifact_service=artifact_service,
session_service=session_service,
input_path=str(input_path),
)
session = await cli.run_input_file(
app_name="app",
user_id="user",
root_agent=dummy_root,
artifact_service=artifact_service,
session_service=session_service,
input_path=str(input_path),
)
assert session.state["foo"] == "bar"
assert any("[user]:" in line for line in recorder)
assert any("[assistant]:" in line for line in recorder)
assert session.state["foo"] == "bar"
assert any("[user]:" in line for line in recorder)
assert any("[assistant]:" in line for line in recorder)
# _run_cli (input_file branch)
@pytest.mark.asyncio
async def test_run_cli_with_input_file(fake_agent, tmp_path: Path) -> None:
"""run_cli should process an input file without raising and without saving."""
parent_dir, folder_name = fake_agent
input_json = {"state": {}, "queries": ["ping"]}
input_path = tmp_path / "in.json"
input_path.write_text(json.dumps(input_json))
"""run_cli should process an input file without raising and without saving."""
parent_dir, folder_name = fake_agent
input_json = {"state": {}, "queries": ["ping"]}
input_path = tmp_path / "in.json"
input_path.write_text(json.dumps(input_json))
await cli.run_cli(
agent_parent_dir=str(parent_dir),
agent_folder_name=folder_name,
input_file=str(input_path),
saved_session_file=None,
save_session=False,
)
await cli.run_cli(
agent_parent_dir=str(parent_dir),
agent_folder_name=folder_name,
input_file=str(input_path),
saved_session_file=None,
save_session=False,
)
# _run_cli (interactive + save session branch)
@pytest.mark.asyncio
async def test_run_cli_save_session(fake_agent, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""run_cli should save a session file when save_session=True."""
parent_dir, folder_name = fake_agent
async def test_run_cli_save_session(
fake_agent, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""run_cli should save a session file when save_session=True."""
parent_dir, folder_name = fake_agent
# Simulate user typing 'exit' followed by session id 'sess123'
responses = iter(["exit", "sess123"])
monkeypatch.setattr("builtins.input", lambda *_a, **_k: next(responses))
# Simulate user typing 'exit' followed by session id 'sess123'
responses = iter(["exit", "sess123"])
monkeypatch.setattr("builtins.input", lambda *_a, **_k: next(responses))
session_file = Path(parent_dir) / folder_name / "sess123.session.json"
if session_file.exists():
session_file.unlink()
session_file = Path(parent_dir) / folder_name / "sess123.session.json"
if session_file.exists():
session_file.unlink()
await cli.run_cli(
agent_parent_dir=str(parent_dir),
agent_folder_name=folder_name,
input_file=None,
saved_session_file=None,
save_session=True,
)
await cli.run_cli(
agent_parent_dir=str(parent_dir),
agent_folder_name=folder_name,
input_file=None,
saved_session_file=None,
save_session=True,
)
assert session_file.exists()
data = json.loads(session_file.read_text())
# The saved JSON should at least contain id and events keys
assert "id" in data and "events" in data
assert session_file.exists()
data = json.loads(session_file.read_text())
# The saved JSON should at least contain id and events keys
assert "id" in data and "events" in data
@pytest.mark.asyncio
async def test_run_interactively_whitespace_and_exit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
async def test_run_interactively_whitespace_and_exit(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""run_interactively should skip blank input, echo once, then exit."""
# make a session that belongs to dummy agent
svc = cli.InMemorySessionService()

View File

@@ -17,214 +17,239 @@
from __future__ import annotations
import click
import os
import pytest
import subprocess
import google.adk.cli.cli_create as cli_create
from pathlib import Path
from typing import Any, Dict, List, Tuple
import subprocess
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple
import click
import google.adk.cli.cli_create as cli_create
import pytest
# Helpers
class _Recorder:
"""A callable object that records every invocation."""
"""A callable object that records every invocation."""
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: D401
self.calls.append((args, kwargs))
def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: D401
self.calls.append((args, kwargs))
# Fixtures
@pytest.fixture(autouse=True)
def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None:
"""Silence click output in every test."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
"""Silence click output in every test."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
@pytest.fixture()
def agent_folder(tmp_path: Path) -> Path:
"""Return a temporary path that will hold generated agent sources."""
return tmp_path / "agent"
"""Return a temporary path that will hold generated agent sources."""
return tmp_path / "agent"
# _generate_files
def test_generate_files_with_api_key(agent_folder: Path) -> None:
"""Files should be created with the API-key backend and correct .env flags."""
cli_create._generate_files(
str(agent_folder),
google_api_key="dummy-key",
model="gemini-2.0-flash-001",
)
"""Files should be created with the API-key backend and correct .env flags."""
cli_create._generate_files(
str(agent_folder),
google_api_key="dummy-key",
model="gemini-2.0-flash-001",
)
env_content = (agent_folder / ".env").read_text()
assert "GOOGLE_API_KEY=dummy-key" in env_content
assert "GOOGLE_GENAI_USE_VERTEXAI=0" in env_content
assert (agent_folder / "agent.py").exists()
assert (agent_folder / "__init__.py").exists()
env_content = (agent_folder / ".env").read_text()
assert "GOOGLE_API_KEY=dummy-key" in env_content
assert "GOOGLE_GENAI_USE_VERTEXAI=0" in env_content
assert (agent_folder / "agent.py").exists()
assert (agent_folder / "__init__.py").exists()
def test_generate_files_with_gcp(agent_folder: Path) -> None:
"""Files should be created with Vertex AI backend and correct .env flags."""
cli_create._generate_files(
str(agent_folder),
google_cloud_project="proj",
google_cloud_region="us-central1",
model="gemini-2.0-flash-001",
)
"""Files should be created with Vertex AI backend and correct .env flags."""
cli_create._generate_files(
str(agent_folder),
google_cloud_project="proj",
google_cloud_region="us-central1",
model="gemini-2.0-flash-001",
)
env_content = (agent_folder / ".env").read_text()
assert "GOOGLE_CLOUD_PROJECT=proj" in env_content
assert "GOOGLE_CLOUD_LOCATION=us-central1" in env_content
assert "GOOGLE_GENAI_USE_VERTEXAI=1" in env_content
env_content = (agent_folder / ".env").read_text()
assert "GOOGLE_CLOUD_PROJECT=proj" in env_content
assert "GOOGLE_CLOUD_LOCATION=us-central1" in env_content
assert "GOOGLE_GENAI_USE_VERTEXAI=1" in env_content
def test_generate_files_overwrite(agent_folder: Path) -> None:
"""Existing files should be overwritten when generating again."""
agent_folder.mkdir(parents=True, exist_ok=True)
(agent_folder / ".env").write_text("OLD")
"""Existing files should be overwritten when generating again."""
agent_folder.mkdir(parents=True, exist_ok=True)
(agent_folder / ".env").write_text("OLD")
cli_create._generate_files(
str(agent_folder),
google_api_key="new-key",
model="gemini-2.0-flash-001",
)
cli_create._generate_files(
str(agent_folder),
google_api_key="new-key",
model="gemini-2.0-flash-001",
)
assert "GOOGLE_API_KEY=new-key" in (agent_folder / ".env").read_text()
assert "GOOGLE_API_KEY=new-key" in (agent_folder / ".env").read_text()
def test_generate_files_permission_error(monkeypatch: pytest.MonkeyPatch, agent_folder: Path) -> None:
"""PermissionError raised by os.makedirs should propagate."""
monkeypatch.setattr(os, "makedirs", lambda *a, **k: (_ for _ in ()).throw(PermissionError()))
with pytest.raises(PermissionError):
cli_create._generate_files(str(agent_folder), model="gemini-2.0-flash-001")
def test_generate_files_permission_error(
monkeypatch: pytest.MonkeyPatch, agent_folder: Path
) -> None:
"""PermissionError raised by os.makedirs should propagate."""
monkeypatch.setattr(
os, "makedirs", lambda *a, **k: (_ for _ in ()).throw(PermissionError())
)
with pytest.raises(PermissionError):
cli_create._generate_files(str(agent_folder), model="gemini-2.0-flash-001")
def test_generate_files_no_params(agent_folder: Path) -> None:
"""No backend parameters → minimal .env file is generated."""
cli_create._generate_files(str(agent_folder), model="gemini-2.0-flash-001")
"""No backend parameters → minimal .env file is generated."""
cli_create._generate_files(str(agent_folder), model="gemini-2.0-flash-001")
env_content = (agent_folder / ".env").read_text()
for key in ("GOOGLE_API_KEY", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION", "GOOGLE_GENAI_USE_VERTEXAI"):
assert key not in env_content
env_content = (agent_folder / ".env").read_text()
for key in (
"GOOGLE_API_KEY",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_CLOUD_LOCATION",
"GOOGLE_GENAI_USE_VERTEXAI",
):
assert key not in env_content
# run_cmd
def test_run_cmd_overwrite_reject(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""User rejecting overwrite should trigger click.Abort."""
agent_name = "agent"
agent_dir = tmp_path / agent_name
agent_dir.mkdir()
(agent_dir / "dummy.txt").write_text("dummy")
def test_run_cmd_overwrite_reject(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""User rejecting overwrite should trigger click.Abort."""
agent_name = "agent"
agent_dir = tmp_path / agent_name
agent_dir.mkdir()
(agent_dir / "dummy.txt").write_text("dummy")
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))
monkeypatch.setattr(os.path, "exists", lambda _p: True)
monkeypatch.setattr(os, "listdir", lambda _p: ["dummy.txt"])
monkeypatch.setattr(click, "confirm", lambda *a, **k: False)
monkeypatch.setattr(os, "getcwd", lambda: str(tmp_path))
monkeypatch.setattr(os.path, "exists", lambda _p: True)
monkeypatch.setattr(os, "listdir", lambda _p: ["dummy.txt"])
monkeypatch.setattr(click, "confirm", lambda *a, **k: False)
with pytest.raises(click.Abort):
cli_create.run_cmd(
agent_name,
model="gemini-2.0-flash-001",
google_api_key=None,
google_cloud_project=None,
google_cloud_region=None,
)
with pytest.raises(click.Abort):
cli_create.run_cmd(
agent_name,
model="gemini-2.0-flash-001",
google_api_key=None,
google_cloud_project=None,
google_cloud_region=None,
)
# Prompt helpers
def test_prompt_for_google_cloud(monkeypatch: pytest.MonkeyPatch) -> None:
"""Prompt should return the project input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "test-proj")
assert cli_create._prompt_for_google_cloud(None) == "test-proj"
"""Prompt should return the project input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "test-proj")
assert cli_create._prompt_for_google_cloud(None) == "test-proj"
def test_prompt_for_google_cloud_region(monkeypatch: pytest.MonkeyPatch) -> None:
"""Prompt should return the region input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "asia-northeast1")
assert cli_create._prompt_for_google_cloud_region(None) == "asia-northeast1"
def test_prompt_for_google_cloud_region(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Prompt should return the region input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "asia-northeast1")
assert cli_create._prompt_for_google_cloud_region(None) == "asia-northeast1"
def test_prompt_for_google_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
"""Prompt should return the API-key input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "api-key")
assert cli_create._prompt_for_google_api_key(None) == "api-key"
"""Prompt should return the API-key input."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "api-key")
assert cli_create._prompt_for_google_api_key(None) == "api-key"
def test_prompt_for_model_gemini(monkeypatch: pytest.MonkeyPatch) -> None:
"""Selecting option '1' should return the default Gemini model string."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "1")
assert cli_create._prompt_for_model() == "gemini-2.0-flash-001"
"""Selecting option '1' should return the default Gemini model string."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "1")
assert cli_create._prompt_for_model() == "gemini-2.0-flash-001"
def test_prompt_for_model_other(monkeypatch: pytest.MonkeyPatch) -> None:
"""Selecting option '2' should return placeholder and call secho."""
called: Dict[str, bool] = {}
"""Selecting option '2' should return placeholder and call secho."""
called: Dict[str, bool] = {}
monkeypatch.setattr(click, "prompt", lambda *a, **k: "2")
monkeypatch.setattr(click, "prompt", lambda *a, **k: "2")
def _fake_secho(*_a: Any, **_k: Any) -> None:
called["secho"] = True
monkeypatch.setattr(click, "secho", _fake_secho)
assert cli_create._prompt_for_model() == "<FILL_IN_MODEL>"
assert called.get("secho") is True
def _fake_secho(*_a: Any, **_k: Any) -> None:
called["secho"] = True
monkeypatch.setattr(click, "secho", _fake_secho)
assert cli_create._prompt_for_model() == "<FILL_IN_MODEL>"
assert called.get("secho") is True
# Backend selection helper
def test_prompt_to_choose_backend_api(monkeypatch: pytest.MonkeyPatch) -> None:
"""Choosing API-key backend returns (api_key, None, None)."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "1")
monkeypatch.setattr(cli_create, "_prompt_for_google_api_key", lambda _v: "api-key")
"""Choosing API-key backend returns (api_key, None, None)."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "1")
monkeypatch.setattr(
cli_create, "_prompt_for_google_api_key", lambda _v: "api-key"
)
api_key, proj, region = cli_create._prompt_to_choose_backend(None, None, None)
assert api_key == "api-key"
assert proj is None and region is None
api_key, proj, region = cli_create._prompt_to_choose_backend(None, None, None)
assert api_key == "api-key"
assert proj is None and region is None
def test_prompt_to_choose_backend_vertex(monkeypatch: pytest.MonkeyPatch) -> None:
"""Choosing Vertex backend returns (None, project, region)."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "2")
monkeypatch.setattr(cli_create, "_prompt_for_google_cloud", lambda _v: "proj")
monkeypatch.setattr(cli_create, "_prompt_for_google_cloud_region", lambda _v: "region")
api_key, proj, region = cli_create._prompt_to_choose_backend(None, None, None)
assert api_key is None
assert proj == "proj"
assert region == "region"
def test_prompt_to_choose_backend_vertex(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Choosing Vertex backend returns (None, project, region)."""
monkeypatch.setattr(click, "prompt", lambda *a, **k: "2")
monkeypatch.setattr(cli_create, "_prompt_for_google_cloud", lambda _v: "proj")
monkeypatch.setattr(
cli_create, "_prompt_for_google_cloud_region", lambda _v: "region"
)
api_key, proj, region = cli_create._prompt_to_choose_backend(None, None, None)
assert api_key is None
assert proj == "proj"
assert region == "region"
# prompt_str
def test_prompt_str_non_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""_prompt_str should retry until a non-blank string is provided."""
responses = iter(["", " ", "valid"])
monkeypatch.setattr(click, "prompt", lambda *_a, **_k: next(responses))
assert cli_create._prompt_str("dummy") == "valid"
"""_prompt_str should retry until a non-blank string is provided."""
responses = iter(["", " ", "valid"])
monkeypatch.setattr(click, "prompt", lambda *_a, **_k: next(responses))
assert cli_create._prompt_str("dummy") == "valid"
# gcloud fallback helpers
def test_get_gcp_project_from_gcloud_fail(monkeypatch: pytest.MonkeyPatch) -> None:
"""Failure of gcloud project lookup should return empty string."""
monkeypatch.setattr(
subprocess,
"run",
lambda *_a, **_k: (_ for _ in ()).throw(FileNotFoundError()),
)
assert cli_create._get_gcp_project_from_gcloud() == ""
def test_get_gcp_project_from_gcloud_fail(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Failure of gcloud project lookup should return empty string."""
monkeypatch.setattr(
subprocess,
"run",
lambda *_a, **_k: (_ for _ in ()).throw(FileNotFoundError()),
)
assert cli_create._get_gcp_project_from_gcloud() == ""
def test_get_gcp_region_from_gcloud_fail(monkeypatch: pytest.MonkeyPatch) -> None:
"""CalledProcessError should result in empty region string."""
monkeypatch.setattr(
subprocess,
"run",
lambda *_a, **_k: (_ for _ in ()).throw(subprocess.CalledProcessError(1, "gcloud")),
)
assert cli_create._get_gcp_region_from_gcloud() == ""
def test_get_gcp_region_from_gcloud_fail(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""CalledProcessError should result in empty region string."""
monkeypatch.setattr(
subprocess,
"run",
lambda *_a, **_k: (_ for _ in ()).throw(
subprocess.CalledProcessError(1, "gcloud")
),
)
assert cli_create._get_gcp_region_from_gcloud() == ""

View File

@@ -17,70 +17,74 @@
from __future__ import annotations
import click
from pathlib import Path
import shutil
import pytest
import subprocess
import tempfile
import types
import google.adk.cli.cli_deploy as cli_deploy
from pathlib import Path
from typing import Any, Callable, Dict, List, Tuple
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Tuple
from unittest import mock
import click
import google.adk.cli.cli_deploy as cli_deploy
import pytest
# Helpers
class _Recorder:
"""A callable object that records every invocation."""
"""A callable object that records every invocation."""
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __init__(self) -> None:
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
def __call__(self, *args: Any, **kwargs: Any) -> None:
self.calls.append((args, kwargs))
def __call__(self, *args: Any, **kwargs: Any) -> None:
self.calls.append((args, kwargs))
# Fixtures
@pytest.fixture(autouse=True)
def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None:
"""Suppress click.echo to keep test output clean."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
"""Suppress click.echo to keep test output clean."""
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
@pytest.fixture()
def agent_dir(tmp_path: Path) -> Callable[[bool], Path]:
"""Return a factory that creates a dummy agent directory tree."""
"""Return a factory that creates a dummy agent directory tree."""
def _factory(include_requirements: bool) -> Path:
base = tmp_path / "agent"
base.mkdir()
(base / "agent.py").write_text("# dummy agent")
(base / "__init__.py").touch()
if include_requirements:
(base / "requirements.txt").write_text("pytest\n")
return base
def _factory(include_requirements: bool) -> Path:
base = tmp_path / "agent"
base.mkdir()
(base / "agent.py").write_text("# dummy agent")
(base / "__init__.py").touch()
if include_requirements:
(base / "requirements.txt").write_text("pytest\n")
return base
return _factory
return _factory
# _resolve_project
def test_resolve_project_with_option() -> None:
"""It should return the explicit project value untouched."""
assert cli_deploy._resolve_project("my-project") == "my-project"
"""It should return the explicit project value untouched."""
assert cli_deploy._resolve_project("my-project") == "my-project"
def test_resolve_project_from_gcloud(monkeypatch: pytest.MonkeyPatch) -> None:
"""It should fall back to `gcloud config get-value project` when no value supplied."""
monkeypatch.setattr(
subprocess,
"run",
lambda *a, **k: types.SimpleNamespace(stdout="gcp-proj\n"),
)
"""It should fall back to `gcloud config get-value project` when no value supplied."""
monkeypatch.setattr(
subprocess,
"run",
lambda *a, **k: types.SimpleNamespace(stdout="gcp-proj\n"),
)
with mock.patch("click.echo") as mocked_echo:
assert cli_deploy._resolve_project(None) == "gcp-proj"
mocked_echo.assert_called_once()
with mock.patch("click.echo") as mocked_echo:
assert cli_deploy._resolve_project(None) == "gcp-proj"
mocked_echo.assert_called_once()
# to_cloud_run
@@ -90,81 +94,83 @@ def test_to_cloud_run_happy_path(
agent_dir: Callable[[bool], Path],
include_requirements: bool,
) -> None:
"""
End-to-end execution test for `to_cloud_run` covering both presence and
absence of *requirements.txt*.
"""
tmp_dir = Path(tempfile.mkdtemp())
src_dir = agent_dir(include_requirements)
"""
End-to-end execution test for `to_cloud_run` covering both presence and
absence of *requirements.txt*.
"""
tmp_dir = Path(tempfile.mkdtemp())
src_dir = agent_dir(include_requirements)
copy_recorder = _Recorder()
run_recorder = _Recorder()
copy_recorder = _Recorder()
run_recorder = _Recorder()
# Cache the ORIGINAL copytree before patching
original_copytree = cli_deploy.shutil.copytree
# Cache the ORIGINAL copytree before patching
original_copytree = cli_deploy.shutil.copytree
def _recording_copytree(*args: Any, **kwargs: Any):
copy_recorder(*args, **kwargs)
return original_copytree(*args, **kwargs)
def _recording_copytree(*args: Any, **kwargs: Any):
copy_recorder(*args, **kwargs)
return original_copytree(*args, **kwargs)
monkeypatch.setattr(cli_deploy.shutil, "copytree", _recording_copytree)
# Skip actual cleanup so that we can inspect generated files later.
monkeypatch.setattr(cli_deploy.shutil, "rmtree", lambda *_a, **_k: None)
monkeypatch.setattr(subprocess, "run", run_recorder)
monkeypatch.setattr(cli_deploy.shutil, "copytree", _recording_copytree)
# Skip actual cleanup so that we can inspect generated files later.
monkeypatch.setattr(cli_deploy.shutil, "rmtree", lambda *_a, **_k: None)
monkeypatch.setattr(subprocess, "run", run_recorder)
cli_deploy.to_cloud_run(
agent_folder=str(src_dir),
project="proj",
region="asia-northeast1",
service_name="svc",
app_name="app",
temp_folder=str(tmp_dir),
port=8080,
trace_to_cloud=True,
with_ui=True,
verbosity="info",
session_db_url="sqlite://",
adk_version="0.0.5",
)
cli_deploy.to_cloud_run(
agent_folder=str(src_dir),
project="proj",
region="asia-northeast1",
service_name="svc",
app_name="app",
temp_folder=str(tmp_dir),
port=8080,
trace_to_cloud=True,
with_ui=True,
verbosity="info",
session_db_url="sqlite://",
adk_version="0.0.5",
)
# Assertions
assert len(copy_recorder.calls) == 1, "Agent sources must be copied exactly once."
assert run_recorder.calls, "gcloud command should be executed at least once."
assert (tmp_dir / "Dockerfile").exists(), "Dockerfile must be generated."
# Assertions
assert (
len(copy_recorder.calls) == 1
), "Agent sources must be copied exactly once."
assert run_recorder.calls, "gcloud command should be executed at least once."
assert (tmp_dir / "Dockerfile").exists(), "Dockerfile must be generated."
# Manual cleanup because we disabled rmtree in the monkeypatch.
shutil.rmtree(tmp_dir, ignore_errors=True)
# Manual cleanup because we disabled rmtree in the monkeypatch.
shutil.rmtree(tmp_dir, ignore_errors=True)
def test_to_cloud_run_cleans_temp_dir(
monkeypatch: pytest.MonkeyPatch,
agent_dir: Callable[[bool], Path],
) -> None:
"""`to_cloud_run` should always delete the temporary folder on exit."""
tmp_dir = Path(tempfile.mkdtemp())
src_dir = agent_dir(False)
"""`to_cloud_run` should always delete the temporary folder on exit."""
tmp_dir = Path(tempfile.mkdtemp())
src_dir = agent_dir(False)
deleted: Dict[str, Path] = {}
deleted: Dict[str, Path] = {}
def _fake_rmtree(path: str | Path, *a: Any, **k: Any) -> None:
deleted["path"] = Path(path)
def _fake_rmtree(path: str | Path, *a: Any, **k: Any) -> None:
deleted["path"] = Path(path)
monkeypatch.setattr(cli_deploy.shutil, "rmtree", _fake_rmtree)
monkeypatch.setattr(subprocess, "run", _Recorder())
monkeypatch.setattr(cli_deploy.shutil, "rmtree", _fake_rmtree)
monkeypatch.setattr(subprocess, "run", _Recorder())
cli_deploy.to_cloud_run(
agent_folder=str(src_dir),
project="proj",
region=None,
service_name="svc",
app_name="app",
temp_folder=str(tmp_dir),
port=8080,
trace_to_cloud=False,
with_ui=False,
verbosity="info",
session_db_url=None,
adk_version="0.0.5",
)
cli_deploy.to_cloud_run(
agent_folder=str(src_dir),
project="proj",
region=None,
service_name="svc",
app_name="app",
temp_folder=str(tmp_dir),
port=8080,
trace_to_cloud=False,
with_ui=False,
verbosity="info",
session_db_url=None,
adk_version="0.0.5",
)
assert deleted["path"] == tmp_dir
assert deleted["path"] == tmp_dir