import json import subprocess import tempfile import unittest from pathlib import Path from unittest.mock import patch from scripts import run_mvp_meeting as cli from src.meeting_lab.models.meeting_context import load_meeting_context from src.meeting_lab.orchestration import mvp as mvp_api from src.meeting_lab.orchestration.mvp import MvpMeetingConfig, MvpRunResult from src.meeting_lab.protocol.generate_direct_protocol import DirectProtocolResult from src.meeting_lab.transcription.whisper import TranscriptionError, TranscriptionResult def context_data(): return { "schema_version": "1", "meeting": { "meeting_id": "programmatic-test", "title": "Programmatic Test", "language": "de", "date": None, "objective": "API prüfen", "notes": "", }, "participants": [ { "participant_id": "person-1", "display_name": "Test Person", "aliases": [], "role": "Projektleitung", "department": None, "attendance_status": "present", "notes": None, } ], "speaker_mappings": {"SPEAKER_00": "person-1"}, "mentioned_people": [], "organization": {"name": "Example", "departments": []}, "known_entities": {}, "context_rules": {"do_not_infer_responsibilities": True}, } def fake_transcribe(audio, model, output, language, **kwargs): output.mkdir(parents=True, exist_ok=True) raw = output / "whisper_raw.json" transcript = output / "transcript.json" text = output / "transcript.txt" metadata = output / "runtime_metadata.json" raw.write_text('{"transcription": []}\n', encoding="utf-8") transcript.write_text( json.dumps( { "text": "Ein kurzer Besprechungstext.", "segments": [ { "id": 0, "start": 0.0, "end": 1.0, "text": "Ein kurzer Besprechungstext.", } ], } ), encoding="utf-8", ) text.write_text("Ein kurzer Besprechungstext.\n", encoding="utf-8") metadata.write_text('{"runtime_seconds": 0.1}\n', encoding="utf-8") return TranscriptionResult(output, raw, transcript, text, metadata, 0.1) def fake_protocol(transcript, context, **kwargs): rendered_context = load_meeting_context(context) assert rendered_context.meeting_id == "programmatic-test" return DirectProtocolResult( protocol_text="# Meeting Protocol\n", exact_prompt="prompt", model_metadata={"model": kwargs["model"]}, runtime_metadata={"request_count": 1}, raw_response={"response": "# Meeting Protocol\n"}, ) class MvpApiTests(unittest.TestCase): def config(self, root: Path): audio = root / "meeting.wav" model = root / "model.bin" audio.write_bytes(b"audio") model.write_bytes(b"model") return MvpMeetingConfig( audio_file=audio, whisper_model=model, output_root=root / "runs", model="test:model", ) def test_programmatic_context_is_persisted_without_source_yaml(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) config = self.config(root) events = [] with ( patch.object(mvp_api, "transcribe_audio", side_effect=fake_transcribe), patch.object( mvp_api, "generate_direct_protocol", side_effect=fake_protocol ), patch.object(subprocess, "run") as subprocess_run, ): result = mvp_api.run_mvp_meeting( config, meeting_context=context_data(), progress_sink=events.append ) self.assertEqual(result.exit_code, 0) context_path = result.run_dir / "context/meeting_context.yaml" self.assertTrue(context_path.is_file()) persisted = load_meeting_context(context_path) self.assertEqual(persisted.meeting_id, "programmatic-test") self.assertEqual(persisted.speaker_mappings, {"SPEAKER_00": "person-1"}) subprocess_run.assert_not_called() self.assertEqual( [(event.stage, event.status) for event in events], [ ("preparing", "started"), ("preparing", "completed"), ("transcription", "started"), ("transcription", "completed"), ("protocol_generation", "started"), ("protocol_generation", "completed"), ("completed", "completed"), ], ) self.assertTrue(all(event.progress is None for event in events)) def test_failure_emits_terminal_failure_event(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) config = self.config(root) events = [] with patch.object( mvp_api, "transcribe_audio", side_effect=TranscriptionError("stopped"), ): result = mvp_api.run_mvp_meeting(config, progress_sink=events.append) self.assertEqual(result.exit_code, 2) self.assertEqual(events[-1].stage, "failed") self.assertEqual(events[-1].status, "failed") self.assertIn("transcription", events[-1].message) metadata = json.loads((result.run_dir / "run_metadata.json").read_text()) self.assertEqual(metadata["status"], "failed") def test_cli_defaults_and_wrapper_delegate_without_subprocess(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) config = self.config(root) args = cli.parse_args( [str(config.audio_file), "--whisper-model", str(config.whisper_model)] ) expected = MvpRunResult(0, root / "run", root / "run/protocol.md") with patch.object(cli, "run_mvp_meeting", return_value=expected) as api: actual = cli.run(args, context_override=context_data()) self.assertEqual(actual, (0, expected.run_dir, expected.protocol_path)) delegated = api.call_args.args[0] self.assertEqual(delegated.diarization, "off") self.assertEqual(delegated.language, "de") self.assertEqual(delegated.whisper_executable, "whisper-cli") self.assertEqual(api.call_args.kwargs["meeting_context"], context_data()) if __name__ == "__main__": unittest.main()