Align main with the packaged release and fetch connected history so the updater detects and rebuilds the first update. Guard local branch work before upstream installation, force only the admitted incomplete release pin, preserve existing command files, and explain incompatible packages. Exercise empty-directory publication races and delayed launches. Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
360 lines
18 KiB
Bash
360 lines
18 KiB
Bash
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
for command in git jq python3; do require_command "$command"; done
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf -- "$test_tmp"' EXIT
|
|
export OMARCHY_TEST_ROOT="$test_tmp"
|
|
mkdir -p "$test_tmp/bin" "$test_tmp/package/resources" "$test_tmp/share" "$test_tmp/seed"
|
|
|
|
# Real Git exercises patch checks and preservation; all package, desktop and
|
|
# service commands are mocks. No command reaches the live user installation.
|
|
git -C "$test_tmp/seed" init -q -b main
|
|
printf 'venv/\n.hermes-bootstrap-complete\napps/desktop/release/\n__pycache__/\n' >"$test_tmp/seed/.gitignore"
|
|
printf 'before\n' >"$test_tmp/seed/runtime.txt"
|
|
mkdir -p "$test_tmp/seed/apps/desktop/src"
|
|
printf 'desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
|
|
mkdir -p "$test_tmp/seed/hermes_cli"
|
|
cat >"$test_tmp/seed/hermes_cli/main.py" <<'PY'
|
|
import os
|
|
from pathlib import Path
|
|
|
|
def _write_desktop_build_stamp(project_root, *, source_mode):
|
|
home = Path(os.environ['HERMES_HOME'])
|
|
assert project_root == home / 'hermes-agent'
|
|
assert source_mode is False
|
|
assert (project_root / 'apps/desktop/release/linux-unpacked/resources/app.asar').is_file()
|
|
(home / 'desktop-build-stamp.json').write_text('upstream build stamp')
|
|
with (Path(os.environ['OMARCHY_TEST_ROOT']) / 'events').open('a') as log:
|
|
log.write('build-stamp\n')
|
|
PY
|
|
git -C "$test_tmp/seed" add .
|
|
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm fixture
|
|
release_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
|
|
printf 'after\n' >"$test_tmp/seed/runtime.txt"
|
|
git -C "$test_tmp/seed" diff >"$test_tmp/share/runtime.patch"
|
|
printf 'before\n' >"$test_tmp/seed/runtime.txt"
|
|
printf 'newer desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
|
|
git -C "$test_tmp/seed" add apps/desktop/src/main.js
|
|
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm newer-main
|
|
origin_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
|
|
export OMARCHY_TEST_RELEASE_COMMIT="$release_commit"
|
|
printf '{"branch":"main","commit":"%s"}\n' "$release_commit" >"$test_tmp/package/resources/install-stamp.json"
|
|
printf 'packaged app\n' >"$test_tmp/package/resources/app.asar"
|
|
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/package/Hermes"
|
|
touch "$test_tmp/package/chrome-sandbox"
|
|
chmod 755 "$test_tmp/package/Hermes"
|
|
chmod 4755 "$test_tmp/package/chrome-sandbox"
|
|
|
|
cat >"$test_tmp/share/install.sh" <<'MOCK'
|
|
#!/bin/bash
|
|
set -e
|
|
printf 'bootstrap\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
printf '%s\n' "$@" >"$OMARCHY_TEST_ROOT/install-args"
|
|
[[ ${OMARCHY_TEST_INSTALL_FAIL:-0} != 1 ]] || exit 7
|
|
commit=$OMARCHY_TEST_RELEASE_COMMIT
|
|
force=false
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--dir) runtime=$2; shift ;;
|
|
--commit) commit=$2; shift ;;
|
|
--force-commit) force=true ;;
|
|
--hermes-home) [[ $2 == "$HERMES_HOME" ]] ;;
|
|
esac
|
|
shift
|
|
done
|
|
mkdir -p -- "${runtime%/*}"
|
|
if [[ ! -d $runtime ]]; then
|
|
git clone -q --depth 1 "file://$OMARCHY_TEST_ROOT/seed" "$runtime"
|
|
else
|
|
git -C "$runtime" checkout -q main
|
|
git -C "$runtime" pull -q --ff-only origin main
|
|
fi
|
|
git -C "$runtime" fetch -q origin "$commit"
|
|
if [[ $force == true ]] || ! git -C "$runtime" merge-base --is-ancestor "$commit" HEAD; then
|
|
git -C "$runtime" checkout -q --detach "$commit"
|
|
fi
|
|
mkdir -p "$runtime/venv/bin"
|
|
git -C "$runtime" rev-parse HEAD >"$runtime/venv/dependency-commit"
|
|
printf '#!/bin/bash\nexit 0\n' >"$runtime/venv/bin/hermes"
|
|
chmod +x "$runtime/venv/bin/hermes"
|
|
printf '#!/bin/bash\nexec /usr/bin/python3 "$@"\n' >"$runtime/venv/bin/python"
|
|
chmod +x "$runtime/venv/bin/python"
|
|
[[ ${OMARCHY_TEST_NO_MARKER:-0} == 1 ]] || touch "$runtime/.hermes-bootstrap-complete"
|
|
mkdir -p "$HOME/.local/bin"
|
|
for command in hermes hermes-agent hermes-acp; do
|
|
rm -f "$HOME/.local/bin/$command"
|
|
printf 'native runtime shim\n' >"$HOME/.local/bin/$command"
|
|
done
|
|
MOCK
|
|
|
|
cat >"$test_tmp/bin/omarchy-pkg-add" <<'MOCK'
|
|
#!/bin/bash
|
|
printf 'package %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events"
|
|
[[ ${OMARCHY_TEST_PACKAGE_FAIL:-0} != 1 ]]
|
|
MOCK
|
|
cat >"$test_tmp/bin/git" <<'MOCK'
|
|
#!/bin/bash
|
|
if [[ ${OMARCHY_TEST_FETCH_FAIL:-0} == 1 && " $* " == *" --unshallow "* ]]; then exit 8; fi
|
|
exec /usr/bin/git "$@"
|
|
MOCK
|
|
cat >"$test_tmp/bin/omarchy-install-hermes-cli" <<'MOCK'
|
|
#!/bin/bash
|
|
printf 'handoff\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
exit 1
|
|
MOCK
|
|
cat >"$test_tmp/bin/setsid" <<'MOCK'
|
|
#!/bin/bash
|
|
exec "$@"
|
|
MOCK
|
|
cat >"$test_tmp/bin/cp" <<'MOCK'
|
|
#!/bin/bash
|
|
if [[ ${OMARCHY_TEST_COPY_FAIL:-0} == 1 ]]; then
|
|
touch "${@: -1}/partial-copy"
|
|
exit 9
|
|
fi
|
|
exec /usr/bin/cp "$@"
|
|
MOCK
|
|
cat >"$test_tmp/bin/mv" <<'MOCK'
|
|
#!/bin/bash
|
|
if [[ ${OMARCHY_TEST_COPY_RACE:-0} == 1 && $1 == -T ]]; then
|
|
mkdir -p "${@: -1}"
|
|
fi
|
|
exec /usr/bin/mv "$@"
|
|
MOCK
|
|
cat >"$test_tmp/bin/uwsm-app" <<'MOCK'
|
|
#!/bin/bash
|
|
[[ $1 == -- ]] || exit 1
|
|
shift
|
|
exec "$@"
|
|
MOCK
|
|
cat >"$test_tmp/bin/hermes-desktop" <<'MOCK'
|
|
#!/bin/bash
|
|
sleep 0.05
|
|
native="$HERMES_HOME/hermes-agent/apps/desktop/release/linux-unpacked"
|
|
if [[ -x $native/Hermes && -f $native/resources/app.asar ]]; then
|
|
printf 'launch\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
else
|
|
printf 'launch-before-copy\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
fi
|
|
MOCK
|
|
cat >"$test_tmp/bin/systemctl" <<'MOCK'
|
|
#!/bin/bash
|
|
printf 'theme-stop\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
MOCK
|
|
cat >"$test_tmp/bin/systemd-run" <<'MOCK'
|
|
#!/bin/bash
|
|
printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
# Join the mock asynchronous launch so every test owns its full lifetime.
|
|
for (( attempt=0; attempt<100; attempt++ )); do
|
|
if grep -q '^launch' "$OMARCHY_TEST_ROOT/events"; then exit 0; fi
|
|
sleep 0.01
|
|
done
|
|
exit 1
|
|
MOCK
|
|
chmod +x "$test_tmp/bin/"*
|
|
|
|
# Substitute only system package paths in a scratch copy of the actual script.
|
|
python3 - "$ROOT/bin/omarchy-install-ai-hermes" "$test_tmp" <<'PY'
|
|
from pathlib import Path
|
|
import sys
|
|
source, scratch = Path(sys.argv[1]), Path(sys.argv[2])
|
|
script = source.read_text()
|
|
for original, replacement in {
|
|
'/opt/hermes-desktop': str(scratch / 'package'),
|
|
'/usr/share/hermes-desktop': str(scratch / 'share'),
|
|
'/usr/bin/hermes-desktop': str(scratch / 'bin/hermes-desktop'),
|
|
}.items():
|
|
script = script.replace(original, replacement)
|
|
(scratch / 'installer').write_text(script)
|
|
PY
|
|
|
|
new_home() {
|
|
test_home="$test_tmp/$1"
|
|
hermes_home="$test_home/.hermes"
|
|
runtime="$hermes_home/hermes-agent"
|
|
native="$runtime/apps/desktop/release/linux-unpacked"
|
|
mkdir -p "$test_home"
|
|
: >"$test_tmp/events"
|
|
}
|
|
run_installer() {
|
|
HOME="$test_home" HERMES_HOME="${OMARCHY_TEST_HOME:-$hermes_home}" PATH="$test_tmp/bin:$PATH" \
|
|
bash "$test_tmp/installer" >"$test_tmp/output" 2>&1
|
|
}
|
|
assert_stopped() {
|
|
if grep -Eq '^(launch|theme-|build-stamp)' "$test_tmp/events"; then fail "$1"; fi
|
|
}
|
|
|
|
new_home fresh
|
|
run_installer || fail "fresh setup succeeds" "$(cat "$test_tmp/output")"
|
|
expected=$(printf '%s\n' --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$hermes_home")
|
|
[[ $(cat "$test_tmp/install-args") == "$expected" ]] || fail "upstream installer receives the pinned main arguments"
|
|
[[ $(head -3 "$test_tmp/events") == $'package hermes-desktop\nhandoff\nbootstrap' ]] || fail "package and CLI handoff precede runtime bootstrap"
|
|
grep -qx launch "$test_tmp/events" || fail "native app is copied before launch"
|
|
[[ $(sed -n '4p' "$test_tmp/events") == build-stamp ]] || fail "upstream build stamp follows the app copy and precedes launch"
|
|
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "the upstream helper records the completed packaged build"
|
|
[[ $(cat "$runtime/runtime.txt") == after ]] || fail "the release runtime receives its patch"
|
|
[[ $(stat -c %a "$native/chrome-sandbox") == 755 ]] || fail "the user sandbox is not setuid"
|
|
[[ $(stat -c %a "$test_tmp/package/chrome-sandbox") == 4755 ]] || fail "package sandbox permissions remain unchanged"
|
|
[[ $(git -C "$runtime" symbolic-ref --short HEAD) == main && $(git -C "$runtime" rev-parse main) == "$release_commit" ]] || fail "main starts at the release rather than the clone tip"
|
|
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "dependencies are installed for the release"
|
|
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "first update has connected history"
|
|
# Reproduce the updater's checkout/count/pull sequence while origin stays put.
|
|
git clone -q "$runtime" "$test_tmp/first-update"
|
|
git -C "$test_tmp/first-update" remote set-url origin "file://$test_tmp/seed"
|
|
git -C "$test_tmp/first-update" fetch -q origin main
|
|
git -C "$test_tmp/first-update" checkout -q main
|
|
[[ $(git -C "$test_tmp/first-update" rev-list HEAD..origin/main --count) == 1 ]] || fail "first update detects work even when origin has not moved since install"
|
|
git -C "$test_tmp/first-update" pull -q --ff-only origin main
|
|
[[ $(git -C "$test_tmp/first-update" rev-parse HEAD) == "$origin_commit" ]] || fail "first update fast-forwards to origin"
|
|
pass "fresh setup pins main, patches the matching runtime and copies the complete app before launch"
|
|
|
|
printf 'user app\n' >"$native/resources/app.asar"
|
|
printf 'user build stamp\n' >"$hermes_home/desktop-build-stamp.json"
|
|
: >"$test_tmp/events"
|
|
run_installer || fail "repeat setup succeeds" "$(cat "$test_tmp/output")"
|
|
! grep -qx bootstrap "$test_tmp/events" || fail "repeat setup does not bootstrap again"
|
|
! grep -qx build-stamp "$test_tmp/events" || fail "existing app never reruns the build stamp writer"
|
|
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'user build stamp' ]] || fail "existing native build stamp remains unchanged"
|
|
[[ $(cat "$native/resources/app.asar") == 'user app' ]] || fail "existing native app remains unchanged"
|
|
pass "repeat setup accepts the applied patch and preserves the existing native app"
|
|
|
|
# Advancing the runtime must never reinstall the release or reapply its patch.
|
|
printf 'new main\n' >"$runtime/runtime.txt"
|
|
git -C "$runtime" add runtime.txt
|
|
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm update
|
|
: >"$test_tmp/events"
|
|
run_installer || fail "a complete updated runtime and native app are reused"
|
|
[[ $(cat "$runtime/runtime.txt") == 'new main' ]] || fail "updated runtime is not release-patched"
|
|
mv "$native" "$test_tmp/saved-native"
|
|
: >"$test_tmp/events"
|
|
run_installer && fail "a newer runtime cannot receive an older native app"
|
|
[[ ! -e $native ]] || fail "no mismatched native app was copied"
|
|
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
|
|
assert_stopped "a missing updated app prevents launch and theme setup"
|
|
pass "updated runtimes are preserved and never seeded with the old packaged app"
|
|
|
|
new_home dirty-desktop
|
|
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
|
printf 'local desktop edit\n' >"$runtime/apps/desktop/src/main.js"
|
|
: >"$test_tmp/events"
|
|
run_installer && fail "modified desktop sources cannot be certified as the packaged build"
|
|
[[ ! -e $native && ! -e $hermes_home/desktop-build-stamp.json ]] || fail "modified desktop sources receive neither packaged app nor build stamp"
|
|
[[ $(cat "$runtime/apps/desktop/src/main.js") == 'local desktop edit' ]] || fail "desktop source edits are preserved"
|
|
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "modified desktop sources have build guidance"
|
|
assert_stopped "modified desktop sources prevent stamping, launch and theme setup"
|
|
pass "a matching commit with modified desktop sources is preserved without seeding or stamping"
|
|
|
|
for failure in package install marker; do
|
|
new_home "$failure-failure"
|
|
case "$failure" in
|
|
package) OMARCHY_TEST_PACKAGE_FAIL=1 run_installer && fail "package failure stops setup" ;;
|
|
install) OMARCHY_TEST_INSTALL_FAIL=1 run_installer && fail "installer failure stops setup" ;;
|
|
marker) OMARCHY_TEST_NO_MARKER=1 run_installer && fail "missing marker stops setup" ;;
|
|
esac
|
|
[[ ! -e $native ]] || fail "failed setup does not seed the app"
|
|
assert_stopped "failed setup prevents launch and theme setup"
|
|
done
|
|
pass "package, upstream installer and readiness failures stop before launch"
|
|
|
|
for failure in copy race; do
|
|
new_home "$failure-failure"
|
|
if [[ $failure == "copy" ]]; then
|
|
OMARCHY_TEST_COPY_FAIL=1 run_installer && fail "copy failure stops setup"
|
|
[[ ! -e $native ]] || fail "partial copy is never published"
|
|
else
|
|
OMARCHY_TEST_COPY_RACE=1 run_installer && fail "concurrent native app stops publication"
|
|
[[ -d $native && -z $(ls -A "$native") ]] || fail "concurrent empty app directory is preserved"
|
|
fi
|
|
[[ -z $(find "${native%/*}" -maxdepth 1 -name '.linux-unpacked.*' -print) ]] || fail "owned staging directory is cleaned up"
|
|
assert_stopped "publication failure prevents launch"
|
|
done
|
|
pass "failed copies and concurrent app creation preserve existing work and clean only staging"
|
|
|
|
new_home incomplete-native
|
|
run_installer || fail "incomplete native fixture sets up"
|
|
rm "$native/resources/app.asar"
|
|
: >"$test_tmp/events"
|
|
run_installer && fail "incomplete existing app requires repair"
|
|
[[ ! -e $native/resources/app.asar ]] || fail "incomplete existing app is not overwritten"
|
|
assert_stopped "incomplete native app prevents launch"
|
|
pass "an incomplete existing native app is preserved"
|
|
|
|
new_home patch-conflict
|
|
run_installer || fail "patch conflict fixture sets up"
|
|
printf 'local edit\n' >"$runtime/runtime.txt"
|
|
: >"$test_tmp/events"
|
|
run_installer && fail "unexpected patch conflict stops setup"
|
|
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "conflicting runtime changes are preserved"
|
|
assert_stopped "patch conflict prevents launch"
|
|
rm "$runtime/.hermes-bootstrap-complete"
|
|
: >"$test_tmp/events"
|
|
run_installer && fail "incomplete modified runtime cannot be reset by upstream installer"
|
|
! grep -qx bootstrap "$test_tmp/events" || fail "modified runtime never reaches upstream installer"
|
|
pass "patch conflicts and incomplete modified runtimes retain local changes and stop safely"
|
|
|
|
new_home full-history-retry
|
|
git clone -q "$test_tmp/seed" "$runtime"
|
|
git -C "$runtime" checkout -q --detach "$release_commit"
|
|
run_installer || fail "clean incomplete full-history release checkout is repaired" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "full-history retry pins before installing dependencies"
|
|
[[ $(git -C "$runtime" rev-parse HEAD) == "$release_commit" && -f $native/resources/app.asar ]] || fail "full-history retry seeds the matching release"
|
|
pass "full-history retries force the guarded release pin before dependency setup"
|
|
|
|
new_home local-main
|
|
git clone -q "$test_tmp/seed" "$runtime"
|
|
printf 'local branch work\n' >"$runtime/keep"
|
|
git -C "$runtime" add keep
|
|
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm local-work
|
|
local_main=$(git -C "$runtime" rev-parse main)
|
|
git -C "$runtime" checkout -q --detach "$release_commit"
|
|
run_installer && fail "local main commits cannot be reset by upstream installation"
|
|
! grep -qx bootstrap "$test_tmp/events" || fail "local main is checked before upstream installer"
|
|
[[ $(git -C "$runtime" rev-parse main) == "$local_main" ]] || fail "local main commit stays referenced"
|
|
pass "detached release checkouts do not hide local main work from the installer guard"
|
|
|
|
new_home deepen-retry
|
|
OMARCHY_TEST_FETCH_FAIL=1 run_installer && fail "history fetch failure stops setup"
|
|
[[ ! -e $native ]] || fail "failed history fetch does not seed the app"
|
|
assert_stopped "failed history fetch prevents launch"
|
|
: >"$test_tmp/events"
|
|
run_installer || fail "history fetch can be retried after runtime setup" "$(cat "$test_tmp/output")"
|
|
! grep -qx bootstrap "$test_tmp/events" || fail "history retry does not repeat upstream installation"
|
|
pass "a history fetch failure can be retried without reinstalling the ready runtime"
|
|
|
|
new_home existing-commands
|
|
mkdir -p "$test_home/.local/bin"
|
|
printf 'foreign wrapper\n' >"$test_home/.local/bin/hermes"
|
|
printf 'symlink target\n' >"$test_home/target"
|
|
ln -s "$test_home/target" "$test_home/.local/bin/hermes-agent"
|
|
ln -s "$test_home/missing" "$test_home/.local/bin/hermes-acp"
|
|
run_installer || fail "existing commands are preserved before upstream replaces them" "$(cat "$test_tmp/output")"
|
|
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
|
|
[[ ${#backups[@]} == 1 && -d ${backups[0]} ]] || fail "one backup directory preserves existing command names"
|
|
[[ $(cat "${backups[0]}/hermes") == 'foreign wrapper' ]] || fail "foreign wrapper bytes are saved"
|
|
[[ $(readlink "${backups[0]}/hermes-agent") == "$test_home/target" && $(readlink "${backups[0]}/hermes-acp") == "$test_home/missing" ]] || fail "working and broken symlinks are saved as links"
|
|
[[ $(cat "$test_home/target") == 'symlink target' ]] || fail "upstream does not overwrite the original symlink target"
|
|
grep -qF "${backups[0]}" "$test_tmp/output" || fail "backup location is reported"
|
|
pass "pre-existing command files and symlinks are backed up before replacement"
|
|
|
|
new_home old-package
|
|
mv "$test_tmp/package/resources/install-stamp.json" "$test_tmp/saved-install-stamp.json"
|
|
run_installer && fail "an old installed package cannot bootstrap"
|
|
grep -q 'omarchy update' "$test_tmp/output" || fail "old package has actionable upgrade guidance"
|
|
! grep -qx handoff "$test_tmp/events" || fail "old package is rejected before CLI handoff"
|
|
! grep -qx bootstrap "$test_tmp/events" || fail "old package never reaches upstream installer"
|
|
mv "$test_tmp/saved-install-stamp.json" "$test_tmp/package/resources/install-stamp.json"
|
|
pass "old package fails with upgrade guidance before changing the runtime or CLI"
|
|
|
|
new_home custom-profile
|
|
hermes_home="$test_home/custom home"
|
|
runtime="$hermes_home/hermes-agent"
|
|
OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail "profile setup succeeds"
|
|
[[ -x $runtime/apps/desktop/release/linux-unpacked/Hermes ]] || fail "profile uses the canonical root runtime"
|
|
grep -qxF "$hermes_home" "$test_tmp/install-args" || fail "canonical custom home reaches upstream installer"
|
|
pass "custom profile paths normalize to the shared Hermes home"
|