Merge pull request #338 from sabiut/feature/integration-tests

feat: add integration tests for build artifacts
This commit is contained in:
Aaddrick
2026-04-12 15:21:51 -04:00
committed by GitHub
6 changed files with 502 additions and 1 deletions

View File

@@ -53,6 +53,11 @@ jobs:
artifact_suffix: ${{ matrix.artifact_suffix }}
release_tag: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
test-artifacts:
name: Test Build Artifacts
needs: [build-amd64]
uses: ./.github/workflows/test-artifacts.yml
build-arm64:
name: Build Packages (arm64 - ${{ matrix.artifact_suffix }})
needs: test-flags
@@ -80,7 +85,7 @@ jobs:
release:
name: Create Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [test-flags, build-amd64, build-arm64]
needs: [test-flags, build-amd64, build-arm64, test-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write

52
.github/workflows/test-artifacts.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Test Build Artifacts (Reusable)
on:
workflow_call:
permissions:
contents: read
jobs:
test-artifact:
strategy:
fail-fast: false
matrix:
include:
- format: deb
artifact: package-amd64-deb
container: ""
- format: rpm
artifact: package-amd64-rpm
container: "fedora:42"
- format: appimage
artifact: package-amd64-appimage
container: ""
name: Validate ${{ matrix.format }} package
runs-on: ubuntu-latest
container: ${{ matrix.container || '' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ${{ matrix.artifact }}
path: artifacts/
- name: Install test dependencies (Fedora)
if: matrix.format == 'rpm'
run: dnf install -y findutils file nodejs npm
- name: Install test dependencies (Ubuntu)
if: matrix.format != 'rpm'
run: |
sudo apt-get update
sudo apt-get install -y file libfuse2 nodejs npm
- name: Run artifact tests
run: |
chmod +x tests/test-artifact-${{ matrix.format }}.sh
tests/test-artifact-${{ matrix.format }}.sh artifacts/

View 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

View 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++))
}
fail() {
printf '[FAIL] %s\n' "$*" >&2
((_fail_count++))
}
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
View 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 [[ $pkg_info == *'Package: claude-desktop'* ]]; then
pass "Package name is claude-desktop"
else
fail "Package name is not claude-desktop"
fi
if [[ $pkg_info == *'Architecture: amd64'* ]]; then
pass "Architecture is amd64"
else
fail "Architecture is not amd64"
fi
if [[ $pkg_info == *'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"; 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

View 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 [[ $rpm_info =~ 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"; 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