mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-19 02:12:46 +08:00
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
This commit is contained in:
136
tests/unit/test_tool_executor.py
Normal file
136
tests/unit/test_tool_executor.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user