The stub exports UV_PYTHON so mise builds Hermes against 3.13, which Hermes requires and Arch's Python is past. Exported, it survived the exec into Hermes itself and reached every command the agent shells out to. Hermes is a coding agent that runs commands in the user's own repositories, so a `uv venv` or `uv sync` there resolved 3.13 as well: on a project declaring requires-python >=3.14, uv warns that the interpreter contradicts it and builds the venv anyway. Dropping it at the handover keeps the pin over the install, where it belongs. mise x resolves the tool it already installed without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
285 lines
14 KiB
Bash
Executable File
285 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
mock_bin="$test_tmp/bin"
|
|
test_home="$test_tmp/home"
|
|
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-cmd-missing" <<'SH'
|
|
#!/bin/bash
|
|
! command -v "$1" >/dev/null 2>&1
|
|
SH
|
|
|
|
# `mise where` must fail so the installer sees no Hermes behind the stub.
|
|
cat >"$mock_bin/mise" <<'SH'
|
|
#!/bin/bash
|
|
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
|
|
[[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]] && exit 0
|
|
[[ $1 != "where" ]]
|
|
SH
|
|
|
|
chmod +x "$mock_bin"/*
|
|
|
|
run_installer() {
|
|
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
|
|
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
|
|
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
|
HOME="$test_home" \
|
|
PATH="$mock_bin:$PATH" \
|
|
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 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 --version, as the real one does: foreign wrappers
|
|
# below exec it, and the installer probes them by running exactly that.
|
|
mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
|
|
printf '%s\n' "#!/bin/bash" 'echo "hermes-agent 0.0.0-test"' >"$test_home/.hermes/hermes-agent/venv/bin/hermes"
|
|
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"
|
|
|
|
# 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"
|
|
|
|
# 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"
|