Files
VoiceStudio/backend/tests/test_no_module_stubs.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

45 lines
2.2 KiB
Python

"""Regression guard for the collection-time sys.modules stub class.
Seven modules in this directory used to install bare ``types.ModuleType``
stubs for ``core.config`` (and test_capture_ws.py for ``services.*``) at
module level. pytest imports every test module during *collection*, so the
stubs leaked process-wide before a single test ran: in any mixed invocation
(``pytest tests/... backend/tests/...``) later lazy imports resolved the
stub — tests/test_router_smoke.py's ``from main import app`` died with
``ImportError: cannot import name 'find_ffmpeg' from 'services.ffmpeg_utils'
(unknown location)`` and tests/test_longform_e2e.py's
``monkeypatch.setattr("core.config.OUTPUTS_DIR", ...)`` died with
``AttributeError: module 'core' has no attribute 'config'``.
This test runs after collection has imported every sibling module, so any
reintroduced module-level stub trips it even in a backend/tests-only run.
A stub is recognizable because a bare ModuleType has neither ``__file__``
(real module) nor ``__path__`` (namespace package).
If you need a fake module in a test, use ``monkeypatch.setitem(sys.modules,
name, fake)`` inside the test — pytest restores it. For hermetic data dirs,
rely on conftest.py's ``OMNIVOICE_DATA_DIR`` redirect instead of stubbing
``core.config``.
"""
import sys
# Backend packages whose identity later tests depend on. Top-level third-party
# modules are out of scope (some legitimately lack __file__, e.g. frozen ones).
_GUARDED_PREFIXES = ("core.", "api.", "services.", "schemas.", "utils.")
def test_no_collection_time_sys_modules_stubs():
offenders = []
for name, mod in list(sys.modules.items()):
if mod is None or not name.startswith(_GUARDED_PREFIXES):
continue
if getattr(mod, "__file__", None) is None and not hasattr(mod, "__path__"):
offenders.append(name)
assert not offenders, (
"Stub module(s) found in sys.modules: "
f"{offenders}. Some test module installed a bare ModuleType at import "
"time; that leaks process-wide from pytest collection and breaks "
"every later import of the real module in mixed runs. Use "
"monkeypatch.setitem(sys.modules, ...) inside the test instead."
)