Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

415 lines
14 KiB
Python

# -*- coding: utf-8 -*-
# pylint: disable=protected-access
"""Tests for :func:`get_toolkit` — the single entry point that assembles
the per-chat-turn :class:`Toolkit` from every tool source the framework
manages.
Verifies the assembly rules:
- workspace builtins are always included;
- the four ``Task*`` planning tools are always included;
- :class:`ToolStop` (from ``BackgroundTaskManager``) is always included;
- the four ``Schedule*`` tools only when ``session.config.chat_model_config``
is set (they need a model to fire new runs with);
- team tools are role-gated by ``agent_record.source``: ``"team"`` →
one ``TeamSay`` (worker variant); anything else → the full
leader-side toolset of four;
- caller-supplied ``extra_factory`` results land at the end.
"""
from typing import Any
from unittest import IsolatedAsyncioTestCase
from utils import FakeWorkspaceManager
from agentscope.agent import ContextConfig, ReActConfig
from agentscope.app._manager import (
BackgroundTaskManager,
SchedulerManager,
)
from agentscope.app._service import ResourceAccessService, get_toolkit
from agentscope.app.access import DenyAllResourceAccessPolicy
from agentscope.app.storage import (
AgentData,
AgentRecord,
ChatModelConfig,
SessionConfig,
SessionRecord,
TeamData,
TeamRecord,
)
from agentscope.tool import ToolBase
class _FakeWorkspace:
"""Stand-in for a resolved :class:`WorkspaceBase` — only the three
methods :func:`get_toolkit` calls are implemented."""
def __init__(
self,
tools: list[ToolBase] | None = None,
skills: list | None = None,
mcps: list | None = None,
) -> None:
self._tools = tools or []
self._skills = skills or []
self._mcps = mcps or []
async def list_tools(self) -> list[ToolBase]:
"""Return the configured workspace tools."""
return list(self._tools)
async def list_skills(self) -> list:
"""Return the configured workspace skills."""
return list(self._skills)
async def list_mcps(self) -> list:
"""Return the configured workspace MCP descriptors."""
return list(self._mcps)
class _NullBus:
"""``MessageBus`` placeholder. Team tools only carry the reference;
nothing in :func:`get_toolkit` actually awaits it."""
def _make_agent(*, source: str = "user", name: str = "A") -> AgentRecord:
"""Build a minimal :class:`AgentRecord`."""
return AgentRecord(
user_id="u",
source=source,
data=AgentData(
name=name,
system_prompt=f"You are {name}.",
context_config=ContextConfig(),
react_config=ReActConfig(),
),
)
def _make_session(
*,
user_id: str,
agent_id: str,
with_model: bool,
team_id: str | None = None,
) -> SessionRecord:
"""Build a minimal :class:`SessionRecord`, optionally with a chat
model config."""
cfg = SessionConfig(
workspace_id="ws",
chat_model_config=(
ChatModelConfig(
type="dashscope_credential",
credential_id="c",
model="m",
parameters={},
)
if with_model
else None
),
)
return SessionRecord(
user_id=user_id,
agent_id=agent_id,
config=cfg,
team_id=team_id,
)
class _NoOpStorage:
"""Storage placeholder. ``get_toolkit`` itself does not call any
storage method — the team tools bind a reference for later use.
Exceptions: since :class:`AgentInvite` landed, ``get_toolkit`` calls
``ResourceAccessService.list_resource`` at assembly time to build
the invitable pool, which fans out to
:meth:`StorageBase.list_agents` under the hood. Return an empty
list — the toolkit tests care about which team tools are attached,
not about the pool contents; the non-empty case is covered by the
``AgentInvite`` tests in ``service_team_tools_test.py``.
Additionally, when the session has a ``team_id`` set, ``get_toolkit``
calls ``get_team`` to decide leader-vs-worker; the ``team_id_map``
constructor arg lets a test inject a lookup so the worker branch
can be exercised.
"""
def __init__(
self,
team_id_map: dict[str, TeamRecord] | None = None,
) -> None:
"""Initialize the storage placeholder."""
self._teams = team_id_map or {}
async def list_agents(self, _user_id: str) -> list:
"""List agents for a team."""
return []
async def get_team(
self,
_user_id: str,
team_id: str,
) -> TeamRecord | None:
"""Return the team record for ``team_id``."""
return self._teams.get(team_id)
def _make_access(storage: Any) -> ResourceAccessService:
"""Build a :class:`ResourceAccessService` bound to ``storage`` with a
deny-all policy for ``get_toolkit`` tests.
``list_resource(AGENT)`` therefore fans out to
``storage.list_agents(user_id)`` alone — matching the pre-share
behaviour the toolkit tests were written against.
"""
return ResourceAccessService(
storage=storage,
policy=DenyAllResourceAccessPolicy(),
)
def _tool_names(toolkit: Any) -> list[str]:
"""Extract every registered tool name from a :class:`Toolkit`,
walking its tool groups."""
return [t.name for group in toolkit.tool_groups for t in group.tools]
class _StubTool(ToolBase):
"""Minimal :class:`ToolBase` subclass that satisfies the abstract
methods so :func:`get_toolkit` can register the instance.
Sub-classes override ``name`` and ``description``; nothing in the
tests actually calls the tool, so the implementations are no-ops.
"""
name: str = "stub"
description: str = "stub tool"
input_schema: dict = {}
is_concurrency_safe: bool = True
is_read_only: bool = False
is_state_injected: bool = False
is_external_tool: bool = False
is_mcp: bool = False
mcp_name: str | None = None
async def check_permissions(self, *args: Any, **kwargs: Any) -> None:
"""No-op permission check — the tests do not exercise it."""
async def __call__(self, *args: Any, **kwargs: Any) -> None:
"""No-op invocation — the tests never execute the tool."""
class TestGetToolkitBaseAssembly(IsolatedAsyncioTestCase):
"""User-owned agent (``source="user"``) gets the full set."""
async def test_user_agent_gets_all_sources(self) -> None:
"""A user-owned agent receives workspace, planning, scheduling,
ToolStop, and the four leader-side team tools."""
agent = _make_agent(source="user")
session = _make_session(
user_id="u",
agent_id=agent.id,
with_model=True,
)
class _WsTool(_StubTool):
"""Stub workspace tool registered through ``_FakeWorkspace``."""
name: str = "ws-bash"
description: str = "stub workspace tool"
ws_tool = _WsTool()
workspace = _FakeWorkspace(tools=[ws_tool])
toolkit = await get_toolkit(
storage=_NoOpStorage(), # type: ignore[arg-type]
workspace=workspace, # type: ignore[arg-type]
workspace_manager=FakeWorkspaceManager(),
scheduler_manager=SchedulerManager(
storage=_NoOpStorage(), # type: ignore[arg-type]
message_bus=_NullBus(), # type: ignore[arg-type]
),
background_task_manager=BackgroundTaskManager(
message_bus=_NullBus(), # type: ignore[arg-type]
),
message_bus=_NullBus(), # type: ignore[arg-type]
user_id="u",
agent_record=agent,
session_record=session,
extra_factory=None,
middlewares=[],
resource_access_service=_make_access(_NoOpStorage()),
)
names = set(_tool_names(toolkit))
# Workspace tool present.
self.assertIn("ws-bash", names)
# Planning tools present.
self.assertTrue(
{"TaskCreate", "TaskList", "TaskGet", "TaskUpdate"} <= names,
)
# Background task control present.
self.assertIn("ToolStop", names)
# Schedule control present (model_config is set).
self.assertTrue(
{
"ScheduleCreate",
"ScheduleView",
"ScheduleDelete",
"ScheduleList",
}
<= names,
)
# Leader-side team tools (4 of them).
self.assertTrue(
{"TeamCreate", "AgentCreate", "TeamSay", "TeamDelete"} <= names,
)
class TestGetToolkitWorkerVariant(IsolatedAsyncioTestCase):
"""A session whose team role is worker only gets ``TeamSay``.
The worker/leader distinction is now session-level: a session with
``team_id`` set and ``team.session_id != session.id`` is a worker
regardless of the agent record's ``source`` (an ``AgentInvite``
borrowed session runs on ``source='user'`` but is still a worker)."""
async def test_worker_only_gets_team_say(self) -> None:
"""A worker session receives only ``TeamSay`` from the team
toolset."""
agent = _make_agent(source="team", name="worker")
session = _make_session(
user_id="u",
agent_id=agent.id,
with_model=True,
team_id="t1",
)
# Team exists and its leader session is NOT this one → worker.
team = TeamRecord(
user_id="u",
session_id="leader-sid",
data=TeamData(name="team", description="d"),
)
toolkit = await get_toolkit(
storage=_NoOpStorage(
team_id_map={"t1": team},
), # type: ignore[arg-type]
workspace=_FakeWorkspace(), # type: ignore[arg-type]
workspace_manager=FakeWorkspaceManager(),
scheduler_manager=SchedulerManager(
storage=_NoOpStorage(), # type: ignore[arg-type]
message_bus=_NullBus(), # type: ignore[arg-type]
),
background_task_manager=BackgroundTaskManager(
message_bus=_NullBus(), # type: ignore[arg-type]
),
message_bus=_NullBus(), # type: ignore[arg-type]
user_id="u",
agent_record=agent,
session_record=session,
extra_factory=None,
middlewares=[],
resource_access_service=_make_access(_NoOpStorage()),
)
names = set(_tool_names(toolkit))
# Only TeamSay from the team toolset.
self.assertIn("TeamSay", names)
for missing in (
"TeamCreate",
"AgentCreate",
"TeamDelete",
"AgentInvite",
):
self.assertNotIn(missing, names)
class TestGetToolkitSchedulingGuard(IsolatedAsyncioTestCase):
"""``Schedule*`` tools are only attached when the session has a
model configured."""
async def test_no_schedule_tools_without_model_config(self) -> None:
"""Without a ``chat_model_config`` on the session, the four
``Schedule*`` tools are omitted from the toolkit."""
agent = _make_agent(source="user")
session = _make_session(
user_id="u",
agent_id=agent.id,
with_model=False,
)
toolkit = await get_toolkit(
storage=_NoOpStorage(), # type: ignore[arg-type]
workspace=_FakeWorkspace(), # type: ignore[arg-type]
workspace_manager=FakeWorkspaceManager(),
scheduler_manager=SchedulerManager(
storage=_NoOpStorage(), # type: ignore[arg-type]
message_bus=_NullBus(), # type: ignore[arg-type]
),
background_task_manager=BackgroundTaskManager(
message_bus=_NullBus(), # type: ignore[arg-type]
),
message_bus=_NullBus(), # type: ignore[arg-type]
user_id="u",
agent_record=agent,
session_record=session,
extra_factory=None,
middlewares=[],
resource_access_service=_make_access(_NoOpStorage()),
)
names = set(_tool_names(toolkit))
for missing in (
"ScheduleCreate",
"ScheduleView",
"ScheduleDelete",
"ScheduleList",
):
self.assertNotIn(missing, names)
class TestGetToolkitExtraFactory(IsolatedAsyncioTestCase):
"""Tools returned by ``extra_factory`` end up in the final toolkit."""
async def test_extra_factory_tools_are_attached(self) -> None:
"""Tools returned by ``extra_factory`` end up in the final
toolkit alongside the framework-builtin ones."""
class _ExtraTool(_StubTool):
"""Stub tool emitted by the ``extra_factory`` callback."""
name: str = "my-extra"
description: str = "stub extra tool"
extra_tool = _ExtraTool()
async def factory(
_user_id: str,
_agent_id: str,
_session_id: str,
) -> list[ToolBase]:
"""Stub extra-factory that always returns ``extra_tool``."""
return [extra_tool]
agent = _make_agent()
session = _make_session(
user_id="u",
agent_id=agent.id,
with_model=True,
)
toolkit = await get_toolkit(
storage=_NoOpStorage(), # type: ignore[arg-type]
workspace=_FakeWorkspace(), # type: ignore[arg-type]
workspace_manager=FakeWorkspaceManager(),
scheduler_manager=SchedulerManager(
storage=_NoOpStorage(), # type: ignore[arg-type]
message_bus=_NullBus(), # type: ignore[arg-type]
),
background_task_manager=BackgroundTaskManager(
message_bus=_NullBus(), # type: ignore[arg-type]
),
message_bus=_NullBus(), # type: ignore[arg-type]
user_id="u",
agent_record=agent,
session_record=session,
extra_factory=factory,
middlewares=[],
resource_access_service=_make_access(_NoOpStorage()),
)
self.assertIn("my-extra", _tool_names(toolkit))