Files
claude-desktop-debian/tests/test-artifact-appimage.sh
Sum Abiut b676519c58 test: add headless launch + --doctor smoke tests for AppImage artifact (#592)
The AppImage artifact test only validated package structure (extraction,
AppDir layout, asar contents) — runtime regressions like frame-fix-wrapper
syntax errors, bad asar patches, or Electron startup crashes silently
passed CI. The .deb path already ran `--doctor` as a smoke check; the
AppImage path now has parity plus a 10s headless launch under Xvfb.

`setsid` + `kill -- -PGID` is load-bearing: xvfb-run's EXIT trap leaks
Xvfb on signal kill, so running the whole stack in its own process group
lets the teardown reap xvfb-run, Xvfb, dbus, AppRun, electron, and zygote
children together. `procps` (for pkill), `dbus-x11`, and `xvfb` added to
the CI apt line.

The headless probe catches main-process startup failures only — GPU /
renderer-process crashes like #583 leave the main process alive and pass
this check; that scope disclaimer is inlined at test-artifact-appimage.sh
lines 114-117 so future contributors don't try to claim #583 coverage by
switching Xvfb off.

Co-authored-by: Sum Abiut <sabiut@users.noreply.github.com>
2026-05-16 10:15:39 -04:00

188 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Integration tests for AppImage artifacts
artifact_dir="${1:?Usage: $0 <artifact-dir>}"
artifact_dir="$(cd "$artifact_dir" && pwd)"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=tests/test-artifact-common.sh
source "$script_dir/test-artifact-common.sh"
component_id='io.github.aaddrick.claude-desktop-debian'
# Find the AppImage file (exclude .zsync)
appimage_file=$(find "$artifact_dir" -name '*.AppImage' \
! -name '*.zsync' -type f | head -1)
if [[ -z $appimage_file ]]; then
fail "No AppImage found in $artifact_dir"
print_summary
fi
pass "Found AppImage: $(basename "$appimage_file")"
# --- AppImage is executable ---
chmod +x "$appimage_file"
assert_executable "$appimage_file"
# --- File type check ---
file_type=$(file -b "$appimage_file")
if [[ $file_type == *"ELF"* ]] || [[ $file_type == *"executable"* ]]; then
pass "AppImage is an ELF executable"
else
fail "AppImage file type unexpected: $file_type"
fi
# --- Extract AppImage ---
extract_dir=$(mktemp -d)
cd "$extract_dir" || exit 1
"$appimage_file" --appimage-extract >/dev/null 2>&1
appdir="$extract_dir/squashfs-root"
if [[ -d $appdir ]]; then
pass "--appimage-extract succeeded"
else
fail "--appimage-extract failed (no squashfs-root)"
print_summary
fi
# --- AppDir structure ---
assert_file_exists "$appdir/AppRun"
assert_executable "$appdir/AppRun"
# Top-level desktop entry
if [[ -f "$appdir/${component_id}.desktop" ]]; then
pass "Top-level .desktop file exists"
assert_contains "$appdir/${component_id}.desktop" \
'Type=Application' "Desktop entry Type correct"
assert_contains "$appdir/${component_id}.desktop" \
'Exec=AppRun' "Desktop entry Exec points to AppRun"
else
fail "No top-level .desktop file"
fi
# Desktop entry in standard location
assert_file_exists \
"$appdir/usr/share/applications/${component_id}.desktop"
# Top-level icon
if [[ -f "$appdir/${component_id}.png" ]]; then
pass "Top-level icon present"
else
fail "No top-level icon found"
fi
# .DirIcon
assert_file_exists "$appdir/.DirIcon"
# AppStream metadata
assert_file_exists \
"$appdir/usr/share/metainfo/${component_id}.appdata.xml"
# --- Electron binary ---
electron_path="$appdir/usr/lib/node_modules/electron/dist/electron"
assert_file_exists "$electron_path"
assert_executable "$electron_path"
# --- Launcher library ---
assert_file_exists "$appdir/usr/lib/claude-desktop/launcher-common.sh"
# --- AppRun content ---
assert_contains "$appdir/AppRun" 'launcher-common.sh' \
"AppRun sources launcher-common.sh"
assert_contains "$appdir/AppRun" 'run_doctor' \
"AppRun references run_doctor"
assert_contains "$appdir/AppRun" 'build_electron_args' \
"AppRun calls build_electron_args"
# --- App contents (asar) ---
resources_dir="$appdir/usr/lib/node_modules/electron/dist/resources"
validate_app_contents "$resources_dir" "${component_id}.desktop"
# --- Doctor smoke test ---
# Some --doctor checks fail in CI (no display, etc.); we only care that
# the script itself didn't crash via signal or exec failure (>=127).
doctor_exit=0
"$appimage_file" --doctor >/dev/null 2>&1 || doctor_exit=$?
if [[ $doctor_exit -lt 127 ]]; then
pass "--doctor runs without crashing (exit: $doctor_exit)"
else
fail "--doctor crashed (exit: $doctor_exit)"
fi
# --- Headless launch smoke test ---
# Catches startup-only regressions (asar/frame-fix-wrapper syntax errors)
# that pure structure checks miss.
#
# Scope: main-process startup failures only. GPU/renderer-process
# crashes (e.g. #583-class) leave the main process alive and pass
# this check — Xvfb has no GPU, so Electron falls back to SwiftShader
# and the GPU-crash path isn't exercised here.
if command -v xvfb-run &>/dev/null \
&& command -v dbus-run-session &>/dev/null \
&& command -v setsid &>/dev/null; then
# XDG_CACHE_HOME redirect so the test owns the launcher log.
cache_root=$(mktemp -d)
export XDG_CACHE_HOME="$cache_root"
launcher_log="$cache_root/claude-desktop-debian/launcher.log"
# setsid puts xvfb-run + Xvfb + dbus + AppRun + electron in a fresh
# process group; xvfb-run's EXIT trap alone leaves Xvfb behind on
# TERM, so we need kill -- -PGID below.
# AppRun redirects electron's stdout/stderr into launcher_log;
# xvfb_log captures xvfb-run's own stderr.
xvfb_log=$(mktemp)
setsid xvfb-run -a -s '-screen 0 1280x720x24' \
dbus-run-session -- "$appimage_file" \
>"$xvfb_log" 2>&1 &
launch_pid=$!
# Safety net: covers Ctrl-C, CI timeout, or any earlier `exit` so we
# never leak Xvfb/electron between launch and the explicit kill below.
trap '
kill -KILL -- "-$launch_pid" 2>/dev/null
pkill -KILL -f "$appimage_file" 2>/dev/null
rm -rf "$cache_root" "$xvfb_log"
' EXIT INT TERM
# CI is slow; 10s is the floor for Electron startup.
sleep 10
if kill -0 "$launch_pid" 2>/dev/null; then
pass "AppImage stays alive under Xvfb for 10s"
else
wait "$launch_pid" 2>/dev/null
exit_code=$?
fail "AppImage exited within 10s (exit: $exit_code)"
if [[ -f $launcher_log ]]; then
echo '--- launcher.log (last 40 lines) ---' >&2
tail -40 "$launcher_log" >&2
echo '------------------------------------' >&2
fi
if [[ -s $xvfb_log ]]; then
echo '--- xvfb-run stderr (last 20 lines) ---' >&2
tail -20 "$xvfb_log" >&2
echo '---------------------------------------' >&2
fi
fi
# Negative PID targets the process group.
kill -TERM -- "-$launch_pid" 2>/dev/null || true
sleep 1
kill -KILL -- "-$launch_pid" 2>/dev/null || true
wait "$launch_pid" 2>/dev/null || true
# Sweep any electron child that escaped the group (e.g. zygote).
pkill -KILL -f "$appimage_file" 2>/dev/null || true
rm -rf "$cache_root" "$xvfb_log"
unset XDG_CACHE_HOME
else
# Match the codebase convention (test-artifact-common.sh
# validate_app_contents): tool absence is a skip, not a failure.
# Loud failure on missing tools belongs at the workflow layer.
pass "Skipping launch smoke test (xvfb-run/dbus-run-session/setsid missing)"
fi
# --- Cleanup ---
rm -rf "$extract_dir"
print_summary