/
/
/
1"""
2Tests for ``provider.prompts.register_prompts``.
3
4The prompts module shipped without tests; a refactor that dropped a prompt
5or broke the ``CONF_RES_PROMPTS`` gate would land unobserved. This file
6pins both the gate and the three registered prompts' names + tool references.
7"""
8# mypy: disable-error-code="arg-type, no-untyped-def, type-arg, assignment, operator, misc, union-attr"
9
10from __future__ import annotations
11
12from typing import Any
13from unittest.mock import MagicMock
14
15import pytest
16from fastmcp import Client, FastMCP
17
18from music_assistant.providers.fastmcp_server.prompts import register_prompts
19
20_EXPECTED_NAMES = {"find_and_play", "curate_party_playlist", "now_playing_summary"}
21
22
23def _config(*, prompts_enabled: bool) -> MagicMock:
24 """Build a minimal ``ProviderConfig`` stub gating on ``CONF_RES_PROMPTS``."""
25 cfg = MagicMock()
26 cfg._values = {"res_prompts": prompts_enabled}
27 cfg.get_value = MagicMock(side_effect=lambda key, default=None: cfg._values.get(key, default))
28 return cfg
29
30
31@pytest.fixture
32def mcp_with_prompts() -> FastMCP:
33 """Build a FastMCP root with all three prompts registered (gate ON)."""
34 mcp: FastMCP = FastMCP(name="t")
35 register_prompts(mcp, _config(prompts_enabled=True))
36 return mcp
37
38
39async def test_gate_off_registers_no_prompts() -> None:
40 """``CONF_RES_PROMPTS=False`` skips registration entirely."""
41 mcp: FastMCP = FastMCP(name="t")
42 register_prompts(mcp, _config(prompts_enabled=False))
43
44 async with Client(mcp) as client:
45 prompts = await client.list_prompts()
46 assert prompts == [], f"expected no prompts when gate is off, got {prompts!r}"
47
48
49async def test_gate_on_registers_exactly_three_named_prompts(mcp_with_prompts: FastMCP) -> None:
50 """The three prompt names are exposed verbatim â clients address them by name."""
51 async with Client(mcp_with_prompts) as client:
52 prompts = await client.list_prompts()
53 names = {p.name for p in prompts}
54 assert names == _EXPECTED_NAMES, (
55 f"prompt set drifted from {_EXPECTED_NAMES}; got {names}. "
56 f"Adding or removing a prompt is a public-contract change."
57 )
58
59
60async def test_find_and_play_references_expected_tools(mcp_with_prompts: FastMCP) -> None:
61 """``find_and_play`` orients the LLM toward the right tool chain."""
62 async with Client(mcp_with_prompts) as client:
63 result = await client.get_prompt("find_and_play", {"query": "test", "target_player": "p1"})
64 text = " ".join(m.content.text for m in result.messages if hasattr(m.content, "text"))
65 for tool_name in (
66 "library_search_tracks",
67 "playback_play_media",
68 "queue_get_active_queue",
69 "players_list_players",
70 ):
71 assert tool_name in text, f"missing tool ref {tool_name!r} in find_and_play"
72
73
74async def test_curate_party_playlist_references_playlist_tools(
75 mcp_with_prompts: FastMCP,
76) -> None:
77 """``curate_party_playlist`` references the create + add-tracks chain."""
78 async with Client(mcp_with_prompts) as client:
79 result = await client.get_prompt(
80 "curate_party_playlist", {"theme": "indie", "length_minutes": "30"}
81 )
82 text = " ".join(m.content.text for m in result.messages if hasattr(m.content, "text"))
83 for tool_name in (
84 "library_search_tracks",
85 "playlists_create_playlist",
86 "playlists_add_tracks",
87 ):
88 assert tool_name in text, f"missing tool ref {tool_name!r} in curate_party_playlist"
89
90
91async def test_now_playing_summary_branches_on_player_id(mcp_with_prompts: FastMCP) -> None:
92 """``now_playing_summary`` switches its tool plan based on whether a player_id is given."""
93 async with Client(mcp_with_prompts) as client:
94 with_id = await client.get_prompt("now_playing_summary", {"player_id": "p1"})
95 without_id = await client.get_prompt("now_playing_summary", {})
96
97 def _text(result: Any) -> str:
98 return " ".join(m.content.text for m in result.messages if hasattr(m.content, "text"))
99
100 assert "queue_get_active_queue" in _text(with_id)
101 assert "p1" in _text(with_id)
102 # Without an id we expect the broader list-then-fan-out plan.
103 assert "players_list_players" in _text(without_id)
104