You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-03-30 10:57:20 -07:00
2367901ec5
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 858763407
918 lines
31 KiB
Python
918 lines
31 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 Runner.run_debug helper method."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest import mock
|
|
|
|
from google.adk.agents import Agent
|
|
from google.adk.agents.run_config import RunConfig
|
|
from google.adk.runners import InMemoryRunner
|
|
import pytest
|
|
|
|
|
|
class TestRunDebug:
|
|
"""Tests for Runner.run_debug method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_single_query(self):
|
|
"""Test run_debug with a single string query."""
|
|
# Setup
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="You are a helpful assistant.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
# Mock the runner's run_async to return controlled events
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Hello! I can help you.")]
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute
|
|
events = await runner.run_debug("Hello, how are you?", quiet=True)
|
|
|
|
# Assertions
|
|
assert len(events) == 1
|
|
assert events[0].author == "test_agent"
|
|
assert events[0].content.parts[0].text == "Hello! I can help you."
|
|
|
|
# Verify session was created with defaults
|
|
session = await runner.session_service.get_session(
|
|
app_name=runner.app_name,
|
|
user_id="debug_user_id",
|
|
session_id="debug_session_id",
|
|
)
|
|
assert session is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_multiple_queries(self):
|
|
"""Test run_debug with multiple queries in sequence."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="You are a test bot.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
# Mock responses for multiple queries
|
|
responses = ["First response", "Second response"]
|
|
call_count = 0
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
nonlocal call_count
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text=responses[call_count])]
|
|
call_count += 1
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with multiple queries
|
|
events = await runner.run_debug(
|
|
["First query", "Second query"], quiet=True
|
|
)
|
|
|
|
# Assertions
|
|
assert len(events) == 2
|
|
assert events[0].content.parts[0].text == "First response"
|
|
assert events[1].content.parts[0].text == "Second response"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_always_returns_events(self):
|
|
"""Test that run_debug always returns events."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Response")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Test that events are always returned
|
|
events = await runner.run_debug("Query", quiet=True)
|
|
assert isinstance(events, list)
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_quiet_mode(self, capsys):
|
|
"""Test that quiet=True suppresses printing."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="This should not be printed")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with quiet=True
|
|
await runner.run_debug("Test query", quiet=True)
|
|
|
|
# Check that nothing was printed
|
|
captured = capsys.readouterr()
|
|
assert "This should not be printed" not in captured.out
|
|
assert "User >" not in captured.out
|
|
assert "Session:" not in captured.out
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_custom_session_id(self):
|
|
"""Test run_debug with custom session_id."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Response")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with custom session ID
|
|
await runner.run_debug(
|
|
"Query", session_id="custom_debug_session", quiet=True
|
|
)
|
|
|
|
# Verify session was created with custom ID
|
|
session = await runner.session_service.get_session(
|
|
app_name=runner.app_name,
|
|
user_id="debug_user_id",
|
|
session_id="custom_debug_session",
|
|
)
|
|
assert session is not None
|
|
assert session.id == "custom_debug_session"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_custom_user_id(self):
|
|
"""Test run_debug with custom user_id."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Response")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with custom user_id
|
|
await runner.run_debug("Query", user_id="test_user_123", quiet=True)
|
|
|
|
# Verify session was created with custom user_id
|
|
session = await runner.session_service.get_session(
|
|
app_name=runner.app_name,
|
|
user_id="test_user_123",
|
|
session_id="debug_session_id",
|
|
)
|
|
assert session is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_run_config(self):
|
|
"""Test that run_config is properly passed through to run_async."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
run_config_used = None
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
nonlocal run_config_used
|
|
run_config_used = kwargs.get("run_config")
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Response")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(
|
|
runner, "run_async", side_effect=mock_run_async
|
|
) as mock_method:
|
|
# Create a custom run_config
|
|
custom_config = RunConfig(support_cfc=True)
|
|
|
|
# Execute with custom run_config
|
|
await runner.run_debug("Query", run_config=custom_config, quiet=True)
|
|
|
|
# Verify run_config was passed to run_async
|
|
assert mock_method.called
|
|
call_args = mock_method.call_args
|
|
assert call_args is not None
|
|
assert "run_config" in call_args.kwargs
|
|
assert call_args.kwargs["run_config"] == custom_config
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_session_persistence(self):
|
|
"""Test that multiple calls to run_debug maintain conversation context."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Remember previous messages.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
call_count = 0
|
|
responses = ["First response", "Second response remembering first"]
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
nonlocal call_count
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text=responses[call_count])]
|
|
call_count += 1
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# First call
|
|
events1 = await runner.run_debug("First message", quiet=True)
|
|
assert events1[0].content.parts[0].text == "First response"
|
|
|
|
# Second call to same session
|
|
events2 = await runner.run_debug("Second message", quiet=True)
|
|
assert (
|
|
events2[0].content.parts[0].text
|
|
== "Second response remembering first"
|
|
)
|
|
|
|
# Verify both calls used the same session
|
|
session = await runner.session_service.get_session(
|
|
app_name=runner.app_name,
|
|
user_id="debug_user_id",
|
|
session_id="debug_session_id",
|
|
)
|
|
assert session is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_filters_none_text(self):
|
|
"""Test that run_debug filters out 'None' text and empty parts."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Yield events with various text values
|
|
events = [
|
|
mock.Mock(
|
|
author="test_agent",
|
|
content=mock.Mock(parts=[mock.Mock(text="Valid text")]),
|
|
),
|
|
mock.Mock(
|
|
author="test_agent",
|
|
content=mock.Mock(parts=[mock.Mock(text="None")]),
|
|
), # Should be filtered
|
|
mock.Mock(
|
|
author="test_agent",
|
|
content=mock.Mock(parts=[mock.Mock(text="")]),
|
|
), # Should be filtered
|
|
mock.Mock(
|
|
author="test_agent",
|
|
content=mock.Mock(parts=[mock.Mock(text="Another valid")]),
|
|
),
|
|
]
|
|
for event in events:
|
|
yield event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute and capture output
|
|
events = await runner.run_debug("Query", quiet=True)
|
|
|
|
# All 4 events should be returned (filtering is for printing only)
|
|
assert len(events) == 4
|
|
|
|
# But when printing, "None" and empty strings should be filtered
|
|
# This is tested implicitly by the implementation
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_existing_session(self):
|
|
"""Test that run_debug retrieves existing session when AlreadyExistsError occurs."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
# First create a session
|
|
await runner.session_service.create_session(
|
|
app_name=runner.app_name,
|
|
user_id="debug_user_id",
|
|
session_id="existing_session",
|
|
)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [mock.Mock(text="Using existing session")]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with same session ID (should retrieve existing)
|
|
events = await runner.run_debug(
|
|
"Query", session_id="existing_session", quiet=True
|
|
)
|
|
|
|
assert len(events) == 1
|
|
assert events[0].content.parts[0].text == "Using existing session"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_tool_calls(self, capsys):
|
|
"""Test that run_debug properly handles and prints tool calls."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with tools.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# First event: tool call
|
|
mock_call_event = mock.Mock()
|
|
mock_call_event.author = "test_agent"
|
|
mock_call_event.content = mock.Mock()
|
|
mock_function_call = mock.Mock()
|
|
mock_function_call.name = "calculate"
|
|
mock_function_call.args = {"operation": "add", "a": 5, "b": 3}
|
|
mock_part_call = mock.Mock()
|
|
mock_part_call.text = None
|
|
mock_part_call.function_call = mock_function_call
|
|
mock_part_call.function_response = None
|
|
mock_call_event.content.parts = [mock_part_call]
|
|
yield mock_call_event
|
|
|
|
# Second event: tool response
|
|
mock_resp_event = mock.Mock()
|
|
mock_resp_event.author = "test_agent"
|
|
mock_resp_event.content = mock.Mock()
|
|
mock_function_response = mock.Mock()
|
|
mock_function_response.response = {"result": 8}
|
|
mock_part_resp = mock.Mock()
|
|
mock_part_resp.text = None
|
|
mock_part_resp.function_call = None
|
|
mock_part_resp.function_response = mock_function_response
|
|
mock_resp_event.content.parts = [mock_part_resp]
|
|
yield mock_resp_event
|
|
|
|
# Third event: final text response
|
|
mock_text_event = mock.Mock()
|
|
mock_text_event.author = "test_agent"
|
|
mock_text_event.content = mock.Mock()
|
|
mock_text_event.content.parts = [mock.Mock(text="The result is 8")]
|
|
yield mock_text_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
# Execute with verbose=True to see tool calls
|
|
events = await runner.run_debug("Calculate 5 + 3", verbose=True)
|
|
|
|
# Check output was printed
|
|
captured = capsys.readouterr()
|
|
assert "[Calling tool: calculate" in captured.out
|
|
assert "[Tool result:" in captured.out
|
|
assert "The result is 8" in captured.out
|
|
|
|
# Check events were collected
|
|
assert len(events) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_executable_code(self, capsys):
|
|
"""Test that run_debug properly handles executable code parts."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with code execution.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Event with executable code
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
|
|
mock_exec_code = mock.Mock()
|
|
mock_exec_code.language = "python"
|
|
mock_exec_code.code = "print('Hello World')"
|
|
|
|
mock_part = mock.Mock()
|
|
mock_part.text = None
|
|
mock_part.function_call = None
|
|
mock_part.function_response = None
|
|
mock_part.executable_code = mock_exec_code
|
|
mock_part.code_execution_result = None
|
|
mock_part.inline_data = None
|
|
mock_part.file_data = None
|
|
|
|
mock_event.content.parts = [mock_part]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Run some code", verbose=True)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[Executing python code...]" in captured.out
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_code_execution_result(self, capsys):
|
|
"""Test that run_debug properly handles code execution result parts."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with code results.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Event with code execution result
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
|
|
mock_result = mock.Mock()
|
|
mock_result.output = "Hello World\n42"
|
|
|
|
mock_part = mock.Mock()
|
|
mock_part.text = None
|
|
mock_part.function_call = None
|
|
mock_part.function_response = None
|
|
mock_part.executable_code = None
|
|
mock_part.code_execution_result = mock_result
|
|
mock_part.inline_data = None
|
|
mock_part.file_data = None
|
|
|
|
mock_event.content.parts = [mock_part]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug(
|
|
"Show code output",
|
|
verbose=True,
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[Code output: Hello World\n42]" in captured.out
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_inline_data(self, capsys):
|
|
"""Test that run_debug properly handles inline data parts."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with inline data.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Event with inline data (e.g., image)
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
|
|
mock_inline = mock.Mock()
|
|
mock_inline.mime_type = "image/png"
|
|
mock_inline.data = b"fake_image_data"
|
|
|
|
mock_part = mock.Mock()
|
|
mock_part.text = None
|
|
mock_part.function_call = None
|
|
mock_part.function_response = None
|
|
mock_part.executable_code = None
|
|
mock_part.code_execution_result = None
|
|
mock_part.inline_data = mock_inline
|
|
mock_part.file_data = None
|
|
|
|
mock_event.content.parts = [mock_part]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Show image", verbose=True)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[Inline data: image/png]" in captured.out
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_file_data(self, capsys):
|
|
"""Test that run_debug properly handles file data parts."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with file data.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Event with file data
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
|
|
mock_file = mock.Mock()
|
|
mock_file.file_uri = "gs://bucket/path/to/file.pdf"
|
|
|
|
mock_part = mock.Mock()
|
|
mock_part.text = None
|
|
mock_part.function_call = None
|
|
mock_part.function_response = None
|
|
mock_part.executable_code = None
|
|
mock_part.code_execution_result = None
|
|
mock_part.inline_data = None
|
|
mock_part.file_data = mock_file
|
|
|
|
mock_event.content.parts = [mock_part]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Reference file", verbose=True)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[File: gs://bucket/path/to/file.pdf]" in captured.out
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_mixed_parts(self, capsys):
|
|
"""Test that run_debug handles events with multiple part types."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with mixed parts.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Event with multiple part types
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
|
|
# Text part
|
|
mock_text_part = mock.Mock()
|
|
mock_text_part.text = "Here's your result:"
|
|
mock_text_part.function_call = None
|
|
mock_text_part.function_response = None
|
|
mock_text_part.executable_code = None
|
|
mock_text_part.code_execution_result = None
|
|
mock_text_part.inline_data = None
|
|
mock_text_part.file_data = None
|
|
|
|
# Code execution part
|
|
mock_code_part = mock.Mock()
|
|
mock_code_part.text = None
|
|
mock_code_part.function_call = None
|
|
mock_code_part.function_response = None
|
|
mock_exec_code = mock.Mock()
|
|
mock_exec_code.language = "python"
|
|
mock_code_part.executable_code = mock_exec_code
|
|
mock_code_part.code_execution_result = None
|
|
mock_code_part.inline_data = None
|
|
mock_code_part.file_data = None
|
|
|
|
# Result part
|
|
mock_result_part = mock.Mock()
|
|
mock_result_part.text = None
|
|
mock_result_part.function_call = None
|
|
mock_result_part.function_response = None
|
|
mock_result_part.executable_code = None
|
|
mock_result = mock.Mock()
|
|
mock_result.output = "42"
|
|
mock_result_part.code_execution_result = mock_result
|
|
mock_result_part.inline_data = None
|
|
mock_result_part.file_data = None
|
|
|
|
mock_event.content.parts = [
|
|
mock_text_part,
|
|
mock_code_part,
|
|
mock_result_part,
|
|
]
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Mixed response", verbose=True)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Here's your result:" in captured.out
|
|
assert "[Executing python code...]" in captured.out
|
|
assert "[Code output: 42]" in captured.out
|
|
assert len(events) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_long_output_truncation(self, capsys):
|
|
"""Test that run_debug properly truncates long outputs."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with long outputs.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Tool call with long args
|
|
mock_call_event = mock.Mock()
|
|
mock_call_event.author = "test_agent"
|
|
mock_call_event.content = mock.Mock()
|
|
|
|
mock_function_call = mock.Mock()
|
|
mock_function_call.name = "process"
|
|
# Create a long argument string
|
|
mock_function_call.args = {"data": "x" * 100}
|
|
|
|
mock_part_call = mock.Mock()
|
|
mock_part_call.text = None
|
|
mock_part_call.function_call = mock_function_call
|
|
mock_part_call.function_response = None
|
|
mock_part_call.executable_code = None
|
|
mock_part_call.code_execution_result = None
|
|
mock_part_call.inline_data = None
|
|
mock_part_call.file_data = None
|
|
|
|
mock_call_event.content.parts = [mock_part_call]
|
|
yield mock_call_event
|
|
|
|
# Tool response with long result
|
|
mock_resp_event = mock.Mock()
|
|
mock_resp_event.author = "test_agent"
|
|
mock_resp_event.content = mock.Mock()
|
|
|
|
mock_function_response = mock.Mock()
|
|
# Create a long response string
|
|
mock_function_response.response = {"result": "y" * 200}
|
|
|
|
mock_part_resp = mock.Mock()
|
|
mock_part_resp.text = None
|
|
mock_part_resp.function_call = None
|
|
mock_part_resp.function_response = mock_function_response
|
|
mock_part_resp.executable_code = None
|
|
mock_part_resp.code_execution_result = None
|
|
mock_part_resp.inline_data = None
|
|
mock_part_resp.file_data = None
|
|
|
|
mock_resp_event.content.parts = [mock_part_resp]
|
|
yield mock_resp_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Process data", verbose=True)
|
|
|
|
captured = capsys.readouterr()
|
|
# Check that args are truncated at 50 chars
|
|
assert "..." in captured.out
|
|
assert "[Calling tool: process(" in captured.out
|
|
# Check that response is truncated at 100 chars
|
|
assert "[Tool result:" in captured.out
|
|
assert len(events) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_verbose_flag_false(self, capsys):
|
|
"""Test that run_debug hides tool calls when verbose=False (default)."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with tools.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Tool call event
|
|
mock_call_event = mock.Mock()
|
|
mock_call_event.author = "test_agent"
|
|
mock_call_event.content = mock.Mock()
|
|
|
|
mock_function_call = mock.Mock()
|
|
mock_function_call.name = "get_weather"
|
|
mock_function_call.args = {"city": "Tokyo"}
|
|
|
|
mock_part_call = mock.Mock()
|
|
mock_part_call.text = None
|
|
mock_part_call.function_call = mock_function_call
|
|
mock_part_call.function_response = None
|
|
mock_part_call.executable_code = None
|
|
mock_part_call.code_execution_result = None
|
|
mock_part_call.inline_data = None
|
|
mock_part_call.file_data = None
|
|
|
|
mock_call_event.content.parts = [mock_part_call]
|
|
yield mock_call_event
|
|
|
|
# Tool response event
|
|
mock_resp_event = mock.Mock()
|
|
mock_resp_event.author = "test_agent"
|
|
mock_resp_event.content = mock.Mock()
|
|
|
|
mock_function_response = mock.Mock()
|
|
mock_function_response.response = {"weather": "Clear, 25°C"}
|
|
|
|
mock_part_resp = mock.Mock()
|
|
mock_part_resp.text = None
|
|
mock_part_resp.function_call = None
|
|
mock_part_resp.function_response = mock_function_response
|
|
mock_part_resp.executable_code = None
|
|
mock_part_resp.code_execution_result = None
|
|
mock_part_resp.inline_data = None
|
|
mock_part_resp.file_data = None
|
|
|
|
mock_resp_event.content.parts = [mock_part_resp]
|
|
yield mock_resp_event
|
|
|
|
# Final text response
|
|
mock_text_event = mock.Mock()
|
|
mock_text_event.author = "test_agent"
|
|
mock_text_event.content = mock.Mock()
|
|
mock_text_event.content.parts = [
|
|
mock.Mock(text="The weather in Tokyo is clear and 25°C.")
|
|
]
|
|
yield mock_text_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug(
|
|
"What's the weather?",
|
|
verbose=False, # Default - should NOT show tool calls
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
# Should NOT show tool call details
|
|
assert "[Calling tool:" not in captured.out
|
|
assert "[Tool result:" not in captured.out
|
|
# Should show final text response
|
|
assert "The weather in Tokyo is clear and 25°C." in captured.out
|
|
assert len(events) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_verbose_flag_true(self, capsys):
|
|
"""Test that run_debug shows tool calls when verbose=True."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent with tools.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*args, **kwargs):
|
|
# Tool call event
|
|
mock_call_event = mock.Mock()
|
|
mock_call_event.author = "test_agent"
|
|
mock_call_event.content = mock.Mock()
|
|
|
|
mock_function_call = mock.Mock()
|
|
mock_function_call.name = "calculate"
|
|
mock_function_call.args = {"expression": "42 * 3.14"}
|
|
|
|
mock_part_call = mock.Mock()
|
|
mock_part_call.text = None
|
|
mock_part_call.function_call = mock_function_call
|
|
mock_part_call.function_response = None
|
|
mock_part_call.executable_code = None
|
|
mock_part_call.code_execution_result = None
|
|
mock_part_call.inline_data = None
|
|
mock_part_call.file_data = None
|
|
|
|
mock_call_event.content.parts = [mock_part_call]
|
|
yield mock_call_event
|
|
|
|
# Tool response event
|
|
mock_resp_event = mock.Mock()
|
|
mock_resp_event.author = "test_agent"
|
|
mock_resp_event.content = mock.Mock()
|
|
|
|
mock_function_response = mock.Mock()
|
|
mock_function_response.response = {"result": 131.88}
|
|
|
|
mock_part_resp = mock.Mock()
|
|
mock_part_resp.text = None
|
|
mock_part_resp.function_call = None
|
|
mock_part_resp.function_response = mock_function_response
|
|
mock_part_resp.executable_code = None
|
|
mock_part_resp.code_execution_result = None
|
|
mock_part_resp.inline_data = None
|
|
mock_part_resp.file_data = None
|
|
|
|
mock_resp_event.content.parts = [mock_part_resp]
|
|
yield mock_resp_event
|
|
|
|
# Final text response
|
|
mock_text_event = mock.Mock()
|
|
mock_text_event.author = "test_agent"
|
|
mock_text_event.content = mock.Mock()
|
|
mock_text_event.content.parts = [mock.Mock(text="The result is 131.88")]
|
|
yield mock_text_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug(
|
|
"Calculate 42 * 3.14",
|
|
verbose=True, # Should show tool calls
|
|
)
|
|
|
|
captured = capsys.readouterr()
|
|
# Should show tool call details
|
|
assert (
|
|
"[Calling tool: calculate({'expression': '42 * 3.14'})]"
|
|
in captured.out
|
|
)
|
|
assert "[Tool result: {'result': 131.88}]" in captured.out
|
|
# Should also show final text response
|
|
assert "The result is 131.88" in captured.out
|
|
assert len(events) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_empty_parts_list(self, capsys):
|
|
"""Test that run_debug handles events with empty parts list gracefully."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*_args, **_kwargs):
|
|
# Event with empty parts list
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = mock.Mock()
|
|
mock_event.content.parts = [] # Empty parts list
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Test query")
|
|
|
|
captured = capsys.readouterr()
|
|
# Should handle gracefully without crashing
|
|
assert "User > Test query" in captured.out
|
|
assert len(events) == 1
|
|
# Should not print any agent response since parts is empty
|
|
assert "test_agent >" not in captured.out
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_debug_with_none_event_content(self, capsys):
|
|
"""Test that run_debug handles events with None content gracefully."""
|
|
agent = Agent(
|
|
name="test_agent",
|
|
model="gemini-2.5-flash-lite",
|
|
instruction="Test agent.",
|
|
)
|
|
runner = InMemoryRunner(agent=agent)
|
|
|
|
async def mock_run_async(*_args, **_kwargs):
|
|
# Event with None content
|
|
mock_event = mock.Mock()
|
|
mock_event.author = "test_agent"
|
|
mock_event.content = None # None content
|
|
yield mock_event
|
|
|
|
with mock.patch.object(runner, "run_async", side_effect=mock_run_async):
|
|
events = await runner.run_debug("Test query")
|
|
|
|
captured = capsys.readouterr()
|
|
# Should handle gracefully without crashing
|
|
assert "User > Test query" in captured.out
|
|
assert len(events) == 1
|
|
# Should not print any agent response since content is None
|
|
assert "test_agent >" not in captured.out
|