mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 08:36:35 +03:00
feat: add integration tests for build artifacts
Validate deb, rpm, and appimage packages after build in CI. Tests verify package metadata, file layout, desktop entries, icons, launcher scripts, asar contents (frame-fix, cowork, native stub, tray icons), and --doctor smoke tests. Runs as a reusable workflow with matrix strategy (one job per format) between build and release jobs, gating releases on passing artifact validation.
This commit is contained in:
102
tests/test-artifact-appimage.sh
Normal file
102
tests/test-artifact-appimage.sh
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/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"
|
||||
|
||||
# --- Cleanup ---
|
||||
rm -rf "$extract_dir"
|
||||
|
||||
print_summary
|
||||
137
tests/test-artifact-common.sh
Normal file
137
tests/test-artifact-common.sh
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for artifact validation tests
|
||||
|
||||
_pass_count=0
|
||||
_fail_count=0
|
||||
|
||||
pass() {
|
||||
printf '[PASS] %s\n' "$*"
|
||||
_pass_count=$((_pass_count + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[FAIL] %s\n' "$*" >&2
|
||||
_fail_count=$((_fail_count + 1))
|
||||
}
|
||||
|
||||
assert_file_exists() {
|
||||
if [[ -f $1 ]]; then
|
||||
pass "File exists: $1"
|
||||
else
|
||||
fail "File missing: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_dir_exists() {
|
||||
if [[ -d $1 ]]; then
|
||||
pass "Directory exists: $1"
|
||||
else
|
||||
fail "Directory missing: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_executable() {
|
||||
if [[ -x $1 ]]; then
|
||||
pass "Executable: $1"
|
||||
else
|
||||
fail "Not executable: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" pattern="$2" desc="${3:-}"
|
||||
if grep -q "$pattern" "$file" 2>/dev/null; then
|
||||
pass "${desc:-"$file contains '$pattern'"}"
|
||||
else
|
||||
fail "${desc:-"$file does not contain '$pattern'"}"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_command_succeeds() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
pass "$desc"
|
||||
else
|
||||
fail "$desc (exit code: $?)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate app contents inside an Electron resources directory.
|
||||
# $1 = path to the resources/ dir containing app.asar
|
||||
validate_app_contents() {
|
||||
local resources_dir="$1"
|
||||
|
||||
assert_file_exists "$resources_dir/app.asar"
|
||||
assert_dir_exists "$resources_dir/app.asar.unpacked"
|
||||
|
||||
# Check unpacked contents (always available, no asar tool needed)
|
||||
assert_file_exists \
|
||||
"$resources_dir/app.asar.unpacked/node_modules/@ant/claude-native/index.js"
|
||||
assert_file_exists \
|
||||
"$resources_dir/app.asar.unpacked/cowork-vm-service.js"
|
||||
|
||||
# Extract app.asar for deeper inspection if tools available
|
||||
local extract_dir
|
||||
extract_dir=$(mktemp -d)
|
||||
|
||||
local extracted=false
|
||||
if command -v asar &>/dev/null; then
|
||||
asar extract "$resources_dir/app.asar" "$extract_dir/app" \
|
||||
&& extracted=true
|
||||
elif command -v npx &>/dev/null; then
|
||||
npx --yes @electron/asar extract \
|
||||
"$resources_dir/app.asar" "$extract_dir/app" 2>/dev/null \
|
||||
&& extracted=true
|
||||
fi
|
||||
|
||||
if [[ $extracted == true ]]; then
|
||||
# frame-fix files present
|
||||
assert_file_exists "$extract_dir/app/frame-fix-wrapper.js"
|
||||
assert_file_exists "$extract_dir/app/frame-fix-entry.js"
|
||||
|
||||
# package.json main points to frame-fix-entry.js
|
||||
assert_contains "$extract_dir/app/package.json" \
|
||||
'frame-fix-entry.js' \
|
||||
"package.json main field references frame-fix-entry.js"
|
||||
|
||||
# .vite/build/index.js exists (main process code)
|
||||
assert_file_exists "$extract_dir/app/.vite/build/index.js"
|
||||
|
||||
# claude-native stub exists inside asar
|
||||
assert_file_exists \
|
||||
"$extract_dir/app/node_modules/@ant/claude-native/index.js"
|
||||
|
||||
# cowork-vm-service.js exists inside asar
|
||||
assert_file_exists "$extract_dir/app/cowork-vm-service.js"
|
||||
|
||||
# frame-fix-entry.js loads the wrapper
|
||||
assert_contains "$extract_dir/app/frame-fix-entry.js" \
|
||||
'frame-fix-wrapper' \
|
||||
"frame-fix-entry.js loads wrapper"
|
||||
|
||||
# Tray icons present in resources
|
||||
local tray_count
|
||||
tray_count=$(find "$extract_dir/app/resources/" \
|
||||
-name 'Tray*' 2>/dev/null | wc -l)
|
||||
if [[ $tray_count -gt 0 ]]; then
|
||||
pass "Tray icons present ($tray_count files)"
|
||||
else
|
||||
fail "No tray icons found in app resources"
|
||||
fi
|
||||
else
|
||||
pass "Skipping asar extraction (tool not available)"
|
||||
fi
|
||||
|
||||
rm -rf "$extract_dir"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo
|
||||
echo '================================'
|
||||
printf 'Results: %d passed, %d failed\n' "$_pass_count" "$_fail_count"
|
||||
echo '================================'
|
||||
if [[ $_fail_count -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
113
tests/test-artifact-deb.sh
Normal file
113
tests/test-artifact-deb.sh
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration tests for .deb package artifacts
|
||||
|
||||
artifact_dir="${1:?Usage: $0 <artifact-dir>}"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=tests/test-artifact-common.sh
|
||||
source "$script_dir/test-artifact-common.sh"
|
||||
|
||||
# Find the .deb file
|
||||
deb_file=$(find "$artifact_dir" -name '*.deb' -type f | head -1)
|
||||
if [[ -z $deb_file ]]; then
|
||||
fail "No .deb file found in $artifact_dir"
|
||||
print_summary
|
||||
fi
|
||||
pass "Found deb: $(basename "$deb_file")"
|
||||
|
||||
# --- Package metadata ---
|
||||
pkg_info=$(dpkg-deb -I "$deb_file")
|
||||
|
||||
if echo "$pkg_info" | grep -q 'Package: claude-desktop'; then
|
||||
pass "Package name is claude-desktop"
|
||||
else
|
||||
fail "Package name is not claude-desktop"
|
||||
fi
|
||||
|
||||
if echo "$pkg_info" | grep -q 'Architecture: amd64'; then
|
||||
pass "Architecture is amd64"
|
||||
else
|
||||
fail "Architecture is not amd64"
|
||||
fi
|
||||
|
||||
if echo "$pkg_info" | grep -q 'Version:'; then
|
||||
pass "Version field present"
|
||||
else
|
||||
fail "Version field missing"
|
||||
fi
|
||||
|
||||
# --- Install the package ---
|
||||
# Use --force-depends since we only care about file placement
|
||||
if sudo dpkg -i --force-depends "$deb_file" 2>&1; then
|
||||
pass "dpkg -i succeeded"
|
||||
else
|
||||
fail "dpkg -i failed"
|
||||
fi
|
||||
|
||||
# --- File existence checks ---
|
||||
assert_executable '/usr/bin/claude-desktop'
|
||||
assert_file_exists '/usr/share/applications/claude-desktop.desktop'
|
||||
assert_dir_exists '/usr/lib/claude-desktop'
|
||||
assert_file_exists '/usr/lib/claude-desktop/launcher-common.sh'
|
||||
|
||||
# Electron binary
|
||||
electron_path='/usr/lib/claude-desktop/node_modules/electron/dist/electron'
|
||||
assert_file_exists "$electron_path"
|
||||
assert_executable "$electron_path"
|
||||
|
||||
# chrome-sandbox
|
||||
assert_file_exists \
|
||||
'/usr/lib/claude-desktop/node_modules/electron/dist/chrome-sandbox'
|
||||
|
||||
# --- Desktop entry validation ---
|
||||
desktop_file='/usr/share/applications/claude-desktop.desktop'
|
||||
assert_contains "$desktop_file" 'Exec=/usr/bin/claude-desktop' \
|
||||
"Desktop entry Exec field correct"
|
||||
assert_contains "$desktop_file" 'Type=Application' \
|
||||
"Desktop entry Type field correct"
|
||||
assert_contains "$desktop_file" 'Icon=claude-desktop' \
|
||||
"Desktop entry Icon field correct"
|
||||
|
||||
# Validate desktop file syntax if tool available
|
||||
if command -v desktop-file-validate &>/dev/null; then
|
||||
assert_command_succeeds "desktop-file-validate passes" \
|
||||
desktop-file-validate "$desktop_file"
|
||||
fi
|
||||
|
||||
# --- Icons ---
|
||||
icon_dir='/usr/share/icons/hicolor'
|
||||
icon_found=false
|
||||
for size in 16 24 32 48 64 256; do
|
||||
if [[ -f "$icon_dir/${size}x${size}/apps/claude-desktop.png" ]]; then
|
||||
icon_found=true
|
||||
fi
|
||||
done
|
||||
if [[ $icon_found == true ]]; then
|
||||
pass "At least one icon installed in hicolor"
|
||||
else
|
||||
fail "No icons found in hicolor"
|
||||
fi
|
||||
|
||||
# --- Launcher script content ---
|
||||
assert_contains '/usr/bin/claude-desktop' 'launcher-common.sh' \
|
||||
"Launcher sources launcher-common.sh"
|
||||
assert_contains '/usr/bin/claude-desktop' 'run_doctor' \
|
||||
"Launcher references run_doctor"
|
||||
assert_contains '/usr/bin/claude-desktop' 'build_electron_args' \
|
||||
"Launcher calls build_electron_args"
|
||||
|
||||
# --- App contents (asar) ---
|
||||
resources_dir='/usr/lib/claude-desktop/node_modules/electron/dist/resources'
|
||||
validate_app_contents "$resources_dir"
|
||||
|
||||
# --- Doctor smoke test ---
|
||||
# --doctor checks system state; some checks will fail in CI (no display,
|
||||
# etc.) but the script itself should not crash with signal or 127.
|
||||
doctor_exit=0
|
||||
/usr/bin/claude-desktop --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
|
||||
|
||||
print_summary
|
||||
92
tests/test-artifact-rpm.sh
Normal file
92
tests/test-artifact-rpm.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration tests for .rpm package artifacts
|
||||
|
||||
artifact_dir="${1:?Usage: $0 <artifact-dir>}"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=tests/test-artifact-common.sh
|
||||
source "$script_dir/test-artifact-common.sh"
|
||||
|
||||
# Find the .rpm file
|
||||
rpm_file=$(find "$artifact_dir" -name '*.rpm' -type f | head -1)
|
||||
if [[ -z $rpm_file ]]; then
|
||||
fail "No .rpm file found in $artifact_dir"
|
||||
print_summary
|
||||
fi
|
||||
pass "Found rpm: $(basename "$rpm_file")"
|
||||
|
||||
# --- RPM metadata ---
|
||||
rpm_info=$(rpm -qip "$rpm_file" 2>/dev/null)
|
||||
|
||||
if echo "$rpm_info" | grep -q 'Name.*claude-desktop'; then
|
||||
pass "Package name is claude-desktop"
|
||||
else
|
||||
fail "Package name is not claude-desktop"
|
||||
fi
|
||||
|
||||
# --- Install ---
|
||||
if rpm -ivh --nodeps "$rpm_file" 2>&1; then
|
||||
pass "rpm -ivh succeeded"
|
||||
else
|
||||
fail "rpm -ivh failed"
|
||||
fi
|
||||
|
||||
# --- File existence checks ---
|
||||
assert_executable '/usr/bin/claude-desktop'
|
||||
assert_file_exists '/usr/share/applications/claude-desktop.desktop'
|
||||
assert_dir_exists '/usr/lib/claude-desktop'
|
||||
assert_file_exists '/usr/lib/claude-desktop/launcher-common.sh'
|
||||
|
||||
# Electron binary
|
||||
electron_path='/usr/lib/claude-desktop/node_modules/electron/dist/electron'
|
||||
assert_file_exists "$electron_path"
|
||||
assert_executable "$electron_path"
|
||||
|
||||
# chrome-sandbox
|
||||
assert_file_exists \
|
||||
'/usr/lib/claude-desktop/node_modules/electron/dist/chrome-sandbox'
|
||||
|
||||
# --- Desktop entry validation ---
|
||||
desktop_file='/usr/share/applications/claude-desktop.desktop'
|
||||
assert_contains "$desktop_file" 'Exec=/usr/bin/claude-desktop' \
|
||||
"Desktop entry Exec correct"
|
||||
assert_contains "$desktop_file" 'Type=Application' \
|
||||
"Desktop entry Type correct"
|
||||
assert_contains "$desktop_file" 'Icon=claude-desktop' \
|
||||
"Desktop entry Icon correct"
|
||||
|
||||
# --- Icons ---
|
||||
icon_dir='/usr/share/icons/hicolor'
|
||||
icon_found=false
|
||||
for size in 16 24 32 48 64 256; do
|
||||
if [[ -f "$icon_dir/${size}x${size}/apps/claude-desktop.png" ]]; then
|
||||
icon_found=true
|
||||
fi
|
||||
done
|
||||
if [[ $icon_found == true ]]; then
|
||||
pass "At least one icon installed in hicolor"
|
||||
else
|
||||
fail "No icons found in hicolor"
|
||||
fi
|
||||
|
||||
# --- Launcher script content ---
|
||||
assert_contains '/usr/bin/claude-desktop' 'launcher-common.sh' \
|
||||
"Launcher sources launcher-common.sh"
|
||||
assert_contains '/usr/bin/claude-desktop' 'run_doctor' \
|
||||
"Launcher references run_doctor"
|
||||
assert_contains '/usr/bin/claude-desktop' 'build_electron_args' \
|
||||
"Launcher calls build_electron_args"
|
||||
|
||||
# --- App contents (asar) ---
|
||||
resources_dir='/usr/lib/claude-desktop/node_modules/electron/dist/resources'
|
||||
validate_app_contents "$resources_dir"
|
||||
|
||||
# --- Doctor smoke test ---
|
||||
doctor_exit=0
|
||||
/usr/bin/claude-desktop --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
|
||||
|
||||
print_summary
|
||||
Reference in New Issue
Block a user