Files
VoiceStudio/tests/test_stream_error_class_1800.py
T
Palash DebnathandClaude Opus 5 4f4d21ff5b fix(errors): name the backend error class on an unclassified streaming failure
Closes #1800.

Every engine failure the taxonomy cannot classify renders one floor message,
"Generation failed. Check the selected engine and try again." The auto bug
reporter puts that message and a stack of minified bundle frames into the
issue, so unrelated faults arrive as byte-identical reports — roughly a dozen
of the open issues are that same report filed again, and none of them can be
told apart, let alone triaged.

The streaming error frame now carries the exception's TYPE NAME, the frontend
keeps it on StreamingPreviewError, and the report prints it as "Backend error
class: …". A MemoryError and a FileNotFoundError stop being the same issue.

Only the class name — no substring of the exception message is copied, so the
response-safety contract still holds and a test pins that a path in the
exception never reaches the payload. This is the same datum the dub routes
already put on the wire as error_class and the analytics allowlist already
treats as content-free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 12:06:40 -07:00

54 lines
2.3 KiB
Python

"""#1800 — an unclassified streaming failure must carry its exception class.
Every engine failure the taxonomy cannot classify renders the same floor
message, "Generation failed. Check the selected engine and try again." The
auto bug reporter puts that message and a stack of minified bundle frames into
the issue, so roughly a dozen separate faults arrived as byte-identical,
untriageable reports. The exception's TYPE NAME is what separates them.
It is the type name only. No substring of the exception message is copied, so
the response-safety contract in tests/test_response_safety.py still holds.
"""
import pytest
from core.public_errors import stream_failure, stream_generation_failure
def test_unclassified_failure_carries_its_exception_class():
payload = stream_generation_failure(RuntimeError("CUDA error: device-side assert"))
assert payload["code"] == "generation_failed"
assert payload["error_class"] == "RuntimeError"
def test_two_unrelated_failures_are_distinguishable():
# The whole point: these used to be the same report.
a = stream_generation_failure(RuntimeError("boom"))
b = stream_generation_failure(MemoryError("out of memory"))
assert a["detail"] == b["detail"]
assert a["error_class"] != b["error_class"]
def test_the_exception_message_is_never_copied():
secret = "C:/Users/someone/private-voice-sample.wav"
payload = stream_generation_failure(FileNotFoundError(secret))
assert payload["error_class"] == "FileNotFoundError"
for value in payload.values():
assert secret not in str(value)
assert "someone" not in str(value)
def test_a_non_exception_gets_no_class():
# dict(...) of the plain floor payload, unchanged — nothing to name.
payload = stream_generation_failure("not an exception")
assert "error_class" not in payload
assert payload["detail"] == stream_failure("generation_failed")["detail"]
@pytest.mark.parametrize("exc", [RuntimeError("x"), ValueError("y"), OSError("z")])
def test_class_is_present_alongside_a_classified_hint(exc):
# Enrichment and the class name are independent: adding one must not drop
# the other, whichever branch the taxonomy takes.
payload = stream_generation_failure(exc)
assert payload["error_class"] == type(exc).__name__
assert payload["detail"]