182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
|
|
"""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
|