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>
49 lines
2.2 KiB
Python
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
|