mirror of
https://github.com/EvolutionAPI/adk-python.git
synced 2025-12-18 19:32:21 -06:00
chore: reformat the codes using autoformat.sh
PiperOrigin-RevId: 762004002
This commit is contained in:
committed by
Copybara-Service
parent
a2263b1808
commit
ff8a3c9b43
@@ -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()
|
||||
|
||||
@@ -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() == ""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user