Add test suite (63 tests) and CI workflow, fix Settings API bugs
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
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>
This commit is contained in:
181
backend/tests/test_app_controller.py
Normal file
181
backend/tests/test_app_controller.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user