Files
VoiceStudio/tests/test_failure_classify.py
T
ff168048be fix(backend): import omnivoice from source when the editable install is missing (#564) (#573)
#564 ("No module named 'omnivoice'") is the backend failing to import its OWN
package at the first model call (the dub SSE error on dub:upload). `omnivoice`
is an editable install, so an interrupted/offline `uv sync` that installed deps
but never laid the editable record, an antivirus-quarantined
`_editable_impl_omnivoice.pth`, or an upgrade where only the lock-gated drift
sync ran leaves the venv able to start uvicorn yet unable to import omnivoice —
it boots fine and only fails at runtime, so the bootstrap health gate and the
exit-based broken-venv self-heal (which only see a process that won't start)
never catch it.

Fix the whole class at the import layer: main.py now also appends the project
root (the parent of backend/, where the desktop layout always copies
omnivoice/) to sys.path, guarded on omnivoice/__init__.py existing. The backend
then resolves omnivoice from source regardless of the editable-install state —
covering every variant above. Appended (not inserted) so a real
site-packages/editable install keeps precedence and it can't shadow a different
omnivoice; a no-op in Docker (no sibling omnivoice/) and a harmless duplicate
in a dev checkout.

Also routes "No module named 'omnivoice'" through failure.classify() →
BROKEN_VENV so, if it ever still surfaces, the toast points at Clean & Retry
instead of a bare import error. Regression test covers the classify mapping and
its negative guard (a legitimately-named omnivoice_* helper must not match).

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

81 lines
3.6 KiB
Python

"""ASR-robustness failure classification (#551 / #549).
The dub/transcribe "no segments" toast is only actionable if `classify()` names
the failure class so `build_failure()` can attach a hint. These assert the two
new taxonomy classes added for the ASR-robustness fix map to a non-empty hint.
"""
from core import failure
def test_classify_compute_type_unsupported():
# The exact CTranslate2 message on a GPU without efficient fp16 (#551).
reason = (
"Requested float16 compute type, but the target device or backend do "
"not support efficient float16 computation"
)
assert failure.classify(reason) == "COMPUTE_TYPE_UNSUPPORTED"
evt = failure.build_failure(reason, stage="transcribe", include_diagnostic=False)
assert evt["docs_topic"] == "COMPUTE_TYPE_UNSUPPORTED"
assert evt["hint"], "compute-type failure must carry an actionable hint"
def test_classify_transformers_import():
# The transformers ASR-pipeline import failure (#549).
assert failure.classify("Could not import module 'AutoFeatureExtractor'") == (
"TRANSFORMERS_IMPORT"
)
# Substring match on the bare class name too (case-insensitive).
assert failure.classify("AutoFeatureExtractor failed to load") == "TRANSFORMERS_IMPORT"
evt = failure.build_failure(
"Could not import module 'AutoFeatureExtractor'",
stage="transcribe",
include_diagnostic=False,
)
assert evt["hint"], "transformers-import failure must carry an actionable hint"
def test_classify_video_download_classes():
# #554: a non-downloadable link shape → actionable "paste a direct video URL".
assert failure.classify("Unsupported URL: https://www.douyin.com/discover") == (
"UNSUPPORTED_VIDEO_URL"
)
# #536: a transient mid-download drop → "just retry".
assert failure.classify("Unable to download video: [Errno 32] Broken pipe") == (
"VIDEO_DOWNLOAD_NETWORK"
)
assert failure.classify("Connection reset by peer") == "VIDEO_DOWNLOAD_NETWORK"
for cls, reason in (
("UNSUPPORTED_VIDEO_URL", "Unsupported URL: x"),
("VIDEO_DOWNLOAD_NETWORK", "Unable to download video: Broken pipe"),
):
evt = failure.build_failure(reason, stage="download", include_diagnostic=False)
assert evt["docs_topic"] == cls
assert evt["hint"], f"{cls} must carry an actionable hint"
def test_classify_broken_venv_encodings():
# The relocated/corrupted-venv stdlib-bootstrap failure → BROKEN_VENV (the
# Rust self-heal rebuilds it; this names the class for the toast).
assert failure.classify("ModuleNotFoundError: No module named 'encodings'") == (
"BROKEN_VENV"
)
# ...but an app-level import of an 'encodings'-prefixed package must NOT.
assert failure.classify("No module named 'encodings_helper'") == ""
def test_classify_broken_venv_missing_own_package():
# #564: the interpreter starts but the backend can't import its own
# 'omnivoice' package (editable install missing) → BROKEN_VENV so the toast
# points at the self-heal / Clean & Retry instead of a bare import error.
assert failure.classify("ModuleNotFoundError: No module named 'omnivoice'") == (
"BROKEN_VENV"
)
# ...but a legitimately-named 'omnivoice_*' helper package must NOT match
# (the trailing quote in the matcher is the guard).
assert failure.classify("No module named 'omnivoice_helper'") == ""
def test_classify_generic_still_empty():
# A genuinely unknown reason must still classify to "" (no false hint).
assert failure.classify("some totally unrelated failure") == ""