Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
"""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."""
|
2026-04-07 16:57:43 -07:00
|
|
|
from client.models import TranscriptionResult
|
Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
|
|
|
|
|
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."""
|
2026-04-07 16:57:43 -07:00
|
|
|
from client.models import TranscriptionResult
|
Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
|
|
|
|
|
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")
|
2026-04-10 12:01:11 -07:00
|
|
|
# Remote mode must also match (no engine means current mode is 'local')
|
|
|
|
|
controller.config.set("remote.mode", "local")
|
Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
|
|
|
|
|
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."""
|
2026-04-07 16:57:43 -07:00
|
|
|
from client.models import TranscriptionResult
|
Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
|
|
|
|
|
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."""
|
2026-04-07 16:57:43 -07:00
|
|
|
from client.models import TranscriptionResult
|
Add test suite (63 tests) and CI workflow, fix Settings API bugs
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>
2026-04-07 07:48:34 -07:00
|
|
|
|
|
|
|
|
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
|