feat: add BATS unit tests for launcher-common.sh (#395)

* feat: add BATS unit tests for launcher-common.sh

scripts/launcher-common.sh is 798 lines handling critical startup logic —
display detection, Electron arg building, stale lock cleanup, cowork daemon
cleanup, and the full --doctor diagnostic system — but had zero test coverage.
A regression in any of these functions could silently break app launches across
display servers and package formats.

Add 48 BATS tests covering:
- setup_logging / log_message (XDG_CACHE_HOME fallback)
- detect_display_backend (X11, Wayland, XWayland, Niri auto-detect)
- build_electron_args (all display × package-type combinations)
- setup_electron_env (ELECTRON_FORCE_IS_PACKAGED, title bar)
- cleanup_stale_lock (dead PID removal, live PID preservation)
- cleanup_stale_cowork_socket (stale unix socket removal)
- Doctor helpers:
  - _pass / _fail / _warn / _info output
  - _cowork_distro_id
  - _cowork_pkg_hint (distro-specific package mapping)
  - _electron_version

Tests run fully sandboxed:
- HOME, XDG_CACHE_HOME, XDG_CONFIG_HOME, and XDG_RUNTIME_DIR redirected to a temp directory
- Host display variables cleared in setup() to prevent state leakage

* refactor: extract has_electron_arg helper to reduce test boilerplate

Replace repeated loop-and-flag patterns across 7 build_electron_args
tests with a shared has_electron_arg helper that supports glob matching.
Removes ~40 lines of duplicated code with no change in test coverage.
This commit is contained in:
Sum Abiut
2026-04-22 23:49:20 +11:00
committed by GitHub
parent e5e1349e2a
commit 2543ee58bc

479
tests/launcher-common.bats Normal file
View File

@@ -0,0 +1,479 @@
#!/usr/bin/env bats
#
# launcher-common.bats
# Tests for launcher utility functions in scripts/launcher-common.sh
#
SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)"
# Check whether a value exists in the electron_args array.
# Supports glob patterns (e.g., '*WaylandWindowDecorations*').
has_electron_arg() {
local pattern="$1"
local arg
for arg in "${electron_args[@]}"; do
# shellcheck disable=SC2254
[[ $arg == $pattern ]] && return 0
done
return 1
}
setup() {
TEST_TMP=$(mktemp -d)
export TEST_TMP
# Redirect all filesystem-touching functions to temp dirs
export HOME="$TEST_TMP/home"
export XDG_CACHE_HOME="$TEST_TMP/cache"
export XDG_CONFIG_HOME="$TEST_TMP/config"
export XDG_RUNTIME_DIR="$TEST_TMP/run"
mkdir -p "$HOME" "$XDG_CACHE_HOME" "$XDG_CONFIG_HOME" "$XDG_RUNTIME_DIR"
# Clear display/wayland variables to avoid leaking host state
unset DISPLAY
unset WAYLAND_DISPLAY
unset CLAUDE_USE_WAYLAND
unset NIRI_SOCKET
unset XDG_CURRENT_DESKTOP
unset CLAUDE_MENU_BAR
unset COWORK_VM_BACKEND
# shellcheck source=scripts/launcher-common.sh
source "$SCRIPT_DIR/../scripts/launcher-common.sh"
}
teardown() {
if [[ -n "$TEST_TMP" && -d "$TEST_TMP" ]]; then
rm -rf "$TEST_TMP"
fi
}
# =============================================================================
# setup_logging
# =============================================================================
@test "setup_logging: creates log directory and sets log_file" {
run setup_logging
[[ $status -eq 0 ]]
[[ -d "$XDG_CACHE_HOME/claude-desktop-debian" ]]
}
@test "setup_logging: sets log_file under XDG_CACHE_HOME" {
setup_logging
[[ $log_file == "$XDG_CACHE_HOME/claude-desktop-debian/launcher.log" ]]
}
@test "setup_logging: falls back to HOME/.cache when XDG_CACHE_HOME unset" {
unset XDG_CACHE_HOME
setup_logging
[[ $log_dir == "$HOME/.cache/claude-desktop-debian" ]]
[[ -d "$HOME/.cache/claude-desktop-debian" ]]
}
# =============================================================================
# log_message
# =============================================================================
@test "log_message: appends message to log file" {
setup_logging
log_message "test message one"
log_message "test message two"
[[ -f $log_file ]]
run cat "$log_file"
[[ "${lines[0]}" == "test message one" ]]
[[ "${lines[1]}" == "test message two" ]]
}
# =============================================================================
# check_display
# =============================================================================
@test "check_display: fails when no display variables set" {
unset DISPLAY
unset WAYLAND_DISPLAY
run check_display
[[ $status -ne 0 ]]
}
@test "check_display: succeeds with DISPLAY set" {
DISPLAY=":0"
run check_display
[[ $status -eq 0 ]]
}
@test "check_display: succeeds with WAYLAND_DISPLAY set" {
WAYLAND_DISPLAY="wayland-0"
run check_display
[[ $status -eq 0 ]]
}
@test "check_display: succeeds with both set" {
DISPLAY=":0"
WAYLAND_DISPLAY="wayland-0"
run check_display
[[ $status -eq 0 ]]
}
# =============================================================================
# detect_display_backend
# =============================================================================
@test "detect_display_backend: X11 session sets is_wayland=false" {
DISPLAY=":0"
setup_logging
detect_display_backend
[[ $is_wayland == false ]]
}
@test "detect_display_backend: Wayland session sets is_wayland=true" {
WAYLAND_DISPLAY="wayland-0"
setup_logging
detect_display_backend
[[ $is_wayland == true ]]
}
@test "detect_display_backend: defaults to XWayland on Wayland" {
WAYLAND_DISPLAY="wayland-0"
setup_logging
detect_display_backend
[[ $is_wayland == true ]]
[[ $use_x11_on_wayland == true ]]
}
@test "detect_display_backend: CLAUDE_USE_WAYLAND=1 forces native Wayland" {
WAYLAND_DISPLAY="wayland-0"
CLAUDE_USE_WAYLAND=1
setup_logging
detect_display_backend
[[ $is_wayland == true ]]
[[ $use_x11_on_wayland == false ]]
}
@test "detect_display_backend: Niri detected via NIRI_SOCKET forces native Wayland" {
WAYLAND_DISPLAY="wayland-0"
NIRI_SOCKET="/tmp/niri.sock"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == false ]]
}
@test "detect_display_backend: Niri detected via XDG_CURRENT_DESKTOP forces native Wayland" {
WAYLAND_DISPLAY="wayland-0"
XDG_CURRENT_DESKTOP="niri"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == false ]]
}
@test "detect_display_backend: Niri in colon-separated XDG_CURRENT_DESKTOP" {
WAYLAND_DISPLAY="wayland-0"
XDG_CURRENT_DESKTOP="niri:GNOME"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == false ]]
}
@test "detect_display_backend: Niri case-insensitive detection" {
WAYLAND_DISPLAY="wayland-0"
XDG_CURRENT_DESKTOP="NIRI"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == false ]]
}
@test "detect_display_backend: non-Niri Wayland keeps XWayland default" {
WAYLAND_DISPLAY="wayland-0"
XDG_CURRENT_DESKTOP="sway"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == true ]]
}
@test "detect_display_backend: Niri not forced when CLAUDE_USE_WAYLAND already set" {
# CLAUDE_USE_WAYLAND=1 already forces native, Niri detection shouldn't conflict
WAYLAND_DISPLAY="wayland-0"
CLAUDE_USE_WAYLAND=1
NIRI_SOCKET="/tmp/niri.sock"
setup_logging
detect_display_backend
[[ $use_x11_on_wayland == false ]]
}
# =============================================================================
# build_electron_args
# =============================================================================
@test "build_electron_args: X11 deb - only CustomTitlebar disabled" {
is_wayland=false
setup_logging
build_electron_args deb
has_electron_arg '--disable-features=CustomTitlebar'
# shellcheck disable=SC2314 # last command in test, ! works correctly
! has_electron_arg '--no-sandbox'
}
@test "build_electron_args: X11 appimage - includes --no-sandbox" {
is_wayland=false
setup_logging
build_electron_args appimage
has_electron_arg '--no-sandbox'
}
@test "build_electron_args: Wayland XWayland deb - includes x11 platform and no-sandbox" {
is_wayland=true
use_x11_on_wayland=true
setup_logging
build_electron_args deb
has_electron_arg '--ozone-platform=x11'
has_electron_arg '--no-sandbox'
}
@test "build_electron_args: Wayland native deb - includes wayland platform flags" {
is_wayland=true
use_x11_on_wayland=false
setup_logging
build_electron_args deb
has_electron_arg '--ozone-platform=wayland'
has_electron_arg '--enable-wayland-ime'
has_electron_arg '*WaylandWindowDecorations*'
}
@test "build_electron_args: Wayland appimage - always includes --no-sandbox" {
is_wayland=true
use_x11_on_wayland=true
setup_logging
build_electron_args appimage
has_electron_arg '--no-sandbox'
}
@test "build_electron_args: Wayland native nix - includes --no-sandbox" {
is_wayland=true
use_x11_on_wayland=false
setup_logging
build_electron_args nix
has_electron_arg '--no-sandbox'
}
@test "build_electron_args: Wayland native includes text-input-version=3" {
is_wayland=true
use_x11_on_wayland=false
setup_logging
build_electron_args deb
has_electron_arg '--wayland-text-input-version=3'
}
# =============================================================================
# setup_electron_env
# =============================================================================
@test "setup_electron_env: sets ELECTRON_FORCE_IS_PACKAGED" {
setup_electron_env
[[ $ELECTRON_FORCE_IS_PACKAGED == 'true' ]]
}
@test "setup_electron_env: sets ELECTRON_USE_SYSTEM_TITLE_BAR" {
setup_electron_env
[[ $ELECTRON_USE_SYSTEM_TITLE_BAR == '1' ]]
}
# =============================================================================
# cleanup_stale_lock
# =============================================================================
@test "cleanup_stale_lock: no lock file - returns 0" {
mkdir -p "$XDG_CONFIG_HOME/Claude"
run cleanup_stale_lock
[[ $status -eq 0 ]]
}
@test "cleanup_stale_lock: removes stale lock (dead PID)" {
local config_dir="$XDG_CONFIG_HOME/Claude"
mkdir -p "$config_dir"
# Use PID 99999999 which almost certainly doesn't exist
ln -s "myhost-99999999" "$config_dir/SingletonLock"
setup_logging
cleanup_stale_lock
[[ ! -L "$config_dir/SingletonLock" ]]
}
@test "cleanup_stale_lock: keeps lock for running process" {
local config_dir="$XDG_CONFIG_HOME/Claude"
mkdir -p "$config_dir"
# Use our own PID (guaranteed to be running)
ln -s "myhost-$$" "$config_dir/SingletonLock"
setup_logging
cleanup_stale_lock
# Lock should still exist
[[ -L "$config_dir/SingletonLock" ]]
}
@test "cleanup_stale_lock: handles non-numeric PID in lock target" {
local config_dir="$XDG_CONFIG_HOME/Claude"
mkdir -p "$config_dir"
ln -s "myhost-notanumber" "$config_dir/SingletonLock"
setup_logging
run cleanup_stale_lock
[[ $status -eq 0 ]]
# Lock should still exist (function returns early on non-numeric)
[[ -L "$config_dir/SingletonLock" ]]
}
@test "cleanup_stale_lock: handles regular file (not symlink)" {
local config_dir="$XDG_CONFIG_HOME/Claude"
mkdir -p "$config_dir"
echo "not a symlink" > "$config_dir/SingletonLock"
setup_logging
run cleanup_stale_lock
[[ $status -eq 0 ]]
# Regular file should not be touched
[[ -f "$config_dir/SingletonLock" ]]
}
# =============================================================================
# cleanup_stale_cowork_socket
# =============================================================================
@test "cleanup_stale_cowork_socket: no socket - returns 0" {
run cleanup_stale_cowork_socket
[[ $status -eq 0 ]]
}
@test "cleanup_stale_cowork_socket: removes stale socket file" {
# Create a socket-like file (not a real socket, but -S check needs a socket)
# Use python to create a real unix socket for the test
local sock="$XDG_RUNTIME_DIR/cowork-vm-service.sock"
python3 -c "
import socket, sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(sys.argv[1])
s.close()
" "$sock" 2>/dev/null || skip "Cannot create test unix socket"
setup_logging
# socat connection should fail since nothing is listening
cleanup_stale_cowork_socket
[[ ! -S "$sock" ]]
}
# =============================================================================
# Doctor helper functions
# =============================================================================
@test "_doctor_colors: sets color vars when stdout is a terminal" {
# Force non-terminal to test the else branch
_doctor_colors
# When not a terminal, all should be empty
[[ -z $_green ]]
[[ -z $_red ]]
[[ -z $_yellow ]]
[[ -z $_bold ]]
[[ -z $_reset ]]
}
@test "_pass: outputs PASS with message" {
_doctor_colors
run _pass "test passed"
[[ $output == *"[PASS]"* ]]
[[ $output == *"test passed"* ]]
}
@test "_fail: outputs FAIL with message and increments counter" {
_doctor_colors
_doctor_failures=0
_fail "something broke"
[[ $_doctor_failures -eq 1 ]]
}
@test "_warn: outputs WARN with message" {
_doctor_colors
run _warn "warning message"
[[ $output == *"[WARN]"* ]]
[[ $output == *"warning message"* ]]
}
@test "_info: outputs indented message" {
_doctor_colors
run _info "info message"
[[ $output == *"info message"* ]]
}
# =============================================================================
# _cowork_distro_id
# =============================================================================
@test "_cowork_distro_id: reads ID from /etc/os-release" {
# This test uses the real /etc/os-release on the test system
[[ -f /etc/os-release ]] || skip "No /etc/os-release"
local result
result=$(_cowork_distro_id)
# Should return something non-empty
[[ -n $result ]]
[[ $result != 'unknown' ]]
}
# =============================================================================
# _cowork_pkg_hint
# =============================================================================
@test "_cowork_pkg_hint: debian uses apt" {
local result
result=$(_cowork_pkg_hint debian bubblewrap)
[[ $result == "sudo apt install bubblewrap" ]]
}
@test "_cowork_pkg_hint: ubuntu uses apt" {
local result
result=$(_cowork_pkg_hint ubuntu socat)
[[ $result == "sudo apt install socat" ]]
}
@test "_cowork_pkg_hint: fedora uses dnf" {
local result
result=$(_cowork_pkg_hint fedora bubblewrap)
[[ $result == "sudo dnf install bubblewrap" ]]
}
@test "_cowork_pkg_hint: arch uses pacman" {
local result
result=$(_cowork_pkg_hint arch socat)
[[ $result == "sudo pacman -S socat" ]]
}
@test "_cowork_pkg_hint: qemu maps to distro-specific packages" {
local result
result=$(_cowork_pkg_hint debian qemu)
[[ $result == "sudo apt install qemu-system-x86 qemu-utils" ]]
result=$(_cowork_pkg_hint fedora qemu)
[[ $result == "sudo dnf install qemu-kvm qemu-img" ]]
result=$(_cowork_pkg_hint arch qemu)
[[ $result == "sudo pacman -S qemu-full" ]]
}
@test "_cowork_pkg_hint: unknown distro gives generic message" {
local result
result=$(_cowork_pkg_hint gentoo bubblewrap)
[[ $result == "Install bubblewrap using your package manager" ]]
}
# =============================================================================
# _electron_version
# =============================================================================
@test "_electron_version: reads version from file beside binary" {
mkdir -p "$TEST_TMP/electron"
echo "33.4.0" > "$TEST_TMP/electron/version"
touch "$TEST_TMP/electron/electron"
local result
result=$(_electron_version "$TEST_TMP/electron/electron")
[[ $result == "33.4.0" ]]
}
@test "_electron_version: returns empty when version file missing" {
mkdir -p "$TEST_TMP/electron"
touch "$TEST_TMP/electron/electron"
local result
result=$(_electron_version "$TEST_TMP/electron/electron") || true
[[ -z $result ]]
}