mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
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
137 lines
5.2 KiB
Python
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
|