mcdbus/tests/test_tools.py
Ryan Malloy 326be8d52d Add tool-level tests via Client(mcp) and 4 new shortcut tools
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.
2026-03-05 23:12:28 -07:00

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