test_play.py
python
| 1 | """Tests for ``muse play`` command. |
| 2 | |
| 3 | Tests: |
| 4 | - ``test_play_calls_afplay`` — happy path: subprocess.run(['afplay', path]) called. |
| 5 | - ``test_play_mid_falls_back_to_open`` — MIDI files fall back to ``open``. |
| 6 | - ``test_play_file_not_found_exits_1`` — exit code 1 when file does not exist. |
| 7 | - ``test_play_requires_macos`` — exit code 1 on non-macOS platforms. |
| 8 | """ |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import json |
| 12 | import pathlib |
| 13 | import uuid |
| 14 | from unittest.mock import MagicMock, patch |
| 15 | |
| 16 | import pytest |
| 17 | from typer.testing import CliRunner |
| 18 | |
| 19 | from maestro.muse_cli.app import cli |
| 20 | from maestro.muse_cli.commands.play import _play_path |
| 21 | |
| 22 | runner = CliRunner() |
| 23 | |
| 24 | |
| 25 | def _init_muse_repo(root: pathlib.Path, repo_id: str | None = None) -> str: |
| 26 | rid = repo_id or str(uuid.uuid4()) |
| 27 | muse = root / ".muse" |
| 28 | (muse / "refs" / "heads").mkdir(parents=True) |
| 29 | (muse / "repo.json").write_text( |
| 30 | json.dumps({"repo_id": rid, "schema_version": "1"}) |
| 31 | ) |
| 32 | (muse / "HEAD").write_text("refs/heads/main") |
| 33 | (muse / "refs" / "heads" / "main").write_text("") |
| 34 | return rid |
| 35 | |
| 36 | |
| 37 | def test_play_calls_afplay(tmp_path: pathlib.Path) -> None: |
| 38 | """``muse play <mp3>`` calls subprocess.run(['afplay', <path>]) and exits 0.""" |
| 39 | _init_muse_repo(tmp_path) |
| 40 | artifact = tmp_path / "muse-work" / "jazz_4b_run1.mp3" |
| 41 | artifact.parent.mkdir(parents=True) |
| 42 | artifact.write_bytes(b"MP3DATA") |
| 43 | |
| 44 | with ( |
| 45 | patch("maestro.muse_cli.commands.play.platform.system", return_value="Darwin"), |
| 46 | patch("maestro.muse_cli.commands.play.subprocess.run") as mock_run, |
| 47 | patch.dict("os.environ", {"MUSE_REPO_ROOT": str(tmp_path)}), |
| 48 | ): |
| 49 | mock_run.return_value = MagicMock(returncode=0) |
| 50 | result = runner.invoke(cli, ["play", str(artifact)]) |
| 51 | |
| 52 | assert result.exit_code == 0, result.output |
| 53 | mock_run.assert_called_once() |
| 54 | call_args = mock_run.call_args[0][0] |
| 55 | assert call_args[0] == "afplay" |
| 56 | assert str(artifact.resolve()) in call_args[1] |
| 57 | |
| 58 | |
| 59 | def test_play_mid_falls_back_to_open(tmp_path: pathlib.Path) -> None: |
| 60 | """``muse play <mid>`` falls back to ``open`` and shows a warning.""" |
| 61 | _init_muse_repo(tmp_path) |
| 62 | artifact = tmp_path / "muse-work" / "track.mid" |
| 63 | artifact.parent.mkdir(parents=True) |
| 64 | artifact.write_bytes(b"MIDI") |
| 65 | |
| 66 | with ( |
| 67 | patch("maestro.muse_cli.commands.play.platform.system", return_value="Darwin"), |
| 68 | patch("maestro.muse_cli.commands.play.subprocess.run") as mock_run, |
| 69 | patch.dict("os.environ", {"MUSE_REPO_ROOT": str(tmp_path)}), |
| 70 | ): |
| 71 | mock_run.return_value = MagicMock(returncode=0) |
| 72 | result = runner.invoke(cli, ["play", str(artifact)]) |
| 73 | |
| 74 | assert result.exit_code == 0, result.output |
| 75 | call_args = mock_run.call_args[0][0] |
| 76 | assert call_args[0] == "open" |
| 77 | assert "MIDI" in result.output or "afplay" in result.output or "system default" in result.output |
| 78 | |
| 79 | |
| 80 | def test_play_file_not_found_exits_1(tmp_path: pathlib.Path) -> None: |
| 81 | """``muse play <missing>`` exits 1 with a clear error message.""" |
| 82 | _init_muse_repo(tmp_path) |
| 83 | |
| 84 | with ( |
| 85 | patch("maestro.muse_cli.commands.play.platform.system", return_value="Darwin"), |
| 86 | patch.dict("os.environ", {"MUSE_REPO_ROOT": str(tmp_path)}), |
| 87 | ): |
| 88 | result = runner.invoke(cli, ["play", "no_such_file.mp3"]) |
| 89 | |
| 90 | assert result.exit_code == 1 |
| 91 | |
| 92 | |
| 93 | def test_play_requires_macos(tmp_path: pathlib.Path) -> None: |
| 94 | """``muse play`` exits 1 with a clear message on non-macOS platforms.""" |
| 95 | _init_muse_repo(tmp_path) |
| 96 | artifact = tmp_path / "muse-work" / "beat.mp3" |
| 97 | artifact.parent.mkdir(parents=True) |
| 98 | artifact.write_bytes(b"MP3DATA") |
| 99 | |
| 100 | with ( |
| 101 | patch("maestro.muse_cli.commands.play.platform.system", return_value="Linux"), |
| 102 | patch.dict("os.environ", {"MUSE_REPO_ROOT": str(tmp_path)}), |
| 103 | ): |
| 104 | result = runner.invoke(cli, ["play", str(artifact)]) |
| 105 | |
| 106 | assert result.exit_code == 1 |
| 107 | assert "macOS" in result.output |
| 108 | |
| 109 | |
| 110 | def test_play_path_calls_afplay_directly(tmp_path: pathlib.Path) -> None: |
| 111 | """``_play_path`` helper calls afplay for mp3 files.""" |
| 112 | mp3 = tmp_path / "track.mp3" |
| 113 | mp3.write_bytes(b"MP3") |
| 114 | |
| 115 | with patch("maestro.muse_cli.commands.play.subprocess.run") as mock_run: |
| 116 | mock_run.return_value = MagicMock(returncode=0) |
| 117 | _play_path(mp3) |
| 118 | |
| 119 | mock_run.assert_called_once() |
| 120 | assert mock_run.call_args[0][0][0] == "afplay" |
| 121 | |
| 122 | |
| 123 | def test_play_path_midi_falls_back_to_open(tmp_path: pathlib.Path) -> None: |
| 124 | """``_play_path`` helper falls back to open for .mid files.""" |
| 125 | mid = tmp_path / "track.mid" |
| 126 | mid.write_bytes(b"MIDI") |
| 127 | |
| 128 | with patch("maestro.muse_cli.commands.play.subprocess.run") as mock_run: |
| 129 | mock_run.return_value = MagicMock(returncode=0) |
| 130 | _play_path(mid) |
| 131 | |
| 132 | mock_run.assert_called_once() |
| 133 | assert mock_run.call_args[0][0][0] == "open" |