Install Hermes for the default agent as the desktop app's self-updating runtime

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>
This commit is contained in:
committed 2026-09-21 19:26:02 -05:00
1 parent b8c382da9e
commit 7eb818e37b
16 files changed
+1211 -1204

No files matched your search

-133
View File
@@ -1,133 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1787760281.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
hermes="$test_home/.local/bin/hermes"
marker="# Written by omarchy-install-hermes-cli."
mkdir -p "$mock_bin" "$test_home/.local/bin" "$test_home/.local/state/omarchy"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
#!/bin/bash
! command -v "$1" >/dev/null 2>&1
SH
mise_log="$test_tmp/mise-log"
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
[[ $1 != "where" ]]
SH
chmod +x "$mock_bin"/*
# The real installer is on PATH so the migration writes today's stub, not a
# copy of it.
run_migration() {
OMARCHY_TEST_DESKTOP_INSTALLED="${1:-0}" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$test_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null 2>&1
}
run_migration || fail "the migration installs the wrapper on a plain install"
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" || fail "the migration writes the Omarchy wrapper"
pass "the migration installs the Hermes wrapper"
before=$(cat "$hermes")
run_migration || fail "rerunning the migration succeeds"
[[ $(cat "$hermes") == "$before" ]] || fail "rerunning the migration leaves the same wrapper"
pass "the migration is idempotent"
chmod -x "$hermes"
run_migration || fail "the migration repairs a non-executable Omarchy wrapper"
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" ||
fail "the migration restores a non-executable Omarchy wrapper"
pass "the migration repairs a non-executable Omarchy wrapper"
rm -f "$hermes"
touch "$test_home/.local/state/omarchy/preinstalls-removed"
run_migration || fail "the migration succeeds for users who removed the preinstalls"
[[ ! -e $hermes ]] || fail "the migration respects the preinstalls opt-out"
pass "the migration skips users who removed the preinstalls"
rm -f "$test_home/.local/state/omarchy/preinstalls-removed"
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes"
[[ ! -e $hermes ]] || fail "the migration writes nothing when Hermes Desktop owns Hermes"
pass "the migration stands aside for Hermes Desktop"
# Standing aside is not the same as leaving a second Hermes behind: the wrapper
# an earlier install wrote and the mise copy it points at both go when the
# desktop app owns Hermes, even though the app has not finished setting up.
printf '%s\n' "#!/bin/bash" "$marker" >"$hermes"
chmod +x "$hermes"
: >"$mise_log"
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes and the old wrapper is present"
[[ ! -e $hermes ]] || fail "the migration removes the Omarchy wrapper when Hermes Desktop owns Hermes"
mise_calls=$(tr '\0' ' ' <"$mise_log")
[[ $mise_calls == *"rm -g "* ]] || fail "the migration removes the global mise Hermes for Hermes Desktop"
[[ $mise_calls == *"uninstall --all "* ]] || fail "the migration uninstalls the mise Hermes for Hermes Desktop"
pass "the migration clears the old Omarchy Hermes for Hermes Desktop"
# ...while anyone else's hermes stays exactly where it is, and is not run.
foreign_ran="$test_tmp/foreign-ran"
foreign_body="#!/bin/bash
touch $foreign_ran
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$foreign_body" >"$hermes"
chmod +x "$hermes"
run_migration 1 || fail "the migration succeeds over a foreign hermes when Hermes Desktop owns Hermes"
[[ -x $hermes && $(cat "$hermes") == "$foreign_body" ]] ||
fail "the migration leaves a foreign hermes alone when Hermes Desktop owns Hermes"
[[ ! -e $foreign_ran ]] || fail "the migration does not run a foreign hermes"
pass "the migration preserves a foreign hermes for Hermes Desktop"
rm -f "$hermes"
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$hermes"
chmod +x "$hermes"
run_migration || fail "the migration succeeds over a foreign hermes command"
[[ $(cat "$hermes") == "$official_body" ]] || fail "the migration leaves a foreign hermes command alone"
pass "the migration preserves a foreign hermes command"
chmod -x "$hermes"
run_migration || fail "the migration succeeds over a non-executable foreign hermes"
[[ -f $hermes && ! -x $hermes && $(cat "$hermes") == "$official_body" ]] ||
fail "the migration leaves a non-executable foreign hermes alone"
pass "the migration preserves a non-executable foreign hermes"
rm -f "$hermes"
ln -s "$test_home/nowhere/hermes" "$hermes"
run_migration || fail "the migration succeeds over a dangling hermes link"
[[ -L $hermes && $(readlink "$hermes") == "$test_home/nowhere/hermes" ]] ||
fail "the migration leaves a dangling hermes link alone"
pass "the migration preserves a dangling hermes link"
rm -f "$hermes"
mkdir "$hermes"
run_migration || fail "the migration succeeds over a directory at the hermes path"
[[ -d $hermes ]] || fail "the migration leaves a directory at the hermes path alone"
pass "the migration preserves a directory at the hermes path"
rmdir "$hermes"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
chmod +x "$hermes"
run_migration || fail "the migration succeeds over a wrapper that mentions the installer"
grep -qxF "$marker" "$hermes" && fail "the migration does not rewrite a wrapper that merely mentions the installer"
pass "the migration preserves a wrapper that merely mentions the installer"
+226 -522
View File
@@ -9,568 +9,272 @@ trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
hermes="$test_home/.local/bin/hermes"
ran="$test_tmp/ran"
mise_log="$test_tmp/mise-log"
pkg_log="$test_tmp/pkg-log"
mkdir -p "$mock_bin" "$test_home/.local/bin"
: >"$mise_log"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
# Installing the package is the first thing the runtime setup does, so a mock
# that fails there proves the installer would have installed without running
# the rest of the setup, which hermes-desktop-install-test.sh covers.
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
! command -v "$1" >/dev/null 2>&1
printf '%s\n' "$*" >>"$OMARCHY_TEST_PKG_LOG"
exit 1
SH
# `mise where` must fail so the installer sees no Hermes behind the stub.
#
# With OMARCHY_TEST_MISE_X_HERMES=1, `mise x -- hermes ...` emulates the Hermes
# the Omarchy stub runs, so the readiness probe can be exercised through a
# mise-installed hermes and not only the foreign and desktop wrappers. Off by
# default, so `mise x` stays silent for every test that does not opt in.
# Hermes never comes from mise any more; the only thing the installer asks
# mise is to remove what the retired wrapper built. `where` finds that
# environment and `ls -g` lists it as requested when a test says it was built,
# until uninstalled and unrequested respectively. Every call is logged.
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
if [[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]]; then
printf '%s\n' "$OMARCHY_TEST_MISE_ROOT"
exit 0
fi
if [[ $1 == "x" && ${OMARCHY_TEST_MISE_X_HERMES:-0} == 1 ]]; then
# Args are `x <tool> -- hermes <hermes-args...>`; skip to what follows hermes.
shift
while (( $# )) && [[ $1 != "--" ]]; do shift; done
shift 2
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "[-q QUERY, --query QUERY] [--tui]"
else
echo "hermes-agent 0.0.0-test"
fi
exit 0
fi
[[ $1 != "where" ]]
printf 'mise %s\n' "$*" >>"$OMARCHY_TEST_MISE_LOG"
case "$1" in
where)
[[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_MISE_LOG.removed ]]
;;
ls)
if [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_MISE_LOG.unrequested ]]; then
echo '{"pipx:hermes-agent[extras=all]": [{"version": "latest"}]}'
else
echo '{}'
fi
;;
rm)
touch "$OMARCHY_TEST_MISE_LOG.unrequested"
;;
uninstall)
touch "$OMARCHY_TEST_MISE_LOG.removed"
rm -f "${OMARCHY_TEST_SHIM:-}"
;;
esac
SH
chmod +x "$mock_bin"/*
# ~/.local/bin is on PATH the way Omarchy puts it there, after the mocks, so
# `hermes` resolves to the command under test unless a test shadows it.
run_installer() {
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-0}" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
OMARCHY_TEST_PKG_LOG="$pkg_log" \
OMARCHY_TEST_RAN="$ran" \
HOME="$test_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 2>&1
PATH="$mock_bin:$test_home/.local/bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" "$@" >"$test_tmp/output" 2>&1
}
stub_marker="# Written by omarchy-install-hermes-cli."
python_pin="3.13"
app_stub_body='#!/bin/bash
exec /home/x/.hermes/hermes-agent/venv/bin/hermes "$@"'
# Writing the stub must not provision anything: user setup calls this on every
# machine, including the ones that never run Hermes.
: >"$mise_log"
rm -f "$test_home/.local/bin/hermes"
run_installer 0 || fail "installer failed with no desktop installed"
[[ -x $test_home/.local/bin/hermes ]] || fail "installer writes a hermes stub when the desktop is absent"
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the stub records which command wrote it"
tr '\0' ' ' <"$mise_log" | grep -q "use -g --quiet uv" &&
fail "writing the stub does not install uv"
pass "writing the Hermes stub provisions nothing"
# The desktop app owns Hermes, so our own stub must go rather than sit there
# answering `hermes` until the app's bootstrap replaces it.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ ! -e $test_home/.local/bin/hermes ]] ||
fail "the desktop taking over removes the stub this command wrote"
pass "installing the desktop app removes the CLI stub"
# ...but the app's own hermes is not ours to delete.
printf '%s\n' "$app_stub_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 || true
[[ -x $test_home/.local/bin/hermes ]] ||
fail "the desktop app's own hermes command survives"
pass "the app's own hermes command is left alone"
# A copy mise cannot vouch for is still a second Hermes.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 1 || true
tr '\0' '\n' <"$mise_log" | grep -q "uninstall" ||
fail "takeover removes a mise copy even when it is not healthy"
pass "takeover removes an unhealthy mise copy"
# --check answers about Hermes being usable, not about the venv appearing. The
# venv exists from the python-deps stage, several stages before the command.
rm -rf "$test_home/.hermes"
rm -f "$test_home/.local/bin/hermes"
run_installer 1 --check && fail "--check reports Hermes missing before the app installs it"
# The venv command answers the readiness probes, as the real one does: foreign
# wrappers below exec it, and the installer runs both before trusting them.
mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
cat >"$test_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
# A hermes that runs, defines the flags omarchy-agent passes unless a test says
# otherwise, and records that it ran. With "ours" it names the runtime the way
# upstream's launcher does; without, it is a command from somewhere else.
write_hermes() {
cat >"$hermes" <<'SH'
#!/bin/bash
touch "$OMARCHY_TEST_RAN"
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "[-q QUERY, --query QUERY] [--tui]"
printf '%s\n' "${OMARCHY_TEST_HERMES_HELP-[-q QUERY, --query QUERY] [--tui]}"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes"
run_installer 1 --check && fail "--check waits for the install to finish, not just the venv"
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 1 --check || fail "--check reports Hermes present once the app has finished"
pass "--check follows the app's completed install"
# An executable called hermes that belongs to something else is not this
# install being ready.
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_installer 1 --check && fail "--check rejects a hermes command belonging to something else"
pass "--check rejects a foreign hermes command"
# A hermes the user installed themselves -- the official installer, a wrapper of
# their own -- is not ours to replace. --check follows whether it runs, and
# installing steps aside so the default agent uses it.
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check || fail "--check accepts a working foreign hermes command"
run_installer 0 || fail "installing over a foreign hermes command returns success"
run_installer 0 --now || fail "--now over a foreign hermes command returns success"
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
fail "a foreign hermes command is left untouched"
pass "a foreign hermes command is preserved and satisfies --check"
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 --check &&
fail "--check rejects a foreign Hermes without native prompted sessions"
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 &&
fail "installing refuses a foreign Hermes without native prompted sessions"
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
fail "an older foreign Hermes command is left untouched"
pass "a foreign Hermes must support native prompted sessions"
# Broken foreign paths are still foreign. They cannot be used, so --check says
# so and the installer refuses rather than replacing them.
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
chmod -x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a non-executable foreign hermes"
run_installer 0 && fail "the installer does not succeed over a non-executable foreign hermes"
[[ -f $test_home/.local/bin/hermes && ! -x $test_home/.local/bin/hermes ]] ||
fail "a non-executable foreign hermes is left untouched"
pass "a non-executable foreign hermes is preserved"
# The executable bit is not enough: a wrapper whose interpreter is gone passes
# -x and still cannot run. The probe has to run it to find out, and finding
# out never touches the file.
broken_interp_body="#!$test_home/nowhere/python3
print('hermes')"
printf '%s\n' "$broken_interp_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a foreign hermes whose interpreter is missing"
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose interpreter is missing"
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose interpreter is missing"
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_interp_body" ]] ||
fail "a foreign hermes whose interpreter is missing is left untouched"
pass "a foreign hermes with a missing interpreter is preserved and rejected"
# Likewise a wrapper that execs a target that is no longer there.
broken_target_body="#!/bin/bash
exec $test_home/nowhere/hermes \"\$@\""
printf '%s\n' "$broken_target_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a foreign hermes whose target is missing"
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose target is missing"
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose target is missing"
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_target_body" ]] ||
fail "a foreign hermes whose target is missing is left untouched"
pass "a foreign hermes with a missing target is preserved and rejected"
foreign_target="$test_home/foreign/hermes"
mkdir -p "$(dirname "$foreign_target")"
printf '%s\n' "$official_body" >"$foreign_target"
chmod +x "$foreign_target"
rm -f "$test_home/.local/bin/hermes"
ln -s "$foreign_target" "$test_home/.local/bin/hermes"
run_installer 0 --check || fail "--check accepts a foreign link to a working hermes command"
run_installer 0 || fail "the installer succeeds over a foreign link to a working hermes command"
run_installer 0 --now || fail "--now succeeds over a foreign link to a working hermes command"
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$foreign_target" ]] ||
fail "a foreign link to a working hermes command is left untouched"
pass "a foreign link to a working hermes command is preserved"
rm -f "$test_home/.local/bin/hermes"
ln -s "$test_home/nowhere/hermes" "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a dangling hermes link"
run_installer 0 && fail "the installer does not succeed over a dangling hermes link"
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$test_home/nowhere/hermes" ]] ||
fail "a dangling hermes link is left untouched"
pass "a dangling hermes link is preserved"
# A directory passes -x on search permission alone. It is still not a command.
rm -f "$test_home/.local/bin/hermes"
mkdir "$test_home/.local/bin/hermes"
run_installer 0 --check && fail "--check rejects a directory at the hermes path"
run_installer 0 && fail "the installer does not succeed over a directory at the hermes path"
[[ -d $test_home/.local/bin/hermes ]] || fail "a directory at the hermes path is left untouched"
pass "a directory at the hermes path is preserved and rejected"
# Mentioning the installer is not the same as being written by it.
rmdir "$test_home/.local/bin/hermes"
mentions_body="#!/bin/bash
# Replaces the stub omarchy-install-hermes-cli used to write.
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$mentions_body" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 || fail "installing over a wrapper that mentions the installer returns success"
[[ $(cat "$test_home/.local/bin/hermes") == "$mentions_body" ]] ||
fail "a wrapper that merely mentions the installer is left untouched"
pass "ownership needs the exact marker line, not a mention"
# Our own stub is ours to rewrite, so reinstalling refreshes it to the current
# template.
rm -f "$test_home/.local/bin/hermes"
printf '%s\n' "#!/bin/bash" "$stub_marker" "# stale template" >"$test_home/.local/bin/hermes"
chmod +x "$test_home/.local/bin/hermes"
run_installer 0 || fail "reinstalling over our own stub succeeds"
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the refreshed stub still carries the marker"
grep -q "stale template" "$test_home/.local/bin/hermes" && fail "reinstalling rewrites our own stub"
grep -q "exec env -u UV_PYTHON mise x" "$test_home/.local/bin/hermes" || fail "the refreshed stub is the current template"
pass "reinstalling refreshes the Omarchy stub"
mkdir -p "$test_tmp/mise/hermes-agent/lib/python$python_pin"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 || fail "reinstalling replaces an older owned Hermes environment"
tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes environment is removed from mise config"
tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "an older owned Hermes environment is uninstalled"
pass "reinstalling replaces an older owned Hermes environment"
# The mise-installed path is what a machine without the desktop app runs, and
# --check gates the default agent there too. The stub is present and its mise
# environment resolves, so readiness turns on the hermes mise runs -- exercised
# here in both directions, since the desktop and foreign cases cover only their
# own wrappers.
run_mise_check() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_WHERE_OK=1 \
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
OMARCHY_TEST_MISE_X_HERMES=1 \
OMARCHY_TEST_HERMES_CAPABLE="$1" \
HOME="$test_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --check >/dev/null 2>&1
}
run_mise_check 1 || fail "--check accepts a mise-installed hermes that runs the seeded session"
run_mise_check 0 && fail "--check rejects a mise-installed hermes without the flags omarchy-agent passes"
pass "--check follows the mise-installed hermes it would actually run"
rm -f "$test_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 &&
fail "installing refuses to claim an unmarked Hermes mise environment"
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "an unmarked Hermes mise environment is never removed"
[[ ! -e $test_home/.local/bin/hermes ]] ||
fail "an unmarked Hermes mise environment is not given an Omarchy wrapper"
pass "a Hermes mise environment needs wrapper ownership before replacement"
# install/user/mise.sh is sourced by install/user/all.sh through run_logged,
# which runs it under `bash -eE` and hands its exit code back to
# omarchy-provision-user's `set -euo pipefail`. Everything that finalizes a user
# -- the default browser, the mailto handler, the first-install migration
# markers, the finalize-user marker -- runs after that source, so this leaf
# returning non-zero costs the user all of it. The Hermes installer is the only
# line in it that can fail, and it does exactly that whenever hermes-desktop is
# installed but the app has not been launched yet: the case a second user on a
# shared machine hits on their first login.
mise_sh_home="$test_tmp/mise-sh-home"
mkdir -p "$mise_sh_home/.local/bin"
cat >"$mock_bin/omarchy-mise-install" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin/omarchy-mise-install"
# Desktop installed, nothing bootstrapped: omarchy-install-hermes-cli exits 1.
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$mise_sh_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 &&
fail "the Hermes installer exits non-zero when the desktop app has not set Hermes up"
# Sourced exactly as run_logged does it.
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$mise_sh_home" \
PATH="$mock_bin:$ROOT/bin:$PATH" \
bash -eE -c 'source "$1"' bash "$ROOT/install/user/mise.sh" >/dev/null 2>&1 ||
fail "user setup survives a Hermes install that cannot finish"
pass "user setup survives a Hermes install that cannot finish"
# UV_PYTHON pins the interpreter Hermes is built against. Left in the
# environment it reaches Hermes itself and every command the agent shells out
# to, so a `uv` run in the user's own project resolves 3.13 there as well --
# uv only warns that this contradicts the project's requires-python, then
# builds the venv anyway. The stub drops it before handing over.
leak_home="$test_tmp/leak-home"
leak_bin="$test_tmp/leak-bin"
leak_log="$test_tmp/leak-log"
leak_prefix="$test_tmp/leak-prefix"
mkdir -p "$leak_home/.local/bin" "$leak_bin" "$leak_prefix/hermes-agent/lib/python$python_pin"
# A mise whose `where` satisfies the stub's probe, so the stub goes straight to
# handing over, and whose `x` records the UV_PYTHON it was handed.
cat >"$leak_bin/mise" <<SH
#!/bin/bash
case \$1 in
where) echo "$leak_prefix" ;;
x) printf '%s' "\${UV_PYTHON-}" >"$leak_log" ;;
esac
SH
chmod +x "$leak_bin/mise"
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$leak_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 ||
fail "the installer writes a stub for the leak check"
HOME="$leak_home" PATH="$leak_bin:$mock_bin:$PATH" \
"$leak_home/.local/bin/hermes" --version >/dev/null 2>&1
[[ -f $leak_log ]] || fail "the stub reaches the command it wraps"
[[ -z $(cat "$leak_log") ]] ||
fail "the interpreter pin does not follow Hermes into the commands it runs"
pass "the interpreter pin does not follow Hermes into the commands it runs"
# --owns is the one answer to whether the wrapper on PATH is this installer's.
# Remove Preinstalls and the migration both ask it rather than carrying their
# own copy of the marker, so a change to what ownership means reaches them.
owns_home="$test_tmp/owns-home"
mkdir -p "$owns_home/.local/bin"
run_owns() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$owns_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --owns
if [[ ${1:-} == "ours" ]]; then
printf '# stands in for: exec "%s/.hermes/hermes-agent/venv/bin/python" "$@"\n' "$test_home" >>"$hermes"
fi
chmod +x "$hermes"
}
rm -f "$owns_home/.local/bin/hermes"
run_owns && fail "--owns says no when there is no wrapper at all"
legacy_marker="# Written by omarchy-install-hermes-cli."
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$owns_home/.local/bin/hermes"
chmod +x "$owns_home/.local/bin/hermes"
run_owns || fail "--owns recognises the stub this installer wrote"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." \
>"$owns_home/.local/bin/hermes"
run_owns && fail "--owns needs the exact marker line, not a mention"
# Quoting the marker inside a longer line is not the same as carrying it: the
# match is whole-line, so a wrapper describing what it replaced stays the
# user's.
printf '%s\n' "#!/bin/bash" "# Replaced '$stub_marker' with my own." \
>"$owns_home/.local/bin/hermes"
run_owns && fail "--owns needs the marker to be the whole line, not part of one"
rm -f "$owns_home/.local/bin/hermes"
ln -s "$test_home/.local/bin/hermes" "$owns_home/.local/bin/hermes"
run_owns && fail "--owns disclaims a symlink, whatever it resolves to"
rm -f "$owns_home/.local/bin/hermes"
pass "--owns answers for the wrapper this installer wrote and nothing else"
# The marker lives in exactly one place. Every other caller asks --owns, so a
# second copy is drift waiting to happen.
marker_copies=$(grep -rl "Written by omarchy-install-hermes-cli" \
"$ROOT/bin" "$ROOT/install" "$ROOT/migrations" 2>/dev/null | wc -l)
(( marker_copies == 1 )) ||
fail "only omarchy-install-hermes-cli spells out the ownership marker"
pass "the ownership marker is written down once"
# --remove tears down a Hermes CLI this installer owns, so Remove Hermes can
# clear one the desktop app never superseded. It turns on the same ownership as
# the rest of the file, so its cases mirror that split.
remove_home="$test_tmp/remove-home"
mkdir -p "$remove_home/.local/bin"
run_remove() {
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$remove_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --remove
write_legacy_stub() {
printf '%s\n' "#!/bin/bash" "$legacy_marker" 'touch "$OMARCHY_TEST_RAN"' >"$1"
chmod +x "$1"
}
rm -f "$remove_home/.local/bin/hermes"
: >"$mise_log"
run_remove || fail "--remove succeeds when there is nothing to remove"
# No stub means no proof the mise environment -- if one even exists -- is
# Omarchy's, so nothing may reach mise at all.
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "--remove leaves mise alone when nothing proves ownership"
pass "--remove is idempotent when no Hermes CLI is present"
# The app's launcher used to call this with no arguments; a default of --now
# would turn every launch into an install.
: >"$pkg_log"
run_installer && fail "the installer runs without a mode"
grep -q 'Usage' "$test_tmp/output" || fail "a missing mode prints usage"
[[ ! -s $pkg_log ]] || fail "a missing mode installs nothing"
pass "the installer needs its mode named"
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
: >"$mise_log"
run_remove || fail "--remove succeeds tearing down an owned CLI"
tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "--remove drops the mise tool from config"
tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "--remove uninstalls the mise tool"
[[ ! -e $remove_home/.local/bin/hermes ]] || fail "--remove takes the stub it owns"
pass "--remove tears down the mise CLI and the stub this installer owns"
rm -f "$hermes"
run_installer --check && fail "--check reports a Hermes with nothing at the command's path"
pass "--check is false with no hermes command"
# When mise still resolves the tool after the teardown, the environment
# survived whatever uninstall claimed, and --remove has to say so.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
OMARCHY_TEST_MISE_WHERE_OK=1 run_remove && fail "--remove claims success while mise still resolves the tool"
pass "--remove fails when the mise environment survives the teardown"
# The retired wrapper built Hermes through mise when run, so it is never run to
# find out whether Hermes is there, whether it is the file or a link to it.
write_legacy_stub "$hermes"
rm -f "$ran"
run_installer --check && fail "--check reports the retired mise wrapper as a Hermes"
[[ ! -e $ran ]] || fail "--check runs the retired mise wrapper"
rm -f "$hermes"
write_legacy_stub "$test_tmp/stub"
ln -s "$test_tmp/stub" "$hermes"
run_installer --check && fail "--check reports a link to the retired wrapper as a Hermes"
[[ ! -e $ran ]] || fail "--check runs the retired wrapper through a link"
rm -f "$hermes"
pass "--check never runs the retired mise wrapper"
foreign_remove_body="#!/bin/bash
exec /usr/local/bin/my-own-hermes \"\$@\""
printf '%s\n' "$foreign_remove_body" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
: >"$mise_log"
OMARCHY_TEST_MISE_WHERE_OK=1 run_remove || fail "--remove succeeds with a foreign hermes present"
[[ -f $remove_home/.local/bin/hermes && $(cat "$remove_home/.local/bin/hermes") == "$foreign_remove_body" ]] ||
fail "--remove leaves a hermes it does not own untouched"
# The wrapper may front a mise environment the user built against the very same
# spec; without the marker there is no telling, so the environment stays too.
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
fail "--remove never removes a mise environment it cannot prove is Omarchy's"
pass "--remove leaves a Hermes the user installed themselves"
write_hermes
run_installer --check || fail "--check follows a hermes that runs and takes seeded sessions"
pass "--check is true for a working hermes"
# Judged by what is left, not by what rm claimed: a stub that survives the
# teardown is a CLI still installed, and --remove has to say so.
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes"
chmod +x "$remove_home/.local/bin/hermes"
chmod 555 "$remove_home/.local/bin"
run_remove && fail "--remove claims success while the stub survives"
chmod 755 "$remove_home/.local/bin"
rm -f "$remove_home/.local/bin/hermes"
pass "--remove fails when the stub cannot be removed"
# The flags have to be defined by the help, not merely mentioned in it, and
# both of them: omarchy-agent passes --query to seed the session and --tui to
# keep it interactive.
OMARCHY_TEST_HERMES_HELP='Run with --tui for a terminal session; see --query in the docs.' run_installer --check &&
fail "--check accepts flags that are only mentioned"
OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query"
OMARCHY_TEST_HERMES_HELP='[-q QUERY, --query QUERY]' run_installer --check && fail "--check accepts a hermes without --tui"
pass "--check needs both flags defined, not mentioned"
# The app's marker says its install once landed, not that it is still there. A
# wrapper whose runtime has since gone answers for nothing, so readiness runs
# the command, exactly as it does for a hermes the user installed themselves.
ready_home="$test_tmp/ready-home"
mkdir -p "$ready_home/.hermes/hermes-agent/venv/bin" "$ready_home/.local/bin"
touch "$ready_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
printf '%s\n' "#!/bin/bash" "exec $ready_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$ready_home/.local/bin/hermes"
chmod +x "$ready_home/.local/bin/hermes"
chmod -x "$hermes"
run_installer --check && fail "--check accepts a hermes that is not executable"
rm -f "$hermes"
mkdir "$hermes"
run_installer --check && fail "--check accepts a directory at the command's path"
rmdir "$hermes"
ln -s "$test_tmp/nowhere" "$hermes"
run_installer --check && fail "--check accepts a dangling link"
rm -f "$hermes"
pass "--check rejects what is not a command that runs"
run_ready_check() {
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$ready_home" \
PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-install-hermes-cli" --check >/dev/null 2>&1
# A hermes the user set up themselves is what the default agent will run, so
# --now installs nothing beside it.
write_hermes
before=$(cat "$hermes")
: >"$pkg_log"
run_installer --now || fail "--now succeeds over a working hermes of the user's own" "$(cat "$test_tmp/output")"
[[ $(cat "$hermes") == "$before" ]] || fail "--now leaves the user's hermes as it was"
[[ ! -s $pkg_log ]] || fail "--now installs a package beside a working hermes"
pass "--now leaves a working hermes of the user's own alone"
# One that runs but predates seeded sessions is theirs to update, not ours to
# replace with the app.
: >"$pkg_log"
OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --now && fail "--now replaces a hermes that predates seeded sessions"
grep -q 'Update it' "$test_tmp/output" || fail "an old hermes gets update guidance" "$(cat "$test_tmp/output")"
[[ ! -s $pkg_log ]] || fail "an old hermes has a package installed over it"
pass "--now tells the user to update a hermes that predates seeded sessions"
rm -f "$hermes"
: >"$pkg_log"
run_installer --now && fail "--now carries on past the mocked package failure"
grep -qx 'hermes-desktop' "$pkg_log" || fail "--now installs the hermes-desktop package" "$(cat "$test_tmp/output")"
pass "--now installs Hermes Desktop when no Hermes answers"
# The retired wrapper is not a Hermes for --now either, and still is not run.
write_legacy_stub "$hermes"
rm -f "$ran"
: >"$pkg_log"
run_installer --now && fail "--now carries on past the mocked package failure"
grep -qx 'hermes-desktop' "$pkg_log" || fail "--now installs over the retired wrapper"
[[ ! -e $ran ]] || fail "--now runs the retired wrapper"
rm -f "$hermes"
pass "--now installs over the retired mise wrapper without running it"
# With the app installed, a command that runs is not the whole answer: until
# the runtime carries the marker upstream writes last and the packaged app has
# been seeded, --now still has minutes of work, so --check says no and the menu
# opens a terminal for it rather than running it where nobody can see.
runtime="$test_home/.hermes/hermes-agent"
native="$runtime/apps/desktop/release/linux-unpacked/resources"
write_hermes ours
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls an app with no runtime installed"
mkdir -p "$runtime" "$native"
touch "$runtime/.hermes-bootstrap-complete"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls an app whose packaged build is not seeded installed"
touch "$native/app.asar" "$native/install-stamp.json"
printf '#!/bin/bash\nexit 0\n' >"$native/../Hermes"
chmod +x "$native/../Hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows a finished install"
pass "--check says no while --now still has work to do behind the app"
# With the app installed, the terminal has to be on the app's Hermes: a working
# command from somewhere else beside a finished runtime is not installed, so
# --now gets to put the runtime's own command back.
write_hermes
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls the app installed while the command is somebody else's"
write_hermes ours
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the runtime's own command"
pass "--check needs the app's own command once the app is installed"
# Installed and finished: nothing to do, and quickly, because choosing the
# agent from the menu runs this.
: >"$pkg_log"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --now || fail "--now accepts a finished install" "$(cat "$test_tmp/output")"
[[ ! -s $pkg_log && ! -s $test_tmp/output ]] || fail "a finished install is set up again" "$(cat "$test_tmp/output")"
pass "--now has nothing to do once the app and its runtime are in"
[[ ! -s $mise_log ]] || fail "the installer touched mise" "$(cat "$mise_log")"
pass "Hermes never goes through mise"
# A PATH that finds no hermes at all is not in the way: the theme unit asks
# from a service whose PATH has no ~/.local/bin and runs the command by path.
run_installer_bare_path() {
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-0}" \
OMARCHY_TEST_MISE_LOG="$mise_log" OMARCHY_TEST_PKG_LOG="$pkg_log" OMARCHY_TEST_RAN="$ran" \
HOME="$test_home" PATH="$mock_bin:/usr/bin" \
bash "$ROOT/bin/omarchy-install-hermes-cli" "$@" >"$test_tmp/output" 2>&1
}
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer_bare_path --check || fail "--check from a PATH without ~/.local/bin still follows a finished install" "$(cat "$test_tmp/output")"
pass "--check does not need ~/.local/bin on the caller's PATH"
run_ready_check && fail "--check rejects the app's wrapper when its runtime is gone"
# What omarchy-agent runs is whichever hermes PATH finds first, and Omarchy
# puts mise's shims ahead of ~/.local/bin. A command ahead of the one the probe
# vets means the agent would run something else, so it is not installed, and
# --now says what is in the way rather than reporting a Hermes that is not the
# one the agent gets.
cp "$hermes" "$mock_bin/hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls a shadowed command installed"
rm -rf "$test_home/.hermes" "$test_home/.local/bin/.hermes-before-desktop."*
run_installer --check && fail "--check calls a shadowed command of the user's own installed"
: >"$pkg_log"
run_installer --now && fail "--now reports a shadowed command as ready"
grep -qF "$mock_bin/hermes" "$test_tmp/output" || fail "--now names the command in the way" "$(cat "$test_tmp/output")"
[[ ! -s $pkg_log ]] || fail "a shadowed command has the app installed over it"
rm -f "$mock_bin/hermes"
run_installer --check || fail "--check follows the command once nothing shadows it"
pass "a hermes ahead of ~/.local/bin on PATH is reported, not installed over"
cat >"$ready_home/.hermes/hermes-agent/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 "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check || fail "--check accepts the app's wrapper once it runs"
pass "readiness runs the app's command rather than trusting its marker"
# The retired wrapper's environment goes with the wrapper's proof, wherever
# that is. --retire-mise is the migration's whole job; --now does the same 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`.
write_legacy_stub "$hermes"
: >"$mise_log"; rm -f "$mise_log.removed" "$mise_log.unrequested"
OMARCHY_TEST_MISE_BUILT=1 run_installer --retire-mise || fail "--retire-mise succeeds over the wrapper" "$(cat "$test_tmp/output")"
[[ ! -e $hermes ]] || fail "--retire-mise removes the wrapper"
grep -qF "mise rm -g pipx:hermes-agent[extras=all]" "$mise_log" || fail "--retire-mise removes the global mise Hermes" "$(cat "$mise_log")"
grep -qF "mise uninstall --all pipx:hermes-agent[extras=all]" "$mise_log" || fail "--retire-mise uninstalls the mise Hermes" "$(cat "$mise_log")"
: >"$mise_log"
run_installer --retire-mise || fail "--retire-mise succeeds with nothing of Omarchy's"
[[ ! -s $mise_log ]] || fail "--retire-mise asks mise about an environment nothing proves Omarchy's" "$(cat "$mise_log")"
pass "--retire-mise removes the wrapper and the environment it built, and only with proof"
# A release whose help lists only the old probe's --oneshot marker cannot run
# the seeded --tui --query session omarchy-agent starts, so it is not ready.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "--oneshot"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release without the flags omarchy-agent passes"
pass "a release listing only --oneshot is not prompt-ready"
# A release that lists --tui-theme and --query-log but has dropped the bare
# --tui/--query omarchy-agent passes must not read as ready on the substring
# alone. The probe matches at a flag boundary for exactly this case.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "[--tui-theme THEME] [--query-log FILE]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release whose flags only contain --tui/--query as a substring"
pass "a flag that merely contains --tui or --query is not prompt-ready"
# Each flag answers for itself: a release that kept --tui but dropped --query,
# or the reverse, cannot run the seeded session either, so neither grep may
# ride on the other's match.
for kept in '--tui' '-q QUERY, --query QUERY'; do
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<SH
#!/bin/bash
if [[ \${1:-} == "chat" && \${2:-} == "--help" ]]; then
echo "[$kept]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts a release listing only $kept"
done
pass "either flag alone is not prompt-ready"
# An underscore continues a flag name just as a dash does: --tui_mode is not
# --tui.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo "[--tui_mode MODE] [--query_log FILE]"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts flags that extend --tui/--query with an underscore"
pass "an underscore continuation is not the bare flag"
# A flag mentioned in another option's help text is not that option. Hermes
# already writes "With --tui:" into --dev's description, so prose has to stay
# prose even when both names appear in it.
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
#!/bin/bash
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
echo " --dev With --tui: run sources via tsx"
echo " --log FILE Where --query output lands"
else
echo "hermes-agent 0.0.0-test"
fi
SH
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
run_ready_check && fail "--check accepts flags that appear only in option descriptions"
pass "a flag mentioned in prose is not a defined option"
# The shim mise put ahead of ~/.local/bin is what the agent would run until
# the environment goes; it goes with the uninstall, and so does the saved
# wrapper, since once the environment is gone the proof would only keep
# --check saying no and claim an environment the user builds later.
write_hermes
saved="$test_home/.local/bin/.hermes-before-desktop.abc123"
mkdir -p "$saved"
write_legacy_stub "$saved/hermes"
cp "$hermes" "$mock_bin/hermes"
: >"$mise_log"; rm -f "$mise_log.removed" "$mise_log.unrequested"
run_installer --check && fail "--check calls a handover with the shim still ahead finished"
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$mock_bin/hermes" run_installer --now || fail "--now finishes the handover over a saved wrapper" "$(cat "$test_tmp/output")"
grep -qF "mise uninstall --all pipx:hermes-agent[extras=all]" "$mise_log" || fail "--now retires the environment the saved wrapper proves Omarchy's" "$(cat "$mise_log")"
[[ ! -e $mock_bin/hermes ]] || fail "the shim is gone with the environment"
[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its empty directory go once the environment is gone"
run_installer --check || fail "--check follows a finished handover" "$(cat "$test_tmp/output")"
pass "--now retires the mise Hermes once the runtime installer has saved the wrapper aside"
+231 -38
View File
@@ -53,20 +53,39 @@ 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
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"
@@ -80,33 +99,38 @@ if [[ $force == true ]] || ! git -C "$runtime" merge-base --is-ancestor "$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"
# 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"
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
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 ]]
[[ ${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/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 "$@"
@@ -149,28 +173,28 @@ 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'
# 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])
script = source.read_text()
for original, replacement in {
substitutions = {
'/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)
}
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() {
@@ -179,11 +203,24 @@ new_home() {
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:$PATH" \
bash "$test_tmp/installer" >"$test_tmp/output" 2>&1
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
@@ -193,9 +230,10 @@ 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"
[[ $(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"
@@ -284,8 +322,10 @@ run_installer && fail "incomplete existing app requires repair"
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
run_installer || fail "patch conflict fixture sets up"
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"
@@ -297,6 +337,17 @@ run_installer && fail "incomplete modified runtime cannot be reset by upstream i
! 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"
@@ -305,6 +356,10 @@ run_installer || fail "clean incomplete full-history release checkout is repaire
[[ $(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"
@@ -312,10 +367,70 @@ 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"
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"
@@ -345,10 +460,88 @@ 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"
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"
+244
View File
@@ -0,0 +1,244 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1790017600.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
hermes="$test_home/.local/bin/hermes"
marker="# Written by omarchy-install-hermes-cli."
tool='pipx:hermes-agent[extras=all]'
mise_log="$test_tmp/mise-log"
mkdir -p "$mock_bin" "$test_home/.local/bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-default-agent" <<'SH'
#!/bin/bash
printf '%s\n' "${OMARCHY_TEST_DEFAULT_AGENT:-}"
SH
# `where` finds the environment mise built when a test says it did, and `ls -g`
# lists it as requested, until uninstalled and unrequested respectively; a test
# can make either removal stick. Every call is logged, so a test can tell
# being asked from being told to remove.
cat >"$mock_bin/mise" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_MISE_LOG"
case "$1" in
where)
[[ ${OMARCHY_TEST_MISE_STUCK:-0} == 1 ]] && exit 0
[[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_MISE_LOG.removed ]]
;;
ls)
[[ ${OMARCHY_TEST_MISE_LS_FAIL:-0} == 1 ]] && exit 1
if [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_MISE_LOG.unrequested ]]; then
echo '{"pipx:hermes-agent[extras=all]": [{"version": "latest"}]}'
else
echo '{}'
fi
;;
rm)
[[ ${OMARCHY_TEST_MISE_RM_STUCK:-0} == 1 ]] || touch "$OMARCHY_TEST_MISE_LOG.unrequested"
;;
uninstall)
touch "$OMARCHY_TEST_MISE_LOG.removed"
;;
esac
SH
chmod +x "$mock_bin"/*
# A PATH with no mise on it at all, for a machine that has none: the mocks
# minus theirs, and links to every system command but the real one, so the
# ambient PATH cannot supply what the test says is missing.
no_mise_path="$test_tmp/bin-without-mise:$test_home/.local/bin:$ROOT/bin:$test_tmp/usr-bin-without-mise"
mkdir -p "$test_tmp/bin-without-mise"
for mock in "$mock_bin"/*; do
[[ $(basename "$mock") == mise ]] || ln -s "$mock" "$test_tmp/bin-without-mise/"
done
# A directory of links made here, never a copy of /usr/bin that could itself be
# a link to it: the rm below must only ever remove a link of this test's own.
mkdir "$test_tmp/usr-bin-without-mise"
ln -s "$(realpath /usr/bin)"/* "$test_tmp/usr-bin-without-mise/"
rm -f "$test_tmp/usr-bin-without-mise/mise"
! PATH="$no_mise_path" command -v mise >/dev/null 2>&1 || fail "the PATH built for a machine without mise still finds one"
# The real installer is on PATH: the migration asks it what to retire and
# whether a Hermes still answers before telling the user how to get one back.
# ~/.local/bin is on PATH the way Omarchy puts it there, after the mocks.
run_migration() {
: >"$mise_log"
rm -f "$mise_log.removed" "$mise_log.unrequested"
OMARCHY_TEST_MISE_LOG="$mise_log" \
HOME="$test_home" \
PATH="${OMARCHY_TEST_PATH:-$mock_bin:$test_home/.local/bin:$ROOT/bin:$PATH}" \
bash -euo pipefail "$migration" >"$test_tmp/output" 2>&1
}
write_stub() {
printf '%s\n' "#!/bin/bash" "$marker" "exec mise x '$tool' -- hermes \"\$@\"" >"$hermes"
chmod +x "$hermes"
}
write_stub
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds over the Omarchy wrapper" "$(cat "$test_tmp/output")"
[[ ! -e $hermes ]] || fail "the migration removes the wrapper Omarchy wrote"
grep -qxF "rm -g $tool" "$mise_log" || fail "the migration removes the global mise Hermes" "$(cat "$mise_log")"
grep -qxF "uninstall --all $tool" "$mise_log" || fail "the migration uninstalls the mise Hermes" "$(cat "$mise_log")"
pass "the migration retires the wrapper and the Hermes mise built"
write_stub
run_migration || fail "the migration succeeds over a wrapper nobody ran"
[[ ! -e $hermes ]] || fail "the migration removes a wrapper nobody ran"
! grep -q '^rm -g' "$mise_log" || fail "nothing built means nothing to uninstall"
pass "a wrapper nobody ran goes without an uninstall"
run_migration || fail "the migration succeeds with nothing to do"
[[ ! -s $mise_log ]] || fail "with nothing of Omarchy's left, mise is not asked" "$(cat "$mise_log")"
pass "the migration is a no-op once the wrapper is gone"
# Anyone else's hermes stays exactly where it is, is not run, and does not vouch
# for a mise environment being Omarchy's.
foreign_ran="$test_tmp/foreign-ran"
foreign_body="#!/bin/bash
touch $foreign_ran
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$foreign_body" >"$hermes"
chmod +x "$hermes"
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds over a foreign hermes"
[[ -x $hermes && $(cat "$hermes") == "$foreign_body" ]] || fail "a hermes the user set up stays as it is"
[[ ! -e $foreign_ran ]] || fail "the migration runs a foreign hermes"
! grep -q '^rm -g' "$mise_log" || fail "a user's mise environment is removed on the strength of their wrapper"
pass "the migration leaves a hermes the user set up alone"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
chmod +x "$hermes"
run_migration || fail "the migration succeeds over a wrapper that mentions the installer"
[[ -x $hermes ]] || fail "a wrapper that merely mentions the installer is removed"
pass "a wrapper that merely mentions the installer stays"
# The wrapper was written as a regular file, so a link is someone else's
# arrangement even when it lands on the marked file.
rm -f "$hermes"
printf '%s\n' "#!/bin/bash" "$marker" >"$test_tmp/stub"
ln -s "$test_tmp/stub" "$hermes"
run_migration || fail "the migration succeeds over a link to the wrapper"
[[ -L $hermes ]] || fail "a link to the wrapper is removed"
rm -f "$hermes"
ln -s "$test_home/nowhere" "$hermes"
run_migration || fail "the migration succeeds over a dangling link"
[[ -L $hermes ]] || fail "a dangling link is removed"
rm -f "$hermes"
mkdir "$hermes"
run_migration || fail "the migration succeeds over a directory at the command's path"
[[ -d $hermes ]] || fail "a directory at the command's path is removed"
rmdir "$hermes"
pass "the migration leaves links and directories at the command's path alone"
# Without the wrapper nothing proves a mise environment is Omarchy's, the app
# being installed included: a user who built the same spec keeps it.
rm -f "$hermes"
OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds with the app installed and no wrapper"
! grep -q '^rm -g' "$mise_log" || fail "a mise environment without the wrapper is removed" "$(cat "$mise_log")"
pass "a mise environment without the wrapper is nobody's to remove"
# The environment goes before the wrapper does, because the wrapper is the
# only proof a rerun would have. A removal that leaves the environment behind
# keeps the wrapper, says how to finish by hand, and leaves the migration
# pending; once it can finish, it does.
write_stub
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_MISE_STUCK=1 run_migration && fail "a removal that left the environment behind counts as done"
[[ -x $hermes ]] || fail "a failed removal takes the wrapper anyway"
grep -qF "mise uninstall --all '$tool'" "$test_tmp/output" || fail "a failed removal says how to finish by hand" "$(cat "$test_tmp/output")"
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration finishes once the environment can go" "$(cat "$test_tmp/output")"
[[ ! -e $hermes ]] || fail "the retry takes the wrapper once the environment is gone"
pass "a removal that cannot finish leaves the wrapper and the migration pending"
# `mise rm -g` exits 0 whether or not it removed anything, so an environment
# uninstalled but still requested in the global config -- where `mise up` would
# build it again -- is judged by the listing, not the exit code.
write_stub
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_MISE_RM_STUCK=1 run_migration && fail "an environment still requested counts as removed"
[[ -x $hermes ]] || fail "a still-requested environment takes the wrapper anyway"
grep -qF "mise rm -g '$tool'" "$test_tmp/output" || fail "a still-requested environment says how to finish by hand" "$(cat "$test_tmp/output")"
pass "an environment mise still requests is not counted as gone"
# A listing that cannot be read is not an answer: the wrapper stays and the
# migration stays pending rather than deleting the proof on a guess.
write_stub
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_MISE_LS_FAIL=1 run_migration && fail "an unreadable listing counts as retired"
[[ -x $hermes ]] || fail "an unreadable listing takes the wrapper anyway"
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration finishes once the listing can be read" "$(cat "$test_tmp/output")"
[[ ! -e $hermes ]] || fail "the retry takes the wrapper once the listing answers"
pass "a mise listing that cannot be read leaves the migration pending"
# Without mise the listing cannot be read either, and a request it may still
# hold would build Hermes again the day mise is back; the wrapper stays, the
# migration stays pending, and the way out names mise first.
write_stub
OMARCHY_TEST_PATH="$no_mise_path" run_migration && fail "a machine without mise counts the environment as retired"
[[ -x $hermes ]] || fail "without mise the wrapper is taken anyway"
grep -qF "omarchy pkg add mise" "$test_tmp/output" || fail "without mise the way out names mise first" "$(cat "$test_tmp/output")"
pass "without mise the migration stays pending rather than guessing"
# The runtime installer saves whatever held the command aside before upstream's
# installer takes the name, so a user who chose Hermes before this ran has the
# wrapper in that backup and the environment still requested. The copy is proof
# enough, and goes once it has served: left behind it would keep --check
# saying no for a finished install.
saved_dir="$test_home/.local/bin/.hermes-before-desktop.abc123"
mkdir -p "$saved_dir"
write_stub
mv "$hermes" "$saved_dir/hermes"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/python $test_home/.hermes/hermes-agent/hermes \"\$@\"" >"$hermes"
chmod +x "$hermes"
runtime_command=$(cat "$hermes")
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds over a saved wrapper" "$(cat "$test_tmp/output")"
grep -qxF "rm -g $tool" "$mise_log" || fail "a saved wrapper proves the environment Omarchy's" "$(cat "$mise_log")"
[[ ! -e $saved_dir/hermes && ! -d $saved_dir ]] || fail "the saved copy and its empty directory go once the environment is gone"
[[ $(cat "$hermes") == "$runtime_command" ]] || fail "the runtime's command is left alone"
pass "a wrapper saved aside by the runtime installer still retires the environment"
rm -rf "$saved_dir" "$hermes"
# The installer makes real backup directories, so one that is a link is
# somebody else's arrangement: what it points at is neither proof nor ours to
# remove, marker or no marker.
mkdir -p "$test_home/archive"
write_stub
mv "$hermes" "$test_home/archive/hermes"
ln -s "$test_home/archive" "$test_home/.local/bin/.hermes-before-desktop.linked"
OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds over a linked backup directory"
! grep -q '^rm -g' "$mise_log" || fail "a marked file behind a linked backup directory counts as proof"
[[ -f $test_home/archive/hermes ]] || fail "a file behind a linked backup directory is removed"
rm -f "$test_home/.local/bin/.hermes-before-desktop.linked"; rm -rf "$test_home/archive"
pass "a linked backup directory is neither proof nor touched"
# Choosing Hermes again is what installs the runtime, so a default agent whose
# command just went is told so; one that still answers, or another agent, is not.
write_stub
OMARCHY_TEST_DEFAULT_AGENT=hermes OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds for a Hermes default agent"
grep -q 'omarchy default agent hermes' "$test_tmp/output" || fail "a default agent that just went is told how to come back" "$(cat "$test_tmp/output")"
cat >"$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 "$hermes"
OMARCHY_TEST_DEFAULT_AGENT=hermes run_migration || fail "the migration succeeds with a working Hermes"
! grep -q 'default agent' "$test_tmp/output" || fail "a working Hermes gets reinstall guidance"
rm -f "$hermes"
OMARCHY_TEST_DEFAULT_AGENT=codex run_migration || fail "the migration succeeds for another default agent"
! grep -q 'default agent' "$test_tmp/output" || fail "another default agent gets Hermes guidance"
pass "the migration says how to reinstall a Hermes that was the default agent"
+2 -37
View File
@@ -24,15 +24,6 @@ cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
SH
# The CLI teardown is the installer's own, exercised in hermes-cli-test.sh; here
# it is mocked to a logger so this test stays about what Remove Hermes does with
# ~/.hermes, and to keep real mise out of a run with HOME pointed at a fixture.
cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_INSTALLER_LOG"
exit "${OMARCHY_TEST_INSTALLER_STATUS:-0}"
SH
# The remover asks through gum whether the user's data should go too. The stub
# answers "no" unless a test says otherwise, and logs every call: a real gum
# would hang a test run, and one that answered "yes" on its own would be the
@@ -77,12 +68,9 @@ seed_install() {
# </dev/null pins stdin off a terminal, so these runs exercise the
# non-interactive path no matter where the suite itself is running.
remove() {
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_INSTALLER_STATUS="${OMARCHY_TEST_INSTALLER_STATUS:-0}" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
@@ -92,11 +80,9 @@ remove() {
# script(1) puts the remover on a pty, which is the only way -t 0 answers true
# without a person at a real one; the stubbed gum then supplies the answer.
remove_tty() {
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \
@@ -142,12 +128,6 @@ pass "removal keeps the user's data unasked when there is no terminal"
[[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed"
pass "removal takes the command the app installed"
# Removal also asks the installer to tear down a mise CLI the app superseded, so
# a copy left from before the app took over does not linger once Hermes is gone.
tr '\0' '\n' <"$test_tmp/installer-log" | grep -qx -- '--remove' ||
fail "removal asks the installer to tear down its own CLI"
pass "removal tears down the mise CLI through the installer"
# A hermes command the app did not write survives even when the app did install
# a runtime of its own.
seed_install
@@ -168,10 +148,6 @@ printf 'my local edit\n' >"$test_home/.hermes/hermes-agent/PATCH"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds when the app never finished installing Hermes"
# The stranded pre-desktop CLI is exactly the interrupted-install case, so the
# teardown must be asked for here too, not only when the app's runtime landed.
tr '\0' '\n' <"$test_tmp/installer-log" | grep -qx -- '--remove' ||
fail "removal tears down the CLI even when the app never finished installing"
[[ -d $test_home/.hermes/hermes-agent ]] ||
fail "a Hermes runtime the app never installed survives removal"
[[ -f $test_home/.hermes/hermes-agent/PATCH ]] ||
@@ -235,17 +211,6 @@ OMARCHY_TEST_GUM_STATUS=0 remove_tty ||
fail "a yes takes ~/.hermes whole when the marker never appeared"
pass "removal honors a yes on the named paths without the marker"
# A CLI teardown that fails must not stop the runtime handling, and must not be
# papered over either: the data work still happens, and the failure reaches the
# caller's exit code.
seed_install
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
OMARCHY_TEST_INSTALLER_STATUS=1 remove && fail "a failed CLI teardown surfaces in the exit code"
[[ ! -d $test_home/.hermes/hermes-agent ]] ||
fail "a failed CLI teardown does not stop the runtime removal"
pass "a failed CLI teardown is reported after the runtime is handled"
# Real SQLite writers exercise the kernel's live/deleted file descriptors.
# Package, service and confirmation commands remain confined to the mocks.
python3 - "$test_tmp" <<'PY'
@@ -276,7 +241,7 @@ def setup(name):
(home / '.config/Hermes').mkdir(parents=True)
env = {**os.environ, 'HOME': str(home), 'PATH': f"{scratch / 'bin'}:/usr/bin:/bin",
'OMARCHY_TEST_GUM_STATUS': '0'}
for key in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'):
for key in ('DROP', 'SYSTEMCTL', 'GUM'):
log = home / (key + '.log')
log.touch()
env['OMARCHY_TEST_' + key + '_LOG'] = str(log)
@@ -309,7 +274,7 @@ def blocked(result, home, runtime, child):
assert 'Close Hermes' in result.stderr, result.stderr
assert (runtime / '.hermes-bootstrap-complete').exists()
assert all((home / (name + '.log')).stat().st_size == 0
for name in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'))
for name in ('DROP', 'SYSTEMCTL', 'GUM'))
assert child.poll() is None, 'remover must not kill sessions'
for deleted in (False, True):
+2 -43
View File
@@ -36,9 +36,8 @@ SH
chmod +x "$mock_bin"/*
# $ROOT/bin after the mocks: Remove Preinstalls asks omarchy-install-hermes-cli
# whether the wrapper is Omarchy's rather than matching the marker itself, and
# that is the real command at runtime. The mocks still shadow what they name.
# $ROOT/bin after the mocks, so the real helpers answer wherever a mock does not
# shadow them.
export PATH="$mock_bin:$ROOT/bin:$PATH"
export HOME="$test_home"
export OMARCHY_TEST_PKG_LOG="$pkg_log"
@@ -92,43 +91,3 @@ pass "declining Remove Preinstalls changes nothing"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -f $marker ]] || fail "Remove Preinstalls records the opt-out"
pass "Remove Preinstalls records the opt-out"
# Hermes' wrapper is only a preinstall when omarchy-install-hermes-cli wrote it.
# The desktop app's command and an official install live at the same path and
# are the user's, whether or not any package says so.
hermes="$test_home/.local/bin/hermes"
mkdir -p "$(dirname "$hermes")"
printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ ! -e $hermes ]] || fail "Remove Preinstalls deletes the Omarchy Hermes wrapper"
pass "Remove Preinstalls deletes the Omarchy Hermes wrapper"
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes ]] || fail "Remove Preinstalls keeps the desktop app's Hermes command"
pass "Remove Preinstalls keeps the desktop app's Hermes command"
official_body="#!/bin/bash
unset PYTHONPATH
unset PYTHONHOME
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
printf '%s\n' "$official_body" >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes && $(cat "$hermes") == "$official_body" ]] || fail "Remove Preinstalls keeps an official Hermes install"
pass "Remove Preinstalls keeps an official Hermes install"
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
chmod +x "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -x $hermes ]] || fail "Remove Preinstalls keeps a wrapper that merely mentions the installer"
pass "Remove Preinstalls keeps a wrapper that merely mentions the installer"
rm -f "$hermes"
ln -s "$test_home/nowhere/hermes" "$hermes"
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
[[ -L $hermes ]] || fail "Remove Preinstalls keeps a foreign hermes link"
pass "Remove Preinstalls keeps a foreign hermes link"