"""Tests for backend.app_controller.AppController.""" import sys from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Ensure project root is on path project_root = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(project_root)) from backend.app_controller import AppState # ── basic state ───────────────────────────────────────────────────── def test_initial_state(controller): """A freshly constructed controller should be INITIALIZING and not transcribing.""" assert controller.state == AppState.INITIALIZING assert controller.is_transcribing is False # ── start / stop ──────────────────────────────────────────────────── def test_start_transcription_without_engine(controller): """Starting transcription before the engine is ready should fail gracefully.""" controller.transcription_engine = None success, message = controller.start_transcription() assert success is False assert "not ready" in message.lower() def test_start_stop_cycle(controller): """Full start -> stop cycle with a mocked engine that reports ready.""" engine = MagicMock() engine.is_ready.return_value = True engine.start_recording.return_value = True controller.transcription_engine = engine # Start ok, msg = controller.start_transcription() assert ok is True assert controller.is_transcribing is True assert controller.state == AppState.TRANSCRIBING # Stop ok, msg = controller.stop_transcription() assert ok is True assert controller.is_transcribing is False engine.stop_recording.assert_called_once() def test_double_start_rejected(controller): """Calling start_transcription twice should reject the second call.""" engine = MagicMock() engine.is_ready.return_value = True engine.start_recording.return_value = True controller.transcription_engine = engine controller.start_transcription() success, message = controller.start_transcription() assert success is False assert "already" in message.lower() # ── transcription storage ─────────────────────────────────────────── def test_clear_transcriptions(controller): """clear_transcriptions should empty the list and return the count.""" from client.transcription_engine_realtime import TranscriptionResult controller.transcriptions = [ TranscriptionResult(text="Hello", is_final=True, timestamp=datetime.now(), user_name="Alice"), TranscriptionResult(text="World", is_final=True, timestamp=datetime.now(), user_name="Bob"), ] count = controller.clear_transcriptions() assert count == 2 assert len(controller.transcriptions) == 0 def test_get_transcriptions_text_with_timestamps(controller): """get_transcriptions_text should include [HH:MM:SS] prefixes when requested.""" from client.transcription_engine_realtime import TranscriptionResult ts = datetime(2025, 1, 15, 10, 30, 45) controller.transcriptions = [ TranscriptionResult(text="Test line", is_final=True, timestamp=ts, user_name="User"), ] text = controller.get_transcriptions_text(include_timestamps=True) assert "[10:30:45]" in text assert "User:" in text assert "Test line" in text # ── settings / engine reload ──────────────────────────────────────── def test_apply_settings_triggers_reload_on_model_change(controller): """Changing the transcription model should trigger an engine reload.""" controller.current_model_size = "base.en" controller.current_device_config = "auto" # Patch reload_engine so it doesn't actually try to spin up threads controller.reload_engine = MagicMock(return_value=(True, "reloaded")) reloaded, msg = controller.apply_settings({ "transcription.model": "small.en", }) assert reloaded is True controller.reload_engine.assert_called_once() def test_apply_settings_no_reload_when_same(controller): """If model and device haven't changed, no reload should happen.""" controller.current_model_size = "base.en" controller.current_device_config = "auto" # Ensure config returns the same values controller.config.set("transcription.model", "base.en") controller.config.set("transcription.device", "auto") controller.reload_engine = MagicMock(return_value=(True, "reloaded")) reloaded, msg = controller.apply_settings({ "display.font_size": 20, }) assert reloaded is False controller.reload_engine.assert_not_called() # ── transcription callbacks ───────────────────────────────────────── def test_on_final_transcription_callback_fires(controller): """_on_final_transcription should append and invoke on_transcription callback.""" from client.transcription_engine_realtime import TranscriptionResult received = [] controller.on_transcription = lambda data: received.append(data) controller.is_transcribing = True controller._set_state(AppState.TRANSCRIBING) result = TranscriptionResult( text="Hello world", is_final=True, timestamp=datetime.now(), user_name="Tester", ) controller._on_final_transcription(result) assert len(controller.transcriptions) == 1 assert len(received) == 1 assert received[0]["text"] == "Hello world" assert received[0]["user_name"] == "Tester" assert received[0]["is_preview"] is False def test_on_final_transcription_ignored_when_not_transcribing(controller): """If the controller is not in transcribing state the callback should be a no-op.""" from client.transcription_engine_realtime import TranscriptionResult controller.is_transcribing = False result = TranscriptionResult( text="Should be ignored", is_final=True, timestamp=datetime.now(), user_name="Ghost", ) controller._on_final_transcription(result) assert len(controller.transcriptions) == 0