Fix get_mgr() to use ctx.lifespan_context (works in both request and standalone Client mode). Add autouse fixture to reset event-loop-bound singleton state per test, preventing cross-test asyncio.Event contamination. 21 new tests exercise all 14 tools through FastMCP's in-memory transport. New shortcuts: network_status (NetworkManager), battery_status (UPower), bluetooth_devices (bluez), kwin_windows (KRunner WindowsRunner). 53 tests pass.
239 lines
8.4 KiB
Python
239 lines
8.4 KiB
Python
"""Tool-level tests — every MCP tool exercised through Client(mcp) in-memory transport."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from fastmcp import Client
|
|
from fastmcp.exceptions import ToolError
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Discovery tools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestListServices:
|
|
async def test_returns_services(self, client: Client):
|
|
result = await client.call_tool("list_services", {"bus": "session"})
|
|
text = result.content[0].text
|
|
assert "org.freedesktop.DBus" in text
|
|
assert "D-Bus session bus" in text
|
|
|
|
async def test_include_unique(self, client: Client):
|
|
result = await client.call_tool(
|
|
"list_services", {"bus": "session", "include_unique": True}
|
|
)
|
|
text = result.content[0].text
|
|
assert ":1." in text
|
|
|
|
|
|
class TestIntrospect:
|
|
async def test_dbus_daemon(self, client: Client):
|
|
result = await client.call_tool("introspect", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
})
|
|
text = result.content[0].text
|
|
assert "org.freedesktop.DBus" in text
|
|
assert "ListNames" in text
|
|
|
|
async def test_invalid_path(self, client: Client):
|
|
with pytest.raises(ToolError, match="Invalid D-Bus object path"):
|
|
await client.call_tool("introspect", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "not-a-path",
|
|
})
|
|
|
|
|
|
class TestListObjects:
|
|
async def test_dbus_tree(self, client: Client):
|
|
result = await client.call_tool("list_objects", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
})
|
|
text = result.content[0].text
|
|
assert "Object tree" in text
|
|
assert "/org/freedesktop/DBus" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Interaction tools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCallMethod:
|
|
async def test_ping_void(self, client: Client):
|
|
result = await client.call_tool("call_method", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
"interface": "org.freedesktop.DBus.Peer",
|
|
"method": "Ping",
|
|
})
|
|
text = result.content[0].text
|
|
assert "void" in text.lower()
|
|
|
|
async def test_get_id_returns_hex(self, client: Client):
|
|
result = await client.call_tool("call_method", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
"interface": "org.freedesktop.DBus",
|
|
"method": "GetId",
|
|
})
|
|
text = result.content[0].text
|
|
# GetId returns a hex string, JSON-encoded with quotes
|
|
bus_id = json.loads(text)
|
|
assert isinstance(bus_id, str)
|
|
assert len(bus_id) > 10
|
|
|
|
async def test_bad_method_returns_error(self, client: Client):
|
|
result = await client.call_tool("call_method", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
"interface": "org.freedesktop.DBus",
|
|
"method": "NoSuchMethod",
|
|
})
|
|
text = result.content[0].text
|
|
assert "Error" in text or "error" in text
|
|
|
|
|
|
class TestGetProperty:
|
|
async def test_read_property(self, client: Client):
|
|
# Read the Features property from the DBus daemon
|
|
result = await client.call_tool("get_property", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
"interface": "org.freedesktop.DBus",
|
|
"property_name": "Features",
|
|
})
|
|
text = result.content[0].text
|
|
# Features returns an array of strings
|
|
features = json.loads(text)
|
|
assert isinstance(features, list)
|
|
|
|
|
|
class TestGetAllProperties:
|
|
async def test_dbus_daemon(self, client: Client):
|
|
result = await client.call_tool("get_all_properties", {
|
|
"bus": "session",
|
|
"service": "org.freedesktop.DBus",
|
|
"object_path": "/org/freedesktop/DBus",
|
|
"interface": "org.freedesktop.DBus",
|
|
})
|
|
text = result.content[0].text
|
|
assert "Properties of" in text
|
|
assert "Features" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shortcut tools — existing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSendNotification:
|
|
async def test_sends(self, client: Client):
|
|
result = await client.call_tool("send_notification", {
|
|
"summary": "mcdbus test",
|
|
"body": "automated test notification",
|
|
"timeout": 1000,
|
|
})
|
|
text = result.content[0].text
|
|
assert "Notification sent" in text
|
|
assert "id:" in text
|
|
|
|
|
|
class TestListSystemdUnits:
|
|
async def test_returns_table(self, client: Client):
|
|
result = await client.call_tool("list_systemd_units", {})
|
|
text = result.content[0].text
|
|
assert "Systemd units" in text
|
|
assert "| Unit |" in text
|
|
|
|
async def test_pattern_filter(self, client: Client):
|
|
result = await client.call_tool(
|
|
"list_systemd_units", {"pattern": "dbus*"}
|
|
)
|
|
text = result.content[0].text
|
|
assert "dbus" in text.lower()
|
|
|
|
|
|
class TestMediaPlayerControl:
|
|
async def test_no_players_graceful(self, client: Client):
|
|
# With no explicit player, it auto-discovers; if none found, returns message
|
|
result = await client.call_tool(
|
|
"media_player_control", {"action": "play"}
|
|
)
|
|
text = result.content[0].text
|
|
# Either "No MPRIS media players" or a player response — both are valid
|
|
assert "Player:" in text or "No MPRIS" in text
|
|
|
|
async def test_bad_action(self, client: Client):
|
|
result = await client.call_tool(
|
|
"media_player_control", {"action": "invalid_action"}
|
|
)
|
|
text = result.content[0].text
|
|
# auto-discovers player first; if none found, "No MPRIS" message
|
|
# if player found, "Unknown action" message
|
|
assert "Unknown action" in text or "No MPRIS" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shortcut tools — new
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestNetworkStatus:
|
|
async def test_returns_status(self, client: Client):
|
|
result = await client.call_tool("network_status", {})
|
|
text = result.content[0].text
|
|
assert "Network Status" in text or "NetworkManager not available" in text
|
|
|
|
async def test_has_state(self, client: Client):
|
|
result = await client.call_tool("network_status", {})
|
|
text = result.content[0].text
|
|
if "not available" not in text:
|
|
assert "State:" in text
|
|
|
|
|
|
class TestBatteryStatus:
|
|
async def test_returns_result(self, client: Client):
|
|
result = await client.call_tool("battery_status", {})
|
|
text = result.content[0].text
|
|
# Desktop may not have battery — both outcomes are valid
|
|
assert (
|
|
"Battery Status" in text
|
|
or "No batteries found" in text
|
|
or "No power devices" in text
|
|
or "UPower not available" in text
|
|
)
|
|
|
|
|
|
class TestBluetoothDevices:
|
|
async def test_returns_result(self, client: Client):
|
|
result = await client.call_tool("bluetooth_devices", {})
|
|
text = result.content[0].text
|
|
assert (
|
|
"Bluetooth Devices" in text
|
|
or "No Bluetooth devices" in text
|
|
or "No Bluetooth objects" in text
|
|
or "bluez not available" in text
|
|
)
|
|
|
|
|
|
class TestKwinWindows:
|
|
async def test_returns_windows(self, client: Client):
|
|
result = await client.call_tool("kwin_windows", {})
|
|
text = result.content[0].text
|
|
assert (
|
|
"KWin Windows" in text
|
|
or "KWin not available" in text
|
|
or "No windows found" in text
|
|
or "No open windows" in text
|
|
)
|
|
|
|
async def test_has_table(self, client: Client):
|
|
result = await client.call_tool("kwin_windows", {})
|
|
text = result.content[0].text
|
|
if "KWin Windows" in text:
|
|
assert "| Window |" in text
|