"""Tests for src/reaper_scripting/ — command protocol and ReaScriptGenerator.""" from __future__ import annotations import ast import sys from pathlib import Path import json import tempfile sys.path.insert(0, str(Path(__file__).parents[1])) import pytest from src.reaper_scripting.commands import ( ProtocolVersionError, ReaScriptCommand, ReaScriptResult, read_result, write_command, ) from src.reaper_scripting import ReaScriptGenerator # ------------------------------------------------------------------ # Phase 1: Command/Result Protocol # ------------------------------------------------------------------ class TestCommandSerialization: """Test JSON round-trip for ReaScriptCommand.""" def test_write_command_produces_json_file(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "cmd.json" write_command(path, cmd) assert path.exists() def test_roundtrip_command_fields(self, tmp_path): cmd = ReaScriptCommand( version=1, action="verify_fx", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=60, track_calibration=[ {"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []}, ], ) path = tmp_path / "cmd.json" write_command(path, cmd) text = path.read_text(encoding="utf-8") data = json.loads(text) assert data["version"] == 1 assert data["action"] == "verify_fx" assert data["rpp_path"] == "C:/song.rpp" assert data["render_path"] == "C:/song.wav" assert data["timeout"] == 60 assert len(data["track_calibration"]) == 1 def test_read_result_roundtrip(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "cmd.json" write_command(path, cmd) result = read_result(path) assert result.version == 1 assert result.status == "ok" assert result.integrated_lufs is None assert result.fx_errors == [] class TestVersionMismatch: """Test ProtocolVersionError on version mismatch.""" def test_version_mismatch_raises(self, tmp_path): path = tmp_path / "bad_result.json" path.write_text( json.dumps({"version": 2, "status": "ok", "message": "", "fx_errors": []}), encoding="utf-8", ) with pytest.raises(ProtocolVersionError) as exc_info: read_result(path) assert "expected 1, got 2" in str(exc_info.value) class TestMissingFile: """Test FileNotFoundError on missing file.""" def test_missing_command_raises_file_not_found(self): with pytest.raises(FileNotFoundError): read_result(Path("nonexistent.json")) def test_missing_result_raises_file_not_found(self, tmp_path): nonexistent = tmp_path / "does_not_exist.json" with pytest.raises(FileNotFoundError): read_result(nonexistent) # ------------------------------------------------------------------ # Phase 2: ReaScriptGenerator # ------------------------------------------------------------------ class TestReaScriptGeneratorOutput: """Test that ReaScriptGenerator produces valid Python code.""" def test_generate_produces_file(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) assert path.exists() def test_generate_output_is_valid_python(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Must not raise tree = ast.parse(source) assert tree is not None def test_contains_required_api_calls(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") required = [ "Main_openProject", "TrackFX_GetCount", "TrackFX_GetFXName", "SetMediaTrackInfo_Value", "CreateTrackSend", "Main_RenderFile", "CalcMediaSrcLoudness", "GetFunctionMetadata", ] for api in required: assert api in source, f"{api} not found in generated script" def test_script_reads_command_path(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Must reference the command JSON path assert "fl_control_command.json" in source assert "fl_control_result.json" in source def test_script_has_handrolled_json_parser(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Must NOT use Python's json module assert "import json" not in source # Must have hand-rolled parser assert "parse_json" in source assert "write_json" in source def test_script_includes_api_check_function(self, tmp_path): cmd = ReaScriptCommand( version=1, action="verify_fx", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") assert "check_api" in source assert "GetFunctionMetadata" in source def test_verify_fx_action_iterates_tracks(self, tmp_path): cmd = ReaScriptCommand( version=1, action="verify_fx", rpp_path="C:/song.rpp", render_path="", timeout=60, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Must iterate all tracks to verify FX assert "CountTracks" in source assert "GetTrack" in source assert "fx_errors" in source def test_calibrate_action_sets_track_volume_pan(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[ {"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []}, { "track_index": 1, "volume": 0.7, "pan": -0.3, "sends": [{"dest_track_index": 5, "level": 0.05}], }, ], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Volume and pan setting assert "D_VOL" in source assert "D_PAN" in source # Send creation assert "CreateTrackSend" in source def test_error_handling_writes_error_result(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") # Must handle errors gracefully and write error status assert '"status": "error"' in source or "'status': 'error'" in source assert "write_json" in source def test_script_has_main_function(self, tmp_path): cmd = ReaScriptCommand( version=1, action="calibrate", rpp_path="C:/song.rpp", render_path="C:/song.wav", timeout=120, track_calibration=[], ) path = tmp_path / "phase2.py" gen = ReaScriptGenerator() gen.generate(path, cmd) source = path.read_text(encoding="utf-8") assert "def main():" in source assert "main()" in source