#!/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" <"$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"