Files
google--adk-python/tests/unittests/agents/llm/task/test_finish_task_tool.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

364 lines
12 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 the finish_task_tool module."""
from __future__ import annotations
from typing import Any
from unittest import mock
from google.adk.agents.llm.task._finish_task_tool import FINISH_TASK_TOOL_NAME
from google.adk.agents.llm.task._finish_task_tool import FinishTaskTool
from pydantic import BaseModel
import pytest
class SampleOutputSchema(BaseModel):
"""Sample Pydantic model for testing output schema."""
result: str
count: int
class NestedOutputSchema(BaseModel):
"""Nested Pydantic model for testing complex schemas."""
name: str
details: dict
def _make_task_agent(
name='test_agent',
mode='task',
output_schema=None,
):
"""Create a mock task agent for FinishTaskTool construction."""
agent = mock.MagicMock()
agent.name = name
agent.mode = mode
agent.output_schema = output_schema
return agent
class TestFinishTaskTool:
"""Tests for the FinishTaskTool class."""
def test_init_without_output_schema(self):
"""Test FinishTaskTool initialization without output schema."""
agent = _make_task_agent()
tool = FinishTaskTool(task_agent=agent)
assert tool.name == FINISH_TASK_TOOL_NAME
assert 'Signal that this agent has completed' in tool.description
assert 'output data' not in tool.description
def test_init_with_output_schema(self):
"""Test FinishTaskTool initialization with output schema."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
assert tool.name == FINISH_TASK_TOOL_NAME
assert tool.output_schema is SampleOutputSchema
assert 'Signal that this agent has completed' in tool.description
assert 'output data' in tool.description
def test_get_declaration_without_output_schema(self):
"""Test function declaration generation without output schema."""
agent = _make_task_agent()
tool = FinishTaskTool(task_agent=agent)
declaration = tool._get_declaration()
assert declaration is not None
assert declaration.name == FINISH_TASK_TOOL_NAME
assert declaration.parameters_json_schema is not None
schema = declaration.parameters_json_schema
assert 'result' in schema.get('properties', {})
def test_get_declaration_with_output_schema(self):
"""Test function declaration generation with output schema."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
declaration = tool._get_declaration()
assert declaration is not None
assert declaration.name == FINISH_TASK_TOOL_NAME
assert declaration.parameters_json_schema is not None
schema = declaration.parameters_json_schema
assert 'result' in schema.get('properties', {})
assert 'count' in schema.get('properties', {})
@pytest.mark.asyncio
async def test_run_async_returns_confirmation(self):
"""Test that run_async returns a confirmation message."""
agent = _make_task_agent()
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
result = await tool.run_async(
args={'result': 'done'}, tool_context=mock_tool_context
)
assert result == 'Task completed.'
@pytest.mark.asyncio
async def test_run_async_with_args(self):
"""Test that run_async validates args and returns confirmation."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
result = await tool.run_async(
args={'result': 'success', 'count': 42},
tool_context=mock_tool_context,
)
assert result == 'Task completed.'
class TestBuildInstruction:
"""Tests for the _build_instruction method."""
def test_instruction_content(self):
"""Test instruction generation contains expected content."""
agent = _make_task_agent()
tool = FinishTaskTool(task_agent=agent)
instruction = tool._build_instruction()
assert 'finish_task' in instruction
assert 'Do NOT call `finish_task` prematurely' in instruction
assert 'call `finish_task` by itself with' in instruction
class TestProcessLlmRequest:
"""Tests for the process_llm_request method."""
@pytest.mark.asyncio
async def test_process_llm_request_adds_tool_and_instruction(self):
"""Test that process_llm_request adds tool and instruction."""
agent = _make_task_agent()
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
mock_tool_context._invocation_context.branch = None
mock_tool_context.session.events = []
mock_llm_request = mock.MagicMock()
mock_llm_request.append_tools = mock.MagicMock()
mock_llm_request.append_instructions = mock.MagicMock()
await tool.process_llm_request(
tool_context=mock_tool_context, llm_request=mock_llm_request
)
# Should add tool via parent's process_llm_request
mock_llm_request.append_tools.assert_called_once_with([tool])
# Should append instruction (at least the base instruction)
mock_llm_request.append_instructions.assert_called()
instruction_arg = mock_llm_request.append_instructions.call_args_list[0][0][
0
]
assert len(instruction_arg) == 1
assert 'finish_task' in instruction_arg[0]
class TestFinishTaskToolName:
"""Tests for the FINISH_TASK_TOOL_NAME constant."""
def test_constant_value(self):
"""Test that the constant has the expected value."""
assert FINISH_TASK_TOOL_NAME == 'finish_task'
class TestFinishTaskToolValidation:
"""Tests for the FinishTaskTool argument validation."""
@pytest.mark.asyncio
async def test_run_async_validation_error_missing_required_field(self):
"""Test that validation error is returned when required fields are missing."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
# Missing 'count' field which is required
result = await tool.run_async(
args={'result': 'success'},
tool_context=mock_tool_context,
)
assert isinstance(result, dict)
assert 'error' in result
assert 'finish_task' in result['error']
assert 'validation errors' in result['error']
assert 'count' in result['error']
@pytest.mark.asyncio
async def test_run_async_validation_error_wrong_type(self):
"""Test that validation error is returned when types are wrong."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
# 'count' should be int, not string
result = await tool.run_async(
args={'result': 'success', 'count': 'not_an_int'},
tool_context=mock_tool_context,
)
assert isinstance(result, dict)
assert 'error' in result
assert 'validation errors' in result['error']
@pytest.mark.asyncio
async def test_run_async_validation_passes_with_valid_args(self):
"""Test that validation passes with valid args."""
agent = _make_task_agent(output_schema=SampleOutputSchema)
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
result = await tool.run_async(
args={'result': 'success', 'count': 42},
tool_context=mock_tool_context,
)
assert result == 'Task completed.'
class TestFinishTaskToolAllSchemaTypes:
"""Tests for FinishTaskTool across all supported SchemaType variants.
Object schemas (BaseModel, dict) use parameters directly.
Non-object schemas (str, int, bool, float, list) are wrapped in a
JSON object with a 'result' key so they can be used as function
parameters.
"""
@pytest.mark.parametrize(
'output_schema, expected_wrapper_key',
[
(SampleOutputSchema, None),
(dict[str, Any], None),
(str, 'result'),
(int, 'result'),
(bool, 'result'),
(float, 'result'),
(list[str], 'result'),
(list[int], 'result'),
(list[SampleOutputSchema], 'result'),
],
ids=[
'BaseModel',
'dict',
'str',
'int',
'bool',
'float',
'list_str',
'list_int',
'list_BaseModel',
],
)
def test_wrapper_key(self, output_schema, expected_wrapper_key):
"""Verify wrapper key is set correctly for each schema type."""
agent = _make_task_agent(output_schema=output_schema)
tool = FinishTaskTool(task_agent=agent)
assert tool._wrapper_key == expected_wrapper_key
@pytest.mark.parametrize(
'output_schema',
[str, int, bool, float, list[str], list[int], list[SampleOutputSchema]],
ids=[
'str',
'int',
'bool',
'float',
'list_str',
'list_int',
'list_BaseModel',
],
)
def test_get_declaration_wrapped_schema(self, output_schema):
"""Non-object schemas produce a declaration with 'result' property."""
agent = _make_task_agent(output_schema=output_schema)
tool = FinishTaskTool(task_agent=agent)
declaration = tool._get_declaration()
assert declaration is not None
assert declaration.name == FINISH_TASK_TOOL_NAME
schema = declaration.parameters_json_schema
assert schema is not None
assert 'result' in schema.get('properties', {})
@pytest.mark.parametrize(
'output_schema, args, expected_output',
[
(
SampleOutputSchema,
{'result': 'done', 'count': 5},
{'result': 'done', 'count': 5},
),
(dict[str, Any], {'key': 'value'}, {'key': 'value'}),
(str, {'result': 'hello'}, 'hello'),
(int, {'result': 42}, 42),
(bool, {'result': True}, True),
(float, {'result': 3.14}, 3.14),
(list[str], {'result': ['a', 'b', 'c']}, ['a', 'b', 'c']),
(list[int], {'result': [1, 2, 3]}, [1, 2, 3]),
(
list[SampleOutputSchema],
{'result': [{'result': 'ok', 'count': 1}]},
[{'result': 'ok', 'count': 1}],
),
(list[str], {'result': []}, []),
],
ids=[
'BaseModel',
'dict',
'str',
'int',
'bool',
'float',
'list_str',
'list_int',
'list_BaseModel',
'list_str_empty',
],
)
@pytest.mark.asyncio
async def test_run_async(self, output_schema, args, expected_output):
"""Verify run_async validates and extracts output for each schema type."""
agent = _make_task_agent(output_schema=output_schema)
tool = FinishTaskTool(task_agent=agent)
mock_tool_context = mock.MagicMock()
result = await tool.run_async(
args=args,
tool_context=mock_tool_context,
)
# Task Delegation API: tool no longer writes actions.finish_task. The
# LlmAgent task wrapper sniffs the FC's `output` arg directly to
# set event.output. The tool just performs schema validation.
assert result == 'Task completed.'
del expected_output # validated_output is no longer surfaced via actions
def test_get_declaration_list_basemodel_defs_at_root(self):
"""Non-object schemas with $defs should hoist $defs to root level."""
agent = _make_task_agent(output_schema=list[SampleOutputSchema])
tool = FinishTaskTool(task_agent=agent)
declaration = tool._get_declaration()
schema = declaration.parameters_json_schema
# $defs must be at the root, not nested inside properties.result
assert '$defs' in schema
assert 'SampleOutputSchema' in schema['$defs']
# The nested result schema should not contain $defs
assert '$defs' not in schema['properties']['result']