#!/usr/bin/env bash # smoke-linux-driver.sh — Lumotia Linux UI smoke driver # # Automates more cells of the smoke-test matrix than smoke-linux.sh by driving # the X11 UI via xdotool + reading the SQLite store directly for History # assertions. # # Prereqs: # - xdotool (apt/dnf/pacman: xdotool) # - sqlite3 CLI (apt/dnf/pacman: sqlite or sqlite3) # - X11 session (Wayland users: launch with GDK_BACKEND=x11 — already set by run.sh) # - Optional: virtual audio source (see docs/release/virtual-audio-setup.md) for the # Capture cell. Without it, Capture stays MANUAL. # # Usage: # ./scripts/smoke-linux-driver.sh [/path/to/lumotia-0.1.0-linux-x86_64.AppImage] # # If no argument is given, the script builds a debug binary via cargo and tests # against that instead. # # Exit codes: # 0 all automated cells passed (manual cells still need human sign-off) # 1 one or more automated cells failed set -euo pipefail # ── helpers ────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BOLD='\033[1m' RESET='\033[0m' REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" cd "$REPO_ROOT" APPIMAGE="${1:-}" BINARY="" USE_APPIMAGE=false CELL_PASS=0 CELL_FAIL=0 CELL_MANUAL=0 # Track whether Capture succeeded so dependent cells know whether to run CAPTURE_RAN=false CAPTURE_STARTED_AT="" cell_header() { printf "\n${BOLD}[CELL %s] %s${RESET}\n" "$1" "$2" } cell_pass() { CELL_PASS=$((CELL_PASS + 1)) printf "${GREEN} ✓ PASS${RESET} %s\n" "$1" } cell_fail() { CELL_FAIL=$((CELL_FAIL + 1)) printf "${RED} ✗ FAIL${RESET} %s\n" "$1" } cell_manual() { CELL_MANUAL=$((CELL_MANUAL + 1)) printf "${YELLOW} ⚠ MANUAL${RESET} %s\n" "$1" } # ── Capability checks ───────────────────────────────────────────────────────── HAS_XDOTOOL=false HAS_SQLITE3=false HAS_VIRTUAL_AUDIO=false if command -v xdotool >/dev/null 2>&1; then HAS_XDOTOOL=true fi if command -v sqlite3 >/dev/null 2>&1; then HAS_SQLITE3=true fi # Check for virtual audio source named "lumotia-test" if command -v pactl >/dev/null 2>&1; then if pactl list sources short 2>/dev/null | grep -q "lumotia-test"; then HAS_VIRTUAL_AUDIO=true fi fi if ! $HAS_XDOTOOL; then printf "${YELLOW}${BOLD}xdotool not found.${RESET} Falling back to smoke-linux.sh behaviour for UI-dependent cells.\n" printf " Install xdotool (apt/dnf/pacman: xdotool) to enable UI automation.\n" printf " Cells 2–6 will be MANUAL except Install and the dogfood drill.\n\n" fi if ! $HAS_SQLITE3; then printf "${YELLOW}${BOLD}sqlite3 not found.${RESET} History search cell (6/7) will be MANUAL.\n" printf " Install sqlite3 (apt/dnf/pacman: sqlite or sqlite3) to enable SQLite automation.\n\n" fi # ── Resolve data dir ────────────────────────────────────────────────────────── # Lumotia uses the platform data dir. On Linux this is ~/.local/share/lumotia. DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/lumotia" DB_PATH="$DATA_DIR/transcripts.db" # ── Cell 1/7: Install ───────────────────────────────────────────────────────── cell_header "1/7" "Install" if [[ -n "$APPIMAGE" ]]; then if [[ ! -f "$APPIMAGE" ]]; then cell_fail "AppImage not found at: $APPIMAGE" elif [[ ! -x "$APPIMAGE" ]]; then printf " AppImage is not executable — setting bit now...\n" chmod +x "$APPIMAGE" if [[ -x "$APPIMAGE" ]]; then BINARY="$APPIMAGE" USE_APPIMAGE=true cell_pass "AppImage executable bit set: $APPIMAGE" else cell_fail "chmod +x failed on: $APPIMAGE" fi else BINARY="$APPIMAGE" USE_APPIMAGE=true cell_pass "AppImage already executable: $APPIMAGE" fi else printf " No AppImage supplied — running cargo build (debug)...\n" if cargo build -p lumotia 2>&1; then BINARY="$REPO_ROOT/target/debug/lumotia" if [[ -f "$BINARY" ]]; then cell_pass "cargo build succeeded. Binary: $BINARY" else cell_fail "cargo build succeeded but binary not found at: $BINARY" BINARY="" fi else cell_fail "cargo build -p lumotia failed. Fix compilation errors before smoke-testing." BINARY="" fi fi # ── Cell 2/7: First-run ─────────────────────────────────────────────────────── cell_header "2/7" "First-run (launch + window detection)" BINARY_PID="" if [[ -z "$BINARY" ]]; then cell_fail "Skipping — no valid binary from Cell 1." elif ! $HAS_XDOTOOL; then # Fallback: plain process-liveness check (same as smoke-linux.sh) LUMOTIA_SMOKE_MODE=1 \ WEBKIT_DISABLE_DMABUF_RENDERER=1 \ GDK_BACKEND=x11 \ DISPLAY="${DISPLAY:-:99}" \ "$BINARY" &>/tmp/lumotia-smoke-driver-launch.log & BINARY_PID=$! printf " Waiting 10 seconds (PID %s)...\n" "$BINARY_PID" ALIVE=true for i in $(seq 1 10); do sleep 1 if ! kill -0 "$BINARY_PID" 2>/dev/null; then ALIVE=false break fi done if kill -0 "$BINARY_PID" 2>/dev/null; then kill -TERM "$BINARY_PID" 2>/dev/null || true sleep 1 kill -KILL "$BINARY_PID" 2>/dev/null || true fi wait "$BINARY_PID" 2>/dev/null || true BINARY_PID="" if $ALIVE; then cell_pass "Binary stayed alive 10 s (no xdotool — window title not verified)." else EXIT_LOG=/tmp/lumotia-smoke-driver-launch.log if grep -qiE 'panic|SIGSEGV|Segmentation fault|thread.*panicked' "$EXIT_LOG" 2>/dev/null; then cell_fail "Binary crashed within 10 s. See /tmp/lumotia-smoke-driver-launch.log" else cell_pass "Binary exited cleanly in headless mode. No panic in log." fi fi else # xdotool available — launch and verify window title WEBKIT_DISABLE_DMABUF_RENDERER=1 \ GDK_BACKEND=x11 \ DISPLAY="${DISPLAY:-:0}" \ "$BINARY" &>/tmp/lumotia-smoke-driver-launch.log & BINARY_PID=$! printf " Waiting 5 s for window to appear (PID %s)...\n" "$BINARY_PID" FOUND_WINDOW="" for i in $(seq 1 5); do sleep 1 if ! kill -0 "$BINARY_PID" 2>/dev/null; then break fi FOUND_WINDOW=$(DISPLAY="${DISPLAY:-:0}" xdotool search --name "Lumotia" 2>/dev/null | head -1 || true) if [[ -n "$FOUND_WINDOW" ]]; then break fi done if [[ -n "$FOUND_WINDOW" ]]; then cell_pass "Window 'Lumotia' found via xdotool (window ID: $FOUND_WINDOW)." # Leave the process running for Capture cell else # Process may still be alive but window not found — check for crash if ! kill -0 "$BINARY_PID" 2>/dev/null; then EXIT_LOG=/tmp/lumotia-smoke-driver-launch.log if grep -qiE 'panic|SIGSEGV|Segmentation fault|thread.*panicked' "$EXIT_LOG" 2>/dev/null; then cell_fail "Binary crashed before window appeared. See /tmp/lumotia-smoke-driver-launch.log" else cell_pass "Binary exited cleanly (headless/no-display mode). No panic in log." fi BINARY_PID="" else # Window not found but process alive — likely no DISPLAY or WebKit not ready cell_manual "Binary alive but 'Lumotia' window not detected within 5 s. Verify manually that the window opens and the app loads." # Kill it — Capture can't run without a confirmed window kill -TERM "$BINARY_PID" 2>/dev/null || true sleep 1 kill -KILL "$BINARY_PID" 2>/dev/null || true wait "$BINARY_PID" 2>/dev/null || true BINARY_PID="" fi fi fi # ── Cell 3/7: Capture ───────────────────────────────────────────────────────── cell_header "3/7" "Capture (record via hotkey)" if ! $HAS_XDOTOOL; then printf " MANUAL: Open the app, talk into the microphone for 5 seconds.\n" printf " Confirm a live transcript appears in the dictation area.\n" cell_manual "xdotool not found — audio capture requires a human tester." elif [[ -z "$BINARY_PID" ]]; then printf " MANUAL: Cell 2 did not leave a running app window.\n" printf " Open the app manually, record for 5 seconds, confirm a transcript appears.\n" cell_manual "No confirmed running window — Capture cell cannot be automated." elif ! $HAS_VIRTUAL_AUDIO; then printf " MANUAL: No virtual audio source named 'lumotia-test' detected.\n" printf " Set one up (see docs/release/virtual-audio-setup.md), then in Lumotia →\n" printf " Settings → Start Here → Microphone, pick 'lumotia-test'.\n" printf " Once configured, re-run this script to automate the Capture cell.\n" cell_manual "Virtual audio source 'lumotia-test' not found — audio capture requires a human tester." else # All prereqs met: focus the window and drive the hotkey # Default record hotkey is Super+Shift+Space; adjust if the user has changed it. RECORD_HOTKEY="${LUMOTIA_RECORD_HOTKEY:-super+shift+space}" FOUND_WINDOW=$(DISPLAY="${DISPLAY:-:0}" xdotool search --name "Lumotia" 2>/dev/null | head -1 || true) if [[ -z "$FOUND_WINDOW" ]]; then printf " MANUAL: Could not re-locate the Lumotia window to send the hotkey.\n" cell_manual "xdotool search failed at Capture time — drive recording manually." else printf " Focusing window and pressing record hotkey (%s)...\n" "$RECORD_HOTKEY" DISPLAY="${DISPLAY:-:0}" xdotool windowfocus --sync "$FOUND_WINDOW" 2>/dev/null || true sleep 0.5 DISPLAY="${DISPLAY:-:0}" xdotool key --clearmodifiers "$RECORD_HOTKEY" 2>/dev/null || true printf " Recording for 5 seconds...\n" CAPTURE_STARTED_AT="$(date +%s)" sleep 5 printf " Pressing record hotkey again to stop...\n" DISPLAY="${DISPLAY:-:0}" xdotool key --clearmodifiers "$RECORD_HOTKEY" 2>/dev/null || true printf " Waiting up to 15 s for transcription to complete...\n" sleep 15 CAPTURE_RAN=true cell_pass "Record hotkey sent twice (start + stop). Transcription settling time elapsed." printf " Note: Actual transcript content is NOT printed here (privacy invariant).\n" fi fi # Tear down the app if it is still running and we own the PID if [[ -n "$BINARY_PID" ]] && kill -0 "$BINARY_PID" 2>/dev/null; then kill -TERM "$BINARY_PID" 2>/dev/null || true sleep 1 kill -KILL "$BINARY_PID" 2>/dev/null || true wait "$BINARY_PID" 2>/dev/null || true fi # ── Cell 4/7: Cleanup ───────────────────────────────────────────────────────── cell_header "4/7" "Cleanup (LLM-cleaned transcript in DB)" if ! $CAPTURE_RAN; then printf " MANUAL: Stop recording. Confirm the cleaned transcript\n" printf " appears beneath the raw transcript in the PostCaptureCard.\n" cell_manual "Capture cell did not run automatically — Cleanup requires a prior automated capture." elif ! $HAS_SQLITE3; then printf " MANUAL: sqlite3 not found — cannot query the database directly.\n" printf " Inspect the PostCaptureCard in the app to confirm cleaned text is present.\n" cell_manual "sqlite3 not found — Cleanup cell assertion requires manual verification." elif [[ ! -f "$DB_PATH" ]]; then printf " Database not found at: %s\n" "$DB_PATH" cell_fail "transcripts.db not found — was the app launched with the correct data dir?" else # Poll for a transcript row created in the last 30 seconds with cleaned_text set printf " Querying %s for a recent cleaned transcript...\n" "$DB_PATH" THIRTY_SECONDS_AGO=$(( $(date +%s) - 30 )) # SQLite stores timestamps as ISO-8601 text. We compare as strings (they sort correctly). THRESHOLD_ISO=$(date -u -d "@$THIRTY_SECONDS_AGO" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null \ || date -u -r "$THIRTY_SECONDS_AGO" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null \ || date -u '+%Y-%m-%dT%H:%M:%S' --date="-30 seconds" 2>/dev/null \ || echo "") if [[ -z "$THRESHOLD_ISO" ]]; then cell_manual "Could not compute ISO timestamp threshold — Cleanup assertion skipped." else CLEANED_COUNT=$(sqlite3 "$DB_PATH" \ "SELECT COUNT(*) FROM transcripts WHERE created_at >= '$THRESHOLD_ISO' AND cleaned_text IS NOT NULL AND cleaned_text != '';" \ 2>/dev/null || echo "0") if [[ "$CLEANED_COUNT" -gt 0 ]]; then cell_pass "Found $CLEANED_COUNT transcript(s) with cleaned_text in the last 30 seconds." else RAW_COUNT=$(sqlite3 "$DB_PATH" \ "SELECT COUNT(*) FROM transcripts WHERE created_at >= '$THRESHOLD_ISO';" \ 2>/dev/null || echo "0") if [[ "$RAW_COUNT" -gt 0 ]]; then cell_fail "Found $RAW_COUNT recent transcript(s) but cleaned_text is NULL — LLM cleanup may have failed." else cell_fail "No recent transcripts in the DB (threshold: $THRESHOLD_ISO). Capture may not have saved." fi fi fi fi # ── Cell 5/7: Export ────────────────────────────────────────────────────────── cell_header "5/7" "Export (Markdown file on disk)" if ! $CAPTURE_RAN; then printf " MANUAL: From the PostCaptureCard or History, export the transcript\n" printf " as Markdown via the native save dialog. Confirm the .md file\n" printf " is created on disk with the expected content.\n" cell_manual "Capture cell did not run — Export requires a prior automated capture." elif ! $HAS_XDOTOOL; then printf " MANUAL: Use the app's export action to save the transcript as Markdown.\n" printf " Confirm the .md file appears in your home directory.\n" cell_manual "xdotool not found — Export keystroke cannot be sent automatically." else # The export shortcut is Ctrl+E (default). The save dialog is native OS — we cannot # drive it with xdotool reliably across all desktop environments. Instead, we check # whether any .md file was created in the last 30 seconds in the user's home directory. printf " Checking for a Markdown export file created in the last 30 seconds...\n" EXPORT_FILE=$(find "$HOME" -maxdepth 3 -name "*.md" -newer /tmp/lumotia-smoke-driver-launch.log \ 2>/dev/null | head -1 || true) if [[ -n "$EXPORT_FILE" ]]; then # Verify frontmatter is present (non-empty --- block at the start of the file) if grep -q "^---" "$EXPORT_FILE" 2>/dev/null; then cell_pass "Export file found with frontmatter present." printf " Path redacted (privacy invariant). Filename suffix: …%s\n" "${EXPORT_FILE: -20}" else cell_fail "Export file found but no YAML frontmatter (--- block) detected at the start." fi else printf " No .md file found. If the native save dialog appeared, accept it and re-run.\n" printf " MANUAL: In Lumotia → PostCaptureCard, press the Export button (or Ctrl+E),\n" printf " save the file, then re-run this script to check Cell 5.\n" cell_manual "No .md export file detected — Export cell requires manual action." fi fi # ── Cell 6/7: History search ────────────────────────────────────────────────── cell_header "6/7" "History search (FTS5 index)" if ! $HAS_SQLITE3; then printf " MANUAL: Open History (sidebar). Type a word from your dictation\n" printf " in the search box. Confirm the transcript appears in results.\n" cell_manual "sqlite3 not found — FTS5 search requires manual verification." elif [[ ! -f "$DB_PATH" ]]; then printf " Database not found at: %s\n" "$DB_PATH" if $CAPTURE_RAN; then cell_fail "transcripts.db not found after an automated capture completed — data dir may be wrong." else printf " No capture has run yet. Complete a recording first to populate the index.\n" cell_manual "No database found and no capture ran — History search cannot be verified automatically." fi else # We cannot read transcript text (privacy invariant). Instead we verify: # 1. The FTS5 virtual table exists. # 2. At least one row exists in the index (implying the index is populated). printf " Checking FTS5 index in %s...\n" "$DB_PATH" FTS_TABLE=$(sqlite3 "$DB_PATH" \ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%fts%' LIMIT 1;" \ 2>/dev/null || echo "") if [[ -z "$FTS_TABLE" ]]; then cell_fail "No FTS5 table found in the database. History search is likely broken." else FTS_ROW_COUNT=$(sqlite3 "$DB_PATH" \ "SELECT COUNT(*) FROM \"$FTS_TABLE\";" \ 2>/dev/null || echo "0") if [[ "$FTS_ROW_COUNT" -gt 0 ]]; then cell_pass "FTS5 table '$FTS_TABLE' exists and contains $FTS_ROW_COUNT indexed row(s)." elif $CAPTURE_RAN; then cell_fail "FTS5 table '$FTS_TABLE' exists but is empty after a capture was completed — indexing may be broken." else printf " FTS5 table exists but is empty (no capture ran to populate it).\n" printf " MANUAL: After completing a recording, verify that History search returns it.\n" cell_manual "FTS5 table is empty — no capture ran to populate the index." fi fi fi # ── Cell 7/7: Uninstall + reinstall preserves transcripts ──────────────────── cell_header "7/7" "Uninstall + reinstall preserves transcripts (dogfood rebrand drill)" DRILL="$REPO_ROOT/scripts/dogfood-rebrand-drill.sh" if [[ ! -f "$DRILL" ]]; then cell_fail "scripts/dogfood-rebrand-drill.sh not found — restore it before smoke-testing." elif [[ ! -x "$DRILL" ]]; then cell_fail "scripts/dogfood-rebrand-drill.sh is not executable — run: chmod +x scripts/dogfood-rebrand-drill.sh" else if bash "$DRILL" >/tmp/lumotia-smoke-driver-drill.log 2>&1; then cell_pass "dogfood-rebrand-drill.sh passed — data-dir migration + preservation verified (8/8 probes)." else cell_fail "dogfood-rebrand-drill.sh reported failures. See /tmp/lumotia-smoke-driver-drill.log for details." tail -20 /tmp/lumotia-smoke-driver-drill.log | sed 's/^/ /' || true fi fi # ── Summary ─────────────────────────────────────────────────────────────────── TOTAL=$((CELL_PASS + CELL_FAIL + CELL_MANUAL)) printf "\n" printf "${BOLD}── Smoke driver summary ──────────────────────────────────────────────${RESET}\n" printf " Automated PASS : %d / %d\n" "$CELL_PASS" "$TOTAL" printf " MANUAL : %d / %d\n" "$CELL_MANUAL" "$TOTAL" printf " FAIL : %d / %d\n" "$CELL_FAIL" "$TOTAL" if $HAS_VIRTUAL_AUDIO && $HAS_XDOTOOL && $HAS_SQLITE3; then printf "\n All prereqs present. Cells closed automatically: Install, First-run,\n" printf " Capture, Cleanup, History search, Uninstall+reinstall (6/7 when audio\n" printf " source is wired to the app). Export is semi-automated (file detection).\n" else printf "\n Missing prereqs:\n" $HAS_XDOTOOL || printf " • xdotool — enables First-run window detection + Capture hotkey driving\n" $HAS_SQLITE3 || printf " • sqlite3 — enables Cleanup + History search assertions\n" $HAS_VIRTUAL_AUDIO || printf " • lumotia-test audio source — see docs/release/virtual-audio-setup.md\n" fi printf "\n" if [[ $CELL_FAIL -eq 0 ]]; then printf "${GREEN}${BOLD}✓ Linux smoke-driver complete: %d/%d automated PASS, %d/%d require manual verification.${RESET}\n\n" \ "$CELL_PASS" "$TOTAL" "$CELL_MANUAL" "$TOTAL" exit 0 else printf "${RED}${BOLD}✗ Linux smoke-driver: %d/%d PASS, %d/%d FAILED, %d/%d manual.${RESET}\n\n" \ "$CELL_PASS" "$TOTAL" "$CELL_FAIL" "$TOTAL" "$CELL_MANUAL" "$TOTAL" exit 1 fi