#!/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_setuid() { if [[ -u $1 ]]; then pass "Setuid bit set: $1" else fail "Setuid bit not set: $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. # Since the v3.0.0 patch-zero rebase the asar is the OFFICIAL bundle # (byte-identical unless a survivor patch ran), so this asserts the # upstream shape — no frame-fix files, no injected desktopName, no # stubbed claude-native. See docs/decisions.md D-002. # $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" # Official unpacked set: the real Rust native binding plus the # node-pty prebuild (arch-dependent subdir, hence find). The 2.x # unpacked stubs are gone by design; cowork-vm-service.js returned # in #776 but lives at the resources/ root (asserted below), never # in app.asar.unpacked. local native_binding pty_prebuild native_binding=$(find "$resources_dir/app.asar.unpacked" \ -name 'claude-native-binding.node' -type f | head -1) if [[ -n $native_binding ]]; then pass 'Unpacked: claude-native-binding.node present' else fail 'Unpacked: claude-native-binding.node missing' fi pty_prebuild=$(find "$resources_dir/app.asar.unpacked" \ -name 'pty.node' -type f | head -1) if [[ -n $pty_prebuild ]]; then pass 'Unpacked: node-pty prebuild present' else fail 'Unpacked: node-pty prebuild missing' fi # Cowork's bundled virtiofsd: the #771 un-gate patch makes it the # universal fallback, and the client resolves it with X_OK — a # repack that drops the exec bit silently kills Cowork on every # host without a client-probed system virtiofsd (the mode-loss # trap in docs/learnings/packaging-permissions.md). if [[ -x $resources_dir/virtiofsd ]]; then pass 'Bundled virtiofsd present and executable' elif [[ -e $resources_dir/virtiofsd ]]; then fail 'Bundled virtiofsd present but not executable' else fail 'Bundled virtiofsd missing from resources/' fi # The bwrap fallback daemon (#776): staged beside app.asar when # patch_cowork_bwrap is active (it is, in every current build). # The launcher spawns it via a system node, so presence is the # contract — no exec bit required. Without it, an opt-in # COWORK_VM_BACKEND=bwrap launch fails at spawn with doctor # pointing at a reinstall. if [[ -f $resources_dir/cowork-vm-service.js ]]; then pass 'Bundled cowork-vm-service.js present (bwrap daemon)' else fail 'Bundled cowork-vm-service.js missing from resources/' fi # 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 # Upstream entry point (main has shipped as index.js and # index.pre.js across releases — assert the stable prefix, # not the exact filename) assert_contains "$extract_dir/app/package.json" \ '"main": ".vite/build/' \ 'package.json main points into .vite/build/' # productName drives WM_CLASS; the build guard asserts the # same invariant at patch time (app-asar.sh) assert_contains "$extract_dir/app/package.json" \ '"productName": "Claude"' \ 'package.json productName is Claude' # Main process bundle exists local main_bundle main_bundle=$(find "$extract_dir/app/.vite/build" \ -maxdepth 1 -name 'index*.js' -type f | head -1) if [[ -n $main_bundle ]]; then pass 'Main process bundle present in .vite/build/' else fail 'No index*.js in .vite/build/' fi else pass "Skipping asar extraction (tool not available)" fi rm -rf "$extract_dir" } # Assert the launcher's --version fast-path (#775): it must print # " " and exit 0. The fast-path exits before # any launch, log-redirect, or sandbox logic, so unlike the launch # smoke test it needs no display, D-Bus, or privilege handling — run # the command directly. Closes the "deb/rpm static-verified only" gap # from the #775 review. # # Usage: run_version_flag_test