Files
VoiceStudio/backend/tests
Palash DebnathandClaude Opus 5 8da377a1cb fix(audiobook): say why a chapter failed, and don't hang when an engine stops (#1321) (#1325)
The report arrived as a raw traceback pasted out of a log file — because that is the only place the reason existed. A failed chapter was a red row and the word "failed"; the SSE event carried the literal string "chapter failed to render", the symptom the user could already see.

- Both the per-chapter and the terminal all-failed events are now built with the shared `core.failure` builder: sanitized text, a guaranteed non-empty reason, error class, docs deeplink and hint. `error` keeps mirroring `reason`, so older frontends and the Stories exporter are unaffected. The chapter list shows the reason inline, full text in the row tooltip.
- **A silent infinite hang on the same path.** asyncio refuses to put `StopIteration` into a Future — `_copy_future_state` raises TypeError inside the event loop's own callback, so the `run_in_executor` future is never completed and the caller waits forever, with no error, event or timeout. Reachable from ordinary input: VoxCPM's `next_and_close` is a bare `next(gen)`, so a generator ending without yielding raises it straight into a GPU-pool worker. Fixed at the pool boundary (`_ResilientGpuPool.submit` plus a matching `_cpu_pool` subclass) as `WorkerStopIteration(RuntimeError)`, keeping the original as `__cause__`.
- An all-failed render now marks the job failed. It previously returned without touching job history, so the row stayed `running` and the next startup's orphan sweep read a hopeless render as interrupted and offered it as resumable (Greptile P1).

Tests: 6 pool-guard tests (two hang without the fix, bounded so a hang fails rather than stalls CI), longform e2e extended for both event shapes, the empty-`str(exc)` floor, and both sides of the job-status branch, plus the chapter-list component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:22:50 -07:00
..