ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
1636 lines
46 KiB
Python
1636 lines
46 KiB
Python
# Copyright 2026 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 json
|
|
import logging
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Tuple
|
|
from unittest import mock
|
|
|
|
import click
|
|
from click.testing import CliRunner
|
|
from google.adk.agents.base_agent import BaseAgent
|
|
from google.adk.cli import cli_tools_click
|
|
from google.adk.evaluation.eval_case import EvalCase
|
|
from google.adk.evaluation.eval_set import EvalSet
|
|
from google.adk.evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
|
|
from google.adk.evaluation.local_eval_sets_manager import LocalEvalSetsManager
|
|
from google.adk.events.event import Event
|
|
from pydantic import BaseModel
|
|
import pytest
|
|
|
|
|
|
class DummyAgent(BaseAgent):
|
|
|
|
def __init__(self, name):
|
|
super().__init__(name=name)
|
|
self.sub_agents = []
|
|
|
|
|
|
root_agent = DummyAgent(name="dummy_agent")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_load_eval_set_from_file():
|
|
with mock.patch(
|
|
"google.adk.evaluation.local_eval_sets_manager.load_eval_set_from_file"
|
|
) as mock_func:
|
|
yield mock_func
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_get_root_agent():
|
|
with mock.patch("google.adk.cli.cli_eval.get_root_agent") as mock_func:
|
|
mock_func.return_value = root_agent
|
|
yield mock_func
|
|
|
|
|
|
# Helpers
|
|
class _Recorder(BaseModel):
|
|
"""Callable that records every invocation."""
|
|
|
|
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(request, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Suppress click output during tests."""
|
|
# Allow tests to opt-out of muting by using the 'unmute_click' marker
|
|
if "unmute_click" in request.keywords:
|
|
return
|
|
monkeypatch.setattr(click, "echo", lambda *a, **k: None)
|
|
# Keep secho for error messages
|
|
# 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.cli_run)
|
|
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.cli_run)
|
|
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("google.adk.cli.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, (result.output, repr(result.exception))
|
|
assert rec.calls, "cli_create.run_cmd must be called"
|
|
|
|
|
|
# cli run
|
|
@pytest.mark.parametrize(
|
|
"cli_args,expected_session_uri,expected_artifact_uri,expected_memory_uri",
|
|
[
|
|
pytest.param(
|
|
[
|
|
"--session_service_uri",
|
|
"memory://",
|
|
"--artifact_service_uri",
|
|
"memory://",
|
|
"--memory_service_uri",
|
|
"memory://",
|
|
],
|
|
"memory://",
|
|
"memory://",
|
|
"memory://",
|
|
id="memory_scheme_uris",
|
|
),
|
|
pytest.param(
|
|
[],
|
|
None,
|
|
None,
|
|
None,
|
|
id="default_uris_none",
|
|
),
|
|
],
|
|
)
|
|
def test_cli_run_service_uris(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
cli_args: list,
|
|
expected_session_uri: Optional[str],
|
|
expected_artifact_uri: Optional[str],
|
|
expected_memory_uri: Optional[str],
|
|
) -> None:
|
|
"""`adk run` should forward service URIs correctly to run_cli."""
|
|
agent_dir = tmp_path / "agent"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Capture the coroutine's locals before closing it
|
|
captured_locals = []
|
|
|
|
def capture_asyncio_run(coro):
|
|
# Extract the locals before closing the coroutine
|
|
if coro.cr_frame is not None:
|
|
captured_locals.append(dict(coro.cr_frame.f_locals))
|
|
coro.close() # Properly close the coroutine to avoid warnings
|
|
|
|
monkeypatch.setattr(cli_tools_click.asyncio, "run", capture_asyncio_run)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["run", *cli_args, str(agent_dir)],
|
|
)
|
|
assert result.exit_code == 0, (result.output, repr(result.exception))
|
|
assert len(captured_locals) == 1, "Expected asyncio.run to be called once"
|
|
|
|
# Verify the kwargs passed to run_cli
|
|
coro_locals = captured_locals[0]
|
|
assert coro_locals.get("session_service_uri") == expected_session_uri
|
|
assert coro_locals.get("artifact_service_uri") == expected_artifact_uri
|
|
assert coro_locals.get("memory_service_uri") == expected_memory_uri
|
|
assert coro_locals["agent_folder_name"] == "agent"
|
|
|
|
|
|
def test_cli_run_basic_with_query(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should invoke run_once_cli."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
mock_run_once = mock.AsyncMock(return_value=0)
|
|
monkeypatch.setattr("google.adk.cli.cli.run_once_cli", mock_run_once)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["run", str(agent_dir), "hello"],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
assert mock_run_once.called
|
|
called_kwargs = mock_run_once.call_args.kwargs
|
|
assert called_kwargs.get("query") == "hello"
|
|
assert called_kwargs.get("agent_folder_name") == "agent"
|
|
|
|
|
|
def test_cli_run_interactive_with_state(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` in interactive mode should pass state to run_cli."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_interactive"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
mock_run_cli = mock.AsyncMock()
|
|
monkeypatch.setattr("google.adk.cli.cli_tools_click.run_cli", mock_run_cli)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["run", str(agent_dir), "--state", '{"x": 1}'],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
assert mock_run_cli.called
|
|
called_kwargs = mock_run_cli.call_args.kwargs
|
|
assert called_kwargs.get("state_str") == '{"x": 1}'
|
|
|
|
|
|
def test_cli_run_options_with_query(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query and options should forward options correctly."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_opts"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
|
|
mock_run_once = mock.AsyncMock(return_value=0)
|
|
monkeypatch.setattr("google.adk.cli.cli.run_once_cli", mock_run_once)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"hello",
|
|
"--state",
|
|
'{"x": 1}',
|
|
"--in_memory",
|
|
"--jsonl",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
assert mock_run_once.called
|
|
called_kwargs = mock_run_once.call_args.kwargs
|
|
assert called_kwargs.get("query") == "hello"
|
|
assert called_kwargs.get("state_str") == '{"x": 1}'
|
|
assert called_kwargs.get("in_memory") is True
|
|
assert called_kwargs.get("jsonl") is True
|
|
|
|
|
|
def test_cli_run_auto_resume_with_query(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should auto-resume if session has active interrupts."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_resume"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Mock session service
|
|
mock_session = mock.Mock()
|
|
mock_session.id = "s123"
|
|
mock_session.user_id = "u123"
|
|
mock_session.app_name = "agent_resume"
|
|
|
|
# Create a mock event with long_running_tool_ids
|
|
mock_event = Event(
|
|
invocation_id="invocation_123",
|
|
long_running_tool_ids={"interrupt_123"},
|
|
author="agent",
|
|
)
|
|
mock_session.events = [mock_event]
|
|
|
|
mock_session_service = mock.AsyncMock()
|
|
mock_session_service.get_session.return_value = mock_session
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.create_session_service_from_options",
|
|
mock.Mock(return_value=mock_session_service),
|
|
)
|
|
|
|
# Mock AgentLoader to avoid loading real files
|
|
mock_agent_loader = mock.Mock()
|
|
mock_agent_loader.load_agent.return_value = mock.Mock(name="agent_resume")
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.AgentLoader",
|
|
mock.Mock(return_value=mock_agent_loader),
|
|
)
|
|
|
|
# Mock Runner
|
|
mock_runner_instance = mock.Mock()
|
|
|
|
# Create an async generator for run_async
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Yield a mock event to simulate engine output
|
|
ev = mock.Mock()
|
|
ev.author = "agent"
|
|
ev.node_info = mock.Mock(path="node")
|
|
ev.content = None
|
|
ev.long_running_tool_ids = []
|
|
# Add model_dump method as it's called in _print_event
|
|
ev.model_dump.return_value = {"author": "agent", "node_path": "node"}
|
|
yield ev
|
|
|
|
mock_runner_instance.run_async.side_effect = mock_run_async
|
|
mock_runner_instance.close = mock.AsyncMock()
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.Runner",
|
|
mock.Mock(return_value=mock_runner_instance),
|
|
)
|
|
|
|
# Mock _to_app to return a mock app
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli._to_app",
|
|
mock.Mock(return_value=mock.Mock(name="agent_resume")),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"approve",
|
|
"--session_id",
|
|
"s123",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
|
|
# Verify run_async was called with FunctionResponse
|
|
called_args = mock_runner_instance.run_async.call_args
|
|
assert called_args is not None
|
|
kwargs = called_args.kwargs
|
|
new_message = kwargs.get("new_message")
|
|
assert new_message is not None
|
|
assert len(new_message.parts) == 1
|
|
part = new_message.parts[0]
|
|
assert part.function_response is not None
|
|
assert part.function_response.id == "interrupt_123"
|
|
assert part.function_response.response == {"result": "approve"}
|
|
|
|
|
|
def test_cli_run_auto_resume_with_query_confirmation_yes(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should auto-resume with confirmation=True if query is positive."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_resume_confirm_yes"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Mock session service
|
|
mock_session = mock.Mock()
|
|
mock_session.id = "s123"
|
|
mock_session.user_id = "u123"
|
|
mock_session.app_name = "agent_resume_confirm_yes"
|
|
|
|
# Create a mock event with long_running_tool_ids
|
|
mock_event = mock.Mock()
|
|
mock_event.long_running_tool_ids = ["interrupt_123"]
|
|
mock_event.invocation_id = "invocation_123"
|
|
|
|
# Mock get_function_calls to return adk_request_confirmation
|
|
fc = mock.Mock()
|
|
fc.id = "interrupt_123"
|
|
fc.name = "adk_request_confirmation"
|
|
mock_event.get_function_calls.return_value = [fc]
|
|
|
|
mock_session.events = [mock_event]
|
|
|
|
mock_session_service = mock.AsyncMock()
|
|
mock_session_service.get_session.return_value = mock_session
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.create_session_service_from_options",
|
|
mock.Mock(return_value=mock_session_service),
|
|
)
|
|
|
|
# Mock AgentLoader to avoid loading real files
|
|
mock_agent_loader = mock.Mock()
|
|
mock_agent_loader.load_agent.return_value = mock.Mock(
|
|
name="agent_resume_confirm_yes"
|
|
)
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.AgentLoader",
|
|
mock.Mock(return_value=mock_agent_loader),
|
|
)
|
|
|
|
# Mock Runner
|
|
mock_runner_instance = mock.Mock()
|
|
|
|
# Create an async generator for run_async
|
|
async def mock_run_async(*args, **kwargs):
|
|
ev = mock.Mock()
|
|
ev.author = "agent"
|
|
ev.node_info = mock.Mock(path="node")
|
|
ev.content = None
|
|
ev.long_running_tool_ids = []
|
|
ev.model_dump.return_value = {"author": "agent", "node_path": "node"}
|
|
yield ev
|
|
|
|
mock_runner_instance.run_async.side_effect = mock_run_async
|
|
mock_runner_instance.close = mock.AsyncMock()
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.Runner",
|
|
mock.Mock(return_value=mock_runner_instance),
|
|
)
|
|
|
|
# Mock _to_app to return a mock app
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli._to_app",
|
|
mock.Mock(return_value=mock.Mock(name="agent_resume_confirm_yes")),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act: Query is "yes" -> confirmed=True
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"yes",
|
|
"--session_id",
|
|
"s123",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
called_args = mock_runner_instance.run_async.call_args
|
|
assert called_args is not None
|
|
kwargs = called_args.kwargs
|
|
assert kwargs.get("invocation_id") == "invocation_123"
|
|
new_message = kwargs.get("new_message")
|
|
assert new_message is not None
|
|
assert len(new_message.parts) == 1
|
|
part = new_message.parts[0]
|
|
assert part.function_response is not None
|
|
assert part.function_response.id == "interrupt_123"
|
|
assert part.function_response.name == "adk_request_confirmation"
|
|
assert part.function_response.response == {"confirmed": True}
|
|
|
|
|
|
def test_cli_run_auto_resume_with_query_confirmation_no(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should auto-resume with confirmation=False if query is negative."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_resume_confirm_no"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Mock session service
|
|
mock_session = mock.Mock()
|
|
mock_session.id = "s123"
|
|
mock_session.user_id = "u123"
|
|
mock_session.app_name = "agent_resume_confirm_no"
|
|
|
|
# Create a mock event with long_running_tool_ids
|
|
mock_event = mock.Mock()
|
|
mock_event.long_running_tool_ids = ["interrupt_123"]
|
|
|
|
# Mock get_function_calls to return adk_request_confirmation
|
|
fc = mock.Mock()
|
|
fc.id = "interrupt_123"
|
|
fc.name = "adk_request_confirmation"
|
|
mock_event.get_function_calls.return_value = [fc]
|
|
|
|
mock_session.events = [mock_event]
|
|
|
|
mock_session_service = mock.AsyncMock()
|
|
mock_session_service.get_session.return_value = mock_session
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.create_session_service_from_options",
|
|
mock.Mock(return_value=mock_session_service),
|
|
)
|
|
|
|
# Mock AgentLoader to avoid loading real files
|
|
mock_agent_loader = mock.Mock()
|
|
mock_agent_loader.load_agent.return_value = mock.Mock(
|
|
name="agent_resume_confirm_no"
|
|
)
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.AgentLoader",
|
|
mock.Mock(return_value=mock_agent_loader),
|
|
)
|
|
|
|
# Mock Runner
|
|
mock_runner_instance = mock.Mock()
|
|
|
|
# Create an async generator for run_async
|
|
async def mock_run_async(*args, **kwargs):
|
|
ev = mock.Mock()
|
|
ev.author = "agent"
|
|
ev.node_info = mock.Mock(path="node")
|
|
ev.content = None
|
|
ev.long_running_tool_ids = []
|
|
ev.model_dump.return_value = {"author": "agent", "node_path": "node"}
|
|
yield ev
|
|
|
|
mock_runner_instance.run_async.side_effect = mock_run_async
|
|
mock_runner_instance.close = mock.AsyncMock()
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.Runner",
|
|
mock.Mock(return_value=mock_runner_instance),
|
|
)
|
|
|
|
# Mock _to_app to return a mock app
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli._to_app",
|
|
mock.Mock(return_value=mock.Mock(name="agent_resume_confirm_no")),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act: Query is "no" -> confirmed=False
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"no",
|
|
"--session_id",
|
|
"s123",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
called_args = mock_runner_instance.run_async.call_args
|
|
assert called_args is not None
|
|
kwargs = called_args.kwargs
|
|
new_message = kwargs.get("new_message")
|
|
assert new_message is not None
|
|
assert len(new_message.parts) == 1
|
|
part = new_message.parts[0]
|
|
assert part.function_response is not None
|
|
assert part.function_response.id == "interrupt_123"
|
|
assert part.function_response.name == "adk_request_confirmation"
|
|
assert part.function_response.response == {"confirmed": False}
|
|
|
|
|
|
def test_cli_run_auto_resume_with_query_confirmation_json(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should auto-resume with JSON response if query is valid JSON."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_resume_confirm_json"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Mock session service
|
|
mock_session = mock.Mock()
|
|
mock_session.id = "s123"
|
|
mock_session.user_id = "u123"
|
|
mock_session.app_name = "agent_resume_confirm_json"
|
|
|
|
# Create a mock event with long_running_tool_ids
|
|
mock_event = mock.Mock()
|
|
mock_event.long_running_tool_ids = ["interrupt_123"]
|
|
|
|
# Mock get_function_calls to return adk_request_confirmation
|
|
fc = mock.Mock()
|
|
fc.id = "interrupt_123"
|
|
fc.name = "adk_request_confirmation"
|
|
mock_event.get_function_calls.return_value = [fc]
|
|
|
|
mock_session.events = [mock_event]
|
|
|
|
mock_session_service = mock.AsyncMock()
|
|
mock_session_service.get_session.return_value = mock_session
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.create_session_service_from_options",
|
|
mock.Mock(return_value=mock_session_service),
|
|
)
|
|
|
|
# Mock AgentLoader to avoid loading real files
|
|
mock_agent_loader = mock.Mock()
|
|
mock_agent_loader.load_agent.return_value = mock.Mock(
|
|
name="agent_resume_confirm_json"
|
|
)
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.AgentLoader",
|
|
mock.Mock(return_value=mock_agent_loader),
|
|
)
|
|
|
|
# Mock Runner
|
|
mock_runner_instance = mock.Mock()
|
|
|
|
# Create an async generator for run_async
|
|
async def mock_run_async(*args, **kwargs):
|
|
ev = mock.Mock()
|
|
ev.author = "agent"
|
|
ev.node_info = mock.Mock(path="node")
|
|
ev.content = None
|
|
ev.long_running_tool_ids = []
|
|
ev.model_dump.return_value = {"author": "agent", "node_path": "node"}
|
|
yield ev
|
|
|
|
mock_runner_instance.run_async.side_effect = mock_run_async
|
|
mock_runner_instance.close = mock.AsyncMock()
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli.Runner",
|
|
mock.Mock(return_value=mock_runner_instance),
|
|
)
|
|
|
|
# Mock _to_app to return a mock app
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli._to_app",
|
|
mock.Mock(return_value=mock.Mock(name="agent_resume_confirm_json")),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act: Query is a JSON string
|
|
json_query = '{"confirmed": true, "payload": {"amount": 100}}'
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
json_query,
|
|
"--session_id",
|
|
"s123",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
called_args = mock_runner_instance.run_async.call_args
|
|
assert called_args is not None
|
|
kwargs = called_args.kwargs
|
|
new_message = kwargs.get("new_message")
|
|
assert new_message is not None
|
|
assert len(new_message.parts) == 1
|
|
part = new_message.parts[0]
|
|
assert part.function_response is not None
|
|
assert part.function_response.id == "interrupt_123"
|
|
assert part.function_response.name == "adk_request_confirmation"
|
|
assert part.function_response.response == {
|
|
"confirmed": True,
|
|
"payload": {"amount": 100},
|
|
}
|
|
|
|
|
|
def test_cli_run_all_options_with_query(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should forward all options correctly."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_all_opts"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
|
|
mock_run_once = mock.AsyncMock(return_value=0)
|
|
monkeypatch.setattr("google.adk.cli.cli.run_once_cli", mock_run_once)
|
|
|
|
replay_file = tmp_path / "replay.json"
|
|
replay_file.write_text('{"queries": ["hello"], "state": {}}')
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"hello",
|
|
"--state",
|
|
'{"x": 1}',
|
|
"--session_id",
|
|
"s123",
|
|
"--replay",
|
|
str(replay_file),
|
|
"--timeout",
|
|
"30s",
|
|
"--in_memory",
|
|
"--session_service_uri",
|
|
"memory://",
|
|
"--artifact_service_uri",
|
|
"memory://",
|
|
"--memory_service_uri",
|
|
"memory://",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0, f"Output: {result.output}"
|
|
assert mock_run_once.called
|
|
called_kwargs = mock_run_once.call_args.kwargs
|
|
assert called_kwargs.get("query") == "hello"
|
|
assert called_kwargs.get("state_str") == '{"x": 1}'
|
|
assert called_kwargs.get("session_id") == "s123"
|
|
assert called_kwargs.get("replay") == str(replay_file)
|
|
assert called_kwargs.get("timeout") == "30s"
|
|
assert called_kwargs.get("in_memory") is True
|
|
assert called_kwargs.get("session_service_uri") == "memory://"
|
|
assert called_kwargs.get("artifact_service_uri") == "memory://"
|
|
assert called_kwargs.get("memory_service_uri") == "memory://"
|
|
assert called_kwargs.get("use_local_storage") is True
|
|
|
|
|
|
def test_cli_run_no_use_local_storage_with_query(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk run` with query should forward --no_use_local_storage correctly."""
|
|
# Arrange
|
|
agent_dir = tmp_path / "agent_no_local"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
|
|
mock_run_once = mock.AsyncMock(return_value=0)
|
|
monkeypatch.setattr("google.adk.cli.cli.run_once_cli", mock_run_once)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Act
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"run",
|
|
str(agent_dir),
|
|
"hello",
|
|
"--no_use_local_storage",
|
|
],
|
|
)
|
|
|
|
# Assert
|
|
assert result.exit_code == 0
|
|
assert mock_run_once.called
|
|
called_kwargs = mock_run_once.call_args.kwargs
|
|
assert called_kwargs.get("use_local_storage") is False
|
|
|
|
|
|
# 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("google.adk.cli.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("google.adk.cli.cli_deploy.to_cloud_run", _boom)
|
|
|
|
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 "Deploy failed: boom" in result.output
|
|
|
|
|
|
def test_cli_deploy_cloud_run_passthrough_args(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Extra args after '--' should be passed through to the gcloud command."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_cloud_run", rec)
|
|
|
|
agent_dir = tmp_path / "agent_passthrough"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
"--region",
|
|
"us-central1",
|
|
str(agent_dir),
|
|
"--",
|
|
"--labels=test-label=test",
|
|
"--memory=1Gi",
|
|
"--cpu=1",
|
|
],
|
|
)
|
|
# Print debug information if the test fails
|
|
if result.exit_code != 0:
|
|
print(f"Exit code: {result.exit_code}")
|
|
print(f"Output: {result.output}")
|
|
print(f"Exception: {result.exception}")
|
|
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
|
|
|
|
# Check that extra_gcloud_args were passed correctly
|
|
called_kwargs = rec.calls[0][1]
|
|
extra_args = called_kwargs.get("extra_gcloud_args")
|
|
assert extra_args is not None
|
|
assert "--labels=test-label=test" in extra_args
|
|
assert "--memory=1Gi" in extra_args
|
|
assert "--cpu=1" in extra_args
|
|
|
|
|
|
def test_cli_deploy_cloud_run_allows_empty_gcloud_args(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""No gcloud args after '--' should be allowed."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_cloud_run", rec)
|
|
|
|
agent_dir = tmp_path / "agent_empty_gcloud"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
"--region",
|
|
"us-central1",
|
|
str(agent_dir),
|
|
"--",
|
|
# No gcloud args after --
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
|
|
|
|
# Check that extra_gcloud_args is empty
|
|
called_kwargs = rec.calls[0][1]
|
|
extra_args = called_kwargs.get("extra_gcloud_args")
|
|
assert extra_args == ()
|
|
|
|
|
|
def test_cli_deploy_cloud_run_interspersed_options(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Options placed after the positional argument should be parsed correctly."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_cloud_run", rec)
|
|
|
|
agent_dir = tmp_path / "agent_interspersed"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
str(agent_dir),
|
|
"--project",
|
|
"test-project",
|
|
"--region",
|
|
"us-central1",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
|
|
|
|
called_kwargs = rec.calls[0][1]
|
|
assert called_kwargs.get("project") == "test-project"
|
|
assert called_kwargs.get("region") == "us-central1"
|
|
|
|
|
|
def test_cli_deploy_cloud_run_rejects_unknown_option_before_separator(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Unknown option placed before '--' separator should be rejected by Click."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_cloud_run", rec)
|
|
|
|
agent_dir = tmp_path / "agent_bad_order"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
str(agent_dir),
|
|
"--labels=test-label=test",
|
|
"--",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 2
|
|
assert "No such option" in result.output
|
|
assert not rec.calls, "cli_deploy.to_cloud_run should not be called"
|
|
|
|
|
|
def test_cli_deploy_cloud_run_forwards_extra_positional_arg(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Extra positional argument before '--' is forwarded to the deployment runner."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_cloud_run", rec)
|
|
|
|
agent_dir = tmp_path / "agent_extra_pos"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
str(agent_dir),
|
|
"unexpected_arg",
|
|
"--",
|
|
"--labels=test-label=test",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
extra_args = rec.calls[0][1].get("extra_gcloud_args")
|
|
assert extra_args == ("unexpected_arg", "--labels=test-label=test")
|
|
|
|
|
|
# cli deploy agent_engine
|
|
def test_cli_deploy_agent_engine_success(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Successful path should call cli_deploy.to_agent_engine."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_agent_engine", rec)
|
|
|
|
agent_dir = tmp_path / "agent_ae"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"agent_engine",
|
|
"--project",
|
|
"test-proj",
|
|
"--region",
|
|
"us-central1",
|
|
str(agent_dir),
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_agent_engine must be invoked"
|
|
called_kwargs = rec.calls[0][1]
|
|
assert called_kwargs.get("project") == "test-proj"
|
|
assert called_kwargs.get("region") == "us-central1"
|
|
|
|
|
|
# cli deploy agent_engine with --otel_to_cloud
|
|
def test_cli_deploy_agent_engine_otel_to_cloud_success(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Successful path should call cli_deploy.to_agent_engine with --otel_to_cloud."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_agent_engine", rec)
|
|
|
|
agent_dir = tmp_path / "agent_ae"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"agent_engine",
|
|
"--project",
|
|
"test-proj",
|
|
"--region",
|
|
"us-central1",
|
|
"--otel_to_cloud",
|
|
str(agent_dir),
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_agent_engine must be invoked"
|
|
called_kwargs = rec.calls[0][1]
|
|
assert called_kwargs.get("project") == "test-proj"
|
|
assert called_kwargs.get("region") == "us-central1"
|
|
assert called_kwargs.get("otel_to_cloud")
|
|
|
|
|
|
# cli deploy gke
|
|
def test_cli_deploy_gke_success(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Successful path should call cli_deploy.to_gke."""
|
|
rec = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.cli_deploy.to_gke", rec)
|
|
|
|
agent_dir = tmp_path / "agent_gke"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"gke",
|
|
"--project",
|
|
"test-proj",
|
|
"--region",
|
|
"us-central1",
|
|
"--cluster_name",
|
|
"my-cluster",
|
|
str(agent_dir),
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert rec.calls, "cli_deploy.to_gke must be invoked"
|
|
called_kwargs = rec.calls[0][1]
|
|
assert called_kwargs.get("project") == "test-proj"
|
|
assert called_kwargs.get("region") == "us-central1"
|
|
assert called_kwargs.get("cluster_name") == "my-cluster"
|
|
|
|
|
|
# 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."""
|
|
orig_import = builtins.__import__
|
|
|
|
def _fake_import(name: str, globals=None, locals=None, fromlist=(), level=0):
|
|
if name == "google.adk.cli.cli_eval" or (level > 0 and "cli_eval" in name):
|
|
raise ModuleNotFoundError(f"Simulating missing {name}")
|
|
return orig_import(name, globals, locals, fromlist, level)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", _fake_import)
|
|
|
|
agent_dir = tmp_path / "agent_missing_deps"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
eval_file = tmp_path / "dummy.json"
|
|
eval_file.touch()
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["eval", str(agent_dir), str(eval_file)],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert isinstance(result.exception, SystemExit)
|
|
assert cli_tools_click.MISSING_EVAL_DEPENDENCIES_MESSAGE in result.output
|
|
|
|
|
|
# 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()
|
|
)
|
|
return rec
|
|
|
|
|
|
def test_cli_web_invokes_uvicorn(
|
|
tmp_path: Path, _patch_uvicorn: _Recorder, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk web` should configure and start uvicorn.Server.run."""
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.fast_api.get_fast_api_app", lambda **_k: object()
|
|
)
|
|
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, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""`adk api_server` should configure and start uvicorn.Server.run."""
|
|
agents_dir = tmp_path / "agents_api"
|
|
agents_dir.mkdir()
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.fast_api.get_fast_api_app", lambda **_k: object()
|
|
)
|
|
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_web_passes_service_uris(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, _patch_uvicorn: _Recorder
|
|
) -> None:
|
|
"""`adk web` should pass service URIs to get_fast_api_app."""
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
|
|
mock_get_app = _Recorder()
|
|
monkeypatch.setattr("google.adk.cli.fast_api.get_fast_api_app", mock_get_app)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"web",
|
|
str(agents_dir),
|
|
"--session_service_uri",
|
|
"sqlite:///test.db",
|
|
"--artifact_service_uri",
|
|
"gs://mybucket",
|
|
"--memory_service_uri",
|
|
"rag://mycorpus",
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert mock_get_app.calls
|
|
called_kwargs = mock_get_app.calls[0][1]
|
|
assert called_kwargs.get("session_service_uri") == "sqlite:///test.db"
|
|
assert called_kwargs.get("artifact_service_uri") == "gs://mybucket"
|
|
assert called_kwargs.get("memory_service_uri") == "rag://mycorpus"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"flag",
|
|
["--allow-unsafe-unpickling", "--allow_unsafe_unpickling"],
|
|
)
|
|
def test_cli_migrate_session_allows_unsafe_unpickling_flag(
|
|
monkeypatch: pytest.MonkeyPatch, flag: str
|
|
) -> None:
|
|
calls: list[dict[str, Any]] = []
|
|
|
|
def fake_upgrade(
|
|
source_db_url: str,
|
|
dest_db_url: str,
|
|
*,
|
|
allow_unsafe_unpickling: bool = False,
|
|
) -> None:
|
|
calls.append({
|
|
"source_db_url": source_db_url,
|
|
"dest_db_url": dest_db_url,
|
|
"allow_unsafe_unpickling": allow_unsafe_unpickling,
|
|
})
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.sessions.migration.migration_runner.upgrade",
|
|
fake_upgrade,
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"migrate",
|
|
"session",
|
|
"--source_db_url",
|
|
"sqlite:///source.db",
|
|
"--dest_db_url",
|
|
"sqlite:///dest.db",
|
|
flag,
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, (result.output, repr(result.exception))
|
|
assert calls == [{
|
|
"source_db_url": "sqlite:///source.db",
|
|
"dest_db_url": "sqlite:///dest.db",
|
|
"allow_unsafe_unpickling": True,
|
|
}]
|
|
|
|
|
|
def test_cli_eval_with_eval_set_file_path(
|
|
mock_load_eval_set_from_file,
|
|
mock_get_root_agent,
|
|
tmp_path,
|
|
):
|
|
agent_path = tmp_path / "my_agent"
|
|
agent_path.mkdir()
|
|
(agent_path / "__init__.py").touch()
|
|
|
|
eval_set_file = tmp_path / "my_evals.json"
|
|
eval_set_file.write_text("{}")
|
|
|
|
mock_load_eval_set_from_file.return_value = EvalSet(
|
|
eval_set_id="my_evals",
|
|
eval_cases=[EvalCase(eval_id="case1", conversation=[])],
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
cli_tools_click.cli_eval,
|
|
[str(agent_path), str(eval_set_file)],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
# Assert that we wrote eval set results
|
|
eval_set_results_manager = LocalEvalSetResultsManager(
|
|
agents_dir=str(tmp_path)
|
|
)
|
|
eval_set_results = eval_set_results_manager.list_eval_set_results(
|
|
app_name="my_agent"
|
|
)
|
|
assert len(eval_set_results) == 1
|
|
|
|
|
|
def test_cli_eval_with_eval_set_id(
|
|
mock_get_root_agent,
|
|
tmp_path,
|
|
):
|
|
app_name = "test_app"
|
|
eval_set_id = "test_eval_set_id"
|
|
agent_path = tmp_path / app_name
|
|
agent_path.mkdir()
|
|
(agent_path / "__init__.py").touch()
|
|
|
|
eval_sets_manager = LocalEvalSetsManager(agents_dir=str(tmp_path))
|
|
eval_sets_manager.create_eval_set(app_name=app_name, eval_set_id=eval_set_id)
|
|
eval_sets_manager.add_eval_case(
|
|
app_name=app_name,
|
|
eval_set_id=eval_set_id,
|
|
eval_case=EvalCase(eval_id="case1", conversation=[]),
|
|
)
|
|
eval_sets_manager.add_eval_case(
|
|
app_name=app_name,
|
|
eval_set_id=eval_set_id,
|
|
eval_case=EvalCase(eval_id="case2", conversation=[]),
|
|
)
|
|
|
|
result = CliRunner().invoke(
|
|
cli_tools_click.cli_eval,
|
|
[str(agent_path), "test_eval_set_id:case1,case2"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
# Assert that we wrote eval set results
|
|
eval_set_results_manager = LocalEvalSetResultsManager(
|
|
agents_dir=str(tmp_path)
|
|
)
|
|
eval_set_results = eval_set_results_manager.list_eval_set_results(
|
|
app_name=app_name
|
|
)
|
|
assert len(eval_set_results) == 1
|
|
|
|
|
|
def test_cli_create_eval_set(tmp_path: Path):
|
|
app_name = "test_app"
|
|
eval_set_id = "test_eval_set"
|
|
agent_path = tmp_path / app_name
|
|
agent_path.mkdir()
|
|
(agent_path / "__init__.py").touch()
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["eval_set", "create", str(agent_path), eval_set_id],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
eval_set_file = agent_path / f"{eval_set_id}.evalset.json"
|
|
assert eval_set_file.exists()
|
|
with open(eval_set_file, "r") as f:
|
|
eval_set_data = json.load(f)
|
|
assert eval_set_data["eval_set_id"] == eval_set_id
|
|
assert eval_set_data["eval_cases"] == []
|
|
|
|
|
|
def test_cli_add_eval_case_with_session(tmp_path: Path):
|
|
app_name = "test_app_add_2"
|
|
eval_set_id = "test_eval_set_add_2"
|
|
agent_path = tmp_path / app_name
|
|
agent_path.mkdir()
|
|
(agent_path / "__init__.py").touch()
|
|
|
|
scenarios_file = tmp_path / "scenarios2.json"
|
|
scenarios_file.write_text(
|
|
'{"scenarios": [{"starting_prompt": "hello", "conversation_plan":'
|
|
' "world"}]}'
|
|
)
|
|
session_file = tmp_path / "session2.json"
|
|
session_file.write_text(
|
|
'{"app_name": "test_app_add_2", "user_id": "test_user", "state": {}}'
|
|
)
|
|
|
|
runner = CliRunner()
|
|
runner.invoke(
|
|
cli_tools_click.main,
|
|
["eval_set", "create", str(agent_path), eval_set_id],
|
|
catch_exceptions=False,
|
|
)
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"eval_set",
|
|
"add_eval_case",
|
|
str(agent_path),
|
|
eval_set_id,
|
|
"--scenarios_file",
|
|
str(scenarios_file),
|
|
"--session_input_file",
|
|
str(session_file),
|
|
],
|
|
catch_exceptions=False,
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
eval_set_file = agent_path / f"{eval_set_id}.evalset.json"
|
|
assert eval_set_file.exists()
|
|
with open(eval_set_file, "r") as f:
|
|
eval_set_data = json.load(f)
|
|
assert len(eval_set_data["eval_cases"]) == 1
|
|
eval_case = eval_set_data["eval_cases"][0]
|
|
assert eval_case["eval_id"] == "734909ff"
|
|
assert eval_case["session_input"]["app_name"] == "test_app_add_2"
|
|
|
|
|
|
def test_cli_add_eval_case_skip_existing(tmp_path: Path):
|
|
app_name = "test_app_add_3"
|
|
eval_set_id = "test_eval_set_add_3"
|
|
agent_path = tmp_path / app_name
|
|
agent_path.mkdir()
|
|
(agent_path / "__init__.py").touch()
|
|
|
|
scenarios_file = tmp_path / "scenarios3.json"
|
|
scenarios_file.write_text(
|
|
'{"scenarios": [{"starting_prompt": "hello", "conversation_plan":'
|
|
' "world"}]}'
|
|
)
|
|
session_file = tmp_path / "session3.json"
|
|
session_file.write_text(
|
|
'{"app_name": "test_app_add_3", "user_id": "test_user", "state": {}}'
|
|
)
|
|
|
|
runner = CliRunner()
|
|
runner.invoke(
|
|
cli_tools_click.main,
|
|
["eval_set", "create", str(agent_path), eval_set_id],
|
|
catch_exceptions=False,
|
|
)
|
|
result1 = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"eval_set",
|
|
"add_eval_case",
|
|
str(agent_path),
|
|
eval_set_id,
|
|
"--scenarios_file",
|
|
str(scenarios_file),
|
|
"--session_input_file",
|
|
str(session_file),
|
|
],
|
|
catch_exceptions=False,
|
|
)
|
|
eval_set_file = agent_path / f"{eval_set_id}.evalset.json"
|
|
with open(eval_set_file, "r") as f:
|
|
eval_set_data1 = json.load(f)
|
|
|
|
result2 = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"eval_set",
|
|
"add_eval_case",
|
|
str(agent_path),
|
|
eval_set_id,
|
|
"--scenarios_file",
|
|
str(scenarios_file),
|
|
"--session_input_file",
|
|
str(session_file),
|
|
],
|
|
catch_exceptions=False,
|
|
)
|
|
with open(eval_set_file, "r") as f:
|
|
eval_set_data2 = json.load(f)
|
|
|
|
assert result1.exit_code == 0
|
|
assert result2.exit_code == 0
|
|
assert len(eval_set_data1["eval_cases"]) == 1
|
|
assert len(eval_set_data2["eval_cases"]) == 1
|
|
|
|
|
|
def test_cli_deploy_cloud_run_gcloud_arg_conflict(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Extra gcloud args that conflict with ADK deploy args should raise ClickException."""
|
|
|
|
def _mock_to_cloud_run(*_a, **kwargs):
|
|
# Import and call the validation function
|
|
from google.adk.cli.cli_deploy import _validate_gcloud_extra_args
|
|
|
|
# Build the same set of managed args as the real function would
|
|
adk_managed_args = {"--source", "--project", "--port", "--verbosity"}
|
|
if kwargs.get("region"):
|
|
adk_managed_args.add("--region")
|
|
_validate_gcloud_extra_args(
|
|
kwargs.get("extra_gcloud_args"), adk_managed_args
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"google.adk.cli.cli_deploy.to_cloud_run", _mock_to_cloud_run
|
|
)
|
|
|
|
agent_dir = tmp_path / "agent_conflict"
|
|
agent_dir.mkdir()
|
|
runner = CliRunner()
|
|
|
|
# Test with conflicting --project arg
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
"--region",
|
|
"us-central1",
|
|
str(agent_dir),
|
|
"--",
|
|
"--project=conflict-project", # This should conflict
|
|
],
|
|
)
|
|
|
|
expected_msg = (
|
|
"The argument '--project' conflicts with ADK's automatic configuration."
|
|
" ADK will set this argument automatically, so please remove it from your"
|
|
" command."
|
|
)
|
|
assert expected_msg in result.output
|
|
|
|
# Test with conflicting --port arg
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
str(agent_dir),
|
|
"--",
|
|
"--port=9000", # This should conflict
|
|
],
|
|
)
|
|
|
|
expected_msg = (
|
|
"The argument '--port' conflicts with ADK's automatic configuration. ADK"
|
|
" will set this argument automatically, so please remove it from your"
|
|
" command."
|
|
)
|
|
assert expected_msg in result.output
|
|
|
|
# Test with conflicting --region arg
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
[
|
|
"deploy",
|
|
"cloud_run",
|
|
"--project",
|
|
"test-project",
|
|
"--region",
|
|
"us-central1",
|
|
str(agent_dir),
|
|
"--",
|
|
"--region=us-west1", # This should conflict
|
|
],
|
|
)
|
|
|
|
expected_msg = (
|
|
"The argument '--region' conflicts with ADK's automatic configuration."
|
|
" ADK will set this argument automatically, so please remove it from your"
|
|
" command."
|
|
)
|
|
assert expected_msg in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cli_args,expected_log_level",
|
|
[
|
|
pytest.param(
|
|
[],
|
|
"INFO",
|
|
id="default_info",
|
|
),
|
|
pytest.param(
|
|
["--log_level", "DEBUG"],
|
|
"DEBUG",
|
|
id="explicit_debug",
|
|
),
|
|
pytest.param(
|
|
["--log_level", "WARNING"],
|
|
"WARNING",
|
|
id="explicit_warning",
|
|
),
|
|
pytest.param(
|
|
["-v"],
|
|
"DEBUG",
|
|
id="verbose_flag",
|
|
),
|
|
pytest.param(
|
|
["--verbose"],
|
|
"DEBUG",
|
|
id="verbose_long_flag",
|
|
),
|
|
pytest.param(
|
|
["-v", "--log_level", "WARNING"],
|
|
"WARNING",
|
|
id="both_verbose_and_explicit_warning",
|
|
),
|
|
],
|
|
)
|
|
def test_cli_run_log_level(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
cli_args: list[str],
|
|
expected_log_level: str,
|
|
) -> None:
|
|
"""`adk run` should configure log level correctly based on flags."""
|
|
agent_dir = tmp_path / "agent"
|
|
agent_dir.mkdir()
|
|
(agent_dir / "__init__.py").touch()
|
|
(agent_dir / "agent.py").touch()
|
|
|
|
# Mock logs.log_to_tmp_folder
|
|
mock_log_to_tmp_folder = mock.Mock()
|
|
monkeypatch.setattr(
|
|
cli_tools_click.logs, "log_to_tmp_folder", mock_log_to_tmp_folder
|
|
)
|
|
|
|
# Mock asyncio.run to do nothing, preventing full run
|
|
monkeypatch.setattr(cli_tools_click.asyncio, "run", mock.Mock())
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli_tools_click.main,
|
|
["run", *cli_args, str(agent_dir)],
|
|
)
|
|
assert result.exit_code == 0, (result.output, repr(result.exception))
|
|
|
|
# Check if log_to_tmp_folder was called with the correct log level object from `logging` module
|
|
expected_logging_level = getattr(logging, expected_log_level)
|
|
mock_log_to_tmp_folder.assert_called_once()
|
|
kwargs = mock_log_to_tmp_folder.call_args[1]
|
|
assert kwargs.get("level") == expected_logging_level
|