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.api_server.APIServer REST endpoints."""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import pytest_asyncio
|
|
|
|
|
|
|
|
|
|
# Ensure project root is on path
|
|
|
|
|
project_root = Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/status ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_status(api_client):
|
|
|
|
|
resp = await api_client.get("/api/status")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert "state" in data
|
|
|
|
|
assert "is_transcribing" in data
|
|
|
|
|
assert "version" in data
|
|
|
|
|
assert "web_server" in data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/config ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_config(api_client):
|
|
|
|
|
resp = await api_client.get("/api/config")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
# The config should be a dict (the raw config mapping)
|
|
|
|
|
assert isinstance(data, dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── PUT /api/config ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_put_config(api_client, controller):
|
|
|
|
|
"""Updating config via PUT should persist and return success."""
|
|
|
|
|
# Patch reload_engine to avoid heavy lifting
|
|
|
|
|
controller.reload_engine = MagicMock(return_value=(True, "ok"))
|
|
|
|
|
controller.current_model_size = "base.en"
|
|
|
|
|
controller.current_device_config = "auto"
|
|
|
|
|
controller.config.set("transcription.model", "base.en")
|
|
|
|
|
controller.config.set("transcription.device", "auto")
|
|
|
|
|
|
|
|
|
|
resp = await api_client.put(
|
|
|
|
|
"/api/config",
|
|
|
|
|
json={"settings": {"display.font_size": 24}},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
body = resp.json()
|
|
|
|
|
assert body["status"] == "ok"
|
|
|
|
|
|
|
|
|
|
# Verify the value was actually saved
|
|
|
|
|
assert controller.config.get("display.font_size") == 24
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── POST /api/start (engine not ready) ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_start_when_not_ready(api_client, controller):
|
|
|
|
|
"""Starting transcription without an engine should return 400."""
|
|
|
|
|
controller.transcription_engine = None
|
|
|
|
|
resp = await api_client.post("/api/start")
|
|
|
|
|
assert resp.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── POST /api/clear ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_clear(api_client, controller):
|
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
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
controller.transcriptions = [
|
|
|
|
|
TranscriptionResult(text="One", is_final=True, timestamp=datetime.now(), user_name="U"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
resp = await api_client.post("/api/clear")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
body = resp.json()
|
|
|
|
|
assert body["cleared"] == 1
|
|
|
|
|
assert len(controller.transcriptions) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/audio-devices ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_audio_devices(api_client, controller):
|
|
|
|
|
"""Audio devices endpoint should return a list, even when mocked."""
|
|
|
|
|
# Mock sounddevice so the test works without audio hardware
|
|
|
|
|
with patch("backend.app_controller.AppController.get_audio_devices",
|
|
|
|
|
return_value=[{"index": 0, "name": "Mock Mic"}]):
|
|
|
|
|
resp = await api_client.get("/api/audio-devices")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert "devices" in data
|
|
|
|
|
assert len(data["devices"]) >= 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/compute-devices ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_get_compute_devices(api_client, controller):
|
|
|
|
|
resp = await api_client.get("/api/compute-devices")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert "devices" in data
|
|
|
|
|
# At minimum we get the "Auto-detect" entry
|
|
|
|
|
assert any(d["id"] == "auto" for d in data["devices"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/check-update ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_check_update(api_client, controller):
|
|
|
|
|
"""check-update should return a dict with an 'available' key."""
|
|
|
|
|
with patch.object(controller, "check_for_updates",
|
|
|
|
|
return_value={"available": False, "current_version": "1.0.0"}):
|
|
|
|
|
resp = await api_client.get("/api/check-update")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert "available" in data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET /api/version ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_version(api_client):
|
|
|
|
|
resp = await api_client.get("/api/version")
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.json()
|
|
|
|
|
assert "version" in data
|
|
|
|
|
# Should be a non-empty string
|
|
|
|
|
assert isinstance(data["version"], str)
|
|
|
|
|
assert len(data["version"]) > 0
|