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

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:
Developer
2026-04-07 07:48:34 -07:00
parent 9d78fce3f0
commit 5a674ed199
19 changed files with 2372 additions and 10 deletions

View 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