Some checks failed
Release / Bump version and tag (push) Successful in 4s
Sidecar Release / Bump sidecar version and tag (push) Failing after 3s
Tests / Python Backend Tests (push) Failing after 3s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 3m10s
Test suite covering all three layers: Python backend (25 tests): - AppController: state machine, start/stop, callbacks, settings reload - API server: REST endpoints, config CRUD, status, devices - Config: dot-notation get/set, persistence, nested paths - Main headless: ready event port format validation Svelte frontend (14 tests via Vitest): - Backend store: exported properties/methods, port derivation, URLs - Config store: method names (fetchConfig not loadConfig), defaults - Transcriptions store: add/clear/plaintext - File extension regression: ensures $state runes only in .svelte.ts Rust sidecar (24 tests via cargo test): - Platform/arch detection, asset name construction - Ready event deserialization (with extra fields tolerance) - Path construction, version read/write, old version cleanup - Zip extraction, SidecarManager lifecycle CI workflow (.gitea/workflows/test.yml): - Runs on push to main and PRs - Three parallel jobs: Python, Frontend, Rust Also fixes three bugs found during test planning: - Settings: /api/check-updates -> GET /api/check-update - Settings: /api/remote/login -> /api/login - Settings: /api/remote/register -> /api/register Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
|