Merge main and address degraded-state review findings
This commit is contained in:
@@ -89,7 +89,9 @@ jobs:
|
||||
# never been wired into CI, so its cases were a regression test nothing
|
||||
# ran. Cheap (pure bash, stubs pkg-config) and it gates the class.
|
||||
- name: AppImage launcher (AppRun) unit tests
|
||||
run: bash frontend/src-tauri/appimage/AppRun.test.sh
|
||||
run: |
|
||||
bash frontend/src-tauri/appimage/AppRun.test.sh
|
||||
bash scripts/inject-apprun.test.sh
|
||||
|
||||
# `backend/tests/` mounts routers on bare FastAPI apps (no heavy main
|
||||
# import chain) with a hermetic data dir from its conftest.py. It no
|
||||
|
||||
@@ -751,6 +751,17 @@ jobs:
|
||||
"$APPIMAGE" --appimage-extract >/dev/null
|
||||
ROOT="$EXTRACT_DIR/squashfs-root"
|
||||
fail() { echo "FAIL — $1"; find "$ROOT" -maxdepth 5 -type f 2>/dev/null | head -40; exit 1; }
|
||||
# Regression gate: beforeBundleCommand runs before Tauri creates the
|
||||
# AppDir. The v0.4.2 artifact therefore silently shipped Tauri's
|
||||
# stock AppRun and bypassed every WebKit/Mesa compatibility fix.
|
||||
cmp -s "$ROOT/AppRun" "$GITHUB_WORKSPACE/frontend/src-tauri/appimage/AppRun" \
|
||||
|| fail "custom AppRun missing from final AppImage"
|
||||
[ -s "$ROOT/usr/lib/.bundled-webkitgtk-version" ] \
|
||||
|| fail "bundled WebKitGTK version marker missing"
|
||||
cmp -s \
|
||||
"$ROOT/usr/lib/.bundled-webkitgtk-version" \
|
||||
"$GITHUB_WORKSPACE/frontend/src-tauri/target/.tauri/bundled-webkitgtk-version" \
|
||||
|| fail "bundled WebKitGTK version marker is stale or mismatched"
|
||||
# Thin uv-venv installer: verify the AppImage carries the shell binary,
|
||||
# the bundled uv sidecar, and the backend source resources.
|
||||
{ [ -f "$ROOT/AppRun" ] || find "$ROOT" -type f \( -name "VoiceStudio" -o -name "omnivoice-studio" \) | grep -q .; } || fail "shell binary / AppRun missing"
|
||||
|
||||
@@ -42,6 +42,7 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
||||
### Fixed
|
||||
|
||||
- Backend journal, dictation reset, voice-catalog, and crash-notification failures are now visible and retryable instead of being silently ignored. (#1459)
|
||||
- Linux releases now verify that the AppImage actually contains the compatibility launcher, instead of silently shipping Tauri's stock launcher and opening as a blank window on newer Mesa systems. (#1464)
|
||||
- Patched dependency releases now cover 35 Python and Rust security advisories without weakening VoiceStudio's GPU or offline-runtime compatibility. (#1456, #1472, #1473, #1474, #1475, #1476, #1477)
|
||||
- Curated models now install and repair from reviewed, immutable revisions; custom MOSS remote code requires an explicit safety opt-in. (#1453)
|
||||
- YouTube imports that require a signed-in session can now use an explicitly selected `cookies.txt` export for one import; VoiceStudio never reads browser cookies silently and makes two best-effort attempts to delete its temporary copy. (#1429, #1432) — thanks @dongqing1968-sudo and @phamvandu9595-tech!
|
||||
|
||||
@@ -273,7 +273,8 @@ def dub_abort(job_id: str):
|
||||
had_procs = bool(_active_procs.get(job_id))
|
||||
_kill_job_procs(job_id)
|
||||
try:
|
||||
task_manager.cancel_task(job_id)
|
||||
if task_manager.cancel_task(job_id) is False:
|
||||
raise RuntimeError("task cancellation was declined")
|
||||
except Exception as exc:
|
||||
logger.warning("Dub task cancellation failed")
|
||||
raise HTTPException(
|
||||
|
||||
@@ -241,7 +241,7 @@ async def import_persona(file: UploadFile = File(...)):
|
||||
raise
|
||||
except Exception:
|
||||
cleaned = _cleanup(written)
|
||||
logger.exception("persona import failed")
|
||||
logger.warning("Persona import failed")
|
||||
detail = ("Import failed; no files were kept." if cleaned else
|
||||
"Import failed, and temporary files could not be removed. Close any app using them and retry cleanup.")
|
||||
raise HTTPException(status_code=500, detail=detail)
|
||||
|
||||
@@ -985,7 +985,9 @@ async def _do_clean_audio(audio, tmp_dir, clean_id):
|
||||
except asyncio.TimeoutError:
|
||||
conversion_fallback = True
|
||||
logger.warning("Final clean-audio conversion timed out; returning the cleaned source format")
|
||||
if not os.path.exists(final_path):
|
||||
if conversion_fallback:
|
||||
shutil.copy2(clean_path, final_path)
|
||||
elif not os.path.exists(final_path):
|
||||
shutil.copy2(clean_path, final_path)
|
||||
|
||||
headers = {"X-Clean-Filename": clean_filename}
|
||||
|
||||
@@ -265,7 +265,10 @@ def env_opt_out() -> bool:
|
||||
return (os.environ.get(MODE_ENV) or "").strip().lower() in _OPT_OUT_VALUES
|
||||
|
||||
|
||||
def explicit_endpoint() -> str:
|
||||
_PREF_READ_FAILED = object()
|
||||
|
||||
|
||||
def explicit_endpoint():
|
||||
"""The endpoint the user explicitly configured, or "".
|
||||
|
||||
Same resolution the download paths use: ``HF_ENDPOINT`` env (what
|
||||
@@ -282,7 +285,8 @@ def explicit_endpoint() -> str:
|
||||
|
||||
return str(prefs.get("hf_endpoint", "") or "").strip().rstrip("/")
|
||||
except Exception:
|
||||
return ""
|
||||
logger.warning("Endpoint preference could not be read; using manual mode")
|
||||
return _PREF_READ_FAILED
|
||||
|
||||
|
||||
def mode() -> str:
|
||||
@@ -291,7 +295,8 @@ def mode() -> str:
|
||||
Settings (including explicitly choosing the official endpoint)."""
|
||||
if env_opt_out():
|
||||
return "manual"
|
||||
if explicit_endpoint():
|
||||
endpoint = explicit_endpoint()
|
||||
if endpoint is _PREF_READ_FAILED or endpoint:
|
||||
return "manual"
|
||||
try:
|
||||
from core import prefs
|
||||
@@ -450,6 +455,8 @@ def effective_endpoint() -> Optional[str]:
|
||||
per-download hot path. Never raises."""
|
||||
try:
|
||||
ep = explicit_endpoint()
|
||||
if ep is _PREF_READ_FAILED:
|
||||
return None
|
||||
if ep:
|
||||
return ep
|
||||
if mode() != "auto":
|
||||
|
||||
@@ -122,7 +122,17 @@ async def enable(app) -> ShareState:
|
||||
server.should_exit = True
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.shield(_task), timeout=2)
|
||||
except asyncio.CancelledError:
|
||||
_server = server
|
||||
_state = ShareState(True, port, pin, lan_ipv4_addresses())
|
||||
app.state.network_share = _state
|
||||
raise
|
||||
except Exception as exc:
|
||||
if _task.done():
|
||||
_server = _task = None
|
||||
_state = ShareState()
|
||||
app.state.network_share = _state
|
||||
raise RuntimeError("share listener failed to start") from exc
|
||||
_server = server
|
||||
_state = ShareState(True, port, pin, lan_ipv4_addresses())
|
||||
app.state.network_share = _state
|
||||
|
||||
@@ -612,8 +612,8 @@ class OmniVoiceBackend(TTSBackend):
|
||||
try:
|
||||
import services.model_manager as mm
|
||||
if mm.model is not None:
|
||||
mm.model = None
|
||||
mm.free_vram()
|
||||
mm.model = None
|
||||
except Exception as exc:
|
||||
logger.warning("Shared voice model unload did not complete")
|
||||
raise RuntimeError(
|
||||
|
||||
+27
-29
@@ -19,13 +19,12 @@ Tauri 2's AppImage bundler auto-generates an `AppRun` shell launcher inside the
|
||||
config key in Tauri 2.x as of this writing. The chosen injection strategy is:
|
||||
|
||||
> **A custom `AppRun` template sourced from `frontend/src-tauri/appimage/AppRun`
|
||||
> is copied into the AppImage staging directory by a `beforeBundleCommand`
|
||||
> (Tauri 2 supports this hook).**
|
||||
> is installed into Tauri's project-local AppImage tool cache by a
|
||||
> `beforeBundleCommand`. Tauri then copies that launcher into the AppDir.**
|
||||
|
||||
The script that copies it lives at `scripts/inject-apprun.sh` and is invoked
|
||||
from `package.json` via the existing `bun run build` pipeline, so the
|
||||
operation is wired into the same `cargo tauri build --bundles appimage` call
|
||||
the release pipeline already uses.
|
||||
The script that seeds the cache lives at `scripts/inject-apprun.sh`. The final
|
||||
release smoke test extracts the AppImage and compares its `AppRun` byte-for-byte
|
||||
with the source template, so a stock launcher cannot ship silently again.
|
||||
|
||||
---
|
||||
|
||||
@@ -58,25 +57,21 @@ Tauri's `bundle.linux.appimage` accepts `bundleMediaFramework: bool` and
|
||||
`files: HashMap<PathBuf, PathBuf>` but **not** an `appRun` / `template` key.
|
||||
Tracking issue: `tauri-apps/tauri#7616` is still open.
|
||||
|
||||
### B. `beforeBundleCommand` hook (CHOSEN)
|
||||
### B. Replace the staged AppDir from `beforeBundleCommand` (REJECTED)
|
||||
|
||||
Tauri 2 added `build.beforeBundleCommand` as a sibling to `beforeBuildCommand`.
|
||||
It runs AFTER `cargo build` but BEFORE the bundler packs the AppDir into an
|
||||
AppImage. This is exactly the right point to swap the AppRun:
|
||||
- The AppDir staging directory exists at a predictable path
|
||||
(`target/${profile}/bundle/appimage/*.AppDir/`)
|
||||
- The auto-generated `AppRun` is already in place
|
||||
- We just overwrite it with our version before `appimagetool` runs
|
||||
`beforeBundleCommand` runs before `tauri-bundler` creates the AppDir. The old
|
||||
implementation globbed for that future directory, found nothing, exited zero,
|
||||
and v0.4.2 shipped Tauri's stock launcher. This timing cannot be made reliable.
|
||||
|
||||
Tradeoffs:
|
||||
- ✓ No re-packing the squashfs after the fact (faster, simpler)
|
||||
- ✓ One-line tauri.conf.json change + small shell script
|
||||
- ✓ Cross-platform safe: the hook only fires when Linux + AppImage are in the
|
||||
active target list, so macOS/Windows builds are unaffected
|
||||
- ✗ The staging path is glob-discovered (the .AppDir name follows
|
||||
`productName`), so the script handles `productName` changes gracefully
|
||||
### C. Seed Tauri's local AppImage tool cache (CHOSEN)
|
||||
|
||||
### C. Post-bundle re-pack with `appimagetool`
|
||||
With `bundle.useLocalToolsDir`, Tauri reads its launcher from
|
||||
`target/.tauri/AppRun-<arch>` and copies it into the newly created AppDir. The
|
||||
hook installs our launcher at that existing extension point before the bundler
|
||||
runs. `bundle.linux.appimage.files` carries the generated WebKitGTK version
|
||||
marker into `usr/lib`, where the launcher reads it.
|
||||
|
||||
### D. Post-bundle re-pack with `appimagetool`
|
||||
|
||||
Status: **rejected.** This would require unpacking the AppImage's squashfs
|
||||
after Tauri produces it, replacing AppRun, re-running `appimagetool --no-appstream`,
|
||||
@@ -87,7 +82,7 @@ release-pipeline dep. Strategy B avoids both.
|
||||
|
||||
## Chosen strategy
|
||||
|
||||
**Strategy B — `beforeBundleCommand` hook + custom AppRun template.**
|
||||
**Strategy C — project-local Tauri tool cache + custom AppRun template.**
|
||||
|
||||
### Files
|
||||
|
||||
@@ -95,8 +90,10 @@ release-pipeline dep. Strategy B avoids both.
|
||||
|---|---|
|
||||
| `frontend/src-tauri/appimage/AppRun` | The custom launcher shell script (source of truth, version-controlled) |
|
||||
| `frontend/src-tauri/appimage/AppRun.test.sh` | Shell unit test (W-1) — 4 cases for the WebKit version conditional |
|
||||
| `scripts/inject-apprun.sh` | Glob the AppDir under `frontend/src-tauri/target/release/bundle/appimage/*.AppDir/` and `cp -f` the AppRun in. Idempotent. |
|
||||
| `frontend/src-tauri/tauri.conf.json` | `build.beforeBundleCommand` wires the hook into the bundler pipeline |
|
||||
| `scripts/inject-apprun.sh` | Seed `target/.tauri/AppRun-<arch>` and generate the bundled WebKitGTK marker |
|
||||
| `scripts/inject-apprun.test.sh` | Regression test for cache seeding, executable mode and version stamping |
|
||||
| `frontend/src-tauri/tauri.conf.json` | Enable the local tool cache and package its generated marker |
|
||||
| `.github/workflows/release.yml` | Extract the final artifact and reject a stock launcher or missing marker |
|
||||
|
||||
### Wire-up
|
||||
|
||||
@@ -104,14 +101,15 @@ release-pipeline dep. Strategy B avoids both.
|
||||
// frontend/src-tauri/tauri.conf.json
|
||||
{
|
||||
"build": {
|
||||
"beforeBundleCommand": "bash ../../scripts/inject-apprun.sh"
|
||||
"beforeBundleCommand": "bash ../scripts/inject-apprun.sh"
|
||||
// ... existing keys
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The hook is a no-op when `--bundles appimage` is not in the active target
|
||||
list (the script `glob`s for the AppDir; if none exist, it exits 0).
|
||||
The hook is a no-op on non-Linux hosts. On Linux, an unknown architecture or
|
||||
missing WebKitGTK version is a build failure rather than a silently broken
|
||||
artifact.
|
||||
|
||||
### Why `WEBKIT_DISABLE_COMPOSITING_MODE` is conditional, not unconditional
|
||||
|
||||
@@ -130,7 +128,7 @@ When Tauri 2 ships a first-class `appRun` template key (see open
|
||||
`tauri-apps/tauri#7616`), the migration is:
|
||||
|
||||
1. Move `frontend/src-tauri/appimage/AppRun` content into the new config key
|
||||
2. Delete `scripts/inject-apprun.sh`
|
||||
2. Delete `scripts/inject-apprun.sh` and its cache-seeding test
|
||||
3. Remove the `beforeBundleCommand` line that invokes it
|
||||
4. Keep `AppRun.test.sh` as-is — it still validates the conditional logic
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# known-broken ranges. Setting it unconditionally regresses healthy WebKit
|
||||
# versions (2.48+) where the compositing path works fine.
|
||||
#
|
||||
# This file is copied into the AppImage staging directory by
|
||||
# This file is installed into Tauri's local tool cache by
|
||||
# scripts/inject-apprun.sh (wired into Tauri's beforeBundleCommand). See
|
||||
# .planning/decisions/apprun-strategy.md for the decision rationale.
|
||||
|
||||
@@ -30,13 +30,13 @@ HERE="$(dirname -- "$(readlink -f -- "$0")")"
|
||||
# dev packages installed, so pkg-config answers with their system's healthy
|
||||
# 2.48 while the bundle runs an older lib — skipping a workaround the running
|
||||
# library needs). inject-apprun.sh stamps the bundled version into
|
||||
# .bundled-webkitgtk-version at build time, where it is knowable by
|
||||
# usr/lib/.bundled-webkitgtk-version at build time, where it is knowable by
|
||||
# construction; the host pkg-config path survives only as a fallback for
|
||||
# bundles predating the stamp. OMNIVOICE_APPRUN_WK_MARKER exists for the
|
||||
# unit tests to point at a fixture marker.
|
||||
_detect_webkit_workaround() {
|
||||
local wk_version="0.0"
|
||||
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/.bundled-webkitgtk-version}"
|
||||
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/usr/lib/.bundled-webkitgtk-version}"
|
||||
if [ -r "$marker" ]; then
|
||||
# Empty/unreadable marker content → "0.0" (unknown) → fail-safe workaround,
|
||||
# same philosophy as the missing-pkg-config branch below.
|
||||
@@ -125,7 +125,7 @@ _prefer_system_webkit() {
|
||||
|
||||
# Only meaningful when we know what we bundled; an unstamped bundle keeps
|
||||
# the old ordering rather than guessing.
|
||||
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/.bundled-webkitgtk-version}"
|
||||
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/usr/lib/.bundled-webkitgtk-version}"
|
||||
[ -r "$marker" ] || return 1
|
||||
local bundled
|
||||
bundled="$(cat "$marker" 2>/dev/null | tr -d '[:space:]')"
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"useLocalToolsDir": true,
|
||||
"targets": [
|
||||
"dmg",
|
||||
"app",
|
||||
@@ -83,6 +84,13 @@
|
||||
"binaries/ffmpeg",
|
||||
"binaries/ffprobe"
|
||||
],
|
||||
"linux": {
|
||||
"appimage": {
|
||||
"files": {
|
||||
"usr/lib/.bundled-webkitgtk-version": "target/.tauri/bundled-webkitgtk-version"
|
||||
}
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "13.3",
|
||||
"signingIdentity": "-",
|
||||
|
||||
+32
-57
@@ -1,75 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Inject our custom AppRun into Tauri's auto-generated AppImage staging dir.
|
||||
# Seed Tauri's AppImage tool cache with VoiceStudio's launcher.
|
||||
#
|
||||
# Wired into tauri.conf.json's `build.beforeBundleCommand`, this script runs
|
||||
# AFTER `cargo build` but BEFORE `appimagetool` packs the .AppDir. We overwrite
|
||||
# the default AppRun (which Tauri generates without WEBKIT_DISABLE_COMPOSITING_MODE
|
||||
# handling) with our conditional launcher.
|
||||
#
|
||||
# Issue: #56 (AppImage white-screen on Fedora 44 / Ubuntu 24.04)
|
||||
# Decision: docs/adr/apprun-strategy.md
|
||||
#
|
||||
# Idempotent + safe on non-Linux: if no AppDir staging exists (e.g. macOS
|
||||
# build, or `--bundles app` only), the script exits 0 cleanly.
|
||||
# Tauri copies target/.tauri/AppRun-<arch> into the AppDir after
|
||||
# beforeBundleCommand returns. Replacing an AppDir/AppRun here cannot work:
|
||||
# the AppDir does not exist until the bundler runs.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
APPRUN_SRC="$REPO_ROOT/frontend/src-tauri/appimage/AppRun"
|
||||
|
||||
# beforeBundleCommand also runs for macOS and Windows bundles.
|
||||
case "${OSTYPE:-}" in
|
||||
linux*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$APPRUN_SRC" ]; then
|
||||
echo "inject-apprun: source not found: $APPRUN_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Tauri's AppImage staging dir: frontend/src-tauri/target/{profile}/bundle/appimage/*.AppDir/
|
||||
# The .AppDir name follows productName (e.g. "VoiceStudio.AppDir").
|
||||
# Glob across both release and debug profiles in case the caller used --debug.
|
||||
STAGE_BASE_RELEASE="$REPO_ROOT/frontend/src-tauri/target/release/bundle/appimage"
|
||||
STAGE_BASE_DEBUG="$REPO_ROOT/frontend/src-tauri/target/debug/bundle/appimage"
|
||||
TOOLS_DIR="${OMNIVOICE_TAURI_TOOLS_DIR:-$REPO_ROOT/frontend/src-tauri/target/.tauri}"
|
||||
ARCH="${OMNIVOICE_TARGET_ARCH:-$(uname -m)}"
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH=x86_64 ;;
|
||||
aarch64|arm64) ARCH=aarch64 ;;
|
||||
armv7l|armhf) ARCH=armhf ;;
|
||||
*)
|
||||
echo "inject-apprun: unsupported Linux architecture: $ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
found=0
|
||||
for stage_base in "$STAGE_BASE_RELEASE" "$STAGE_BASE_DEBUG"; do
|
||||
if [ ! -d "$stage_base" ]; then
|
||||
continue
|
||||
fi
|
||||
# Use a glob loop instead of `find` to avoid surprises with names containing spaces.
|
||||
shopt -s nullglob
|
||||
for appdir in "$stage_base"/*.AppDir; do
|
||||
if [ -d "$appdir" ]; then
|
||||
echo "inject-apprun: replacing AppRun in $appdir"
|
||||
cp -f "$APPRUN_SRC" "$appdir/AppRun"
|
||||
chmod 755 "$appdir/AppRun"
|
||||
# Stamp the bundled WebKitGTK version (#961 follow-up). The AppImage
|
||||
# bundles THIS build host's libwebkit2gtk, so the host's pkg-config
|
||||
# answer here is the version the shipped bundle will actually run —
|
||||
# knowable by construction at bundle time, unknowable reliably at
|
||||
# runtime (a user's pkg-config reports their SYSTEM's version, which
|
||||
# LD_LIBRARY_PATH overrides with the bundled copy). AppRun's workaround
|
||||
# auto-detection reads this marker first and only falls back to host
|
||||
# pkg-config when the marker is absent (bundles predating the stamp).
|
||||
wk_bundled="$(pkg-config --modversion webkit2gtk-4.1 2>/dev/null \
|
||||
|| pkg-config --modversion webkit2gtk-4.0 2>/dev/null \
|
||||
|| echo "")"
|
||||
if [ -n "$wk_bundled" ]; then
|
||||
printf '%s\n' "$wk_bundled" > "$appdir/.bundled-webkitgtk-version"
|
||||
echo "inject-apprun: stamped bundled WebKitGTK version: $wk_bundled"
|
||||
else
|
||||
echo "inject-apprun: WARNING — could not read the bundled WebKitGTK version (pkg-config missing?); AppRun will use its runtime fallback" >&2
|
||||
fi
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
done
|
||||
mkdir -p "$TOOLS_DIR"
|
||||
install -m 755 "$APPRUN_SRC" "$TOOLS_DIR/AppRun-$ARCH"
|
||||
|
||||
if [ $found -eq 0 ]; then
|
||||
# Not necessarily an error — beforeBundleCommand runs unconditionally even
|
||||
# when the active target list does not include appimage. Stay quiet so
|
||||
# macOS/Windows builds do not see noisy stderr.
|
||||
echo "inject-apprun: no AppDir staging found (skipping — not an AppImage build)"
|
||||
# Tauri's appimage.files copies this into usr/lib. AppRun reads the marker
|
||||
# there to compare the bundled WebKitGTK with the host copy it may prefer.
|
||||
WK_VERSION="${OMNIVOICE_WEBKIT_VERSION:-$(pkg-config --modversion webkit2gtk-4.1 2>/dev/null \
|
||||
|| pkg-config --modversion webkit2gtk-4.0 2>/dev/null || true)}"
|
||||
if [ -z "$WK_VERSION" ]; then
|
||||
echo "inject-apprun: bundled WebKitGTK version is unavailable" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$WK_VERSION" > "$TOOLS_DIR/bundled-webkitgtk-version"
|
||||
|
||||
exit 0
|
||||
echo "inject-apprun: seeded AppRun-$ARCH (WebKitGTK $WK_VERSION)"
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
OMNIVOICE_TAURI_TOOLS_DIR="$TMP/tools" \
|
||||
OMNIVOICE_TARGET_ARCH=amd64 \
|
||||
OMNIVOICE_WEBKIT_VERSION=2.48.7 \
|
||||
bash "$REPO_ROOT/scripts/inject-apprun.sh"
|
||||
|
||||
cmp -s \
|
||||
"$REPO_ROOT/frontend/src-tauri/appimage/AppRun" \
|
||||
"$TMP/tools/AppRun-x86_64"
|
||||
[ -x "$TMP/tools/AppRun-x86_64" ]
|
||||
[ "$(cat "$TMP/tools/bundled-webkitgtk-version")" = "2.48.7" ]
|
||||
|
||||
mkdir -p "$TMP/squashfs-root/usr/lib"
|
||||
cp "$TMP/tools/bundled-webkitgtk-version" \
|
||||
"$TMP/squashfs-root/usr/lib/.bundled-webkitgtk-version"
|
||||
cmp -s \
|
||||
"$TMP/squashfs-root/usr/lib/.bundled-webkitgtk-version" \
|
||||
"$TMP/tools/bundled-webkitgtk-version"
|
||||
printf '%s\n' '2.48.6' > "$TMP/squashfs-root/usr/lib/.bundled-webkitgtk-version"
|
||||
if cmp -s \
|
||||
"$TMP/squashfs-root/usr/lib/.bundled-webkitgtk-version" \
|
||||
"$TMP/tools/bundled-webkitgtk-version"; then
|
||||
echo "FAIL: stale packaged WebKitGTK marker was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: Tauri AppImage tool cache seeded"
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Optional API data may degrade, but the failure must remain observable."""
|
||||
from contextlib import contextmanager
|
||||
|
||||
from api.routers import openai_compat, system
|
||||
from core import db, run_sentinel
|
||||
import importlib
|
||||
|
||||
|
||||
def test_voice_catalog_db_failure_keeps_shape_and_warns(monkeypatch, caplog):
|
||||
openai_compat = importlib.import_module("api.routers.openai_compat")
|
||||
db = importlib.import_module("core.db")
|
||||
@contextmanager
|
||||
def broken_db():
|
||||
raise OSError("database unavailable")
|
||||
@@ -22,6 +22,8 @@ def test_voice_catalog_db_failure_keeps_shape_and_warns(monkeypatch, caplog):
|
||||
|
||||
|
||||
def test_notification_probe_failures_keep_shape_and_warn(monkeypatch, caplog):
|
||||
system = importlib.import_module("api.routers.system")
|
||||
run_sentinel = importlib.import_module("core.run_sentinel")
|
||||
monkeypatch.setattr(
|
||||
run_sentinel,
|
||||
"newest_record",
|
||||
@@ -32,7 +34,7 @@ def test_notification_probe_failures_keep_shape_and_warn(monkeypatch, caplog):
|
||||
"_crashed_last_session",
|
||||
lambda: (_ for _ in ()).throw(OSError("log unavailable")),
|
||||
)
|
||||
with caplog.at_level("WARNING", logger="omnivoice.system"):
|
||||
with caplog.at_level("WARNING"):
|
||||
result = system.system_notifications()
|
||||
|
||||
assert set(result) == {"notifications", "count"}
|
||||
|
||||
@@ -3,7 +3,7 @@ Tests for the dictation router (GET /dictation/models, GET/POST /dictation/prefs
|
||||
— the exact contract the frontend dictation UI binds to.
|
||||
"""
|
||||
import os
|
||||
import services
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -82,6 +82,7 @@ def test_set_prefs_accepts_repo_id_and_normalizes(client):
|
||||
|
||||
|
||||
def test_reset_failure_does_not_persist_new_preferences(monkeypatch):
|
||||
services = importlib.import_module("services")
|
||||
from api.routers import dictation as dr
|
||||
|
||||
store = {dr.PREF_MODE: "toggle"}
|
||||
|
||||
@@ -215,10 +215,14 @@ def test_shared_voice_unload_failure_is_stable(monkeypatch):
|
||||
with pytest.raises(RuntimeError) as caught:
|
||||
backend.unload()
|
||||
assert str(caught.value) == "The shared voice model could not be unloaded. Retry after the current generation finishes."
|
||||
assert manager.model is not None
|
||||
monkeypatch.setattr(manager, "free_vram", lambda: None)
|
||||
backend.unload()
|
||||
assert manager.model is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_network_start_failure_retains_listener_for_disable(monkeypatch):
|
||||
async def test_terminal_network_start_failure_resets_state(monkeypatch):
|
||||
network_share = importlib.import_module("services.network_share")
|
||||
task = asyncio.get_running_loop().create_future()
|
||||
task.set_exception(RuntimeError("/secret/listener"))
|
||||
@@ -234,4 +238,31 @@ async def test_network_start_failure_retains_listener_for_disable(monkeypatch):
|
||||
with pytest.raises(RuntimeError) as caught:
|
||||
await network_share.enable(app)
|
||||
assert "secret" not in str(caught.value)
|
||||
assert network_share.get_state().enabled is True
|
||||
assert network_share.get_state().enabled is False
|
||||
assert network_share._task is None
|
||||
|
||||
|
||||
def test_dub_abort_false_result_stays_retryable(monkeypatch):
|
||||
dub_core = importlib.import_module("api.routers.dub_core")
|
||||
job = {"id": "job-false"}
|
||||
monkeypatch.setitem(dub_core._dub_jobs, "job-false", job)
|
||||
monkeypatch.setattr(dub_core, "_kill_job_procs", lambda _job_id: None)
|
||||
monkeypatch.setattr(dub_core.task_manager, "cancel_task", lambda _job_id: False)
|
||||
with pytest.raises(HTTPException) as caught:
|
||||
dub_core.dub_abort("job-false")
|
||||
assert caught.value.status_code == 503
|
||||
assert "aborted" not in job
|
||||
|
||||
|
||||
def test_endpoint_pref_read_failure_keeps_manual_mode(monkeypatch):
|
||||
endpoint_race = importlib.import_module("services.endpoint_race")
|
||||
prefs = importlib.import_module("core.prefs")
|
||||
calls = iter([OSError("/secret/pref"), "auto"])
|
||||
def read(_key, _default=""):
|
||||
value = next(calls)
|
||||
if isinstance(value, Exception):
|
||||
raise value
|
||||
return value
|
||||
monkeypatch.delenv("HF_ENDPOINT", raising=False)
|
||||
monkeypatch.setattr(prefs, "get", read)
|
||||
assert endpoint_race.mode() == "manual"
|
||||
|
||||
Reference in New Issue
Block a user