Files
VoiceStudio/tests/test_settings_updates_api.py
T
16294fed44 feat(updates): data-safe updates — pre-migration DB backups, guarded venv heal, release notes + changelog reader (#909)
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>
2026-07-02 23:52:17 +05:30

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