Choosing Hermes as the default agent built it through mise: a pipx environment with no checkout, so `hermes update` had nothing to move, and the only Hermes that could update itself was the one Hermes Desktop set up. Both paths now run the same setup. omarchy-install-hermes-cli installs the hermes-desktop package and runs upstream's installer from it, pinned to the packaged release and started on main, exactly as Install > AI did; omarchy-install-ai-hermes is that plus opening the app. The terminal, the default agent and the app share one runtime, and it updates itself. --check answers whether --now has anything left to do, not merely whether a hermes runs: choosing Hermes from the menu asks first and opens a terminal only on a no, so a yes has to mean no minutes-long step would run where nobody can see it. With the app installed that means the runtime's own command, its completion marker and the seeded packaged app; a finished runtime whose command is gone, somebody else's, or its own but unable to run gets it back from upstream's path stage without bootstrapping again. Either way the command has to be the one PATH finds, because omarchy-agent runs bare `hermes` and Omarchy puts mise's shims ahead of ~/.local/bin; a command in the way is named rather than installed over. The modes are named outright because the app's launcher used to call this command with no arguments to reconcile a mise copy; a default of --now would turn every launch into an install. --check still refuses to run the retired wrapper, since running it built Hermes through mise, and a machine whose migration is pending can still have it on PATH. Provisioning no longer writes the wrapper, Remove Preinstalls no longer looks for it, and the wrapper, the environment it built and what proves them Omarchy's are known to the installer alone: --retire-mise is the migration's whole job, and --now runs the same removal once the runtime installer has saved the wrapper aside, so a user who chose Hermes before their migration ran is not left with mise's shim answering `hermes`. Only the wrapper proves the environment is Omarchy's, at its path or in that saved copy, so the environment goes first and the wrapper last, judged by mise neither having it installed nor still requesting it; a removal that leaves either behind, or a listing that cannot be read, mise missing included, stops with the commands to finish by hand and leaves the migration pending. The migration that once installed the wrapper is kept as a no-op for late updaters, and one whose default agent was Hermes is told to choose it again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com>
568 lines
31 KiB
Bash
568 lines
31 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 '%s\n' "$@" >"$OMARCHY_TEST_ROOT/install-args"
|
|
[[ ${OMARCHY_TEST_INSTALL_FAIL:-0} != 1 ]] || exit 7
|
|
commit=$OMARCHY_TEST_RELEASE_COMMIT
|
|
force=false
|
|
stage=""
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--dir) runtime=$2; shift ;;
|
|
--commit) commit=$2; shift ;;
|
|
--force-commit) force=true ;;
|
|
--stage) stage=$2; shift ;;
|
|
--hermes-home) [[ $2 == "$HERMES_HOME" ]] ;;
|
|
esac
|
|
shift
|
|
done
|
|
# The commands upstream writes last: shims for the two side commands and, for
|
|
# hermes, a launcher into the runtime's venv the way the real one is written.
|
|
write_commands() {
|
|
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
|
|
printf '#!/bin/bash\nexec "%s/venv/bin/hermes" "$@"\n' "$runtime" >"$HOME/.local/bin/hermes"
|
|
chmod +x "$HOME/.local/bin/hermes"
|
|
}
|
|
# The path stage writes the commands alone, without touching the checkout.
|
|
if [[ $stage == "path" ]]; then
|
|
printf 'path\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
write_commands
|
|
exit 0
|
|
fi
|
|
printf 'bootstrap\n' >>"$OMARCHY_TEST_ROOT/events"
|
|
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"
|
|
# The venv command answers the readiness probes the way the real one does: the
|
|
# installer runs the command it leaves on PATH before calling Hermes ready.
|
|
cat >"$runtime/venv/bin/hermes" <<'SH'
|
|
#!/bin/bash
|
|
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
|
|
echo "[-q QUERY, --query QUERY] [--tui]"
|
|
else
|
|
echo "hermes-agent 0.0.0-test"
|
|
fi
|
|
SH
|
|
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"
|
|
write_commands
|
|
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 ]] || exit 1
|
|
touch "$OMARCHY_TEST_ROOT/package-installed"
|
|
MOCK
|
|
cat >"$test_tmp/bin/omarchy-pkg-present" <<'MOCK'
|
|
#!/bin/bash
|
|
[[ -e $OMARCHY_TEST_ROOT/package-installed ]]
|
|
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/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"
|
|
MOCK
|
|
chmod +x "$test_tmp/bin/"*
|
|
|
|
# Substitute only system package paths in scratch copies of the actual scripts.
|
|
# The runtime setup lives in omarchy-install-hermes-cli, which the desktop
|
|
# installer finds on PATH under its own name.
|
|
python3 - "$ROOT/bin" "$test_tmp" <<'PY'
|
|
from pathlib import Path
|
|
import sys
|
|
source, scratch = Path(sys.argv[1]), Path(sys.argv[2])
|
|
substitutions = {
|
|
'/opt/hermes-desktop': str(scratch / 'package'),
|
|
'/usr/share/hermes-desktop': str(scratch / 'share'),
|
|
'/usr/bin/hermes-desktop': str(scratch / 'bin/hermes-desktop'),
|
|
}
|
|
for name, target in (('omarchy-install-ai-hermes', scratch / 'installer'),
|
|
('omarchy-install-hermes-cli', scratch / 'bin/omarchy-install-hermes-cli')):
|
|
script = (source / name).read_text()
|
|
for original, replacement in substitutions.items():
|
|
script = script.replace(original, replacement)
|
|
target.write_text(script)
|
|
target.chmod(0o755)
|
|
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"
|
|
rm -f "$test_tmp/package-installed"
|
|
: >"$test_tmp/events"
|
|
}
|
|
# The app opens in the background, so a run that got that far is joined to it
|
|
# before anything is asserted; one that stopped earlier started nothing.
|
|
run_installer() {
|
|
HOME="$test_home" HERMES_HOME="${OMARCHY_TEST_HOME:-$hermes_home}" PATH="$test_tmp/bin:$test_home/.local/bin:$PATH" \
|
|
bash "$test_tmp/installer" >"$test_tmp/output" 2>&1 || return
|
|
for (( attempt=0; attempt<200; attempt++ )); do
|
|
if grep -q '^launch' "$test_tmp/events"; then return 0; fi
|
|
sleep 0.01
|
|
done
|
|
return 1
|
|
}
|
|
# ~/.local/bin is on PATH the way Omarchy puts it there, after the mocks.
|
|
run_cli() {
|
|
HOME="$test_home" HERMES_HOME="${OMARCHY_TEST_HOME:-$hermes_home}" PATH="$test_tmp/bin:$test_home/.local/bin:$PATH" \
|
|
bash "$test_tmp/bin/omarchy-install-hermes-cli" "$@" >"$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 -2 "$test_tmp/events") == $'package hermes-desktop\nbootstrap' ]] || fail "the package precedes runtime bootstrap" "$(cat "$test_tmp/events")"
|
|
[[ $(sed -n '3p' "$test_tmp/events") == build-stamp ]] || fail "upstream build stamp follows the app copy" "$(cat "$test_tmp/events")"
|
|
[[ $(tail -1 "$test_tmp/events") == launch ]] || fail "the app opens only once setup and the theme hand-over are in place" "$(cat "$test_tmp/events")"
|
|
grep -qx theme-start "$test_tmp/events" || fail "setup hands Hermes the theme"
|
|
[[ $(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"
|
|
|
|
# A runtime already at the release, edited where the patch lands, before Omarchy
|
|
# has prepared it: the conflict is reported and nothing is touched.
|
|
new_home patch-conflict
|
|
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
|
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"
|
|
|
|
# Once set up, a runtime is the user's to edit; a finished install is not
|
|
# re-patched or re-verified, only opened.
|
|
new_home finished-edit
|
|
run_installer || fail "finished-edit fixture sets up" "$(cat "$test_tmp/output")"
|
|
printf 'local edit\n' >"$runtime/runtime.txt"
|
|
: >"$test_tmp/events"
|
|
run_installer || fail "a finished install with local edits is accepted" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "local edits to a finished runtime are preserved"
|
|
[[ $(cat "$test_tmp/events") == $'package hermes-desktop\nlaunch' ]] || fail "a finished install is only opened" "$(cat "$test_tmp/events")"
|
|
pass "a finished install is left as the user has it"
|
|
|
|
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"
|
|
|
|
# A main that is neither the release nor origin/main is kept under another name
|
|
# and main still starts at the release: a user's own commits stay reachable,
|
|
# and a clone whose main is off origin/main only because upstream rewrote its
|
|
# history is not refused for work it never did.
|
|
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 "a runtime whose main carries other work still sets up" "$(cat "$test_tmp/output")"
|
|
[[ $(git -C "$runtime" rev-parse main) == "$release_commit" ]] || fail "main starts at the release"
|
|
kept=$(git -C "$runtime" for-each-ref --format='%(objectname)' 'refs/heads/main-before-omarchy-*')
|
|
[[ $kept == "$local_main" ]] || fail "what main pointed at is kept under another name" "$kept"
|
|
grep -q 'main-before-omarchy-' "$test_tmp/output" || fail "the kept branch is named in the output"
|
|
[[ -f $native/resources/app.asar ]] || fail "setup carries on to seed the app"
|
|
pass "work on main is kept under another name rather than refused"
|
|
|
|
# A shallow.lock nothing has touched for a minute is what a probe that killed
|
|
# Hermes mid-fetch leaves behind; it must not stop the history fetch for good.
|
|
new_home stale-lock
|
|
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
|
printf 'stale\n' >"$runtime/.git/shallow.lock"
|
|
touch -d '5 minutes ago' "$runtime/.git/shallow.lock"
|
|
run_installer || fail "a stale shallow.lock does not stop setup" "$(cat "$test_tmp/output")"
|
|
[[ ! -e $runtime/.git/shallow.lock ]] || fail "the stale lock is cleared"
|
|
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "the history fetch went ahead after the stale lock"
|
|
pass "a stale shallow.lock is cleared rather than left to block every history fetch"
|
|
|
|
# The same old lock with a git still working in the runtime is somebody's: it
|
|
# is waited for, not taken. Each way a live git is found gets its own run: a
|
|
# stand-in git that lives until this test releases it, only after setup has
|
|
# said it is waiting, and that says if its lock was stolen while it lived; it
|
|
# then leaves the lock behind orphaned the way a killed fetch would.
|
|
live_lock_case() {
|
|
local name=$1 where=$2 output_line='Waiting for Hermes' attempt
|
|
shift 2
|
|
new_home "$name"
|
|
ln -s "$test_home" "$test_tmp/$name-link"
|
|
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
|
printf 'live\n' >"$runtime/.git/shallow.lock"
|
|
touch -d '5 minutes ago' "$runtime/.git/shallow.lock"
|
|
rm -f "$test_tmp/lock-stolen" "$test_tmp/release-git"
|
|
(cd "$where" && { if (( $# )); then export "$@"; fi; } && export LOCK="$runtime/.git/shallow.lock" && exec -a git bash -c 'for (( i = 0; i < 900; i++ )); do [[ -e "$1" ]] && exit; [[ -e $LOCK ]] || { touch "$2"; exit; }; sleep 0.1; done' _ "$test_tmp/release-git" "$test_tmp/lock-stolen") &
|
|
fake_git=$!
|
|
: >"$test_tmp/output"
|
|
# The runtime is reached through a link, as a symlinked home would, since
|
|
# /proc reports canonical paths and the runtime path keeps links.
|
|
OMARCHY_TEST_HOME="$test_tmp/$name-link/.hermes" run_installer &
|
|
installer=$!
|
|
for (( attempt = 0; attempt < 300; attempt++ )); do
|
|
grep -q "$output_line" "$test_tmp/output" 2>/dev/null && break
|
|
sleep 0.1
|
|
done
|
|
if ! grep -q "$output_line" "$test_tmp/output"; then
|
|
touch "$test_tmp/release-git"; wait "$installer" || true
|
|
fail "setup says it is waiting for the live git ($name)" "$(cat "$test_tmp/output")"
|
|
fi
|
|
[[ ! -e $test_tmp/lock-stolen ]] || fail "the lock was cleared while a git still worked in the runtime ($name)"
|
|
touch "$test_tmp/release-git"
|
|
wait "$installer" || fail "setup goes ahead once the git is gone ($name)" "$(cat "$test_tmp/output")"
|
|
wait "$fake_git" 2>/dev/null || true
|
|
[[ ! -e $test_tmp/lock-stolen ]] || fail "the lock was cleared while a git still worked in the runtime ($name)"
|
|
[[ ! -e $runtime/.git/shallow.lock ]] || fail "the orphaned lock is cleared once nothing holds it ($name)"
|
|
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "the history fetch went ahead after the wait ($name)"
|
|
}
|
|
# Found by its working directory.
|
|
live_lock_case live-lock-cwd "$test_tmp/live-lock-cwd/.hermes/hermes-agent"
|
|
pass "a lock a live git holds is waited for, not taken, through a linked runtime path"
|
|
# Found by GIT_DIR alone, with a trailing slash, from elsewhere, with a large
|
|
# environment: read whole and NUL-delimited, or it would go unseen.
|
|
big_env=$(head -c 120000 /dev/zero | tr '\0' 'x')
|
|
live_lock_case live-lock-env "$test_tmp" BIG_ENV="$big_env" GIT_DIR="$test_tmp/live-lock-env/.hermes/hermes-agent/.git/"
|
|
pass "a git working from elsewhere with GIT_DIR naming the runtime is found by its environment"
|
|
|
|
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 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"
|
|
|
|
# Choosing Hermes as the default agent runs the same setup, short of opening
|
|
# the app: the package, the runtime, the seeded app and the theme hand-over.
|
|
new_home terminal-agent
|
|
run_cli --now || fail "the default agent path sets Hermes up" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$test_tmp/events") == $'package hermes-desktop\nbootstrap\nbuild-stamp\ntheme-stop\ntheme-start' ]] ||
|
|
fail "the default agent path installs the app's runtime without opening the app" "$(cat "$test_tmp/events")"
|
|
[[ -x $test_home/.local/bin/hermes && -f $native/resources/app.asar ]] || fail "the default agent path leaves the command and the seeded app in place"
|
|
: >"$test_tmp/events"
|
|
run_cli --now || fail "a finished install is accepted by the default agent path" "$(cat "$test_tmp/output")"
|
|
[[ ! -s $test_tmp/events ]] || fail "a finished install is set up again" "$(cat "$test_tmp/events")"
|
|
run_cli --check || fail "--check follows the installed runtime"
|
|
pass "choosing Hermes as the default agent installs the app's runtime without opening the app"
|
|
|
|
# A Hermes the user set up themselves is what the default agent runs, and
|
|
# nothing is installed beside it. Asked for the app by name, Omarchy installs
|
|
# the package first, and then the runtime supersedes it with the command saved.
|
|
new_home own-hermes
|
|
mkdir -p "$test_home/.local/bin"
|
|
cat >"$test_home/.local/bin/hermes" <<'SH'
|
|
#!/bin/bash
|
|
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
|
|
echo "[-q QUERY, --query QUERY] [--tui]"
|
|
else
|
|
echo "hermes-agent 0.0.0-user"
|
|
fi
|
|
SH
|
|
chmod +x "$test_home/.local/bin/hermes"
|
|
own_hermes=$(cat "$test_home/.local/bin/hermes")
|
|
run_cli --now || fail "the default agent path accepts the user's own Hermes" "$(cat "$test_tmp/output")"
|
|
[[ ! -s $test_tmp/events ]] || fail "a working Hermes of the user's own has the app installed beside it" "$(cat "$test_tmp/events")"
|
|
[[ $(cat "$test_home/.local/bin/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is left alone"
|
|
run_installer || fail "the app installs over the user's own Hermes" "$(cat "$test_tmp/output")"
|
|
grep -qx bootstrap "$test_tmp/events" || fail "the app sets up its own runtime"
|
|
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
|
|
[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is saved aside"
|
|
pass "the default agent path stands aside for the user's own Hermes; the app supersedes it"
|
|
|
|
# A finished runtime whose command is gone, or not its own, gets its command
|
|
# back from upstream's path stage alone: no bootstrap, the runtime untouched,
|
|
# and what held the name saved aside. Until then --check says no, so the menu
|
|
# opens a terminal for the repair rather than running the agent on the wrong
|
|
# Hermes.
|
|
new_home command-repair
|
|
run_cli --now || fail "command-repair fixture sets up" "$(cat "$test_tmp/output")"
|
|
rm "$test_home/.local/bin/hermes"
|
|
run_cli --check && fail "--check calls a runtime installed without its command"
|
|
: >"$test_tmp/events"
|
|
run_cli --now || fail "a missing command is restored" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$test_tmp/events") == path ]] || fail "a missing command is restored without bootstrapping again" "$(cat "$test_tmp/events")"
|
|
run_cli --check || fail "--check follows the restored command"
|
|
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/somebody-elses-hermes \"\$@\"" >"$test_home/.local/bin/hermes"
|
|
chmod +x "$test_home/.local/bin/hermes"
|
|
run_cli --check && fail "--check calls the app installed while the terminal is on another Hermes"
|
|
: >"$test_tmp/events"
|
|
run_cli --now || fail "a foreign command beside the app's runtime is replaced" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$test_tmp/events") == path ]] || fail "a foreign command is replaced without bootstrapping again" "$(cat "$test_tmp/events")"
|
|
grep -lF somebody-elses-hermes "$test_home/.local/bin/".hermes-before-desktop.*/hermes >/dev/null 2>&1 || fail "the foreign command is saved aside"
|
|
grep -qF "$hermes_home/" "$test_home/.local/bin/hermes" || fail "the runtime's own command is back"
|
|
run_cli --check || fail "--check follows the runtime's own command"
|
|
chmod -x "$test_home/.local/bin/hermes"
|
|
run_cli --check && fail "--check calls the runtime's own command installed when it cannot run"
|
|
: >"$test_tmp/events"
|
|
run_cli --now || fail "the runtime's own command is rewritten when it cannot run" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$test_tmp/events") == path && -x $test_home/.local/bin/hermes ]] || fail "a command that cannot run is rewritten by the path stage alone" "$(cat "$test_tmp/events")"
|
|
pass "a finished runtime gets its own command back without bootstrapping again"
|
|
|
|
# A hermes ahead of ~/.local/bin on PATH is what the default agent would run,
|
|
# so a finished install behind it is not installed, and --now names it rather
|
|
# than setting anything up again.
|
|
cp "$test_home/.local/bin/hermes" "$test_tmp/bin/hermes"
|
|
run_cli --check && fail "--check calls a shadowed install installed"
|
|
: >"$test_tmp/events"
|
|
run_cli --now && fail "--now reports a shadowed install as ready"
|
|
grep -qF "$test_tmp/bin/hermes" "$test_tmp/output" || fail "--now names the command in the way" "$(cat "$test_tmp/output")"
|
|
[[ ! -s $test_tmp/events ]] || fail "a shadowed install is set up again" "$(cat "$test_tmp/events")"
|
|
rm -f "$test_tmp/bin/hermes"
|
|
run_cli --check || fail "--check follows the install once nothing shadows it"
|
|
pass "a hermes ahead of ~/.local/bin on PATH is reported, not set up over"
|
|
|
|
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"
|
|
|
|
# New releases moved the stamp writer out of main. Keep the earlier cases on
|
|
# the old layout and exercise a fresh installation with the relocated helper.
|
|
git -C "$test_tmp/seed" mv hermes_cli/main.py hermes_cli/main_desktop.py
|
|
printf 'raise AssertionError("legacy module imported after desktop split")\n' >"$test_tmp/seed/hermes_cli/main.py"
|
|
git -C "$test_tmp/seed" add hermes_cli/main.py
|
|
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm split-desktop
|
|
release_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"
|
|
new_home split-desktop
|
|
run_installer || fail "setup supports the relocated desktop helper" "$(cat "$test_tmp/output")"
|
|
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "relocated helper writes the build stamp"
|
|
grep -qx launch "$test_tmp/events" || fail "setup launches after the relocated helper writes the stamp"
|
|
pass "new releases use the relocated desktop stamp writer"
|