mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 17:51:20 +08:00
Merge remote-tracking branch 'origin/master' into codex/workspace-skills
This commit is contained in:
@@ -3,9 +3,16 @@ from types import SimpleNamespace
|
||||
import mcp
|
||||
import pytest
|
||||
|
||||
from astrbot.core.agent.agent import Agent
|
||||
from astrbot.core.agent.handoff import HandoffTool
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import FunctionTool
|
||||
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
|
||||
from astrbot.core.message.components import Image
|
||||
from astrbot.core.provider.func_tool_manager import (
|
||||
FunctionToolManager,
|
||||
_PermissionGuardedTool,
|
||||
)
|
||||
|
||||
|
||||
class _DummyEvent:
|
||||
@@ -29,6 +36,32 @@ def _build_run_context(message_components: list[object] | None = None):
|
||||
return ContextWrapper(context=ctx)
|
||||
|
||||
|
||||
def test_build_handoff_toolset_keeps_permission_guards_for_default_tools():
|
||||
mgr = FunctionToolManager()
|
||||
plugin_tool = FunctionTool(
|
||||
name="admin_only_mcp",
|
||||
description="admin tool",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
)
|
||||
handoff = HandoffTool(Agent(name="child"))
|
||||
mgr.func_list = [plugin_tool, handoff]
|
||||
|
||||
event = _DummyEvent()
|
||||
context = SimpleNamespace(
|
||||
get_config=lambda **_kwargs: {
|
||||
"provider_settings": {"computer_use_runtime": "none"}
|
||||
},
|
||||
get_llm_tool_manager=lambda: mgr,
|
||||
)
|
||||
run_context = ContextWrapper(context=SimpleNamespace(event=event, context=context))
|
||||
|
||||
toolset = FunctionToolExecutor._build_handoff_toolset(run_context, tools=None)
|
||||
|
||||
assert toolset is not None
|
||||
assert isinstance(toolset.get_tool("admin_only_mcp"), _PermissionGuardedTool)
|
||||
assert toolset.get_tool("transfer_to_child") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_handoff_image_urls_normalizes_filters_and_appends_event_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -997,6 +997,57 @@ class TestEnsurePersonaAndSkills:
|
||||
|
||||
assert req.func_tool is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persona_empty_tools_filters_late_builtin_tools(
|
||||
self, mock_event, mock_context, mock_provider
|
||||
):
|
||||
module = ama
|
||||
persona = {"name": "locked", "prompt": "No tools.", "tools": []}
|
||||
mock_context.persona_manager.resolve_selected_persona = AsyncMock(
|
||||
return_value=("locked", persona, None, False)
|
||||
)
|
||||
mock_context.get_config.return_value = {
|
||||
"provider_settings": {
|
||||
"web_search": True,
|
||||
"websearch_provider": "baidu_ai_search",
|
||||
}
|
||||
}
|
||||
config = module.MainAgentBuildConfig(
|
||||
tool_call_timeout=60,
|
||||
provider_settings={
|
||||
"web_search": True,
|
||||
"websearch_provider": "baidu_ai_search",
|
||||
},
|
||||
computer_use_runtime="none",
|
||||
)
|
||||
req = ProviderRequest(prompt="hello")
|
||||
req.conversation = MagicMock(persona_id="locked", history="[]")
|
||||
|
||||
with (
|
||||
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
|
||||
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
|
||||
):
|
||||
mock_runner = MagicMock()
|
||||
mock_runner.reset = AsyncMock()
|
||||
mock_runner_cls.return_value = mock_runner
|
||||
|
||||
result = await module.build_main_agent(
|
||||
event=mock_event,
|
||||
plugin_context=mock_context,
|
||||
config=config,
|
||||
provider=mock_provider,
|
||||
req=req,
|
||||
apply_reset=False,
|
||||
)
|
||||
assert result is not None
|
||||
try:
|
||||
assert result.provider_request.func_tool is None or (
|
||||
result.provider_request.func_tool.empty()
|
||||
)
|
||||
finally:
|
||||
if result.reset_coro:
|
||||
result.reset_coro.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subagent_dedupe_uses_default_persona_tools(
|
||||
self, mock_event, mock_context
|
||||
|
||||
@@ -8,6 +8,36 @@ import pytest
|
||||
from astrbot.core.tools.cron_tools import FutureTaskTool
|
||||
|
||||
|
||||
def _context(cron_mgr, *, umo: str = "test:group:shared", sender_id: str = "user-1"):
|
||||
return SimpleNamespace(
|
||||
context=SimpleNamespace(
|
||||
context=SimpleNamespace(cron_manager=cron_mgr),
|
||||
event=SimpleNamespace(
|
||||
unified_msg_origin=umo,
|
||||
get_sender_id=lambda: sender_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _job(job_id: str, *, umo: str = "test:group:shared", sender_id: str = "user-1"):
|
||||
return SimpleNamespace(
|
||||
job_id=job_id,
|
||||
name=f"name-{job_id}",
|
||||
job_type="active_agent",
|
||||
run_once=False,
|
||||
cron_expression="0 8 * * *",
|
||||
enabled=True,
|
||||
next_run_time=None,
|
||||
payload={
|
||||
"session": umo,
|
||||
"sender_id": sender_id,
|
||||
"note": f"note-{job_id}",
|
||||
"origin": "tool",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_future_task_schema_has_action_and_create_cron_guidance():
|
||||
"""The merged tool should expose action routing and unambiguous cron guidance."""
|
||||
tool = FutureTaskTool()
|
||||
@@ -124,3 +154,71 @@ async def test_future_task_edit_updates_existing_job():
|
||||
},
|
||||
)
|
||||
assert result == "Updated future task job-1 (new name)."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_future_task_edit_rejects_same_umo_different_sender():
|
||||
"""Same-session users should not edit another sender's task."""
|
||||
tool = FutureTaskTool()
|
||||
existing_job = _job("job-1", sender_id="admin-user")
|
||||
cron_mgr = SimpleNamespace(
|
||||
db=SimpleNamespace(get_cron_job=AsyncMock(return_value=existing_job)),
|
||||
update_job=AsyncMock(),
|
||||
)
|
||||
|
||||
result = await tool.call(
|
||||
_context(cron_mgr, sender_id="attacker-user"),
|
||||
action="edit",
|
||||
job_id="job-1",
|
||||
note="attacker note",
|
||||
)
|
||||
|
||||
assert result == "error: you can only edit your own future tasks."
|
||||
cron_mgr.update_job.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_future_task_delete_rejects_same_umo_different_sender():
|
||||
"""Same-session users should not delete another sender's task."""
|
||||
tool = FutureTaskTool()
|
||||
existing_job = _job("job-1", sender_id="admin-user")
|
||||
cron_mgr = SimpleNamespace(
|
||||
db=SimpleNamespace(get_cron_job=AsyncMock(return_value=existing_job)),
|
||||
delete_job=AsyncMock(),
|
||||
)
|
||||
|
||||
result = await tool.call(
|
||||
_context(cron_mgr, sender_id="attacker-user"),
|
||||
action="delete",
|
||||
job_id="job-1",
|
||||
)
|
||||
|
||||
assert result == "error: you can only delete your own future tasks."
|
||||
cron_mgr.delete_job.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_future_task_list_filters_by_umo_and_sender():
|
||||
"""List mode should show only tasks owned by the current sender."""
|
||||
tool = FutureTaskTool()
|
||||
own_job = _job("own-job", sender_id="user-1")
|
||||
same_umo_other_sender = _job("other-sender-job", sender_id="user-2")
|
||||
different_umo_same_sender = _job(
|
||||
"other-umo-job",
|
||||
umo="test:group:other",
|
||||
sender_id="user-1",
|
||||
)
|
||||
cron_mgr = SimpleNamespace(
|
||||
list_jobs=AsyncMock(
|
||||
return_value=[own_job, same_umo_other_sender, different_umo_same_sender]
|
||||
)
|
||||
)
|
||||
|
||||
result = await tool.call(
|
||||
_context(cron_mgr, sender_id="user-1"),
|
||||
action="list",
|
||||
)
|
||||
|
||||
assert "own-job" in result
|
||||
assert "other-sender-job" not in result
|
||||
assert "other-umo-job" not in result
|
||||
|
||||
Reference in New Issue
Block a user