mirror of
https://github.com/EvolutionAPI/adk-python.git
synced 2025-07-14 09:51:25 -06:00
test(cli): Add unit tests for CLI functionality
Copybara import of the project: -- f60707a22905f30040808b41b7e3510a47a80fc6 by K <51281148+K-dash@users.noreply.github.com>: test(cli): Add unit tests for CLI functionality This commit introduces unit tests for the following CLI-related components: - cli_deploy.py: Tests for the cloud deployment feature. - cli_create.py: Tests for the agent creation feature. - cli.py: Tests for the main CLI execution logic. - cli_tools_click.py: Tests for the Click-based CLI tools. -- 7be2159a475d0785619fea5e40c70e6461a7f4e1 by K <51281148+K-dash@users.noreply.github.com>: fix test_cli_eval_success_path COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/577 from K-dash:test/add-unit-tests-for-cli 69f12d3a27d9c50a46ef269075e050f498dee67a PiperOrigin-RevId: 756602765
This commit is contained in:
parent
2cbbf88135
commit
8963300518
256
tests/unittests/cli/utils/test_cli.py
Normal file
256
tests/unittests/cli/utils/test_cli.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unit tests for utilities in cli."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import click
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
import google.adk.cli.cli as cli
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
class _Recorder:
|
||||||
|
"""Callable that records every invocation."""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _patch_types_and_runner(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Replace google.genai.types and Runner with lightweight fakes."""
|
||||||
|
|
||||||
|
# Dummy Part / Content
|
||||||
|
class _Part:
|
||||||
|
def __init__(self, text: str | None = "") -> None:
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
class _Content:
|
||||||
|
def __init__(self, role: str, parts: List[_Part]) -> None:
|
||||||
|
self.role = role
|
||||||
|
self.parts = parts
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli.types, "Part", _Part)
|
||||||
|
monkeypatch.setattr(cli.types, "Content", _Content)
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
yield parent_dir, "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] = []
|
||||||
|
|
||||||
|
def _echo(msg: str) -> None:
|
||||||
|
recorder.append(msg)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
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()
|
||||||
|
sess = svc.create_session(app_name="dummy", user_id="u")
|
||||||
|
artifact_service = cli.InMemoryArtifactService()
|
||||||
|
root_agent = types.SimpleNamespace(name="root")
|
||||||
|
|
||||||
|
# fake user input: blank -> 'hello' -> 'exit'
|
||||||
|
answers = iter([" ", "hello", "exit"])
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *_a, **_k: next(answers))
|
||||||
|
|
||||||
|
# capture assisted echo
|
||||||
|
echoed: list[str] = []
|
||||||
|
monkeypatch.setattr(click, "echo", lambda msg: echoed.append(msg))
|
||||||
|
|
||||||
|
await cli.run_interactively(root_agent, artifact_service, sess, svc)
|
||||||
|
|
||||||
|
# verify: assistant echoed once with 'echo:hello'
|
||||||
|
assert any("echo:hello" in m for m in echoed)
|
||||||
|
|
||||||
|
|
||||||
|
# run_cli (resume branch)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_cli_resume_saved_session(tmp_path: Path, fake_agent, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""run_cli should load previous session, print its events, then re-enter interactive mode."""
|
||||||
|
parent_dir, folder = fake_agent
|
||||||
|
|
||||||
|
# stub Session.model_validate_json to return dummy session with two events
|
||||||
|
user_content = types.SimpleNamespace(parts=[types.SimpleNamespace(text="hi")])
|
||||||
|
assistant_content = types.SimpleNamespace(parts=[types.SimpleNamespace(text="hello!")])
|
||||||
|
dummy_session = types.SimpleNamespace(
|
||||||
|
id="sess",
|
||||||
|
app_name=folder,
|
||||||
|
user_id="u",
|
||||||
|
events=[
|
||||||
|
types.SimpleNamespace(author="user", content=user_content, partial=False),
|
||||||
|
types.SimpleNamespace(author="assistant", content=assistant_content, partial=False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli.Session, "model_validate_json", staticmethod(lambda _s: dummy_session))
|
||||||
|
monkeypatch.setattr(cli.InMemorySessionService, "append_event", lambda *_a, **_k: None)
|
||||||
|
# interactive inputs: immediately 'exit'
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *_a, **_k: "exit")
|
||||||
|
|
||||||
|
# collect echo output
|
||||||
|
captured: list[str] = []
|
||||||
|
monkeypatch.setattr(click, "echo", lambda m: captured.append(m))
|
||||||
|
|
||||||
|
saved_path = tmp_path / "prev.session.json"
|
||||||
|
saved_path.write_text("{}") # contents not used – patched above
|
||||||
|
|
||||||
|
await cli.run_cli(
|
||||||
|
agent_parent_dir=str(parent_dir),
|
||||||
|
agent_folder_name=folder,
|
||||||
|
input_file=None,
|
||||||
|
saved_session_file=str(saved_path),
|
||||||
|
save_session=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ④ ensure both historical messages were printed
|
||||||
|
assert any("[user]: hi" in m for m in captured)
|
||||||
|
assert any("[assistant]: hello!" in m for m in captured)
|
230
tests/unittests/cli/utils/test_cli_create.py
Normal file
230
tests/unittests/cli/utils/test_cli_create.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Tests for utilities in cli_create."""
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
class _Recorder:
|
||||||
|
"""A callable object that records every invocation."""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def agent_folder(tmp_path: Path) -> Path:
|
||||||
|
"""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",
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_for_model_other(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Selecting option '2' should return placeholder and call secho."""
|
||||||
|
called: Dict[str, bool] = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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_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() == ""
|
168
tests/unittests/cli/utils/test_cli_deploy.py
Normal file
168
tests/unittests/cli/utils/test_cli_deploy.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Tests for utilities in cli_deploy."""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import click
|
||||||
|
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 unittest import mock
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
class _Recorder:
|
||||||
|
"""A callable object that records every invocation."""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def agent_dir(tmp_path: Path) -> Callable[[bool], Path]:
|
||||||
|
"""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
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch("click.echo") as mocked_echo:
|
||||||
|
assert cli_deploy._resolve_project(None) == "gcp-proj"
|
||||||
|
mocked_echo.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# to_cloud_run
|
||||||
|
@pytest.mark.parametrize("include_requirements", [True, False])
|
||||||
|
def test_to_cloud_run_happy_path(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
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)
|
||||||
|
|
||||||
|
copy_recorder = _Recorder()
|
||||||
|
run_recorder = _Recorder()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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://",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
deleted: Dict[str, 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())
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert deleted["path"] == tmp_dir
|
258
tests/unittests/cli/utils/test_cli_tools_click.py
Normal file
258
tests/unittests/cli/utils/test_cli_tools_click.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Tests for utilities in cli_tool_click."""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from google.adk.cli import cli_tools_click
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
class _Recorder:
|
||||||
|
"""Callable that records every invocation."""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
# Fixtures
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Suppress click output during tests."""
|
||||||
|
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
|
||||||
|
|
||||||
|
|
||||||
|
# validate_exclusive
|
||||||
|
def test_validate_exclusive_allows_single() -> None:
|
||||||
|
"""Providing exactly one exclusive option should pass."""
|
||||||
|
ctx = click.Context(cli_tools_click.main)
|
||||||
|
param = SimpleNamespace(name="replay")
|
||||||
|
assert cli_tools_click.validate_exclusive(ctx, param, "file.json") == "file.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_exclusive_blocks_multiple() -> None:
|
||||||
|
"""Providing two exclusive options should raise UsageError."""
|
||||||
|
ctx = click.Context(cli_tools_click.main)
|
||||||
|
param1 = SimpleNamespace(name="replay")
|
||||||
|
param2 = SimpleNamespace(name="resume")
|
||||||
|
|
||||||
|
# First option registers fine
|
||||||
|
cli_tools_click.validate_exclusive(ctx, param1, "replay.json")
|
||||||
|
|
||||||
|
# Second option triggers conflict
|
||||||
|
with pytest.raises(click.UsageError):
|
||||||
|
cli_tools_click.validate_exclusive(ctx, param2, "resume.json")
|
||||||
|
|
||||||
|
|
||||||
|
# cli create
|
||||||
|
def test_cli_create_cmd_invokes_run_cmd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""`adk create` should forward arguments to cli_create.run_cmd."""
|
||||||
|
rec = _Recorder()
|
||||||
|
monkeypatch.setattr(cli_tools_click.cli_create, "run_cmd", rec)
|
||||||
|
|
||||||
|
app_dir = tmp_path / "my_app"
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli_tools_click.main,
|
||||||
|
["create", "--model", "gemini", "--api_key", "key123", str(app_dir)],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert rec.calls, "cli_create.run_cmd must be called"
|
||||||
|
|
||||||
|
|
||||||
|
# cli run
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cli_run_invokes_run_cli(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""`adk run` should call run_cli via asyncio.run with correct parameters."""
|
||||||
|
rec = _Recorder()
|
||||||
|
monkeypatch.setattr(cli_tools_click, "run_cli", lambda **kwargs: rec(kwargs))
|
||||||
|
monkeypatch.setattr(cli_tools_click.asyncio, "run", lambda coro: coro) # pass-through
|
||||||
|
|
||||||
|
# create dummy agent directory
|
||||||
|
agent_dir = tmp_path / "agent"
|
||||||
|
agent_dir.mkdir()
|
||||||
|
(agent_dir / "__init__.py").touch()
|
||||||
|
(agent_dir / "agent.py").touch()
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli_tools_click.main, ["run", str(agent_dir)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert rec.calls and rec.calls[0][0][0]["agent_folder_name"] == "agent"
|
||||||
|
|
||||||
|
|
||||||
|
# cli deploy cloud_run
|
||||||
|
def test_cli_deploy_cloud_run_success(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Successful path should call cli_deploy.to_cloud_run once."""
|
||||||
|
rec = _Recorder()
|
||||||
|
monkeypatch.setattr(cli_tools_click.cli_deploy, "to_cloud_run", rec)
|
||||||
|
|
||||||
|
agent_dir = tmp_path / "agent2"
|
||||||
|
agent_dir.mkdir()
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli_tools_click.main,
|
||||||
|
[
|
||||||
|
"deploy",
|
||||||
|
"cloud_run",
|
||||||
|
"--project",
|
||||||
|
"proj",
|
||||||
|
"--region",
|
||||||
|
"asia-northeast1",
|
||||||
|
str(agent_dir),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_deploy_cloud_run_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Exception from to_cloud_run should be caught and surfaced via click.secho."""
|
||||||
|
def _boom(*_a: Any, **_k: Any) -> None: # noqa: D401
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli_tools_click.cli_deploy, "to_cloud_run", _boom)
|
||||||
|
|
||||||
|
# intercept click.secho(error=True) output
|
||||||
|
captured: List[str] = []
|
||||||
|
monkeypatch.setattr(click, "secho", lambda msg, **__: captured.append(msg))
|
||||||
|
|
||||||
|
agent_dir = tmp_path / "agent3"
|
||||||
|
agent_dir.mkdir()
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli_tools_click.main, ["deploy", "cloud_run", str(agent_dir)])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert any("Deploy failed: boom" in m for m in captured)
|
||||||
|
|
||||||
|
|
||||||
|
# cli eval
|
||||||
|
def test_cli_eval_missing_deps_raises(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""If cli_eval sub-module is missing, command should raise ClickException."""
|
||||||
|
# Ensure .cli_eval is not importable
|
||||||
|
orig_import = builtins.__import__
|
||||||
|
|
||||||
|
def _fake_import(name: str, *a: Any, **k: Any):
|
||||||
|
if name.endswith(".cli_eval") or name == "google.adk.cli.cli_eval":
|
||||||
|
raise ModuleNotFoundError()
|
||||||
|
return orig_import(name, *a, **k)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", _fake_import)
|
||||||
|
|
||||||
|
|
||||||
|
# cli web & api_server (uvicorn patched)
|
||||||
|
@pytest.fixture()
|
||||||
|
def _patch_uvicorn(monkeypatch: pytest.MonkeyPatch) -> _Recorder:
|
||||||
|
"""Patch uvicorn.Config/Server to avoid real network operations."""
|
||||||
|
rec = _Recorder()
|
||||||
|
|
||||||
|
class _DummyServer:
|
||||||
|
def __init__(self, *a: Any, **k: Any) -> None: ...
|
||||||
|
def run(self) -> None:
|
||||||
|
rec()
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli_tools_click.uvicorn, "Config", lambda *a, **k: object())
|
||||||
|
monkeypatch.setattr(cli_tools_click.uvicorn, "Server", lambda *_a, **_k: _DummyServer())
|
||||||
|
monkeypatch.setattr(cli_tools_click, "get_fast_api_app", lambda **_k: object())
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_web_invokes_uvicorn(tmp_path: Path, _patch_uvicorn: _Recorder) -> None:
|
||||||
|
"""`adk web` should configure and start uvicorn.Server.run."""
|
||||||
|
agents_dir = tmp_path / "agents"
|
||||||
|
agents_dir.mkdir()
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli_tools_click.main, ["web", str(agents_dir)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _patch_uvicorn.calls, "uvicorn.Server.run must be called"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_api_server_invokes_uvicorn(tmp_path: Path, _patch_uvicorn: _Recorder) -> None:
|
||||||
|
"""`adk api_server` should configure and start uvicorn.Server.run."""
|
||||||
|
agents_dir = tmp_path / "agents_api"
|
||||||
|
agents_dir.mkdir()
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli_tools_click.main, ["api_server", str(agents_dir)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _patch_uvicorn.calls, "uvicorn.Server.run must be called"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_eval_success_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Test the success path of `adk eval` by fully executing it with a stub module, up to summary generation."""
|
||||||
|
import sys, types
|
||||||
|
|
||||||
|
# stub cli_eval module
|
||||||
|
stub = types.ModuleType("google.adk.cli.cli_eval")
|
||||||
|
|
||||||
|
class _EvalMetric:
|
||||||
|
def __init__(self, metric_name: str, threshold: float) -> None: ...
|
||||||
|
|
||||||
|
class _EvalResult:
|
||||||
|
def __init__(self, eval_set_file: str, final_eval_status: str) -> None:
|
||||||
|
self.eval_set_file = eval_set_file
|
||||||
|
self.final_eval_status = final_eval_status
|
||||||
|
|
||||||
|
# minimal enum-like namespace
|
||||||
|
_EvalStatus = types.SimpleNamespace(PASSED="PASSED", FAILED="FAILED")
|
||||||
|
|
||||||
|
# helper funcs
|
||||||
|
stub.EvalMetric = _EvalMetric
|
||||||
|
stub.EvalResult = _EvalResult
|
||||||
|
stub.EvalStatus = _EvalStatus
|
||||||
|
stub.MISSING_EVAL_DEPENDENCIES_MESSAGE = "stub msg"
|
||||||
|
|
||||||
|
stub.get_evaluation_criteria_or_default = lambda _p: {"foo": 1.0}
|
||||||
|
stub.get_root_agent = lambda _p: object()
|
||||||
|
stub.try_get_reset_func = lambda _p: None
|
||||||
|
stub.parse_and_get_evals_to_run = lambda _paths: {"set1.json": ["e1", "e2"]}
|
||||||
|
stub.run_evals = lambda *_a, **_k: iter(
|
||||||
|
[_EvalResult("set1.json", "PASSED"), _EvalResult("set1.json", "FAILED")]
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli_tools_click.asyncio, "run", lambda coro: list(coro))
|
||||||
|
|
||||||
|
# inject stub
|
||||||
|
sys.modules["google.adk.cli.cli_eval"] = stub
|
||||||
|
|
||||||
|
# create dummy agent directory
|
||||||
|
agent_dir = tmp_path / "agent5"
|
||||||
|
agent_dir.mkdir()
|
||||||
|
(agent_dir / "__init__.py").touch()
|
||||||
|
|
||||||
|
# inject monkeypatch
|
||||||
|
monkeypatch.setattr(cli_tools_click.envs, "load_dotenv_for_agent", lambda *a, **k: None)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli_tools_click.main,
|
||||||
|
["eval", str(agent_dir), str(tmp_path / "dummy_eval.json")],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Eval Run Summary" in result.output
|
||||||
|
assert "Tests passed: 1" in result.output
|
||||||
|
assert "Tests failed: 1" in result.output
|
Loading…
Reference in New Issue
Block a user