"""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)