Merge main and address degraded-state review findings

This commit is contained in:
debpalash
2026-08-10 09:21:54 +00:00
17 changed files with 187 additions and 105 deletions
+3 -1
View File
@@ -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
+11
View File
@@ -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"
+1
View File
@@ -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!
+2 -1
View File
@@ -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(
+1 -1
View File
@@ -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)
+3 -1
View File
@@ -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}
+10 -3
View File
@@ -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":
+10
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 Cproject-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
+4 -4
View File
@@ -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:]')"
+8
View File
@@ -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
View File
@@ -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)"
+33
View File
@@ -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"
+6 -4
View File
@@ -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"}
+2 -1
View File
@@ -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"}
+33 -2
View File
@@ -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"