Files
VoiceStudio/tests/test_model_checkpoint_resolve.py
T
aa17e3319f fix(model): route every OMNIVOICE_MODEL read through the resolver; tighten WinError 193 match (#693, #705) (#707)
Follow-up from independent verification of #693/#705.

#693 (whole-class): the resolver only guarded the model-load site. A leaked
engine id in OMNIVOICE_MODEL still hit four other raw reads — most importantly
preload_model()'s model_info() probe, which failed on the bad value and
SILENTLY disabled warm-up (first /generate then ate the full load). Plus the
Settings 'model_checkpoint' display, the loaded-models list, and the engine_id
baked into exported persona bundles. Route all of them through
resolve_omnivoice_checkpoint() (personas keeps its '' unset marker, sanitizing
only a set value). Add a source-level recurrence guard so a future raw read
can't reintroduce the class.

#705: tighten 'winerror 193' -> '[winerror 193]' so the substring can't also
match WinError 1930-1939 (the portable 'is not a valid win32 application'
clause still covers non-Windows formatting).

48 tests pass (resolver + guard + audio-guard + route inventory); edited
routers/services import clean.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 03:37:46 +05:30

78 lines
2.9 KiB
Python

"""Regression tests for #693 — a leaked engine id ("omnivoice") in
OMNIVOICE_MODEL must not be passed to OmniVoice.from_pretrained() (which 500s
with "omnivoice is not a local folder and is not a valid model identifier").
The resolver self-heals: only a HF repo id (org/repo) or an existing local dir
is honored; anything else falls back to the default.
"""
import pytest
from services.model_manager import (
resolve_omnivoice_checkpoint,
_DEFAULT_OMNIVOICE_CHECKPOINT,
)
def _set(monkeypatch, val):
if val is None:
monkeypatch.delenv("OMNIVOICE_MODEL", raising=False)
else:
monkeypatch.setenv("OMNIVOICE_MODEL", val)
def test_default_when_unset(monkeypatch):
_set(monkeypatch, None)
assert resolve_omnivoice_checkpoint() == _DEFAULT_OMNIVOICE_CHECKPOINT
@pytest.mark.parametrize("leaked", ["omnivoice", "voxcpm2", "cosyvoice", "kittentts"])
def test_bare_engine_id_falls_back(monkeypatch, leaked):
"""#693: an engine id (no '/', not a path) must self-heal to the default."""
_set(monkeypatch, leaked)
assert resolve_omnivoice_checkpoint() == _DEFAULT_OMNIVOICE_CHECKPOINT
@pytest.mark.parametrize("repo", ["k2-fsa/OmniVoice", "some-org/some-model"])
def test_valid_hf_repo_id_is_kept(monkeypatch, repo):
_set(monkeypatch, repo)
assert resolve_omnivoice_checkpoint() == repo
def test_existing_local_dir_is_kept(monkeypatch, tmp_path):
d = tmp_path / "mymodel"
d.mkdir()
_set(monkeypatch, str(d))
assert resolve_omnivoice_checkpoint() == str(d)
def test_blank_or_whitespace_falls_back(monkeypatch):
_set(monkeypatch, " ")
assert resolve_omnivoice_checkpoint() == _DEFAULT_OMNIVOICE_CHECKPOINT
def test_omnivoice_model_is_only_read_through_the_resolver():
"""#693 recurrence guard: the raw OMNIVOICE_MODEL env var may only be read
inside resolve_omnivoice_checkpoint() (the resolver) and the personas export
guard (which routes the value through the resolver). Any other raw read
reintroduces the whole bug class — a bare engine id reaching a HF call, or a
leaked value mislabeling a UI field / exported persona bundle."""
import pathlib
backend = pathlib.Path(__file__).resolve().parents[1] / "backend"
allowed = {"services/model_manager.py", "api/routers/personas.py"}
offenders = []
for py in backend.rglob("*.py"):
if "__pycache__" in py.parts:
continue
rel = py.relative_to(backend).as_posix()
if rel in allowed:
continue
for i, line in enumerate(py.read_text(encoding="utf-8").splitlines(), 1):
# note the closing quote — excludes the unrelated OMNIVOICE_MODEL_LOAD_TIMEOUT
if "environ" in line and ('OMNIVOICE_MODEL"' in line or "OMNIVOICE_MODEL'" in line):
offenders.append(f"{rel}:{i}")
assert not offenders, (
"raw OMNIVOICE_MODEL reads outside the resolver (route them through "
"resolve_omnivoice_checkpoint()): " + ", ".join(offenders)
)