Files
leocrm/tests/test_m6_miniapp_hosts.py
T
Agent Zero 335762dd3d feat(M6): Weitere Hosts — MiniApps in Fenstern + AI-Agenten-Ausgabe-Bloecke (#364)
- Windows-Host: openMiniAppWindow-Helper + MiniAppWindowContent (windowStore);
  Oeffnen-Buttons im Chat-Block (MiniAppBlock) und Dashboard-Widget
- AI-Agenten-Host: Core-Tool send_miniapp (app/ai/miniapp_tools.py) —
  miniapp-Block in Agent-Chat (approval_request-Praezedenz), Permission
  fail-closed gegen aufrufenden User pro App; Registrierung im lifespan
- agent_loop: tool_context + agent_name (Raum-Aufloesung)
- Fix: MiniAppBlock nutzt useMiniapps (component-Feld) statt Legacy /comm/miniapps
- Tests: M6 7/7 (TDD rot->gruen), Backend-Regression 57/57, Vitest 26/26
  (4 neue Window-Tests), tsc clean, build OK
2026-08-31 01:04:33 +02:00

172 lines
6.5 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"