/
/
/
1"""End-to-end tests for debug_inspect_player/queue/provider."""
2
3from __future__ import annotations
4
5from types import SimpleNamespace
6from typing import Any
7
8import pytest
9from fastmcp import Client
10from fastmcp.exceptions import ToolError
11
12
13def _player_full(
14 player_id: str, *, available: bool = True, enabled: bool = True
15) -> SimpleNamespace:
16 return SimpleNamespace(
17 player_id=player_id,
18 name=f"Player {player_id}",
19 available=available,
20 enabled=enabled,
21 hidden=False,
22 powered=True,
23 volume_level=42,
24 playback_state=SimpleNamespace(value="idle"),
25 current_media=None,
26 active_group=None,
27 synced_to=None,
28 last_error=None,
29 extra_data={"flavor": "test"},
30 state=SimpleNamespace(power="on", active_group=None, synced_to=None),
31 )
32
33
34async def _call(mcp: Any, tool_name: str, **kwargs: Any) -> Any:
35 async with Client(mcp) as client:
36 return await client.call_tool(f"debug_{tool_name}", kwargs)
37
38
39async def test_inspect_player_returns_raw_and_state(mounted_debug: Any, mock_mass: Any) -> None:
40 """``debug_inspect_player`` returns raw object and state object separately."""
41 player = _player_full("kitchen")
42 mock_mass.players.get_player = lambda pid: player if pid == "kitchen" else None
43 result = await _call(mounted_debug, "inspect_player", player_id="kitchen")
44 data = result.data
45 assert data.player_id == "kitchen"
46 assert data.raw["name"] == "Player kitchen"
47 assert data.state["power"] == "on"
48 assert data.truncated is False
49
50
51async def test_inspect_unavailable_player_still_returns_state(
52 mounted_debug: Any, mock_mass: Any
53) -> None:
54 """Unavailable players still surface full raw and state â the point of debug_inspect."""
55 player = _player_full("broken", available=False)
56 player.last_error = "auth expired"
57 mock_mass.players.get_player = lambda pid: player if pid == "broken" else None
58 result = await _call(mounted_debug, "inspect_player", player_id="broken")
59 assert result.data.raw["available"] is False
60 assert result.data.raw["last_error"] == "auth expired"
61
62
63async def test_inspect_disabled_player_still_returns_state(
64 mounted_debug: Any, mock_mass: Any
65) -> None:
66 """Disabled players still surface full raw and state â the point of debug_inspect."""
67 player = _player_full("off", enabled=False)
68 mock_mass.players.get_player = lambda pid: player if pid == "off" else None
69 result = await _call(mounted_debug, "inspect_player", player_id="off")
70 assert result.data.raw["enabled"] is False
71
72
73async def test_inspect_unknown_player_raises_tool_error(mounted_debug: Any, mock_mass: Any) -> None:
74 """Unknown player_id raises ToolError (AC #9)."""
75 mock_mass.players.get_player = lambda pid: None # noqa: ARG005
76 with pytest.raises(ToolError, match="player_id='nope' not found"):
77 await _call(mounted_debug, "inspect_player", player_id="nope")
78
79
80async def test_inspect_queue_returns_raw_and_current_item(
81 mounted_debug: Any, mock_mass: Any
82) -> None:
83 """``debug_inspect_queue`` returns raw queue and current_item resolved."""
84 queue = SimpleNamespace(queue_id="kitchen", current_index=0, items_count=3, state="playing")
85 current = SimpleNamespace(uri="library://track/1", title="Test", duration=180)
86 mock_mass.player_queues.get = lambda qid: queue if qid == "kitchen" else None
87 mock_mass.player_queues.get_active_queue = lambda pid: queue # noqa: ARG005
88 queue.current_item = current
89 result = await _call(mounted_debug, "inspect_queue", queue_id="kitchen")
90 assert result.data.queue_id == "kitchen"
91 assert result.data.current_item is not None
92 assert result.data.current_item["title"] == "Test"
93
94
95async def test_inspect_provider_returns_raw_and_manifest(
96 mounted_debug: Any, mock_mass: Any
97) -> None:
98 """``debug_inspect_provider`` returns raw provider and manifest separately."""
99 manifest = SimpleNamespace(domain="yandex_music", name="Yandex Music", multi_instance=False)
100 prov = SimpleNamespace(
101 instance_id="yandex_music_1",
102 domain="yandex_music",
103 name="Yandex Music",
104 available=True,
105 last_error=None,
106 lookup_key="yandex_music_1",
107 supported_features=["search", "browse"],
108 manifest=manifest,
109 )
110 mock_mass.get_provider = lambda iid: prov if iid == "yandex_music_1" else None
111 result = await _call(mounted_debug, "inspect_provider", instance_id="yandex_music_1")
112 assert result.data.raw["domain"] == "yandex_music"
113 assert result.data.manifest["domain"] == "yandex_music"
114
115
116async def test_inspect_property_raise_renders_placeholder(
117 mounted_debug: Any, mock_mass: Any
118) -> None:
119 """Property that raises renders ``<raise: ExceptionType>`` placeholder (AC #2)."""
120
121 class _Bad:
122 player_id = "broken"
123 name = "Broken"
124 available = True
125 enabled = True
126 state = SimpleNamespace(power="on")
127
128 @property
129 def volume_level(self) -> None:
130 raise RuntimeError("not ready")
131
132 mock_mass.players.get_player = lambda pid: _Bad() if pid == "broken" else None
133 result = await _call(mounted_debug, "inspect_player", player_id="broken")
134 assert result.data.raw["volume_level"] == "<raise: RuntimeError>"
135