* docs(install): per-OS install pages + drift validator + CI gate
Splits the 600-line README install section into self-contained per-OS docs
under docs/install/{macos,windows,linux,docker}.md plus a Top-10
troubleshooting index. Each OS doc is end-to-end: a user opens it and
reaches a working app following only commands inside that file.
Adds:
- docs/install/{macos,windows,linux,docker}.md (OS-specific install paths)
- docs/install/troubleshooting.md (top 10 install errors)
- docs/engines/cosyvoice.md (closes #55 docs half)
- docs/features/diarization.md (pyannote license flow)
- docs/setup/huggingface-token.md (3-source cascade guide)
- scripts/validate-install-docs.py (INST-06 docs-drift gate)
- tests/scripts/test_validate_install_docs.py (B-5: validator self-tests)
- .github/workflows/ci.yml step running the validator on every PR
Implements INST-02 (README routing), INST-03 (macOS Gatekeeper anchor),
INST-12 docs half (Windows torch-compile-oom anchor), DOCS-01..05.
The validator is a one-way diff: every `<!-- validate -->`-tagged line
in docs must appear in scripts/desktop-prod.sh after normalisation
(prompt-prefix strip, CRLF, trailing whitespace, blank-and-comment skip).
A `<!-- validate: skip -->` marker opts out for human-readability blocks.
Its own 10 unit tests catch regressions in the gate itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(deeplinks): links.py + error_docs_map (Python + TS mirror)
Adds the single source of truth for the project repo URL and the 4-class
error → docs taxonomy that both the in-app ErrorBoundary deeplink button
(Wave 2 Task 3) and the Phase 5 bug reporter will consume.
New:
- backend/core/links.py — PROJECT_REPO_URL + BLOB_MAIN resolver
(Tauri config first, pyproject fallback)
- backend/core/error_docs_map.py — lookup(error_class) → docs URL
- frontend/src/utils/errorDocsMap.ts (TS mirror with classifyError helper)
- tests/backend/core/test_links.py + test_error_docs_map.py
- frontend/src/utils/errorDocsMap.test.ts
Resolves checker B-6 (links.py ownership) and Open Question #3 (which fork
the deeplinks resolve to — the Tauri updater endpoint wins, which points
at the desktop app fork debpalash/OmniVoice-Studio).
The TS BASE constant is documented as the second hardcoded URL drift site;
the keys-sync test (`test_keys_match_python_map` equivalent) guards the
4-class taxonomy contract between Python + TS halves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ui): Settings → API Keys panel + ErrorBoundary docs deeplink
Wave 2 AUTH-03 UI half + ErrorBoundary deeplink wiring.
ErrorBoundary fallback now renders an "Open docs for this error" button
that classifies the thrown Error message (heuristic: pkg_resources → 401 /
HfHubHTTP → WebKit / white screen → quarantine / Gatekeeper) and opens the
matching docs anchor via Tauri shell.open (with a window.open fallback
in browser dev mode).
ApiKeysPanel consumes the Wave 1 resolver state endpoint:
- 3 source rows (App / Env var / HF CLI) with set/unset indicator,
masked token preview, whoami username + green check
- "Active" badge on whichever source is currently serving the cascade
- App-row only: Save (POST /api/settings/hf-token) +
Clear (DELETE with optional "also clear HF CLI" confirm dialog)
- "Test now" button refetches state (invalidates the resolver's
validation cache via the same endpoint hit)
Panel mounted in the existing Settings → Credentials tab; the legacy
HF_TOKEN row from CREDENTIAL_FIELDS is filtered out so the two paths
don't fight over the same key.
Threat T-02-02: the panel never displays the full token. The masked
value comes from the resolver state endpoint; the full token only
crosses the IPC boundary on Save (POST) and is cleared from local
state on success.
Closes AUTH-03 fully (Wave 1 backend + this Wave 2 UI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(perf): INST-12 Disable torch.compile (Windows) toggle (backend + UI)
Wave 2 Task 4 — full INST-12 delivery per checker B-2/B-7 v0.3.0 fat-release
decision. Both the docs half (windows.md anchor, shipped in earlier commit)
and the runtime toggle are now in Phase 1.
Backend:
- backend/services/settings_store.py: adds get_text/set_text helpers for
non-secret config (refuses to write to the encrypted hf_token key).
- backend/api/routers/settings.py: GET + PUT
/api/settings/perf/torch-compile-disabled, both under the existing
loopback guard (threat T-02-04).
- backend/services/engine_env.py: new `build_engine_env()` helper that
centralises HF_TOKEN/YOUR_HF_TOKEN injection from the 3-source resolver
AND injects TORCH_COMPILE_DISABLE=1 when the flag is set on win32.
Phase 2 SubprocessBackend launchers should adopt the same helper.
- backend/services/sonitranslate.py: migrated to engine_env.build_engine_env()
while preserving the source-level `env["HF_TOKEN"]` sentinel that
test_sonitranslate_module_uses_resolver checks.
Frontend:
- frontend/src/components/settings/PerformancePanel.{jsx,css,test.jsx}:
toggle UI with the explainer for #65; renders disabled with a "not
applicable" badge on macOS/Linux.
- frontend/src/pages/Settings.jsx: mounts the panel into the Credentials
tab alongside the API Keys panel.
Tests:
- tests/backend/test_perf_settings.py: 7 backend tests (default state,
PUT persistence, T-02-04 non-loopback rejection, settings_store round-
trip, env injection on win32, NO injection on macOS/Linux, NO injection
when disabled).
- frontend PerformancePanel.test.jsx: 5 tests (renders from GET state,
PUT on toggle, disabled on non-Windows platforms, pre-enabled state).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(planning): Wave 2 SUMMARY + REQUIREMENTS status updates
- .planning/phases/01.../01-02-SUMMARY.md: full implementation report
per template (truths, commits, tests, deviations, drift-site
acknowledgments per W-3, launcher seam name for Phase 2,
taxonomy keys for Phase 5).
- .planning/REQUIREMENTS.md: flips Wave 2 closures to Done:
AUTH-03, INST-02, INST-03 (docs half), INST-06, INST-12,
DOCS-01..05.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
"""Tests for backend/api/routers/settings.py perf endpoints + engine_env helper.
|
|
|
|
Covers INST-12 (Disable torch.compile Windows toggle):
|
|
- GET /api/settings/perf/torch-compile-disabled returns the default + platform.
|
|
- PUT round-trips through the settings_store (persisted as text "1"/"0").
|
|
- PUT from a non-loopback origin is rejected with 403 (threat T-02-04).
|
|
- The engine_env helper injects TORCH_COMPILE_DISABLE=1 only on win32 when
|
|
the flag is set; on macOS/Linux the var is never injected.
|
|
- When the flag is unset/false, the var is never injected.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def fresh_app(monkeypatch, tmp_path):
|
|
"""Same isolation pattern as tests/backend/test_engine_spawn_token.py —
|
|
new tmp DB + a fresh settings router instance per test."""
|
|
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
|
monkeypatch.delenv("HF_TOKEN", raising=False)
|
|
monkeypatch.delenv("HUGGING_FACE_HUB_TOKEN", raising=False)
|
|
|
|
for mod in list(sys.modules):
|
|
if (
|
|
mod == "core" or mod.startswith("core.")
|
|
or mod == "services" or mod.startswith("services.")
|
|
or mod == "api" or mod.startswith("api.")
|
|
):
|
|
del sys.modules[mod]
|
|
|
|
from core import db as _db
|
|
_db.init_db()
|
|
|
|
from fastapi import FastAPI
|
|
from api.routers import settings as settings_router
|
|
|
|
app = FastAPI()
|
|
app.include_router(settings_router.router)
|
|
return app
|
|
|
|
|
|
def _client(app):
|
|
from fastapi.testclient import TestClient
|
|
return TestClient(app, client=("127.0.0.1", 12345))
|
|
|
|
|
|
def test_get_default_state(fresh_app, monkeypatch):
|
|
"""Fresh DB → enabled=False, platform=<runtime>."""
|
|
import huggingface_hub
|
|
monkeypatch.setattr(huggingface_hub, "get_token", lambda: None)
|
|
c = _client(fresh_app)
|
|
r = c.get("/api/settings/perf/torch-compile-disabled")
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["enabled"] is False
|
|
assert body["platform"] in {"darwin", "linux", "win32"}
|
|
|
|
|
|
def test_put_enabled_true_persists(fresh_app, monkeypatch):
|
|
import huggingface_hub
|
|
monkeypatch.setattr(huggingface_hub, "get_token", lambda: None)
|
|
c = _client(fresh_app)
|
|
r = c.put(
|
|
"/api/settings/perf/torch-compile-disabled",
|
|
json={"enabled": True},
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["enabled"] is True
|
|
r2 = c.get("/api/settings/perf/torch-compile-disabled")
|
|
assert r2.json()["enabled"] is True
|
|
|
|
|
|
def test_put_non_loopback_rejected(fresh_app):
|
|
"""T-02-04: a PUT from a non-loopback origin is 403."""
|
|
from fastapi.testclient import TestClient
|
|
with TestClient(fresh_app, client=("10.0.0.5", 12345)) as c:
|
|
r = c.put(
|
|
"/api/settings/perf/torch-compile-disabled",
|
|
json={"enabled": True},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_value_round_trips_via_settings_store(fresh_app, monkeypatch):
|
|
import huggingface_hub
|
|
monkeypatch.setattr(huggingface_hub, "get_token", lambda: None)
|
|
c = _client(fresh_app)
|
|
c.put("/api/settings/perf/torch-compile-disabled", json={"enabled": True})
|
|
from services import settings_store
|
|
assert settings_store.get_text("perf.torch_compile_disabled") == "1"
|
|
c.put("/api/settings/perf/torch-compile-disabled", json={"enabled": False})
|
|
assert settings_store.get_text("perf.torch_compile_disabled") == "0"
|
|
|
|
|
|
def test_env_injection_when_enabled_on_windows(monkeypatch, tmp_path):
|
|
"""build_engine_env injects TORCH_COMPILE_DISABLE=1 on win32 + flag true."""
|
|
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
|
for mod in list(sys.modules):
|
|
if mod == "core" or mod.startswith("core.") or mod == "services" or mod.startswith("services."):
|
|
del sys.modules[mod]
|
|
from core import db as _db
|
|
_db.init_db()
|
|
|
|
from services import settings_store, engine_env
|
|
settings_store.set_text("perf.torch_compile_disabled", "1")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
# token resolver must not blow up — monkeypatch.resolve to return None
|
|
from services import token_resolver
|
|
monkeypatch.setattr(token_resolver, "resolve", lambda **kw: None)
|
|
|
|
env = engine_env.build_engine_env(base_env={})
|
|
assert env.get("TORCH_COMPILE_DISABLE") == "1"
|
|
|
|
|
|
def test_no_env_injection_on_non_windows(monkeypatch, tmp_path):
|
|
"""On macOS/Linux the var is never injected, even when the flag is set."""
|
|
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
|
for mod in list(sys.modules):
|
|
if mod == "core" or mod.startswith("core.") or mod == "services" or mod.startswith("services."):
|
|
del sys.modules[mod]
|
|
from core import db as _db
|
|
_db.init_db()
|
|
|
|
from services import settings_store, engine_env, token_resolver
|
|
settings_store.set_text("perf.torch_compile_disabled", "1")
|
|
monkeypatch.setattr(token_resolver, "resolve", lambda **kw: None)
|
|
|
|
monkeypatch.setattr(sys, "platform", "darwin")
|
|
env = engine_env.build_engine_env(base_env={})
|
|
assert "TORCH_COMPILE_DISABLE" not in env
|
|
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
env = engine_env.build_engine_env(base_env={})
|
|
assert "TORCH_COMPILE_DISABLE" not in env
|
|
|
|
|
|
def test_no_env_injection_when_disabled(monkeypatch, tmp_path):
|
|
"""Flag false on win32 → var not injected."""
|
|
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
|
for mod in list(sys.modules):
|
|
if mod == "core" or mod.startswith("core.") or mod == "services" or mod.startswith("services."):
|
|
del sys.modules[mod]
|
|
from core import db as _db
|
|
_db.init_db()
|
|
|
|
from services import settings_store, engine_env, token_resolver
|
|
settings_store.set_text("perf.torch_compile_disabled", "0")
|
|
monkeypatch.setattr(token_resolver, "resolve", lambda **kw: None)
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
env = engine_env.build_engine_env(base_env={})
|
|
assert "TORCH_COMPILE_DISABLE" not in env
|