Files
VoiceStudio/tests/test_scrub.py
T
debpalashandClaude Opus 4.8 ee08f56e1b feat(diagnostics): local self-check + scrubbed bug-report pipeline
Closes the gap between 'something broke' and 'a useful GitHub issue
exists' — entirely within the local-first constraint: the only outbound
path remains the user's own browser opening a prefilled issues/new URL.

Backend:
- core/scrub.py: privacy scrubber for anything leaving the machine —
  env-var secret values (*TOKEN*|*KEY*|*SECRET*|*PASSWORD*), credential
  shapes (hf_/ghp_/github_pat_/sk-), home dirs on all three OSes
- core/diagnose.py: 9-check self-check (device+GPU, ffmpeg, HF token,
  disk, data-dir writability, RAM, engine registry, hub reachability),
  pre-scrubbed, ASCII-safe output
- GET /system/diagnose + 'python main.py --diagnose' (exit 0/1)
- /system/info: hardware inventory (os_version, cpu_model, cpu_count,
  ram_total_gb, gpu_name, vram_total_gb, disk_free_gb), cached statics

Frontend:
- utils/bugReport.js: single source for the prefilled-URL builder —
  scrubText twin, hardware context capture, scrubbed error+stack embed,
  URL-length cap; ReportBugButton refactored onto it
- ErrorBoundary 'Report this bug' action with the error attached
- utils/errorToast.jsx toastErrorWithReport(); wired into export toasts
- Settings > About 'Run self-check' with per-check status badges

Tests: 27 pytest (scrub, diagnose) + 15 vitest (bugReport); existing
suites green; verified live (--diagnose, TestClient, vite build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:43:03 +05:30

106 lines
3.6 KiB
Python

"""core.scrub — privacy scrubber for diagnostic/bug-report text.
The scrubber is the last gate before text can reach a prefilled GitHub
Issues URL, so these tests pin the exact redaction behavior per platform
path style and per credential shape.
"""
import os
import pytest
from core.scrub import scrub_text, REDACTED
# ── Home directory redaction ──────────────────────────────────────────────
@pytest.mark.parametrize(
"raw,expected",
[
("/Users/alice/Library/Logs/app.log", "~/Library/Logs/app.log"),
("/home/bob/.omnivoice/omnivoice.log", "~/.omnivoice/omnivoice.log"),
(r"C:\Users\carol\AppData\Roaming\OmniVoice", r"~\AppData\Roaming\OmniVoice"),
(r"D:\Users\dave\models", r"~\models"),
],
)
def test_home_paths_redacted(raw, expected):
assert scrub_text(raw) == expected
def test_actual_process_home_redacted():
home = os.path.expanduser("~")
assert home not in scrub_text(f"failed to open {home}/some/file.wav")
def test_home_redaction_inside_traceback():
tb = (
'Traceback (most recent call last):\n'
' File "/home/eve/OmniVoice/backend/main.py", line 42, in synth\n'
"FileNotFoundError: /Users/eve/voice.wav not found"
)
out = scrub_text(tb)
assert "/home/eve" not in out
assert "/Users/eve" not in out
assert 'File "~/OmniVoice/backend/main.py"' in out
# ── Credential-shaped substrings ──────────────────────────────────────────
@pytest.mark.parametrize(
"secret",
[
"hf_" + "A" * 34, # HuggingFace token
"ghp_" + "B" * 36, # GitHub classic PAT
"github_pat_" + "C" * 22, # GitHub fine-grained PAT
"sk-" + "d" * 40, # OpenAI-style key
],
)
def test_tokens_redacted(secret):
out = scrub_text(f"auth failed with token={secret} (401)")
assert secret not in out
assert REDACTED in out
@pytest.mark.parametrize(
"benign",
["hf_hub", "hf_pipeline_load", "sk-learn", "ghp_x"],
)
def test_short_identifiers_survive(benign):
# Identifiers shorter than real-token length must NOT be clobbered —
# they're exactly what makes a stack trace debuggable.
assert benign in scrub_text(f"import error in {benign} module")
# ── Env-var secret values ─────────────────────────────────────────────────
def test_env_secret_value_redacted(monkeypatch):
monkeypatch.setenv("TRANSLATE_API_KEY", "super-secret-value-123")
out = scrub_text("request failed: api_key=super-secret-value-123 rejected")
assert "super-secret-value-123" not in out
assert REDACTED in out
def test_env_secret_short_value_not_swept(monkeypatch):
# A short value would shred unrelated text (every "yes" in the report).
monkeypatch.setenv("SOME_PASSWORD", "yes")
assert scrub_text("yes, the export worked") == "yes, the export worked"
def test_env_non_secret_name_untouched(monkeypatch):
monkeypatch.setenv("OMNIVOICE_MODEL", "k2-fsa/OmniVoice")
assert "k2-fsa/OmniVoice" in scrub_text("loading k2-fsa/OmniVoice")
# ── Robustness ────────────────────────────────────────────────────────────
def test_none_and_empty():
assert scrub_text(None) == ""
assert scrub_text("") == ""
def test_non_string_coerced():
assert scrub_text(42) == "42"