Files
VoiceStudio/tests/scripts/test_uninstall_ping.py
Palash Debnath 93025d9a81 fix(dictation): stop the widget stranding an empty square, repair the swept data dirs (#1398)
The dictation hotkey could leave a blank dark square stuck on the desktop with no way to dismiss it. Three defects compounded: the tray listener's effect depended on [state], so it detached across an await on every state change and a press landing in that gap was lost; an idle pill renders null, so the window Rust had already shown was empty; and the opaque chrome background made that empty window a hard-edged square. Nothing could hide it — dismiss() is only reachable from the X button, Esc, or a post-session timer, none of which exist for a session that never started.

Fixed at the invariant rather than the call sites: the listener subscribes once for the component's lifetime, the widget window's chrome background is transparent, and an idle-but-visible window reconciles itself to hidden. The reconcile is polled (a dropped press changes no React state, so there is nothing to key an effect off) and aborts if its effect is torn down mid-check, so it can never hide a dictation that has just started.

Also in scope:

- The rename sweep had repointed three data-dir literals at a brand-named directory that does not exist, so smoke-test.sh verified a directory the backend never writes and desktop-prod.sh silently stopped clearing backend state on Windows. Both invisible on macOS, where they are usually run. A guard test now pins the assignments specifically.
- The dictation model picker's download sizes were wrong for all seven models, in both directions — Parakeet TDT v3 (the recommended default) understated 180 MB against an actual 670 MB, while the low-RAM fallbacks were overstated threefold, discouraging exactly the choice that would have helped. Measured from the published repos and pinned by a test.
- The 0.6B Parakeet models now decode on more threads, capped by host cores and still overridable.
- uninstall.ps1 gained a UTF-8 BOM (Windows PowerShell 5.1 mis-decodes its non-ASCII output without one), and sponsor.yml lost its last OmniVoice references.
2026-08-07 05:39:41 +05:30

202 lines
7.7 KiB
Python

"""The uninstall scripts' opt-in `app_uninstalled` ping — the contract, pinned.
- Sent ONLY when the user opted in: consent is read from the SAME prefs store
the app writes (`prefs.json: analytics_enabled`), AND the backend-written
`analytics_info.json` must exist (the backend only writes it while analytics
is enabled and removes it on opt-out — its presence is itself consent-gated).
- Content-free: app version, OS name, random per-install id. Nothing else.
- Best-effort: 2s timeout, silent failure, never blocks the uninstall.
- Honest: exactly one console line when it sends; silence when not opted in.
- Never on dry-run: the ping lives behind the --yes / -Yes gate.
The bash script is exercised for real (fake $HOME + a curl shim that records
its argv); PowerShell isn't runnable on every dev/CI platform, so uninstall.ps1
is pinned by static contract checks against the same requirements.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import stat
import subprocess
import sys
import pytest
REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
SH = os.path.join(REPO, "scripts", "uninstall.sh")
PS1 = os.path.join(REPO, "scripts", "uninstall.ps1")
pytestmark = pytest.mark.skipif(
sys.platform.startswith("win"), reason="bash script is macOS/Linux-only"
)
def _run(tmp_path, *args, consented=True, with_info=True):
"""Run uninstall.sh in a throwaway HOME with a curl shim on PATH."""
home = tmp_path / "home"
data = tmp_path / "data"
data.mkdir(parents=True)
home.mkdir(parents=True)
(data / "prefs.json").write_text(
json.dumps({"analytics_enabled": bool(consented), "analytics_prompted": True})
)
if with_info:
(data / "analytics_info.json").write_text(
json.dumps(
{
"token": "phc_test_token",
"host": "https://eu.i.posthog.com",
"distinct_id": "11111111-2222-3333-4444-555555555555",
"app_version": "0.3.23",
"platform": "macos",
},
indent=2,
)
)
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
curl_log = tmp_path / "curl_args.txt"
shim = bin_dir / "curl"
shim.write_text(f'#!/bin/sh\nprintf \'%s\\n\' "$@" >> "{curl_log}"\nexit 0\n')
shim.chmod(shim.stat().st_mode | stat.S_IEXEC)
env = dict(os.environ)
env.update(
{
"HOME": str(home),
"OMNIVOICE_DATA_DIR": str(data),
# Keep the shared-model-cache branch inert and inside the sandbox.
"OMNIVOICE_CACHE_DIR": str(tmp_path / "models-nonexistent"),
"PATH": f"{bin_dir}:{env.get('PATH', '')}",
}
)
proc = subprocess.run(
["bash", SH, *args], capture_output=True, text=True, env=env, timeout=60
)
curl_args = curl_log.read_text() if curl_log.exists() else ""
return proc, curl_args
def test_bash_syntax_is_valid():
subprocess.run(["bash", "-n", SH], check=True)
def test_consented_delete_sends_one_honest_ping(tmp_path):
proc, curl = _run(tmp_path, "--yes")
assert proc.returncode == 0, proc.stderr
assert "Sending anonymous uninstall ping (you opted in to analytics)." in proc.stdout
# One capture call, to the host from analytics_info.json, with the ping.
assert "https://eu.i.posthog.com/capture/" in curl
assert curl.count("app_uninstalled") == 1
assert "phc_test_token" in curl
assert "11111111-2222-3333-4444-555555555555" in curl
assert '"app_version":"0.3.23"' in curl.replace(" ", "")
# Best-effort timeout is on the call.
assert re.search(r"^-m\n2$", curl, re.M)
def test_not_consented_sends_nothing_and_prints_nothing(tmp_path):
proc, curl = _run(tmp_path, "--yes", consented=False)
assert proc.returncode == 0, proc.stderr
assert curl == ""
assert "uninstall ping" not in proc.stdout.lower()
def test_missing_info_file_sends_nothing_even_if_prefs_claim_consent(tmp_path):
"""analytics_info.json only exists while the backend had consent AND a
token — a hand-edited prefs.json alone must not produce a ping (there is
no destination to send to)."""
proc, curl = _run(tmp_path, "--yes", consented=True, with_info=False)
assert proc.returncode == 0, proc.stderr
assert curl == ""
assert "uninstall ping" not in proc.stdout.lower()
def test_dry_run_never_pings_even_when_consented(tmp_path):
proc, curl = _run(tmp_path) # no --yes
assert proc.returncode == 0, proc.stderr
assert "DRY RUN" in proc.stdout
assert curl == ""
assert "uninstall ping" not in proc.stdout.lower()
def test_data_is_still_deleted_after_the_ping(tmp_path):
_run(tmp_path, "--yes")
assert not (tmp_path / "data").exists()
# ── uninstall.ps1: static contract checks (PowerShell isn't runnable here) ──
def _ps1_source() -> str:
with open(PS1, encoding="utf-8") as f:
return f.read()
def test_ps1_ping_is_consent_gated_and_best_effort():
src = _ps1_source()
# Consent gate: prefs.json's analytics_enabled AND the backend-written info file.
assert "analytics_info.json" in src
assert "prefs.json" in src
assert re.search(r"analytics_enabled\s+-eq\s+\$true", src)
# Best-effort: 2s timeout inside a try/catch.
assert "-TimeoutSec 2" in src
assert "try {" in src and "} catch {" in src
# Honest single line, matching the bash script's wording.
assert "Sending anonymous uninstall ping (you opted in to analytics)." in src
# The ping only runs on an actual delete (-Yes) — after the dry-run exit.
assert src.index("exit 0") < src.index("app_uninstalled")
def test_ps1_and_sh_carry_no_baked_token():
"""The scripts are generic: the token comes from the backend-written info
file, never from a literal in the repo (the secret scanner would agree)."""
for path in (SH, PS1):
with open(path, encoding="utf-8") as f:
src = f.read()
assert not re.search(r"phc_[A-Za-z0-9]{16,}", src), path
def test_shellcheck_clean_if_available():
"""Run shellcheck when the tool exists (dev machines / CI images that have
it); skip silently elsewhere — the bash -n + functional tests still run."""
if not shutil.which("shellcheck"):
pytest.skip("shellcheck not installed")
proc = subprocess.run(
["shellcheck", "-S", "error", SH], capture_output=True, text=True
)
assert proc.returncode == 0, proc.stdout + proc.stderr
def test_shipped_powershell_scripts_with_non_ascii_carry_a_utf8_bom():
"""A BOM-less UTF-8 .ps1 is decoded with the ANSI code page by Windows
PowerShell 5.1 — still the default shell on Windows 10/11.
`uninstall.ps1` prints em-dashes in `Write-Host` output ("Model cache
(Hugging Face weights — SHARED …)"), so without a BOM the user running the
uninstaller sees mojibake in the very messages that explain what is about
to be deleted. Pinned rather than left to review: the BOM is invisible in a
diff, so an editor that "helpfully" strips it would go unnoticed until a
Windows user reported garbled output (CodeRabbit, #1399).
Scoped to `scripts/` — the vendored trees under `research/` are not ours.
"""
scripts_dir = os.path.join(REPO, "scripts")
offenders = []
for name in sorted(os.listdir(scripts_dir)):
if not name.endswith(".ps1"):
continue
raw = open(os.path.join(scripts_dir, name), "rb").read()
if raw.startswith(b"\xef\xbb\xbf"):
continue
if any(ord(ch) > 127 for ch in raw.decode("utf-8")):
offenders.append(name)
assert not offenders, (
f"these shipped PowerShell scripts contain non-ASCII text but have no "
f"UTF-8 BOM, so Windows PowerShell 5.1 will mis-decode them: {offenders}"
)