diff options
Diffstat (limited to 'tests/test_mcp_multimedia.py')
| -rw-r--r-- | tests/test_mcp_multimedia.py | 138 |
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) |
