* fix(appimage): conditional WEBKIT_DISABLE_COMPOSITING_MODE launcher (#56) WebKitGTK 2.44.x and 2.46.x have a compositing-path regression on Wayland that blanks the AppImage's first paint on Fedora 44 / Ubuntu 24.04. Setting WEBKIT_DISABLE_COMPOSITING_MODE=1 forces the software fallback that works, but blindly setting it on healthy WebKit versions (2.48+) regresses those. This wave adds a conditional AppRun launcher that detects the WebKit version via pkg-config and only sets the env var on the broken ranges (plus a fail-safe when pkg-config is absent or the version is unknown). The launcher is injected into Tauri's AppImage staging dir via a beforeBundleCommand hook — see .planning/decisions/apprun-strategy.md for the spike outcome and rationale (Strategy B chosen). Phase 1 Wave 3 — Plan 01-03 Task 1. Closes #56 frontend half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deb): relocate bundled ffprobe out of /usr/bin to avoid conflicts (#76) Prior versions placed the bundled ffprobe at /usr/bin/ffprobe via Tauri's externalBin, which overwrites the system ffprobe on Ubuntu 26.04 and collides with apt-installed media-package ffprobe. Relocate the .deb-bundled ffprobe to /usr/lib/omnivoice-studio/bin/ffprobe via bundle.linux.deb.files, plus defensive maintainer scripts: - preinst: ensure target dir exists for upgrade flows - postinst: remove legacy /usr/bin/ffprobe ONLY when dpkg confirms our package owns it (never touches a user's distro ffprobe) - postrm: clean up the relocated path tree on purge/remove Rust side (tools.rs::resolve_ffprobe) now probes the new path on Linux, and backend spawn (backend.rs) carries both FFPROBE_PATH (legacy alias) and OMNIVOICE_FFPROBE_PATH (canonical) into the backend env. Python side (ffmpeg_utils.resolve_ffprobe) reads OMNIVOICE_FFPROBE_PATH first, falls back to FFPROBE_PATH, then to shutil.which("ffprobe"). 6 new unit tests cover the env-cascade resolution. Phase 1 Wave 3 — Plan 01-03 Task 2. Closes #76. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): centralised apiBase resolver for Docker LAN access (#80) Docker / LAN browser users hit the preview API at the LAN host's IP, not their local machine — the prior frontend/src/utils/media.js:20 hardcoded http://localhost:3900, which from a LAN client resolved to the client machine itself. Centralise via frontend/src/utils/apiBase.ts: 1. VITE_OMNIVOICE_API override (Docker compose / dev) always wins. 2. Tauri webview → http://localhost:3900 (unchanged behaviour). 3. Plain browser → ${window.location.protocol}//${window.location.hostname}:3900 (follows the page's origin — closes #80). 4. SSR / no-window → http://localhost:3900 (safe fallback). Grep-sweep confirmed media.js:20 was the only hardcode site (Assumption A4 in 01-RESEARCH.md verified). 6 new vitest cases cover the resolver. Phase 1 Wave 3 — Plan 01-03 Task 3. Closes #80 frontend half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(backend): macOS Gatekeeper quarantine probe + INST-01 guard (#54) Adds backend/core/gatekeeper_detect.py which walks up from sys.executable to find the .app bundle and runs `xattr -l` to check for the quarantine extended attribute (com.apple.quarantine). On detection, the lifespan startup probe logs a structured warning and emits a system_error event through the existing event bus with error_class="GATEKEEPER_QUARANTINE", which Wave 2's React ErrorBoundary turns into a docs deeplink. Detection is informational only — we never auto-run `xattr -cr` (the app itself is quarantined and cannot fix its own state per Anti-Pattern in 01-RESEARCH.md). Users get a clear pointer to the workaround docs. GET /system/quarantine-status exposes the structured payload so the frontend can poll on first load. INST-01 (setuptools>=75.0 pin from PR #62) gains a PR-time guard in tests/backend/test_pyproject.py + a user-observable smoke check in scripts/smoke-test.sh (pkg_resources + whisperx import). 7 gatekeeper tests + 1 pyproject test added — all pass. Phase 1 Wave 3 — Plan 01-03 Task 4. Closes #54 backend half (Wave 2 owns the docs page + ErrorBoundary deeplink wiring). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
"""Tests for backend/services/ffmpeg_utils.py — Phase 1 Wave 3 (issue #76).
|
|
|
|
Covers :func:`resolve_ffprobe`'s env-first / PATH-fallback cascade and the
|
|
legacy-name alias (``FFPROBE_PATH`` still works, ``OMNIVOICE_FFPROBE_PATH``
|
|
takes precedence).
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env(monkeypatch):
|
|
"""Strip both ffprobe env vars before each test so we control the cascade."""
|
|
monkeypatch.delenv("OMNIVOICE_FFPROBE_PATH", raising=False)
|
|
monkeypatch.delenv("FFPROBE_PATH", raising=False)
|
|
|
|
|
|
def test_resolve_ffprobe_prefers_omnivoice_env_var(monkeypatch, tmp_path):
|
|
"""OMNIVOICE_FFPROBE_PATH set to a real file → that path wins."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", str(fake))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
def test_resolve_ffprobe_falls_back_to_legacy_FFPROBE_PATH(monkeypatch, tmp_path):
|
|
"""OMNIVOICE_FFPROBE_PATH absent but legacy FFPROBE_PATH set → legacy used."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe-legacy"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setenv("FFPROBE_PATH", str(fake))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
def test_resolve_ffprobe_omnivoice_path_takes_precedence_over_legacy(
|
|
monkeypatch, tmp_path,
|
|
):
|
|
"""Both env vars set → OMNIVOICE_FFPROBE_PATH wins."""
|
|
from services import ffmpeg_utils
|
|
|
|
canonical = tmp_path / "ffprobe-canonical"
|
|
legacy = tmp_path / "ffprobe-legacy"
|
|
for f in (canonical, legacy):
|
|
f.write_text("#!/bin/sh\necho 1\n")
|
|
f.chmod(0o755)
|
|
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", str(canonical))
|
|
monkeypatch.setenv("FFPROBE_PATH", str(legacy))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(canonical)
|
|
|
|
|
|
def test_resolve_ffprobe_falls_back_to_PATH(monkeypatch, tmp_path):
|
|
"""No env var → shutil.which result is returned."""
|
|
from services import ffmpeg_utils
|
|
|
|
# Stub shutil.which inside the module so we do not depend on the host PATH.
|
|
fake = "/fake/path/from/system/ffprobe"
|
|
|
|
def _fake_which(name):
|
|
if name == "ffprobe":
|
|
return fake
|
|
return None
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(_fake_which))
|
|
assert ffmpeg_utils.resolve_ffprobe() == fake
|
|
|
|
|
|
def test_resolve_ffprobe_returns_None_when_nothing_resolves(monkeypatch):
|
|
"""No env, no PATH → returns None (no crash)."""
|
|
from services import ffmpeg_utils
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(lambda _name: None))
|
|
assert ffmpeg_utils.resolve_ffprobe() is None
|
|
|
|
|
|
def test_resolve_ffprobe_env_var_with_command_name_resolves_via_which(
|
|
monkeypatch, tmp_path,
|
|
):
|
|
"""Legacy shape: env var set to a bare command name (not a path) — resolve
|
|
via shutil.which so older Tauri shells that set FFPROBE_PATH=ffprobe still
|
|
work."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
|
|
def _fake_which(name):
|
|
if name == "ffprobe":
|
|
return str(fake)
|
|
return None
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(_fake_which))
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", "ffprobe")
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
class _ShutilStub:
|
|
"""A tiny shim that mimics the parts of shutil ffmpeg_utils touches.
|
|
|
|
monkeypatching the whole module avoids pulling in real PATH lookups while
|
|
keeping the existing `import shutil` call sites untouched.
|
|
"""
|
|
|
|
def __init__(self, which_impl):
|
|
self._which = which_impl
|
|
|
|
def which(self, name):
|
|
return self._which(name)
|