Files
Lumotia/scripts/smoke-linux-driver.sh
Jake 3770815fbf
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia — v0.1 release-completion run
Closes the code-side v0.1 ship gate. All quality gates green:
cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13,
scripts/dogfood-rebrand-drill.sh 8/8.

Phase F — first-run onboarding promoted to v0.1
- FirstRunPage with skip-to-main + failure recovery + event recording
- Six onboarding commands (record/list/has-completed + lumotia_events)
- Storage migration v17 (onboarding_events + lumotia_events tables)

UI hardening (in-scope items from v0.1-ui-hardening.md)
- StatusPill + PostCaptureCard components, 21st preview entry
- Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion)
- Settings 6-section regroup + Help section + Activation log + Privacy toggle
- Error-state copy sweep (DictationPage + SettingsPage, plain-language)
- Global :focus-visible rule, textarea outlines restored
- Ctrl+K / Ctrl+, / Escape bindings in +layout

LLM resilience
- rule_based_extract_tasks (regex-free imperative-verb extractor) +
  extract_tasks_with_fallback wrapper — task extraction never returns zero
- tokio::time::timeout(120s) wraps cleanup/tags/tasks commands

Release artefacts
- LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format)
- v0.1-release-notes, privacy-and-ai-use, install-warnings,
  tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup,
  apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit
- Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed)
- AppImage SHA-256 sidecar in build.yml
- README v0.1 section + Reporting-issues; canonical repo slug

Closure pass — items moved from human-required to code-complete
- KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit
- KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...)
- acquire/release_idle_inhibit Tauri commands, wired in DictationPage
- Diagnostic-bundle frontend wire-up (Settings → Help button)
- WCAG-AA contrast fix via .btn-filled-text utility (no token changes)
- 8 destructive-action sites wrapped in plain-language confirm() guards
- KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed)

Scripts
- pre-tag-verify.sh, tag-day.sh, smoke-linux + driver
- parse-diagnostic-bundle.sh, parse-activation-log.py

Per-item audit trail: docs/release/v0.1-completion-status.md
Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix,
tester recruitment) — see docs/release/v0.1-known-limitations.md.
2026-05-15 06:59:08 +01:00

470 lines
20 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 26 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