Files
VoiceStudio/tests/scripts/test_validate_install_docs.py
Palash DebnathandClaude Opus 4.7 715766cb04 Phase 1 Wave 2: per-OS install docs + Settings UI + error→docs deeplinks (#94)
* 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>
2026-05-20 06:22:10 +05:30

245 lines
7.5 KiB
Python

"""Tests for scripts/validate-install-docs.py — checker B-5.
The validator is a CI gate that prevents docs/install/*.md from drifting out
of sync with scripts/desktop-prod.sh. The validator itself must not drift —
these tests pin the behaviours that matter:
- Clean state passes (exit 0, no errors printed).
- A drift line in a `<!-- validate -->` block fails (exit 1, line + file
surfaced on stderr).
- `<!-- validate: skip -->` opts a block out of the comparison, even when
the block diverges from the canonical script.
- `$ ` and `>>> ` prompt prefixes are stripped before comparison.
- CRLF line endings normalise to LF.
- Trailing whitespace doesn't cause spurious failures.
- Pure-comment and blank lines are skipped.
Each test builds a tmp_path-rooted fixture with the minimum file layout
the validator needs (`docs/install/foo.md` + `scripts/desktop-prod.sh`) so
the tests don't depend on the real repo state.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
SCRIPT_PATH = Path(__file__).resolve().parents[2] / "scripts" / "validate-install-docs.py"
@pytest.fixture
def validator_module():
"""Import scripts/validate-install-docs.py as a module so we can call
`main(root=...)` directly without spawning a subprocess."""
spec = importlib.util.spec_from_file_location("validate_install_docs", SCRIPT_PATH)
assert spec and spec.loader, "spec_from_file_location returned None"
mod = importlib.util.module_from_spec(spec)
sys.modules["validate_install_docs"] = mod
spec.loader.exec_module(mod)
return mod
def _make_root(tmp_path: Path, *, docs: dict[str, str], script: str) -> Path:
"""Lay out the minimum repo skeleton the validator scans."""
(tmp_path / "docs" / "install").mkdir(parents=True)
for name, body in docs.items():
(tmp_path / "docs" / "install" / name).write_text(body, encoding="utf-8")
(tmp_path / "scripts").mkdir(parents=True)
(tmp_path / "scripts" / "desktop-prod.sh").write_text(script, encoding="utf-8")
return tmp_path
def test_clean_state_passes(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"# macOS\n\n"
"<!-- validate -->\n"
"```bash\n"
'APP_NAME="OmniVoice Studio"\n'
"```\n"
),
},
script='APP_NAME="OmniVoice Studio"\n',
)
code = validator_module.main(root=root)
out = capsys.readouterr()
assert code == 0, out.err
assert "OK" in out.out
def test_drift_introduced_fails(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"# macOS\n\n"
"<!-- validate -->\n"
"```bash\n"
"this-line-does-not-exist-in-the-script\n"
"```\n"
),
},
script='APP_NAME="OmniVoice Studio"\n',
)
code = validator_module.main(root=root)
out = capsys.readouterr()
assert code == 1
assert "macos.md" in out.err
assert "this-line-does-not-exist-in-the-script" in out.err
def test_skip_marker_allows_divergence(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"# macOS\n\n"
"<!-- validate: skip -->\n"
"```bash\n"
"this-block-can-diverge-from-script\n"
"```\n"
),
},
script='APP_NAME="OmniVoice Studio"\n',
)
code = validator_module.main(root=root)
out = capsys.readouterr()
assert code == 0, out.err
def test_skip_marker_diverging_block_exit_zero(validator_module, tmp_path, capsys):
"""B-5 case (b): an explicit `validate: skip` block diverging from the
canonical script must NOT cause a non-zero exit."""
root = _make_root(
tmp_path,
docs={
"win.md": (
"<!-- validate: skip -->\n"
"```bash\n"
"setx HF_TOKEN hf_xxx\n"
"```\n"
),
},
script="something-else-entirely\n",
)
assert validator_module.main(root=root) == 0
def test_prompt_prefix_stripped(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"<!-- validate -->\n"
"```bash\n"
"$ bun install\n"
"```\n"
),
},
script="bun install\n",
)
code = validator_module.main(root=root)
assert code == 0, capsys.readouterr().err
def test_python_prompt_prefix_stripped(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"x.md": (
"<!-- validate -->\n"
"```python\n"
">>> bun install\n"
"```\n"
),
},
script="bun install\n",
)
code = validator_module.main(root=root)
assert code == 0, capsys.readouterr().err
def test_crlf_normalization(validator_module, tmp_path, capsys):
"""A docs file with CRLF line endings produces the same result as the LF
version (exit 0 when the canonical content matches)."""
crlf_doc = (
"# macOS\r\n"
"<!-- validate -->\r\n"
"```bash\r\n"
"bun install\r\n"
"```\r\n"
)
root = _make_root(
tmp_path,
docs={"macos.md": crlf_doc},
script="bun install\n",
)
code = validator_module.main(root=root)
assert code == 0, capsys.readouterr().err
def test_trailing_whitespace_tolerated(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"<!-- validate -->\n"
"```bash\n"
"bun install \n" # trailing spaces in docs
"```\n"
),
},
script="bun install\n", # canonical has no trailing spaces
)
code = validator_module.main(root=root)
assert code == 0, capsys.readouterr().err
def test_blank_and_comment_only_lines_skipped(validator_module, tmp_path, capsys):
root = _make_root(
tmp_path,
docs={
"macos.md": (
"<!-- validate -->\n"
"```bash\n"
"\n"
"# A comment that explains the next step\n"
"bun install\n"
"# Another comment\n"
"\n"
"```\n"
),
},
script="bun install\n",
)
code = validator_module.main(root=root)
assert code == 0, capsys.readouterr().err
def test_true_diff_distinguished_from_whitespace(validator_module, tmp_path, capsys):
"""B-5 case (e): a real semantic diff (different command) must fail; a
whitespace-only diff (trailing spaces) must pass."""
# Whitespace-only diff — passes:
root_ws = _make_root(
tmp_path / "ws",
docs={"x.md": "<!-- validate -->\n```bash\nbun install \n```\n"},
script="bun install\n",
)
assert validator_module.main(root=root_ws) == 0
# Real semantic diff — fails:
root_real = _make_root(
tmp_path / "real",
docs={"x.md": "<!-- validate -->\n```bash\nbun install --force\n```\n"},
script="bun install\n",
)
code = validator_module.main(root=root_real)
err = capsys.readouterr().err
assert code == 1
assert "bun install --force" in err