Model Catalogue -> Engines can only ever say "Engine unavailable. Check installation and configuration." and "Last error: A previous engine check failed." for an engine whose package is not importable. That is by design: public_backends() replaces reason and last_error because an availability probe can carry exception text, a local path or a credential, and two of the shipping is_available() implementations do interpolate an exception into their message. So the row cannot explain itself. docs/engines/<engine>.md can -- accurate, and for cosyvoice CI-guarded against the installer registry -- but nothing in frontend/src referenced docs/engines at all, so the point of failure was a dead end. #1746 is that dead end reaching the tracker. Add a registry-authored docs_url next to install_hint and setup_snippet. It is a VoiceStudio-owned constant keyed on the engine id, not probe output, so the public scrub leaves it intact by construction rather than by classification -- which is what keeps the security boundary where the maintainer put it. The row renders it with the same "Learn more" affordance MCPBindingsPanel and RemoteBackendPanel already use, so no new i18n key is needed. The health log line said "Engine health check failed; details withheld" and named neither the engine nor the kind of failure, while the response tells the user to check the backend log and docs/engines asks them to copy that engine's lines. Log the registry id and a stable exception class -- the same class= shape core.public_errors.public_failure() already logs. The diagnostic text stays out and the id is flattened to one token, so tests/test_response_safety.py's existing log-injection test passes unchanged. Not touched: publishing the computed reason itself. That is the product decision the reporter flagged, and the log line is deliberate, not an oversight -- test_engine_health_route_logs_but_does_not_return_private_ diagnostic pins it. Refs #1866 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Chang-Jin-Lee <ckdwls525@gmail.com>
88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""Every TTS engine points at a doc page, and that page exists (#1866).
|
|
|
|
The engine rows in Model Catalogue → Engines cannot explain an unavailable
|
|
engine themselves: `api.public_engine_metadata.public_backends` replaces the
|
|
computed `reason` and `last_error` with fixed strings before they reach the UI,
|
|
because an availability probe can carry exception text or a local path. The doc
|
|
pages under `docs/engines/` are what does explain it, and nothing in the app
|
|
linked to them, so the point of failure was a dead end.
|
|
|
|
`_ENGINE_DOCS` closes that, and this file keeps it honest. A registry entry
|
|
whose file was renamed, or a new engine added without one, would otherwise ship
|
|
a broken "Learn more" link — the kind of drift a mechanical test catches and a
|
|
reviewer does not. Same idea as tests/test_cosyvoice_install_docs.py, which
|
|
cross-checks a docs claim against the installer registry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def _registry():
|
|
from services import tts_backend
|
|
|
|
return tts_backend
|
|
|
|
|
|
def test_every_registered_engine_has_a_doc():
|
|
tb = _registry()
|
|
missing = sorted(set(tb._REGISTRY.keys()) - set(tb._ENGINE_DOCS))
|
|
assert missing == [], (
|
|
"engine(s) with no docs/engines page — add one and map it in "
|
|
f"_ENGINE_DOCS, or the row's Learn more link disappears: {missing}"
|
|
)
|
|
|
|
|
|
def test_no_doc_entry_outlives_its_engine():
|
|
tb = _registry()
|
|
orphans = sorted(set(tb._ENGINE_DOCS) - set(tb._REGISTRY.keys()))
|
|
assert orphans == [], f"_ENGINE_DOCS maps engine ids that no longer exist: {orphans}"
|
|
|
|
|
|
def test_every_mapped_doc_is_really_there():
|
|
tb = _registry()
|
|
broken = sorted(
|
|
f"{bid} -> {path}"
|
|
for bid, path in tb._ENGINE_DOCS.items()
|
|
if not os.path.isfile(os.path.join(_REPO_ROOT, path))
|
|
)
|
|
assert broken == [], f"_ENGINE_DOCS points at files that do not exist: {broken}"
|
|
|
|
|
|
def test_docs_url_is_built_from_the_project_link_constant():
|
|
tb = _registry()
|
|
from core import links
|
|
|
|
url = tb._engine_docs_url("cosyvoice")
|
|
assert url == f"{links.PROJECT_REPO_BLOB_MAIN}/docs/engines/cosyvoice.md"
|
|
# An id the registry does not carry gets no link rather than a 404 URL.
|
|
assert tb._engine_docs_url("not-an-engine") is None
|
|
|
|
|
|
def test_docs_url_survives_the_public_metadata_scrub():
|
|
"""The whole point: `reason` is replaced, `docs_url` is not.
|
|
|
|
A fix that put the doc link anywhere the scrub touches would ship a null
|
|
href to the one screen that needs it.
|
|
"""
|
|
from api.public_engine_metadata import public_backends
|
|
|
|
entry = {
|
|
"id": "cosyvoice",
|
|
"available": False,
|
|
"reason": "cosyvoice package not installed. Install from /home/alice/CosyVoice",
|
|
"last_error": "cosyvoice package not installed.",
|
|
"docs_url": "https://example.invalid/docs/engines/cosyvoice.md",
|
|
"install_hint": "git clone --recursive FunAudioLLM/CosyVoice",
|
|
}
|
|
(public,) = public_backends([entry])
|
|
|
|
assert public["reason"] == "Engine unavailable. Check installation and configuration."
|
|
assert public["last_error"] == "A previous engine check failed."
|
|
assert "/home/alice" not in public["reason"]
|
|
assert public["docs_url"] == entry["docs_url"]
|
|
assert public["install_hint"] == entry["install_hint"]
|