summaryrefslogtreecommitdiff
path: root/tests/test_mcp_multimedia.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_mcp_multimedia.py')
-rw-r--r--tests/test_mcp_multimedia.py138
1 files changed, 138 insertions, 0 deletions
diff --git a/tests/test_mcp_multimedia.py b/tests/test_mcp_multimedia.py
new file mode 100644
index 0000000..3baf155
--- /dev/null
+++ b/tests/test_mcp_multimedia.py
@@ -0,0 +1,138 @@
+"""Protocol-level tests for compose_mml multimedia results."""
+
+import asyncio
+import base64
+import json
+from unittest.mock import patch
+
+from fastmcp import Client
+from mcp.types import AudioContent, ResourceLink, TextContent
+
+from mml_composemusic_mcp.server import mcp
+
+VALID_MML = """A t120 l4 o4 v15 q2
+ c e g c
+"""
+
+
+async def _call_tool(arguments, *, raise_on_error=True):
+ async with Client(mcp) as client:
+ return await client.call_tool(
+ "compose_mml",
+ arguments,
+ raise_on_error=raise_on_error,
+ )
+
+
+def test_compose_returns_audio_content_without_exposing_path(tmp_output_dir):
+ result = asyncio.run(
+ _call_tool({"action": "compose", "mml": VALID_MML, "mode": "ppmck"})
+ )
+
+ assert result.is_error is False
+ assert result.structured_content["success"] is True
+ assert "wav_path" not in result.structured_content
+ assert result.structured_content["audio"] == {
+ "mime_type": "audio/wav",
+ "size_bytes": result.structured_content["audio"]["size_bytes"],
+ "sample_rate": 44100,
+ }
+
+ audio_blocks = [block for block in result.content if isinstance(block, AudioContent)]
+ assert len(audio_blocks) == 1
+ assert audio_blocks[0].mimeType == "audio/wav"
+ decoded = base64.b64decode(audio_blocks[0].data, validate=True)
+ assert len(decoded) > 100_000
+
+ wav_files = list(tmp_output_dir.rglob("output.wav"))
+ assert len(wav_files) == 1
+ assert decoded == wav_files[0].read_bytes()
+ assert result.structured_content["audio"]["size_bytes"] == len(decoded)
+ assert not any(isinstance(block, ResourceLink) for block in result.content)
+
+ text_blocks = [block for block in result.content if isinstance(block, TextContent)]
+ assert len(text_blocks) == 1
+ assert json.loads(text_blocks[0].text) == result.structured_content
+ assert "wav_path" not in text_blocks[0].text
+
+
+def test_compose_failure_is_mcp_tool_error_without_audio(tmp_output_dir):
+ result = asyncio.run(
+ _call_tool(
+ {"action": "compose", "mml": "A t0 c", "mode": "ppmck"},
+ raise_on_error=False,
+ )
+ )
+
+ assert result.is_error is True
+ assert result.structured_content["success"] is False
+ assert result.structured_content["audio"] is None
+ assert not any(isinstance(block, AudioContent) for block in result.content)
+ assert not list(tmp_output_dir.rglob("output.wav"))
+
+
+def test_invalid_validate_result_is_not_mcp_tool_error():
+ result = asyncio.run(
+ _call_tool({"action": "validate", "mml": "A t0 c", "mode": "ppmck"})
+ )
+
+ assert result.is_error is False
+ assert result.structured_content["valid"] is False
+ assert not any(isinstance(block, AudioContent) for block in result.content)
+
+
+def test_invalid_validate_mode_is_mcp_tool_error():
+ result = asyncio.run(
+ _call_tool(
+ {"action": "validate", "mml": VALID_MML, "mode": "unknown"},
+ raise_on_error=False,
+ )
+ )
+
+ assert result.is_error is True
+ assert result.structured_content["valid"] is False
+
+
+def test_unknown_action_is_mcp_tool_error():
+ result = asyncio.run(
+ _call_tool({"action": "unknown"}, raise_on_error=False)
+ )
+
+ assert result.is_error is True
+ assert result.structured_content["success"] is False
+
+
+def test_audio_serialization_failure_is_sanitized_tool_error(tmp_output_dir):
+ with patch(
+ "mml_composemusic_mcp.server.Audio.to_audio_content",
+ side_effect=OSError("C:\\private\\output.wav"),
+ ):
+ result = asyncio.run(
+ _call_tool(
+ {"action": "compose", "mml": VALID_MML, "mode": "ppmck"},
+ raise_on_error=False,
+ )
+ )
+
+ assert result.is_error is True
+ assert result.structured_content["success"] is False
+ assert result.structured_content["audio"] is None
+ assert "C:\\private" not in json.dumps(result.structured_content)
+ assert not any(isinstance(block, AudioContent) for block in result.content)
+
+
+def test_unexpected_execution_failure_is_sanitized_tool_error():
+ with patch(
+ "mml_composemusic_mcp.server._execute_compose_mml",
+ side_effect=OSError("C:\\private\\source.mml"),
+ ):
+ result = asyncio.run(
+ _call_tool(
+ {"action": "compose", "mml": VALID_MML, "mode": "ppmck"},
+ raise_on_error=False,
+ )
+ )
+
+ assert result.is_error is True
+ assert result.structured_content["success"] is False
+ assert "C:\\private" not in json.dumps(result.structured_content)