Files
VoiceStudio/tests/test_models_dir_setting.py
T
debpalashandClaude Fable 5 933743e336 fix: resolve open issue batch — #1172 #1173 #1174 #1185 #1186 #1188
- validate managed binaries before exec; 0-byte GGUF placeholders fail
  actionably instead of "Exec format error" (#1172)
- KittenTTS: tokenizer-measured chunking to the ONNX 512-token cap;
  clear 400 for unspeakable input (#1173)
- clean SIGTERM during weight load: shutdown-aware loader, benign
  cancelled-load classification, lifespan hardening, scoped log
  silencers (transformers load + alembic fileConfig) (#1174)
- broken ASR deep-imports (lightning_fabric) mark the engine
  unavailable with a repair hint and fall through (#1185)
- uv cache + managed Python follow the chosen install drive on
  Windows (cherry-picked cross-drive class fix + spaces/D: tests) (#1186)
- adaptive silence-removal ladder for quiet clone references; localized
  actionable error for truly silent clips, all 21 locales (#1188)
- CHANGELOG: consolidated Unreleased into the quiet one-liner style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 04:29:33 +05:30

127 lines
5.4 KiB
Python

"""#64 — the configurable models-dir settings endpoints (validate + persist +
write the durable env that main.py reads at startup).
Single source of truth: the durable per-user env file (``OMNIVOICE_CACHE_DIR``).
``main.py`` reads it at launch; the GET endpoint reads it back. There is no
second store to diverge from.
"""
from __future__ import annotations
import os
import fastapi
import pytest
from core import user_env
from api.routers import settings as s
@pytest.fixture
def env(tmp_path, monkeypatch):
# Resolve the durable env file via a process-global override so it survives
# module re-import: some tests/backend/* tests stub `core.*` in sys.modules,
# which can give the endpoint's `core.user_env` and this test's a *different*
# module object — a setattr monkeypatch wouldn't reach the endpoint's copy.
envfile = str(tmp_path / "env")
monkeypatch.setenv("OMNIVOICE_ENV_FILE", envfile)
return envfile
def test_set_persists_and_writes_durable_env(env, tmp_path):
target = str(tmp_path / "models")
res = s.set_models_dir(s._ModelsDirBody(path=target))
abs_target = os.path.abspath(target)
assert res["configured"] == abs_target
assert res["restart_required"] is True
# main.py reads this on next launch; GET reads it back — single source:
assert user_env.get_user_env("OMNIVOICE_CACHE_DIR") == abs_target
assert s.get_models_dir()["configured"] == abs_target
assert os.path.isdir(target)
def test_rejects_unwritable_dir(env, monkeypatch, tmp_path):
# OS-neutral: force the mkdir to fail rather than relying on Unix-only
# /dev/null path semantics (cross-platform parity).
def boom(*a, **k):
raise OSError("read-only filesystem")
monkeypatch.setattr(os, "makedirs", boom)
with pytest.raises(fastapi.HTTPException) as ei:
s.set_models_dir(s._ModelsDirBody(path=str(tmp_path / "ro")))
assert ei.value.status_code == 400
def test_rejects_path_with_null_byte(env):
# An embedded NUL would otherwise blow up os.makedirs with a ValueError
# (→ 500). Validate up front and return a clean 400 instead.
with pytest.raises(fastapi.HTTPException) as ei:
s.set_models_dir(s._ModelsDirBody(path="/tmp/mo\x00dels"))
assert ei.value.status_code == 400
def test_clear_reverts_to_default(env):
user_env.set_user_env("OMNIVOICE_CACHE_DIR", "/old")
res = s.set_models_dir(s._ModelsDirBody(path=""))
assert res["configured"] is None
assert res["restart_required"] is True
assert user_env.get_user_env("OMNIVOICE_CACHE_DIR") is None
assert s.get_models_dir()["configured"] is None
def test_get_shape(env):
user_env.set_user_env("OMNIVOICE_CACHE_DIR", "/configured")
res = s.get_models_dir()
assert res["configured"] == "/configured"
assert "effective" in res and "default" in res
def test_path_with_spaces_survives_the_full_persistence_chain(env, tmp_path, monkeypatch):
"""#1186 class: the wizard/Settings dirs regularly contain spaces
('D:\\Program Data\\OmniVoice\\Model Cache'). The durable env file stores
the value as an UNQUOTED dotenv line and main.py re-reads it through
python-dotenv, so a writer/parser quoting regression would truncate at the
first space and silently redirect every model download while Settings
still shows the chosen folder. Pin the whole chain byte-for-byte:
endpoint → env file → load_into_environ → os.environ → GET."""
target = str(tmp_path / "Program Data" / "OmniVoice" / "Model Cache")
res = s.set_models_dir(s._ModelsDirBody(path=target))
abs_target = os.path.abspath(target)
assert res["configured"] == abs_target
assert user_env.get_user_env("OMNIVOICE_CACHE_DIR") == abs_target
# A launcher-injected (stale) value must lose to the durable file (#480),
# and the loaded value must be byte-identical — spaces intact.
monkeypatch.setenv("OMNIVOICE_CACHE_DIR", "/stale/launcher/value")
assert user_env.load_into_environ() is True
assert os.environ["OMNIVOICE_CACHE_DIR"] == abs_target
assert s.get_models_dir()["configured"] == abs_target
def test_windows_drive_paths_with_spaces_round_trip_verbatim(env):
"""#1186 class, non-system-drive half: 'D:\\…' paths with spaces (and '&')
must survive persist → read-back → the exact dotenv parse that
load_into_environ feeds os.environ from, with backslashes, the drive
prefix, and interior spaces untouched. (Path *usability* dropping is
platform-dependent and covered elsewhere; this pins the string layer that
is identical on every OS.)"""
import dotenv
cache = r"D:\Program Data\OmniVoice\Model Cache"
data = r"D:\Program Data\OmniVoice\Voice data & projects"
user_env.set_user_env("OMNIVOICE_CACHE_DIR", cache)
user_env.set_user_env("OMNIVOICE_DATA_DIR", data)
assert user_env.get_user_env("OMNIVOICE_CACHE_DIR") == cache
assert user_env.get_user_env("OMNIVOICE_DATA_DIR") == data
parsed = dotenv.dotenv_values(env)
assert parsed["OMNIVOICE_CACHE_DIR"] == cache
assert parsed["OMNIVOICE_DATA_DIR"] == data
def test_default_is_xdg_aware(env, monkeypatch, tmp_path):
# huggingface_hub's default cache root honors XDG_CACHE_HOME on Linux.
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "xdg"))
for var in ("HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE", "HF_HOME"):
monkeypatch.delenv(var, raising=False)
default = s.get_models_dir()["default"]
assert default == str(tmp_path / "xdg" / "huggingface")