Files
VoiceStudio/backend/tests/conftest.py
T
3f62471bad test: pay down export-router test debt + kill two test-order pollution classes (#1081)
Four pieces of test debt, root-caused and hardened:

1. exports.py test coverage (was: zero dedicated tests): new
   tests/test_exports_api.py (26 tests) covering /export, /export/record,
   /export/history, /export/reveal — happy paths, traversal/containment
   guards (incl. symlink escape), destination validation, error mapping,
   and the mp4 watermark-overlay branch with its plain-copy fallback.
   Two real bugs found and fixed in the router:
   - _safe_destination checked isabs() on realpath()'s output, which is
     always absolute — dead check; a relative destination silently exported
     to a cwd-dependent location instead of the documented 400.
   - _safe_source let "." / ".." through the basename guard (caught only
     later by realpath containment as a confusing 404); now 400 up front.

2. CI-Linux fp16 default-dtype leak (test_prefers_vocals_over_mix,
   test_final_dub_track_and_seg_wav_are_watermarked): not reproducible on
   macOS — instrumenting torch.set_default_dtype across both tests records
   zero non-fp32 sets locally. Both tests now carry an opt-in
   torch_dtype_isolation fixture (save/restore, so the leak can never
   spread), and the conftest guard is demoted to pure insurance. A cheap
   permanent recorder wraps torch.set_default_dtype /
   set_default_tensor_type once torch appears and captures the setter's
   stack only on a non-fp32 set; both fixtures print that stack when they
   fire, so the next CI occurrence names the exact culprit call chain.

3. Test-order pollution (both reported combos): root cause was
   collection-time sys.modules stubbing in backend/tests — seven modules
   installed bare ModuleType stubs for core.config (and test_capture_ws.py
   for services.model_manager/asr_backend/ffmpeg_utils, now all lazily
   imported by the router anyway). pytest imports test modules during
   collection, so the stubs leaked process-wide before any test ran:
   - combo (a): monkeypatch.setattr("core.config.OUTPUTS_DIR", ...) in
     test_longform_e2e died with AttributeError (core never gets a .config
     attribute when the import is satisfied straight from sys.modules).
   - combo (b): test_router_smoke's `from main import app` died with
     ImportError: cannot import name 'find_ffmpeg' (unknown location).
   Fix at source: new backend/tests/conftest.py sets a hermetic
   OMNIVOICE_DATA_DIR (mirroring tests/conftest.py, #878) and the real
   core.config is imported everywhere — zero sys.modules surgery. New
   backend/tests/test_no_module_stubs.py guards the whole class (verified
   fail-before/pass-after against the old stub). Stale rationale comments
   in pyproject.toml and ci.yml updated to match.

4. batched_tts.py TODO(#312): investigated, comment corrected only —
   #312 is closed (the live routes are engine-aware); this module has zero
   call sites and stays an unintegrated experiment. See PR notes.

Full tests/ suite: 2796 passed. backend/tests standalone: 130 passed.
Both pollution combos re-run green in the reported orderings.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:10:35 +05:30

42 lines
2.0 KiB
Python

"""Shared setup for backend/tests — import path + hermetic data dir.
Historically every module in this directory stubbed
``sys.modules["core.config"]`` with a bare 3-4 attribute ``ModuleType``
pointing at its own ``mkdtemp``. That stub leaked **process-wide at
collection time**: pytest imports test modules while collecting, so in any
mixed invocation (``pytest tests/... backend/tests/...``) every *later* lazy
import of ``core.config`` resolved the stub instead of the real module —
``tests/test_router_smoke.py``'s ``from main import app`` died with
ImportError (missing config attrs), and
``monkeypatch.setattr("core.config.X", ...)`` died with AttributeError
(``core`` never gets a ``config`` attribute when the name is satisfied
straight from ``sys.modules``). That was the root cause of the
order-pollution combos around test_longform_e2e (8 AttributeErrors) and
test_router_smoke (24 fixture ImportErrors).
The real ``core.config`` derives every path from ``OMNIVOICE_DATA_DIR`` at
import time, so pointing that env var at a throwaway dir *before* any test
module imports it gives the same hermeticity (issue #878: never touch the
developer's real app state) with zero ``sys.modules`` surgery. This mirrors
``tests/conftest.py``; in a mixed run whichever conftest loads first wins
(``setdefault`` semantics) and both point at a throwaway tmpdir.
Do NOT reintroduce module-level ``sys.modules`` stubs in this directory —
import the real module and rely on this conftest instead.
"""
import os
import sys
import tempfile
# Backend runs with `--app-dir backend`, so tests must do the same.
_BACKEND = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if _BACKEND not in sys.path:
sys.path.insert(0, _BACKEND)
if not os.environ.get("OMNIVOICE_DATA_DIR"):
os.environ["OMNIVOICE_DATA_DIR"] = tempfile.mkdtemp(prefix="omnivoice-test-data-")
if not os.environ.get("OMNIVOICE_ENV_FILE"):
os.environ["OMNIVOICE_ENV_FILE"] = os.path.join(
os.environ["OMNIVOICE_DATA_DIR"], "user-env"
)