* fix: eliminate DB connection leaks, race conditions, and deprecated asyncio API ## DB Connection Leaks (P0) - Convert 38 raw get_db() calls to db_conn() context manager across 14 router files - Connections are now guaranteed to close even when exceptions are raised - profiles.py create_profile: clean up orphaned audio file if DB insert fails - profiles.py lock_profile: consolidate 3 separate conn.close() error paths ## Race Condition (P1) - Add _dub_jobs_lock (threading.Lock) to protect _dub_jobs dict in dub_pipeline.py - get_job/put_job now thread-safe for concurrent dub sessions ## asyncio Deprecation (P2) - Replace 23 asyncio.get_event_loop() calls with asyncio.get_running_loop() - Prevents DeprecationWarning on Python 3.12+ and future breakage on 3.14 ## Quick Fixes - gallery.py preview_voice: remove filesystem path from error response (P2) - dub_pipeline.py parse_vtt_segments: remove redundant `import re` inside loop (P3) - gallery.py _init_gallery_db: use db_conn() context manager (P2) * refactor: extract hooks, centralize isTauri, add pytest-cov ## Frontend - Extract useTTS hook (150 LOC) — TTS generation, streaming, audio ingestion - Extract useProfiles hook (219 LOC) — voice profile CRUD, lock/unlock, preview - Centralize isTauri detection: dialog.js, VoiceGallery.jsx, Settings.jsx now import from utils/media.js instead of 4 different detection patterns ## Backend - Add pytest-cov to dev dependencies - Baseline coverage: 39% across backend/ (214 tests pass) - Add .coverage to .gitignore * feat: add Vitest + checkJs, extract useDubWorkflow + useAppData hooks ## Frontend Testing (new) - Set up Vitest with jsdom environment + @testing-library/react - 11 tests: utils (isTauri, formatTime, constants) + Zustand store (mode, text, dubStep, pill) - Scripts: 'test' (vitest run), 'test:watch' (vitest), 'test:legacy' (node runner) ## App.jsx Decomposition (continued) - Extract useDubWorkflow hook (387 LOC) — upload, ingest, transcribe SSE, translate, generate SSE, abort, stop, cleanup - Extract useAppData hook (181 LOC) — data loading, localStorage persistence, WebSocket real-time updates, model-status pill management ## TypeScript checkJs - Enable checkJs: true in tsconfig.json for IDE-level type checking - 947 existing errors (informational, not blocking builds) - noImplicitAny remains false to avoid blocking * ci: add Vitest step, fix useProfiles duplicate state ## CI - Add 'Run Vitest (frontend)' step — runs 11 unit tests - Override --checkJs false in CI typecheck to avoid 947 pre-existing errors - Rename legacy test step for clarity ## Hooks - Fix useProfiles to accept loadProfiles from parent (useAppData) instead of managing its own duplicate profiles array * refactor: wire hooks into App.jsx — 2067 → 1129 LOC (-45%) App.jsx now delegates to extracted hooks instead of inline logic: - useAppData: data loading, localStorage, WebSocket, model pill - useProfiles: voice profile CRUD, lock/unlock, preview - useTTS: generation, streaming, audio ingestion - useDubWorkflow: upload, transcribe SSE, translate, generate SSE 988 lines removed. All handler logic lives in focused, independently testable hooks. Store selectors and render JSX stay in App.jsx as the shell. Verified: vite build clean, 11 frontend + 214 backend tests pass. * feat: show real-time percentage on model loading pill Backend: register hf_progress listener during _load_model_sync() so download/weight-loading tqdm events update _loading_detail with a progress percentage (0-99%). get_model_status() now includes a 'progress' field that the frontend polls. Frontend: useAppData reads msQuery.data.progress and calls setPillProgress() — the FloatingPill already renders the percentage text and progress bar width from this value. * fix: prevent FileNotFoundError in desktop bundle during model init transformers >=4.52 calls _can_set_experts_implementation() and _can_set_attn_implementation() during PreTrainedModel.__init__, which open the class source file via open(class_file). In a Tauri desktop bundle, module.__file__ points to a path that doesn't exist on disk, causing: FileNotFoundError: .../omnivoice/models/omnivoice.py Override both classmethods on OmniVoice to return static values without filesystem access. OmniVoice doesn't use MoE experts (return False), but does support flex/flash attn (return True). * fix: sync source dirs on every bootstrap, not just first run The Tauri bootstrap previously only copied omnivoice/ and backend/ to Application Support on the first run. Subsequent app updates kept using stale source files, preventing bug fixes from landing. Now ensure_venv_ready() always syncs both directories from the bundle resources before returning, even when the venv is healthy. This fixes the FileNotFoundError crash where the old omnivoice.py lacked the _can_set_experts_implementation override. * ui: premium setup wizard polish - Primary button: solid gradient fill with hover glow + lift + press - Stepper nav: connected pills with glow ring on active step - Welcome cards: glassmorphism with stagger-in animations, lucide icons, left-border accent strip, hover translate - Preflight panel: colored icon pill backgrounds, stagger-slide entrance - Step transitions: fade+slide animation via keyed wrapper - Footnote: shortened paths (~/ notation), Reveal in Finder button - Recommendation banner: gradient background with accent glow - Compact spacing throughout for denser, professional layout * fix: kill zombie backend on clean+retry bootstrap When clean_and_retry_bootstrap removes the project dir, any old uvicorn process still running from the deleted paths remains alive on port 3900. The subsequent retry_bootstrap sees the port is healthy and attaches to the zombie instead of re-bootstrapping. Now explicitly kill any process on the backend port after cleaning, before calling retry_bootstrap. * feat: integrate speaker clones into dubbing interface, sanitize system environment variables for subprocesses, and improve FFMPEG binary path resolution. * fix: restore docker compose default + drop dead setSeed call - deploy/docker-compose.yml: remove profiles: ["cpu"] from the default service so `docker compose up` matches the comment on line 5. With the profile present, no service auto-started. - frontend/src/App.jsx: drop the setSeed call in restoreHistory. The selector was never reintroduced after the App.jsx hooks split, and there is no seed state in the store — seeds are generated fresh per call in useTTS and only read from history items for display. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review — async detection, dub stream, bootstrap fail-fast - backend/services/tts_backend.py: invert async-context detection in _ensure_loaded. The previous code unconditionally caught its own diagnostic RuntimeError and then called asyncio.run() inside a running loop, masking the intended error message. - frontend/src/hooks/useDubWorkflow.js: require a terminal `done` event before reporting dub success. Without this, a dropped stream after partial progress would flip the UI to `done`, refresh history, and play the completion ping as if generation finished. - frontend/src/hooks/useDubWorkflow.js: restore the previous step when tasksCancel() fails. The UI was getting stuck in `stopping` forever on cancel errors. - frontend/src-tauri/src/bootstrap.rs: fail-fast when source sync fails after the existing directory has already been removed. The previous warn-and-continue path could leave the install with no backend/ or omnivoice/ sources and defer the failure to backend startup with a cryptic error. - backend/api/routers/generation.py: add `from e` to the ValueError → HTTPException re-raise (Ruff B904). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: preserve % suffix in TTS generation timer The 100ms timer in useTTS was rewriting generationTime to a plain elapsed-seconds string, which immediately wiped the "(xx%)" download suffix written on the next iteration of the response-body loop. The real-time percentage was flickering on/off as a result. Read the previous value inside the setter and reattach any existing percent suffix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
389 lines
14 KiB
Bash
Executable File
389 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# smoke-test.sh — Automated end-to-end first-launch verification
|
|
#
|
|
# Simulates a REAL end-user fresh install:
|
|
# 1. Wipes all app data (venv, config, tools, logs, HF cache)
|
|
# 2. Builds the debug production bundle
|
|
# 3. Launches the app in the background
|
|
# 4. Polls the backend until it's healthy or timeout
|
|
# 5. Runs health checks against every critical endpoint
|
|
# 6. Checks device detection, model status, region config
|
|
# 7. Kills the app and reports pass/fail
|
|
#
|
|
# This is what you should run BEFORE every release. It catches:
|
|
# - Bootstrap failures (missing deps, bad downloads)
|
|
# - GPU detection regressions
|
|
# - FFmpeg/ffprobe resolution failures
|
|
# - Region mirror misconfig
|
|
# - Model loading crashes
|
|
#
|
|
# Usage:
|
|
# bun run smoke-test # full wipe + build + test
|
|
# bun run smoke-test:quick # skip build, re-test last binary
|
|
# bun run smoke-test:upgrade # keep data, test upgrade path
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
set -euo pipefail
|
|
|
|
APP_ID="com.debpalash.omnivoice-studio"
|
|
TAURI_DIR="frontend/src-tauri"
|
|
APP_NAME="OmniVoice Studio"
|
|
BACKEND_URL="http://127.0.0.1:3900"
|
|
|
|
# Timeouts (seconds)
|
|
BOOTSTRAP_TIMEOUT=600 # 10 min for full venv bootstrap
|
|
HEALTH_TIMEOUT=120 # 2 min for backend to become healthy after bootstrap
|
|
MODEL_TIMEOUT=180 # 3 min for model to load
|
|
|
|
# ── Colors ─────────────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m' # No Color
|
|
|
|
pass() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
fail() { echo -e " ${RED}✗${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
|
header() { echo -e "\n${BOLD}$1${NC}"; }
|
|
|
|
FAILURES=0
|
|
TESTS=0
|
|
APP_PID=""
|
|
|
|
# ── Cleanup on exit ────────────────────────────────────────────────────────
|
|
cleanup() {
|
|
if [ -n "$APP_PID" ] && kill -0 "$APP_PID" 2>/dev/null; then
|
|
info "Killing app (pid $APP_PID)..."
|
|
kill "$APP_PID" 2>/dev/null || true
|
|
sleep 2
|
|
kill -9 "$APP_PID" 2>/dev/null || true
|
|
fi
|
|
# Also kill any orphaned backend
|
|
pkill -f "uvicorn.*3900" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ── Detect platform ───────────────────────────────────────────────────────
|
|
OS="$(uname -s)"
|
|
case "$OS" in
|
|
Darwin) PLATFORM="macos" ;;
|
|
Linux) PLATFORM="linux" ;;
|
|
*) echo "❌ Unsupported platform: $OS"; exit 1 ;;
|
|
esac
|
|
|
|
if [ "$PLATFORM" = "macos" ]; then
|
|
APP_DATA="$HOME/Library/Application Support/${APP_ID}"
|
|
OV_DATA="$HOME/Library/Application Support/OmniVoice"
|
|
else
|
|
APP_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}"
|
|
OV_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/OmniVoice"
|
|
fi
|
|
|
|
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
|
|
|
|
# ── Flags ──────────────────────────────────────────────────────────────────
|
|
SKIP_BUILD=false
|
|
KEEP_DATA=false
|
|
SKIP_MODEL=false
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--skip-build) SKIP_BUILD=true ;;
|
|
--keep-data) KEEP_DATA=true ;;
|
|
--skip-model) SKIP_MODEL=true ;;
|
|
-h|--help)
|
|
echo "Usage: $0 [--skip-build] [--keep-data] [--skip-model]"
|
|
echo ""
|
|
echo " --skip-build Skip cargo build, use last compiled binary"
|
|
echo " --keep-data Don't wipe app data (test upgrade path)"
|
|
echo " --skip-model Skip waiting for TTS model load (saves time)"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
header "🧪 OmniVoice Studio — End-to-End Smoke Test"
|
|
echo " Platform: $PLATFORM | $(date)"
|
|
echo ""
|
|
|
|
# ── Phase 1: Clean ────────────────────────────────────────────────────────
|
|
header "Phase 1: Environment Reset"
|
|
|
|
if [ "$KEEP_DATA" = false ]; then
|
|
info "Wiping app data for fresh install simulation..."
|
|
|
|
for dir in "$APP_DATA" "$OV_DATA"; do
|
|
if [ -d "$dir" ]; then
|
|
rm -rf "$dir"
|
|
pass "Removed: $dir"
|
|
fi
|
|
done
|
|
pass "Clean slate — next launch bootstraps from zero"
|
|
else
|
|
warn "Keeping existing data (upgrade test mode)"
|
|
fi
|
|
|
|
# Kill any existing backend on port 3900
|
|
if lsof -i :3900 >/dev/null 2>&1; then
|
|
info "Killing existing process on port 3900..."
|
|
kill $(lsof -ti :3900) 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
# ── Phase 2: Build ────────────────────────────────────────────────────────
|
|
header "Phase 2: Build"
|
|
|
|
BINARY="${TAURI_DIR}/target/debug/omnivoice-studio"
|
|
|
|
if [ "$SKIP_BUILD" = false ]; then
|
|
info "Building debug bundle (this takes 1-3 min)..."
|
|
|
|
# Remove stale bundle
|
|
if [ "$PLATFORM" = "macos" ]; then
|
|
APP_BUNDLE="${TAURI_DIR}/target/debug/bundle/macos/${APP_NAME}.app"
|
|
[ -d "$APP_BUNDLE" ] && rm -rf "$APP_BUNDLE"
|
|
fi
|
|
|
|
BUILD_LOG=$(mktemp)
|
|
cd frontend
|
|
set +e
|
|
bunx tauri build --debug >"$BUILD_LOG" 2>&1
|
|
BUILD_EXIT=$?
|
|
set -e
|
|
cd ..
|
|
|
|
if [ $BUILD_EXIT -ne 0 ]; then
|
|
if grep -qi "TAURI_SIGNING_PRIVATE_KEY\|private key\|failed to bundle" "$BUILD_LOG"; then
|
|
warn "Non-fatal bundle warning (signing/bundling) — binary is fine"
|
|
else
|
|
echo ""
|
|
tail -20 "$BUILD_LOG"
|
|
rm -f "$BUILD_LOG"
|
|
fail "Build failed with exit code $BUILD_EXIT"
|
|
exit 1
|
|
fi
|
|
fi
|
|
rm -f "$BUILD_LOG"
|
|
|
|
if [ -f "$BINARY" ]; then
|
|
pass "Binary built: $BINARY"
|
|
else
|
|
fail "Binary not found at $BINARY"
|
|
exit 1
|
|
fi
|
|
else
|
|
if [ -f "$BINARY" ]; then
|
|
pass "Using existing binary: $BINARY (--skip-build)"
|
|
else
|
|
fail "No binary found. Run without --skip-build first."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ── Phase 3: Launch & Bootstrap ───────────────────────────────────────────
|
|
header "Phase 3: Launch & Bootstrap"
|
|
|
|
info "Starting app..."
|
|
"$BINARY" &
|
|
APP_PID=$!
|
|
info "App PID: $APP_PID"
|
|
|
|
# Wait for backend to come up
|
|
info "Waiting for backend health (timeout: ${BOOTSTRAP_TIMEOUT}s)..."
|
|
ELAPSED=0
|
|
INTERVAL=5
|
|
while [ $ELAPSED -lt $BOOTSTRAP_TIMEOUT ]; do
|
|
if curl -sf "${BACKEND_URL}/system/info" >/dev/null 2>&1; then
|
|
pass "Backend healthy after ${ELAPSED}s"
|
|
break
|
|
fi
|
|
sleep $INTERVAL
|
|
ELAPSED=$((ELAPSED + INTERVAL))
|
|
|
|
# Check if app crashed
|
|
if ! kill -0 "$APP_PID" 2>/dev/null; then
|
|
fail "App process died during bootstrap (after ${ELAPSED}s)"
|
|
echo ""
|
|
# Show crash log if available
|
|
CRASH_LOG="$OV_DATA/crash_log.txt"
|
|
if [ -f "$CRASH_LOG" ] && [ -s "$CRASH_LOG" ]; then
|
|
echo " 📋 Crash log:"
|
|
tail -20 "$CRASH_LOG" | sed 's/^/ /'
|
|
fi
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if [ $ELAPSED -ge $BOOTSTRAP_TIMEOUT ]; then
|
|
fail "Backend did not start within ${BOOTSTRAP_TIMEOUT}s"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Phase 4: Health Checks ────────────────────────────────────────────────
|
|
header "Phase 4: Health Checks"
|
|
|
|
check_endpoint() {
|
|
local name="$1"
|
|
local url="$2"
|
|
local jq_filter="${3:-}"
|
|
TESTS=$((TESTS + 1))
|
|
|
|
RESPONSE=$(curl -sf "$url" 2>/dev/null) || { fail "$name — HTTP error"; return; }
|
|
|
|
if [ -n "$jq_filter" ]; then
|
|
VALUE=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print($jq_filter)" 2>/dev/null)
|
|
if [ -n "$VALUE" ] && [ "$VALUE" != "None" ]; then
|
|
pass "$name → $VALUE"
|
|
else
|
|
fail "$name — unexpected response"
|
|
fi
|
|
else
|
|
pass "$name → OK"
|
|
fi
|
|
}
|
|
|
|
# Core endpoints
|
|
check_endpoint "GET /system/info" "${BACKEND_URL}/system/info" "d.get('device','?')"
|
|
check_endpoint "GET /sysinfo" "${BACKEND_URL}/sysinfo"
|
|
check_endpoint "GET /model/status" "${BACKEND_URL}/model/status" "d.get('status','?')"
|
|
|
|
# Device detection — the most critical check
|
|
TESTS=$((TESTS + 1))
|
|
DEVICE=$(curl -sf "${BACKEND_URL}/system/info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('device','unknown'))" 2>/dev/null)
|
|
case "$DEVICE" in
|
|
mps|cuda|xpu|cpu)
|
|
pass "Device detection: $DEVICE"
|
|
;;
|
|
*)
|
|
if echo "$DEVICE" | grep -q "privateuseone"; then
|
|
pass "Device detection: DirectML ($DEVICE)"
|
|
else
|
|
fail "Device detection returned unexpected: $DEVICE"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Python version check
|
|
TESTS=$((TESTS + 1))
|
|
PY_VER=$(curl -sf "${BACKEND_URL}/system/info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('python','?'))" 2>/dev/null)
|
|
if echo "$PY_VER" | grep -q "^3\.11"; then
|
|
pass "Python version: $PY_VER"
|
|
else
|
|
fail "Python version unexpected: $PY_VER (expected 3.11.x)"
|
|
fi
|
|
|
|
# Platform check
|
|
TESTS=$((TESTS + 1))
|
|
PLAT=$(curl -sf "${BACKEND_URL}/system/info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('platform','?'))" 2>/dev/null)
|
|
if [ "$PLAT" = "darwin" ] || [ "$PLAT" = "linux" ] || [ "$PLAT" = "win32" ]; then
|
|
pass "Platform: $PLAT"
|
|
else
|
|
fail "Platform unexpected: $PLAT"
|
|
fi
|
|
|
|
# FFmpeg check — try the sysinfo endpoint
|
|
TESTS=$((TESTS + 1))
|
|
FFMPEG_OK=$(curl -sf "${BACKEND_URL}/system/info" | python3 -c "
|
|
import sys,json
|
|
d=json.load(sys.stdin)
|
|
print('ok' if d.get('error') is None else d.get('error','?'))
|
|
" 2>/dev/null)
|
|
if [ "$FFMPEG_OK" = "ok" ]; then
|
|
pass "No startup errors"
|
|
else
|
|
fail "Startup error: $FFMPEG_OK"
|
|
fi
|
|
|
|
# WebSocket endpoint — curl GET returns 403/400/404 (needs WS upgrade)
|
|
# We just verify the route exists by checking it doesn't hard-error.
|
|
TESTS=$((TESTS + 1))
|
|
WS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${BACKEND_URL}/ws/events" 2>/dev/null || echo "000")
|
|
if [ "$WS_CODE" != "000" ]; then
|
|
pass "WebSocket route reachable (/ws/events → HTTP $WS_CODE)"
|
|
else
|
|
fail "WebSocket route unreachable (connection refused)"
|
|
fi
|
|
|
|
# System notifications endpoint
|
|
check_endpoint "GET /system/notifications" "${BACKEND_URL}/system/notifications"
|
|
|
|
# ── Phase 5: Model Loading (optional) ─────────────────────────────────────
|
|
if [ "$SKIP_MODEL" = false ]; then
|
|
header "Phase 5: Model Loading"
|
|
info "Waiting for TTS model to load (timeout: ${MODEL_TIMEOUT}s)..."
|
|
|
|
ELAPSED=0
|
|
while [ $ELAPSED -lt $MODEL_TIMEOUT ]; do
|
|
STATUS=$(curl -sf "${BACKEND_URL}/model/status" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
|
SUB=$(curl -sf "${BACKEND_URL}/model/status" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sub_stage','?'))" 2>/dev/null)
|
|
|
|
if [ "$STATUS" = "ready" ]; then
|
|
TESTS=$((TESTS + 1))
|
|
pass "Model loaded successfully (${ELAPSED}s)"
|
|
break
|
|
elif [ "$STATUS" = "loading" ]; then
|
|
info "Loading... ($SUB) [${ELAPSED}s]"
|
|
fi
|
|
|
|
sleep 10
|
|
ELAPSED=$((ELAPSED + 10))
|
|
done
|
|
|
|
if [ $ELAPSED -ge $MODEL_TIMEOUT ]; then
|
|
TESTS=$((TESTS + 1))
|
|
ERR=$(curl -sf "${BACKEND_URL}/model/status" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error','unknown'))" 2>/dev/null)
|
|
fail "Model did not load within ${MODEL_TIMEOUT}s (error: $ERR)"
|
|
fi
|
|
else
|
|
warn "Skipping model load test (--skip-model)"
|
|
fi
|
|
|
|
# ── Phase 6: Region Config ────────────────────────────────────────────────
|
|
header "Phase 6: Config Verification"
|
|
|
|
TESTS=$((TESTS + 1))
|
|
CONFIG_FILE="$APP_DATA/config.json"
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
REGION=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('region','?'))" 2>/dev/null)
|
|
pass "Region config: $REGION (from $CONFIG_FILE)"
|
|
else
|
|
# No config file = default "auto" — that's correct for fresh install
|
|
pass "Region config: auto (default, no config.json yet)"
|
|
fi
|
|
|
|
# ── Phase 7: Data Directory Structure ─────────────────────────────────────
|
|
header "Phase 7: Data Directories"
|
|
|
|
for dir in "$OV_DATA" "$APP_DATA"; do
|
|
TESTS=$((TESTS + 1))
|
|
if [ -d "$dir" ]; then
|
|
pass "Directory exists: $(basename $dir)/"
|
|
else
|
|
warn "Directory missing: $dir (may be created on first use)"
|
|
fi
|
|
done
|
|
|
|
# ── Results ────────────────────────────────────────────────────────────────
|
|
header "═══════════════════════════════════════════════════════════"
|
|
echo ""
|
|
if [ $FAILURES -eq 0 ]; then
|
|
echo -e " ${GREEN}${BOLD}ALL TESTS PASSED${NC} ($TESTS checks)"
|
|
echo ""
|
|
echo -e " ${CYAN}The app bootstraps correctly from zero and all"
|
|
echo -e " endpoints are healthy. This matches the end-user experience.${NC}"
|
|
else
|
|
echo -e " ${RED}${BOLD}$FAILURES FAILURE(S)${NC} out of $TESTS checks"
|
|
echo ""
|
|
echo -e " ${RED}Fix the failures above before releasing.${NC}"
|
|
fi
|
|
echo ""
|
|
echo " Logs: ~/Library/Logs/OmniVoice/"
|
|
echo " Data: $APP_DATA"
|
|
echo ""
|
|
|
|
exit $FAILURES
|