Files
VoiceStudio/tests/test_routing_redaction.py
b825d99337 fix(engines): revive dead license dialog, refresh matrix on select, surface routing verdict (#905)
Six live-audit fixes for the Engines settings surface:

- P1-A: the Supertonic license dialog was dead since #101 — `useState`
  threw away the state value (`const [, setLicenseDialogFor]`) and the
  imported dialog was never mounted, so "Accept license" did nothing.
  Keep the value and render LICENSE_DIALOGS[selected] with open/onClose/
  onAccepted (accept → matrix reload).
- P1-B: the matrix went stale after "Use" — active badge, Use buttons and
  family-tab captions stayed old until a manual Refresh. Await onSelect,
  then reload() so the picked engine reflects immediately.
- P2-A: consume the /engines/select routing echo. A `cpu_fallback` pick now
  shows a warn-tone toast naming the reason ("running on CPU — …"); the
  plain success toast stays for accelerated/cpu_only. Shared helper used by
  both Settings→Engines and the first-run WizardLibrary.
- P2-B: a CPU-native engine (gpu_compat == ("cpu",)) has nothing to fall
  back FROM, yet on a GPU/MPS host it was mis-classed cpu_fallback (warn).
  New routing rule classifies ("cpu",) as cpu_only (neutral) on any
  accelerator host; multi-target engines that could accelerate elsewhere
  are untouched.
- P3-A: the routing reason was only a badge `title` (unreachable on
  keyboard/touch) — surface it as small visible text under the badge.
- P3-B: an in-process "Test engine" pass is an import/liveness check, not a
  synthesis test — label it "deps OK" instead of a misleading "0 ms"
  latency; subprocess rows keep their real ping latency.

Adds RTL + unit regression tests for all six and updates the routing unit
tests to the corrected cpu-native intent. i18n keys added to en.json.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:22:49 +05:30

49 lines
2.2 KiB
Python

"""Token/path-safety contract for routing reasons (GPU compat matrix, PR 1).
``resolve_routing`` returns raw author strings, but any reason that interpolates
a ``device_name`` or probe note can carry a home path — so the serialization
layer scrubs it with ``core.scrub.scrub_text``. These tests pin that a reason
built from a home-path-bearing note comes out clean after scrubbing, and that a
``None`` reason must NOT be passed through ``scrub_text`` (which would turn it
into ``""`` instead of JSON ``null``).
"""
from __future__ import annotations
from core.device_caps import KERNEL_RISK_MARKER, HostCaps
from core.scrub import scrub_text
from services.engine_routing import resolve_routing
def _caps(family, *, notes=()):
avail = (family, "cpu") if family != "cpu" else ("cpu",)
return HostCaps(family=family, available_families=avail, notes=tuple(notes))
def test_home_path_in_caveat_reason_is_scrubbed():
note = (f"/home/alice/torch GPU (sm_120) not in this build's archs "
f"— {KERNEL_RISK_MARKER}")
r = resolve_routing(("cuda", "cpu"), _caps("cuda", notes=[note]))
raw = r["routing_reason"]
assert "/home/alice" in raw # pre-scrub carries the path
clean = scrub_text(raw)
assert "/home/alice" not in clean # post-scrub it is gone
assert "~" in clean
def test_none_reason_must_not_become_empty_string():
r = resolve_routing(("cuda", "cpu"), _caps("cuda"))
assert r["routing_reason"] is None
# The serialization rule the wiring must follow: scrub only when truthy,
# else preserve JSON null. scrub_text(None) would wrongly yield "".
serialized = scrub_text(r["routing_reason"]) if r["routing_reason"] else None
assert serialized is None
assert scrub_text(None) == "" # documents why the guard is needed
def test_fallback_reason_scrubs_clean_and_nonempty():
# A genuine fallback: multi-target engine lacking the host accel. (A
# cpu-native ("cpu",) engine is neutral cpu_only with a None reason now.)
r = resolve_routing(("mps", "cpu"), _caps("cuda"))
clean = scrub_text(r["routing_reason"])
assert clean and "***REDACTED***" not in clean # no secret to redact here