Files
leocrm/tests/test_m6_miniapp_hosts.py
T
Agent Zero 04e92794de
Check Cross-Plugin Imports / check (push) Has been cancelled
fix(M6): agents/tools-Endpoint — list_for_api statt nichtexistenter list_tools (#364)
- Vorbestands-Bug (live gemessen: 500 "ToolRegistry has no attribute list_tools"):
  automation/agent_routes.py rief registry.list_tools() auf, ToolRegistry
  bietet get_all()/list_for_api() — Route auf list_for_api() mit korrektem
  Feld-Mapping (plugin_name -> plugin) umgestellt
- Regressionstest gesichert (test_agents_tools_route_uses_list_for_api)
- M6-Suite 8/8 gruen
2026-08-31 01:07:56 +02:00

186 lines
7.0 KiB
Python

"""M6 — further hosts tests.
Windows host: MiniApps open in floating windows (frontend — covered by
vitest). AI agent host: the core tool ``send_miniapp`` lets agents embed a
MiniApp as an output block in their chat (approval_request precedent from
agent_loop), permission-checked fail-closed against the calling user.
"""
from __future__ import annotations
import inspect
import pytest
from app.ai.tool_registry import get_tool_registry
@pytest.fixture(autouse=True)
def _clean_registries():
from app.plugins.miniapp_registry import reset_miniapp_registry
reset_miniapp_registry()
yield
reset_miniapp_registry()
get_tool_registry()._tools.pop("send_miniapp", None)
def _ctx(**overrides) -> dict:
# db is always present in production tool contexts (agent_loop passes
# the session); a plain object() stands in for tests where the handler
# only forwards it to mocked collaborators.
ctx = {
"db": object(),
"tenant_id": "00000000-0000-0000-0000-000000000001",
"user_id": "00000000-0000-0000-0000-000000000002",
"agent_name": "TestAgent",
}
ctx.update(overrides)
return ctx
class TestSendMiniAppToolRegistration:
def test_register_registers_tool(self):
from app.ai.miniapp_tools import register_miniapp_tools
register_miniapp_tools()
tool = get_tool_registry().get("send_miniapp")
assert tool is not None
assert tool.plugin_name == "system"
assert tool.category == "ui"
# App id is the only required parameter
props = tool.parameters.get("properties", {})
assert "app_id" in props
assert tool.parameters.get("required") == ["app_id"]
class TestSendMiniAppHandler:
async def test_unknown_app_returns_error(self):
from app.ai.miniapp_tools import _send_miniapp_handler
result = await _send_miniapp_handler({"app_id": "no_such_app"}, _ctx())
assert "not found" in result.lower()
async def test_unknown_app_never_touches_chat(self, monkeypatch):
"""Fail-closed: unknown app must not post anything anywhere."""
from app.ai import miniapp_tools
called = []
monkeypatch.setattr(
miniapp_tools, "_get_komm_contract", lambda: (_ for _ in ()).throw(AssertionError("must not be called"))
)
from app.ai.miniapp_tools import _send_miniapp_handler
await _send_miniapp_handler({"app_id": "nope"}, _ctx())
assert called == []
async def test_permission_denied_returns_error(self, monkeypatch):
"""User without the app permission cannot make the agent send it."""
from app.ai import miniapp_tools
from app.plugins.miniapp_registry import get_miniapp_registry
get_miniapp_registry().register(
app_id="tasks_widget", name="Tasks", plugin_name="tasks",
permission="tasks:read", component="@/components/x",
)
async def fake_resolve(db, user_id, tenant_id):
return {"permissions": set(), "denied": set(), "is_system_admin": False}
monkeypatch.setattr(miniapp_tools, "resolve_permissions", fake_resolve)
monkeypatch.setattr(
miniapp_tools, "_get_komm_contract", lambda: (_ for _ in ()).throw(AssertionError("must not post"))
)
from app.ai.miniapp_tools import _send_miniapp_handler
result = await _send_miniapp_handler({"app_id": "tasks_widget"}, _ctx())
assert "permission" in result.lower()
async def test_posts_miniapp_block_to_agent_room(self, monkeypatch):
"""Happy path: block with app_id + settings lands in the agent room."""
from app.ai import miniapp_tools
from app.plugins.miniapp_registry import get_miniapp_registry
get_miniapp_registry().register(
app_id="open_widget", name="Open", plugin_name="test",
permission="", component="@/components/x",
)
async def fake_resolve(db, user_id, tenant_id):
return {"permissions": {"*:*"}, "denied": set(), "is_system_admin": True}
posted: list[dict] = []
class FakeKomm:
async def find_locked_room_id(self, db, tenant_id, plugin_name, title):
assert plugin_name == "automation"
assert title == "Agent: TestAgent"
return "conv-123"
async def send_message(self, **kwargs):
posted.append(kwargs)
monkeypatch.setattr(miniapp_tools, "resolve_permissions", fake_resolve)
monkeypatch.setattr(miniapp_tools, "_get_komm_contract", lambda: FakeKomm())
from app.ai.miniapp_tools import _send_miniapp_handler
result = await _send_miniapp_handler(
{"app_id": "open_widget", "settings": {"limit": 5}}, _ctx()
)
assert "sent" in result.lower() or "success" in result.lower()
assert len(posted) == 1
msg = posted[0]
assert msg["conversation_id"] == "conv-123"
assert msg["sender_type"] == "agent"
blocks = msg["blocks"]
assert len(blocks) == 1
assert blocks[0]["block_type"] == "miniapp"
assert blocks[0]["block_data"]["app_id"] == "open_widget"
assert blocks[0]["block_data"]["config"] == {"limit": 5}
async def test_missing_room_degrades_gracefully(self, monkeypatch):
"""No agent chat room -> informative result, no crash."""
from app.ai import miniapp_tools
from app.plugins.miniapp_registry import get_miniapp_registry
get_miniapp_registry().register(
app_id="open_widget", name="Open", plugin_name="test", permission=""
)
async def fake_resolve(db, user_id, tenant_id):
return {"permissions": set(), "denied": set(), "is_system_admin": True}
class FakeKomm:
async def find_locked_room_id(self, db, tenant_id, plugin_name, title):
return None
monkeypatch.setattr(miniapp_tools, "resolve_permissions", fake_resolve)
monkeypatch.setattr(miniapp_tools, "_get_komm_contract", lambda: FakeKomm())
from app.ai.miniapp_tools import _send_miniapp_handler
result = await _send_miniapp_handler({"app_id": "open_widget"}, _ctx())
assert "room" in result.lower() or "chat" in result.lower()
class TestAgentLoopContext:
def test_tool_context_carries_agent_name(self):
"""agent_loop must pass the agent's name so tools can find the room."""
from app.ai import agent_loop
src = inspect.getsource(agent_loop)
assert '"agent_name"' in src, "tool_context must include agent_name"
def test_agents_tools_route_uses_list_for_api():
"""M6 verification blocker fixed: /api/v1/agents/tools used to call
registry.list_tools() (nonexistent — 500 on production, pre-existing).
The route must use list_for_api() with the correct field mapping.
"""
import inspect
from app.plugins.builtins.automation.agent_routes import list_tools
src = inspect.getsource(list_tools)
assert "list_for_api()" in src, "route must use list_for_api()"
assert "list_tools()" not in src.replace("list_for_api()", "")