Files
AstrBot/tests/unit/test_tool_executor.py
LIghtJUNction 169a856a3a test: mass-coverage push — ~1000+ new unit tests
Coverage improvements:
- agent/tool/core: 150+ tests (FunctionTool, ToolSet, ToolSchema, run_context, messages, registry)
- provider: 152 tests (entities, Provider ABC, register, manager)
- star module: 58 tests (Context, PluginManager, StarHandler)
- platform: 71 tests (Platform ABC, manager, MessageSession)
- pipeline: 130 tests (process/respond stages, scheduler, bootstrap, conversation_mgr)
- kb+skills: 96 tests (mgr, helpers, chunkers, skill_manager)
- config+builtins: 92 tests (astrbot_config, config_mgr, default, LTM, star main)
- cron+db+event_bus: 89 tests (cron events/manager, PO models, BaseDatabase, EventBus)
- dashboard routes: expanded smoke tests for all route modules
2026-04-29 08:40:26 +08:00

137 lines
5.2 KiB
Python

"""Unit tests for astrbot.core.agent.tool_executor: BaseFunctionToolExecutor abstract signature."""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
class TestBaseFunctionToolExecutor:
"""BaseFunctionToolExecutor cannot be instantiated; metachecks on the ABC."""
def test_cannot_instantiate_abstract(self):
"""BaseFunctionToolExecutor cannot be instantiated directly (abstract)."""
with pytest.raises(TypeError, match="abstract"):
BaseFunctionToolExecutor()
def test_execute_is_abstract_classmethod(self):
"""execute is both a classmethod and abstractmethod."""
assert hasattr(BaseFunctionToolExecutor.execute, "__isabstractmethod__")
assert BaseFunctionToolExecutor.execute.__isabstractmethod__ is True
def test_execute_is_classmethod_descriptor(self):
"""execute is a classmethod (has __func__)."""
assert hasattr(BaseFunctionToolExecutor.execute, "__func__")
def test_concrete_subclass_must_implement_execute(self):
"""A subclass without execute is still abstract."""
class Missing(BaseFunctionToolExecutor):
pass
with pytest.raises(TypeError, match="abstract"):
Missing()
def test_concrete_subclass_with_execute_can_instantiate(self):
"""A subclass that implements execute can be instantiated."""
class Concrete(BaseFunctionToolExecutor):
@classmethod
async def execute(cls, tool, run_context, **tool_args):
yield "result"
instance = Concrete()
assert isinstance(instance, BaseFunctionToolExecutor)
def test_execute_signature_matches(self):
"""execute has the expected parameter names."""
import inspect
sig = inspect.signature(
BaseFunctionToolExecutor.__dict__["execute"].__func__
)
param_names = list(sig.parameters.keys())
assert "tool" in param_names
assert "run_context" in param_names
def test_execute_tool_parameter_type_hint(self):
"""The tool parameter is annotated as FunctionTool."""
import inspect
sig = inspect.signature(
BaseFunctionToolExecutor.__dict__["execute"].__func__
)
tool_param = sig.parameters["tool"]
assert tool_param.annotation is FunctionTool
def test_execute_run_context_type_hint(self):
"""The run_context parameter is annotated as ContextWrapper."""
import inspect
sig = inspect.signature(
BaseFunctionToolExecutor.__dict__["execute"].__func__
)
ctx_param = sig.parameters["run_context"]
origin = getattr(ctx_param.annotation, "__origin__", None)
assert origin is ContextWrapper
def test_execute_returns_async_generator(self):
"""execute return annotation is AsyncGenerator."""
import inspect
sig = inspect.signature(
BaseFunctionToolExecutor.__dict__["execute"].__func__
)
return_annotation = sig.return_annotation
assert "AsyncGenerator" in str(return_annotation)
def test_concrete_subclass_execute_yields(self):
"""A concrete subclass can actually yield values."""
class Tester(BaseFunctionToolExecutor):
@classmethod
async def execute(cls, tool, run_context, **tool_args):
yield "step1"
yield "step2"
import asyncio
tool = FunctionTool(name="test", description="test")
ctx = ContextWrapper(context="test")
gen = Tester.execute(tool, ctx)
results = asyncio.run(async_collect(gen))
assert results == ["step1", "step2"]
def test_concrete_subclass_generic_parameter(self):
"""Subclass can specialize the generic type parameter."""
class TypedExecutor(BaseFunctionToolExecutor[str]):
@classmethod
async def execute(cls, tool, run_context, **tool_args):
yield run_context.context
import asyncio
tool = FunctionTool(name="t", description="t")
ctx = ContextWrapper(context="hello_generic")
gen = TypedExecutor.execute(tool, ctx)
results = asyncio.run(async_collect(gen))
assert results == ["hello_generic"]
def test_subclass_with_kwargs_passthrough(self):
"""execute passes **tool_args through."""
class KwargsExecutor(BaseFunctionToolExecutor):
@classmethod
async def execute(cls, tool, run_context, **tool_args):
yield tool_args
import asyncio
tool = FunctionTool(name="t", description="t")
ctx = ContextWrapper(context="ctx")
gen = KwargsExecutor.execute(tool, ctx, x=1, y="two")
results = asyncio.run(async_collect(gen))
assert results[0] == {"x": 1, "y": "two"}
async def async_collect(async_gen):
"""Collect all items from an async generator into a list."""
results = []
async for item in async_gen:
results.append(item)
return results