"""Tests for the apply_voice_actions MCP tool — decisions -> real FCPXML. Uses an inline fixture rather than examples/sample.fcpxml so the source windows are explicit and the assertions can be exact. """ import shutil import pytest from fcpxml.safe_xml import safe_parse _FIXTURE = """ """ @pytest.fixture def project(tmp_path): path = tmp_path / "proj.fcpxml" path.write_text(_FIXTURE) return path def _out(project): return project.with_name("proj_voice_edit.fcpxml") class TestApplyVoiceActionsHandler: async def test_no_actions_reports_instead_of_writing(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({"filepath": str(project)}) assert "nothing to apply" in result[0].text.lower() assert not _out(project).exists() async def test_all_invalid_actions_writes_nothing(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [{"kind": "teleport", "start": 1.0, "end": 2.0}], }) assert "No valid actions" in result[0].text assert "teleport" in result[0].text assert not _out(project).exists() async def test_applies_zoom_into_the_hosting_clip(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [{ "kind": "zoom", "start": 5.0, "end": 6.0, "params": {"scale": 1.4}, "reason": "argumento central", }], }) assert "argumento central" in result[0].text tree = safe_parse(str(_out(project))) transforms = tree.getroot().findall(".//adjust-transform") assert len(transforms) == 1 async def test_applies_text_title(self, project): from server import handle_apply_voice_actions await handle_apply_voice_actions({ "filepath": str(project), "actions": [{ "kind": "text", "start": 3.0, "end": 4.0, "params": {"content": "SEGURANÇA"}, }], }) titles = safe_parse(str(_out(project))).getroot().findall(".//title") assert len(titles) == 1 texts = [t.text for t in titles[0].iter() if t.text] assert any("SEGURANÇA" in t for t in texts) async def test_applies_marker(self, project): from server import handle_apply_voice_actions await handle_apply_voice_actions({ "filepath": str(project), "actions": [{ "kind": "marker", "start": 2.0, "end": 2.5, "reason": "virada", }], }) markers = safe_parse(str(_out(project))).getroot().findall(".//marker") assert len(markers) == 1 async def test_cut_shortens_the_timeline(self, project): from server import handle_apply_voice_actions before = safe_parse(str(project)).getroot().find(".//asset-clip").get("duration") result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [{"kind": "cut", "start": 5.0, "end": 10.0, "reason": "digressão"}], }) assert "Cuts applied" in result[0].text clips = safe_parse(str(_out(project))).getroot().findall(".//asset-clip") total = sum( int(c.get("duration").split("/")[0]) / int(c.get("duration").split("/")[1].rstrip("s")) for c in clips ) original = int(before.split("/")[0]) / int(before.split("/")[1].rstrip("s")) assert total < original async def test_action_inside_a_cut_is_dropped_and_reported(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "cut", "start": 4.0, "end": 12.0}, {"kind": "zoom", "start": 6.0, "end": 7.0, "reason": "some no material cortado"}, ], }) text = result[0].text assert "Dropped" in text assert "some no material cortado" in text assert safe_parse(str(_out(project))).getroot().findall(".//adjust-transform") == [] async def test_action_beyond_the_media_is_reported_not_silent(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [{"kind": "zoom", "start": 500.0, "end": 501.0}], }) assert "Not placed" in result[0].text assert "outside the edited timeline" in result[0].text async def test_original_file_is_untouched(self, project): from server import handle_apply_voice_actions original = project.read_text() await handle_apply_voice_actions({ "filepath": str(project), "actions": [{"kind": "zoom", "start": 5.0, "end": 6.0}], }) assert project.read_text() == original async def test_mixed_valid_and_invalid_applies_the_valid_ones(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "zoom", "start": 5.0, "end": 6.0}, {"kind": "zoom", "start": 8.0, "end": 9.0, "params": {"scale": 99}}, ], }) assert "Rejected" in result[0].text assert len(safe_parse(str(_out(project))).getroot().findall(".//adjust-transform")) == 1 async def test_respects_explicit_output_path(self, project, tmp_path): from server import handle_apply_voice_actions target = tmp_path / "custom.fcpxml" await handle_apply_voice_actions({ "filepath": str(project), "actions": [{"kind": "marker", "start": 1.0, "end": 2.0}], "output_path": str(target), }) assert target.exists() async def test_output_is_valid_parseable_fcpxml(self, project): from server import handle_apply_voice_actions await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "cut", "start": 2.0, "end": 4.0}, {"kind": "zoom", "start": 10.0, "end": 11.0}, {"kind": "text", "start": 12.0, "end": 13.0, "params": {"content": "OK"}}, ], }) root = safe_parse(str(_out(project))).getroot() assert root.tag == "fcpxml" assert root.find(".//spine") is not None class TestSampleFixtureStillParses: """The applier must not corrupt a real-world document.""" async def test_real_sample_survives_a_zoom(self, tmp_path): from server import handle_apply_voice_actions src = "examples/sample.fcpxml" target = tmp_path / "sample.fcpxml" shutil.copy(src, target) result = await handle_apply_voice_actions({ "filepath": str(target), "actions": [{"kind": "marker", "start": 1.0, "end": 2.0, "reason": "teste"}], }) assert "Voice Actions Applied" in result[0].text class TestPlacementsLandOnTheRightPieceAfterCuts: """Cutting splits a clip into same-named pieces. Placing before cutting duplicated the zoom onto every piece and lost markers outright; a name-based lookup afterwards would always resolve to the first piece. Both bugs shipped past the suite and only showed up on real footage.""" async def test_zoom_lands_on_exactly_one_piece(self, project): from server import handle_apply_voice_actions await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "cut", "start": 2.0, "end": 5.0}, {"kind": "cut", "start": 8.0, "end": 12.0}, {"kind": "zoom", "start": 15.0, "end": 16.0, "params": {"scale": 1.2}}, ], }) root = safe_parse(str(_out(project))).getroot() assert len(root.findall(".//spine/asset-clip")) == 3 assert len(root.findall(".//adjust-transform")) == 1 async def test_zoom_lands_on_the_last_piece_not_the_first(self, project): from server import handle_apply_voice_actions await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "cut", "start": 2.0, "end": 5.0}, {"kind": "zoom", "start": 15.0, "end": 16.0}, ], }) clips = safe_parse(str(_out(project))).getroot().findall(".//spine/asset-clip") # the zoom is at 15s source -> 12s after a 3s cut, i.e. the 2nd piece assert clips[0].find("adjust-transform") is None assert clips[1].find("adjust-transform") is not None async def test_markers_survive_the_cut(self, project): from server import handle_apply_voice_actions result = await handle_apply_voice_actions({ "filepath": str(project), "actions": [ {"kind": "cut", "start": 5.0, "end": 10.0}, {"kind": "marker", "start": 4.9, "end": 5.05, "reason": "emenda"}, {"kind": "marker", "start": 15.0, "end": 15.2, "reason": "depois"}, ], }) assert "Dropped" not in result[0].text markers = safe_parse(str(_out(project))).getroot().findall(".//marker") assert len(markers) == 2