Backend: - core/db_backup.py: WAL-safe SQLite snapshot to omnivoice.db.backup-<version>-<n> before pending alembic migrations run; keep newest 3, prune older; skip >500MB with a log line. Restore is never automatic. - core/db.py: _run_alembic_upgrade now plans the run (up_to_date / pending / unknown_revision), snapshots first when migrations will execute, and raises MigrationError on a mid-flight failure — startup stops with the backup path named instead of continuing on a half-migrated DB. The #552/#547 unknown-revision class stays non-fatal (warn + additive reconcile). - core/changelog.py + GET /api/settings/changelog: parse the shipped CHANGELOG.md (single-line and wrapped bullet styles) into structured releases. - GET /api/settings/db-backup: newest pre-migration backup for the panel. Rust (bootstrap.rs): - #314 heal guard: an exit-signature match alone can no longer delete the venv — venv_rebuild_justified requires a structural problem or a failed direct interpreter probe; a venv that probes healthy is kept and the real error surfaced. Drift/repair remains in-place `uv sync` (non-destructive). - CHANGELOG.md now ships as a bundle resource and is copied/refreshed into the project dir so the changelog endpoint works in packaged installs. Frontend (Settings → Updates): - Available update shows its actual release notes (updater metadata body) through a safe markdown-lite renderer (text nodes only, refs stay plain). - "Your data is backed up before every update" line with the latest backup timestamp from the new endpoint. - "What's new" changelog reader (accordion, newest expanded) over the shipped CHANGELOG.md; GitHub releases list reuses the same renderer. - One-time, non-blocking "What's new" footer pill after an update (persisted last-seen version; fresh installs baseline silently). - All strings via t() with en keys (other locales fall back to English). Tests: db backup/rotation/failure-path units, migration-safety units, changelog parser (both bullet styles + real CHANGELOG.md), endpoint tests, route inventory regenerated, Rust decision-logic + probe tests, vitest suites for renderer/viewer/panel/pill logic. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""Settings → Updates API surface: /api/settings/changelog + /db-backup.
|
|
|
|
Direct handler calls (house convention — same as test_llm_providers_router:
|
|
the loopback guard is router-level and not under test here).
|
|
"""
|
|
import importlib
|
|
import os
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("OMNIVOICE_MODEL", "test")
|
|
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
|
|
|
|
|
|
@pytest.fixture
|
|
def settings_mod():
|
|
return importlib.import_module("api.routers.settings")
|
|
|
|
|
|
def test_changelog_endpoint_returns_structured_releases(settings_mod, tmp_path, monkeypatch):
|
|
f = tmp_path / "CHANGELOG.md"
|
|
f.write_text(
|
|
"## [0.4.0] — 2027-01-01\n\nHeadline.\n\n### Added\n\n- **New.** Thing. (#1)\n\n"
|
|
"## [0.3.9] — 2026-07-02\n\n### Fixed\n\n- **Old.** Fix. (#2)\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("OMNIVOICE_CHANGELOG", str(f))
|
|
|
|
out = settings_mod.get_changelog(limit_versions=1)
|
|
assert out["available"] is True
|
|
assert len(out["releases"]) == 1
|
|
r = out["releases"][0]
|
|
assert r["version"] == "0.4.0"
|
|
assert r["date"] == "2027-01-01"
|
|
assert r["intro"] == "Headline."
|
|
assert r["sections"] == [{"title": "Added", "bullets": ["**New.** Thing. (#1)"]}]
|
|
|
|
|
|
def test_changelog_endpoint_degrades_when_missing(settings_mod, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("OMNIVOICE_CHANGELOG", str(tmp_path / "absent.md"))
|
|
out = settings_mod.get_changelog(limit_versions=5)
|
|
assert out == {"available": False, "releases": []}
|
|
|
|
|
|
def test_db_backup_state_none_then_latest(settings_mod, tmp_path, monkeypatch):
|
|
db = tmp_path / "omnivoice.db"
|
|
monkeypatch.setattr("core.config.DB_PATH", str(db))
|
|
|
|
out = settings_mod.get_db_backup_state()
|
|
assert out["available"] is False and out["latest"] is None and out["count"] == 0
|
|
|
|
conn = sqlite3.connect(str(db))
|
|
conn.execute("CREATE TABLE t (x)")
|
|
conn.commit()
|
|
conn.close()
|
|
from core import db_backup
|
|
|
|
made = db_backup.snapshot_before_migration(str(db), "0.3.9")
|
|
|
|
out = settings_mod.get_db_backup_state()
|
|
assert out["available"] is True
|
|
assert out["latest"]["path"] == made
|
|
assert out["latest"]["created_at"] > 0
|
|
assert out["count"] == 1
|
|
assert out["keep"] == db_backup.KEEP_BACKUPS
|