8000 test(cli): Add unit tests for CLI functionality · aphraz/adk-python@8963300 · GitHub 10000
[go: up one dir, main page]

Skip to content

Commit 8963300

Browse files
K-dashcopybara-github
authored andcommitted
test(cli): Add unit tests for CLI functionality
Copybara import of the project: -- f60707a 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. -- 7be2159 by K <51281148+K-dash@users.noreply.github.com>: fix test_cli_eval_success_path COPYBARA_INTEGRATE_REVIEW=google#577 from K-dash:test/add-unit-tests-for-cli 69f12d3 PiperOrigin-RevId: 756602765
1 parent 2cbbf88 commit 8963300

File tree

4 files changed

+912
-0
lines changed

4 files changed

+912
-0
lines changed

tests/unittests/cli/utils/test_cli.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for utilities in cli."""
16+
17+
from __future__ import annotations
18+
19+
import click
20+
import json
21+
import pytest
22+
import sys
23+
import types
24+
25+
import google.adk.cli.cli as cli
26+
27+
from pathlib import Path
28+
from typing import Any, Dict, List, Tuple
29+
30+
# Helpers
31+
class _Recorder:
32+
"""Callable that records every invocation."""
33+
34+
def __init__(self) -> None:
35+
self.calls: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
36+
37+
def __call__(self, *args: Any, **kwargs: Any) -> None:
38+
self.calls.append((args, kwargs))
39+
40+
41+
# Fixtures
42+
@pytest.fixture(autouse=True)
43+
def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None:
44+
"""Silence click output in every test."""
45+
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
46+
monkeypatch.setattr(click, "secho", lambda *a, **k: None)
47+
48+
49+
@pytest.fixture(autouse=True)
50+
def _patch_types_and_runner(monkeypatch: pytest.MonkeyPatch) -> None:
51+
"""Replace google.genai.types and Runner with lightweight fakes."""
52+
53+
# Dummy Part / Content
54+
class _Part:
55+
def __init__(self, text: str | None = "") -> None:
56+
self.text = text
57+
58+
class _Content:
59+
def __init__(self, role: str, parts: List[_Part]) -> None:
60+
self.role = role
61+
self.parts = parts
62+
63+
monkeypatch.setattr(cli.types, "Part", _Part)
64+
monkeypatch.setattr(cli.types, "Content", _Content)
65+
66+
# Fake Runner yielding a single assistant echo
67+
class _FakeRunner:
68+
def __init__(self, *a: Any, **k: Any) -> None: ...
69+
70+
async def run_async(self, *a: Any, **k: Any):
71+
message = a[2] if len(a) >= 3 else k["new_message"]
72+
text = message.parts[0].text if message.parts else ""
73+
response = _Content("assistant", [_Part(f"echo:{text}")])
74+
yield types.SimpleNamespace(author="assistant", content=response)
75+
76+
monkeypatch.setattr(cli, "Runner", _FakeRunner)
77+
78+
79+
@pytest.fixture()
80+
def fake_agent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
81+
"""Create a minimal importable agent package and patch importlib."""
82+
83+
parent_dir = tmp_path / "agents"
84+
parent_dir.mkdir()
85+
agent_dir = parent_dir / "fake_agent"
86+
agent_dir.mkdir()
87+
# __init__.py exposes root_agent with .name
88+
(agent_dir / "__init__.py").write_text(
89+
"from types import SimpleNamespace\n"
90+
"root_agent = SimpleNamespace(name='fake_root')\n"
91+
)
92+
93+
# Ensure importable via sys.path
94+
sys.path.insert(0, str(parent_dir))
95+
96+
import importlib
97+
98+
module = importlib.import_module("fake_agent")
99+
fake_module = types.SimpleNamespace(agent=module)
100+
101+
monkeypatch.setattr(importlib, "import_module", lambda n: fake_module)
102+
monkeypatch.setattr(cli.envs, "load_dotenv_for_agent", lambda *a, **k: None)
103+
104+
yield parent_dir, "fake_agent"
105+
106+
# Cleanup
107+
sys.path.remove(str(parent_dir))
108+
del sys.modules["fake_agent"]
109+
110+
111+
# _run_input_file
112+
@pytest.mark.asyncio
113+
async def test_run_input_file_outputs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
114+
"""run_input_file should echo user & assistant messages and return a populated session."""
115+
recorder: List[str] = []
116+
117+
def _echo(msg: str) -> None:
118+
recorder.append(msg)
119+
120+
monkeypatch.setattr(click, "echo", _echo)
121+
122+
input_json = {
123+
"state": {"foo": "bar"},
124+
"queries": ["hello world"],
125+
}
126+
input_path = tmp_path / "input.json"
127+
input_path.write_text(json.dumps(input_json))
128+
129+
artifact_service = cli.InMemoryArtifactService()
130+
session_service = cli.InMemorySessionService()
131+
dummy_root = types.SimpleNamespace(name="root")
132+
133+
session = await cli.run_input_file(
134+
app_name="app",
135+
user_id="user",
136+
root_agent=dummy_root,
137+
artifact_service=artifact_service,
138+
session_service=session_service,
139+
input_path=str(input_path),
140+
)
141+
142+
assert session.state["foo"] == "bar"
143+
assert any("[user]:" in line for line in recorder)
144+
assert any("[assistant]:" in line for line in recorder)
145+
146+
147+
# _run_cli (input_file branch)
148+
@pytest.mark.asyncio
149+
async def test_run_cli_with_input_file(fake_agent, tmp_path: Path) -> None:
150+
"""run_cli should process an input file without raising and without saving."""
151+
parent_dir, folder_name = fake_agent
152+
input_json = {"state": {}, "queries": ["ping"]}
153+
input_path = tmp_path / "in.json"
154+
input_path.write_text(json.dumps(input_json))
155+
156+
await cli.run_cli(
157+
agent_parent_dir=str(parent_dir),
158+
agent_folder_name=folder_name,
159+
input_file=str(input_path),
160+
saved_session_file=None,
161+
save_session=False,
162+
)
163+
164+
165+
# _run_cli (interactive + save session branch)
166+
@pytest.mark.asyncio
167+
async def test_run_cli_save_session(fake_agent, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
168+
"""run_cli should save a session file when save_session=True."""
169+
parent_dir, folder_name = fake_agent
170+
171+
# Simulate user typing 'exit' followed by session id 'sess123'
172+
responses = iter(["exit", "sess123"])
173+
monkeypatch.setattr("builtins.input", lambda *_a, **_k: next(responses))
174+
175+
session_file = Path(parent_dir) / folder_name / "sess123.session.json"
176+
if session_file.exists():
177+
session_file.unlink()
178+
179+
await cli.run_cli(
180+
agent_parent_dir=str(parent_dir),
181+
agent_folder_name=folder_name,
182+
input_file=None,
183+
saved_session_file=None,
184+
save_session=True,
185+
)
186+
187+
assert session_file.exists()
188+
data = json.loads(session_file.read_text())
189+
# The saved JSON should at least contain id and events keys
190+
assert "id" in data and "events" in data
191+
192+
193+
@pytest.mark.asyncio
194+
async def test_run_interactively_whitespace_and_exit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
195+
"""run_interactively should skip blank input, echo once, then exit."""
196+
# make a session that belongs to dummy agent
197+
svc = cli.InMemorySessionService()
198+
sess = svc.create_session(app_name="dummy", user_id="u")
199+
artifact_service = cli.InMemoryArtifactService()
200+
root_agent = types.SimpleNamespace(name="root")
201+
202+
# fake user input: blank -> 'hello' -> 'exit'
203+
answers = iter([" ", "hello", "exit"])
204+
monkeypatch.setattr("builtins.input", lambda *_a, **_k: next(answers))
205+
206+
# capture assisted echo
207+
echoed: list[str] = []
208+
monkeypatch.setattr(click, "echo", lambda msg: echoed.append(msg))
209+
210+
await cli.run_interactively(root_agent, artifact_service, sess, svc)
211+
212+
# verify: assistant echoed once with 'echo:hello'
213+
assert any("echo:hello" in m for m in echoed)
214+
215+
216+
# run_cli (resume branch)
217+
@pytest.mark.asyncio
218+
async def test_run_cli_resume_saved_session(tmp_path: Path, fake_agent, monkeypatch: pytest.MonkeyPatch) -> None:
219+
"""run_cli should load previous session, print its events, then re-enter interactive mode."""
220+
parent_dir, folder = fake_agent
221+
222+
# stub Session.model_validate_json to return dummy session with two events
223+
user_content = types.SimpleNamespace(parts=[types.SimpleNamespace(text="hi")])
224+
assistant_content = types.SimpleNamespace(parts=[types.SimpleNamespace(text="hello!")])
225+
dummy_session = types.SimpleNamespace(
226+
id="sess",
227+
app_name=folder,
228+
user_id="u",
229+
events=[
230+
types.SimpleNamespace(author="user", content=user_content, partial=False),
231+
types.SimpleNamespace(author="assistant", content=assistant_content, partial=False),
232+
],
233+
)
234+
monkeypatch.setattr(cli.Session, "model_validate_json", staticmethod(lambda _s: dummy_session))
235+
monkeypatch.setattr(cli.InMemorySessionService, "append_event", lambda *_a, **_k: None)
236+
# interactive inputs: immediately 'exit'
237+
monkeypatch.setattr("builtins.input", lambda *_a, **_k: "exit")
238+
239+
# collect echo output
240+
captured: list[str] = []
241+
monkeypatch.setattr(click, "echo", lambda m: captured.append(m))
242+
243+
saved_path = tmp_path / "prev.session.json"
244+
saved_path.write_text("{}") # contents not used – patched above
245+
246+
await cli.run_cli(
247+
agent_parent_dir=str(parent_dir),
248+
agent_folder_name=folder,
249+
input_file=None,
250+
saved_session_file=str(saved_path),
251+
save_session=False,
252+
)
253+
254+
# ④ ensure both historical messages were printed
255+
assert any("[user]: hi" in m for m in captured)
256+
assert any("[assistant]: hello!" in m for m in captured)

0 commit comments

Comments
 (0)
0