Merge pull request #12844 from omacom/hermes-desktop-default-agent
Install Hermes for the default agent as the desktop app's self-updating runtime
This commit is contained in:
16 files changed
+1648
-1245
No files matched your search
@@ -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"
|
||||
+204
-521
@@ -9,568 +9,251 @@ 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"}]}'
|
||||
elif [[ ${OMARCHY_TEST_MISE_SIBLING:-0} == 1 ]]; then
|
||||
echo '{"pipx:hermes-agent-tools": [{"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"
|
||||
# Hermes is only ever installed through the app: a hermes that works but did
|
||||
# not come from it is not the app's, so --check says no and --now installs the
|
||||
# app over it, the command saved aside by the runtime setup further on.
|
||||
write_hermes
|
||||
run_installer --check && fail "--check calls a hermes without the app installed"
|
||||
: >"$pkg_log"
|
||||
run_installer --now && fail "--now carries on past the mocked package failure"
|
||||
grep -qx 'hermes-desktop' "$pkg_log" || fail "--now installs the app over a hermes from elsewhere" "$(cat "$test_tmp/output")"
|
||||
pass "a hermes that did not come from the app is not the app's; choosing Hermes installs the app"
|
||||
|
||||
# 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"
|
||||
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 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"
|
||||
# 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"
|
||||
|
||||
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
|
||||
# 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"
|
||||
|
||||
# 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_DESKTOP_INSTALLED=1 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_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query"
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 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"
|
||||
|
||||
chmod -x "$hermes"
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a hermes that is not executable"
|
||||
rm -f "$hermes"
|
||||
mkdir "$hermes"
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a directory at the command's path"
|
||||
rmdir "$hermes"
|
||||
ln -s "$test_tmp/nowhere" "$hermes"
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a dangling link"
|
||||
rm -f "$hermes"
|
||||
write_hermes ours
|
||||
pass "--check rejects what is not a command that runs"
|
||||
|
||||
# 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 -f "$mock_bin/hermes"
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the command once nothing shadows it"
|
||||
pass "a hermes ahead of ~/.local/bin on PATH is not the one the agent gets"
|
||||
|
||||
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 global tool whose name merely starts the same way is not the retired one;
|
||||
# read as it, it could never be removed and the migration would stay pending.
|
||||
write_legacy_stub "$hermes"
|
||||
: >"$mise_log"
|
||||
OMARCHY_TEST_MISE_SIBLING=1 run_installer --retire-mise || fail "--retire-mise succeeds beside a tool named like the retired one" "$(cat "$test_tmp/output")"
|
||||
[[ ! -e $hermes ]] || fail "the wrapper goes when only a similarly named tool is requested"
|
||||
! grep -q '^mise rm' "$mise_log" || fail "a similarly named tool is taken for the retired one" "$(cat "$mise_log")"
|
||||
pass "a mise tool named like the retired one does not keep the migration pending"
|
||||
|
||||
# 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"
|
||||
# --now finishing a handover, mise environment and shim included, needs the
|
||||
# package fixtures and lives in hermes-desktop-install-test.sh.
|
||||
@@ -53,20 +53,40 @@ 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"
|
||||
printf '%s\n' "${npm_config_yes:-unset}" >"$OMARCHY_TEST_ROOT/install-npx-answer"
|
||||
[[ ${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 +100,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 +174,48 @@ 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
|
||||
# The only thing the installer asks mise is to remove what the retired wrapper
|
||||
# built. `where` finds that environment and `ls -g` lists it while a test says
|
||||
# it was built and it has not been uninstalled and unrequested; uninstalling
|
||||
# also takes the shim stand-in named in OMARCHY_TEST_SHIM, as mise's does.
|
||||
cat >"$test_tmp/bin/mise" <<'MOCK'
|
||||
#!/bin/bash
|
||||
printf 'mise %s\n' "$*" >>"$OMARCHY_TEST_ROOT/mise-log"
|
||||
case "$1" in
|
||||
where) [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-removed ]] ;;
|
||||
ls)
|
||||
if [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-unrequested ]]; then
|
||||
echo '{"pipx:hermes-agent[extras=all]": [{"version": "latest"}]}'
|
||||
else
|
||||
echo '{}'
|
||||
fi
|
||||
;;
|
||||
rm) touch "$OMARCHY_TEST_ROOT/mise-unrequested" ;;
|
||||
uninstall) touch "$OMARCHY_TEST_ROOT/mise-removed"; rm -f "${OMARCHY_TEST_SHIM:-}" ;;
|
||||
esac
|
||||
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 +224,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" npm_config_yes= \
|
||||
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" npm_config_yes= \
|
||||
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 +251,14 @@ 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"
|
||||
# npx asks before fetching a package it does not have, Playwright's included,
|
||||
# and the floating terminal the menu opens has nobody to answer. The runners
|
||||
# clear the variable, so only the installer can have set it.
|
||||
[[ $(cat "$test_tmp/install-npx-answer") == "true" ]] || fail "upstream installer runs with npx's question answered" "$(cat "$test_tmp/install-npx-answer")"
|
||||
[[ $(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"
|
||||
@@ -236,6 +299,28 @@ run_installer && fail "a newer runtime cannot receive an older native app"
|
||||
[[ ! -e $native ]] || fail "no mismatched native app was copied"
|
||||
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
|
||||
assert_stopped "a missing updated app prevents launch and theme setup"
|
||||
# The refusal comes before anything is touched: a launcher of the user's own
|
||||
# beside that newer runtime is neither saved aside nor replaced by a run that
|
||||
# is going to stop anyway.
|
||||
own_launcher="#!/bin/bash
|
||||
exec \"$test_home/tools/hermes\" \"\$@\""
|
||||
printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes"
|
||||
: >"$test_tmp/events"
|
||||
run_cli --now && fail "a newer runtime with a launcher of its own still stops"
|
||||
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "the stop still carries the build-only guidance" "$(cat "$test_tmp/output")"
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "a run that stops leaves the user's launcher as it was"
|
||||
[[ -z $(find "$test_home/.local/bin" -maxdepth 1 -name '.hermes-before-desktop.*' -print) ]] || fail "a run that stops saves nothing aside"
|
||||
[[ ! -s $test_tmp/events ]] || fail "a run that stops writes no commands" "$(cat "$test_tmp/events")"
|
||||
# A git state that cannot be read is a refusal too, not a clean tree: back at
|
||||
# the release with the index unreadable, the launcher is still left alone.
|
||||
git -C "$runtime" checkout -q --detach "$release_commit"
|
||||
chmod 000 "$runtime/.git/index"
|
||||
: >"$test_tmp/events"
|
||||
run_cli --now && { chmod 644 "$runtime/.git/index"; fail "an unreadable git state is not a clean tree"; }
|
||||
chmod 644 "$runtime/.git/index"
|
||||
grep -q 'Could not read' "$test_tmp/output" || fail "an unreadable git state is named" "$(cat "$test_tmp/output")"
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "an unreadable git state leaves the user's launcher as it was"
|
||||
[[ ! -s $test_tmp/events ]] || fail "an unreadable git state writes no commands" "$(cat "$test_tmp/events")"
|
||||
pass "updated runtimes are preserved and never seeded with the old packaged app"
|
||||
|
||||
new_home dirty-desktop
|
||||
@@ -284,8 +369,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 +384,61 @@ 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"
|
||||
|
||||
# Every refusal comes before anything of the user's is touched, whatever state
|
||||
# the runtime is in: a launcher of their own is neither saved aside nor
|
||||
# replaced, and no bootstrap runs, by a run that is going to stop anyway.
|
||||
own_launcher="#!/bin/bash
|
||||
exec \"$test_home/tools/hermes\" \"\$@\""
|
||||
assert_untouched() {
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "$1: the user's launcher is not as it was"
|
||||
[[ -z $(find "$test_home/.local/bin" -maxdepth 1 -name '.hermes-before-desktop.*' -print) ]] || fail "$1: something was saved aside"
|
||||
[[ ! -s $test_tmp/events ]] || fail "$1: setup ran" "$(cat "$test_tmp/events")"
|
||||
}
|
||||
# A finished runtime at the release whose edit the runtime patch cannot land on.
|
||||
new_home patch-conflict-own-launcher
|
||||
touch "$test_tmp/package-installed"
|
||||
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"
|
||||
printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes"
|
||||
: >"$test_tmp/events"
|
||||
run_cli --now && fail "a conflicting edit still stops setup"
|
||||
grep -q 'patch conflicts' "$test_tmp/output" || fail "a conflicting edit is named" "$(cat "$test_tmp/output")"
|
||||
assert_untouched "a patch conflict"
|
||||
# An unfinished runtime beside a half-built app.
|
||||
new_home incomplete-app
|
||||
touch "$test_tmp/package-installed"
|
||||
OMARCHY_TEST_NO_MARKER=1 HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
||||
mkdir -p "$native/resources"
|
||||
printf 'half\n' >"$native/resources/app.asar"
|
||||
printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes"
|
||||
: >"$test_tmp/events"
|
||||
run_cli --now && fail "a half-built app beside an unfinished runtime still stops setup"
|
||||
grep -q 'is incomplete' "$test_tmp/output" || fail "a half-built app is named" "$(cat "$test_tmp/output")"
|
||||
assert_untouched "a half-built app"
|
||||
# An unfinished runtime whose git state cannot be read is not a clean one.
|
||||
new_home unreadable-incomplete
|
||||
touch "$test_tmp/package-installed"
|
||||
OMARCHY_TEST_NO_MARKER=1 HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
||||
printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes"
|
||||
chmod 000 "$runtime/.git/index"
|
||||
: >"$test_tmp/events"
|
||||
run_cli --now && { chmod 644 "$runtime/.git/index"; fail "an unreadable git state beside an unfinished runtime is not a clean tree"; }
|
||||
chmod 644 "$runtime/.git/index"
|
||||
grep -q 'incomplete at' "$test_tmp/output" || fail "an unreadable git state is refused as incomplete" "$(cat "$test_tmp/output")"
|
||||
assert_untouched "an unreadable git state"
|
||||
pass "a refusal leaves the user's launcher and runtime as they were, whatever state the runtime is in"
|
||||
|
||||
# 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") == launch ]] || 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 +447,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 +458,89 @@ 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 "${OMARCHY_TEST_GIT_ARGV0:-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"
|
||||
# Named by its path, as a git run as /usr/bin/git is.
|
||||
live_lock_case live-lock-path "$test_tmp/live-lock-path/.hermes/hermes-agent" OMARCHY_TEST_GIT_ARGV0=/usr/bin/git
|
||||
pass "a git that names itself by its path is found too"
|
||||
# A process that only names git on its command line, an editor opened on
|
||||
# /usr/bin/git from inside the runtime say, is not a git at work there: the
|
||||
# stale lock goes and setup carries on without waiting.
|
||||
new_home bystander
|
||||
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"
|
||||
rm -f "$test_tmp/release-bystander"
|
||||
(cd "$runtime" && exec -a nvim bash -c 'for (( i = 0; i < 900; i++ )); do [[ -e "$1" ]] && exit; sleep 0.1; done' _ "$test_tmp/release-bystander" /usr/bin/git) &
|
||||
bystander=$!
|
||||
run_installer || { touch "$test_tmp/release-bystander"; fail "setup goes ahead beside a process that only names git" "$(cat "$test_tmp/output")"; }
|
||||
touch "$test_tmp/release-bystander"
|
||||
wait "$bystander" 2>/dev/null || true
|
||||
! grep -q 'Waiting for Hermes' "$test_tmp/output" || fail "a process that only names git is waited for"
|
||||
[[ ! -e $runtime/.git/shallow.lock ]] || fail "the stale lock is cleared beside a process that only names git"
|
||||
pass "a process that only names git on its command line is not waited for"
|
||||
|
||||
new_home deepen-retry
|
||||
OMARCHY_TEST_FETCH_FAIL=1 run_installer && fail "history fetch failure stops setup"
|
||||
@@ -345,10 +570,109 @@ 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 from elsewhere is not the app's: choosing Hermes installs the app
|
||||
# over it, with the previous command saved aside.
|
||||
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 --check && fail "--check calls a Hermes from elsewhere the app's"
|
||||
run_cli --now || fail "choosing Hermes installs the app over a Hermes from elsewhere" "$(cat "$test_tmp/output")"
|
||||
[[ $(head -2 "$test_tmp/events") == $'package hermes-desktop\nbootstrap' ]] || fail "the app and its runtime are installed over a Hermes from elsewhere" "$(cat "$test_tmp/events")"
|
||||
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
|
||||
[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the previous hermes command is saved aside"
|
||||
grep -qF "$hermes_home/" "$test_home/.local/bin/hermes" || fail "the command is now the app's"
|
||||
run_cli --check || fail "--check follows the app's Hermes"
|
||||
pass "Hermes is only installed through the app: a Hermes from elsewhere is superseded, its command saved aside"
|
||||
|
||||
# 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"
|
||||
|
||||
# A machine that chose Hermes before its migration ran: the wrapper's copy the
|
||||
# runtime setup saved aside proves the mise environment Omarchy's, and mise's
|
||||
# shim for it sits ahead of ~/.local/bin. Choosing Hermes again finishes the
|
||||
# handover: the environment goes, the shim and the saved copy with it, and
|
||||
# only then is the command the one PATH finds.
|
||||
new_home handover
|
||||
run_cli --now || fail "handover fixture sets up" "$(cat "$test_tmp/output")"
|
||||
saved="$test_home/.local/bin/.hermes-before-desktop.mise01"
|
||||
mkdir -p "$saved"
|
||||
printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$saved/hermes"
|
||||
cp "$test_home/.local/bin/hermes" "$test_tmp/bin/hermes"
|
||||
: >"$test_tmp/mise-log"; rm -f "$test_tmp/mise-removed" "$test_tmp/mise-unrequested"
|
||||
OMARCHY_TEST_MISE_BUILT=1 run_cli --check && fail "--check calls a handover with the mise environment still there finished"
|
||||
: >"$test_tmp/events"
|
||||
OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$test_tmp/bin/hermes" run_cli --now || fail "--now finishes the handover" "$(cat "$test_tmp/output")"
|
||||
grep -q 'mise uninstall --all' "$test_tmp/mise-log" || fail "the environment the saved wrapper proves Omarchy's is removed" "$(cat "$test_tmp/mise-log")"
|
||||
[[ ! -e $test_tmp/bin/hermes ]] || fail "the shim is gone with the environment"
|
||||
[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its directory go once the environment is gone"
|
||||
[[ ! -s $test_tmp/events ]] || fail "the handover sets nothing up again" "$(cat "$test_tmp/events")"
|
||||
run_cli --check || fail "--check follows the finished handover"
|
||||
pass "choosing Hermes again before the migration finishes the handover: the mise Hermes and its shim go"
|
||||
|
||||
new_home custom-profile
|
||||
hermes_home="$test_home/custom home"
|
||||
|
||||
Executable
+252
@@ -0,0 +1,252 @@
|
||||
#!/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")"
|
||||
# A working Hermes is the app's: the package, its runtime finished and seeded,
|
||||
# and the command upstream's installer wrote for it.
|
||||
app_runtime="$test_home/.hermes/hermes-agent"
|
||||
mkdir -p "$app_runtime/apps/desktop/release/linux-unpacked/resources"
|
||||
touch "$app_runtime/.hermes-bootstrap-complete" "$app_runtime/apps/desktop/release/linux-unpacked/resources/app.asar" "$app_runtime/apps/desktop/release/linux-unpacked/resources/install-stamp.json"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$app_runtime/apps/desktop/release/linux-unpacked/Hermes"
|
||||
chmod +x "$app_runtime/apps/desktop/release/linux-unpacked/Hermes"
|
||||
cat >"$hermes" <<SH
|
||||
#!/bin/bash
|
||||
# stands in for: exec "$app_runtime/venv/bin/python" "$app_runtime/hermes" "\$@"
|
||||
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_DESKTOP_INSTALLED=1 OMARCHY_TEST_DEFAULT_AGENT=hermes run_migration || fail "the migration succeeds with the app's Hermes in place"
|
||||
! grep -q 'default agent' "$test_tmp/output" || fail "the app's own Hermes gets reinstall guidance" "$(cat "$test_tmp/output")"
|
||||
rm -rf "$hermes" "$test_home/.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"
|
||||
@@ -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
|
||||
@@ -50,9 +41,21 @@ if [[ -n ${OMARCHY_TEST_PROMPT_GATE:-} ]]; then
|
||||
fi
|
||||
exit "${OMARCHY_TEST_GUM_STATUS:-1}"
|
||||
SH
|
||||
# A unit that will not stop, when a test says so: disable fails and is-active
|
||||
# keeps answering active.
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
echo "systemctl $*" >>"$OMARCHY_TEST_SYSTEMCTL_LOG"
|
||||
if [[ ${OMARCHY_TEST_UNIT_STUCK:-0} == 1 ]]; then
|
||||
[[ $2 == "disable" ]] && exit 1
|
||||
[[ $2 == "is-active" ]] && echo active
|
||||
fi
|
||||
# A user manager that refuses the disable of a unit that is already down.
|
||||
if [[ ${OMARCHY_TEST_UNIT_DOWN:-0} == 1 ]]; then
|
||||
[[ $2 == "disable" ]] && exit 1
|
||||
[[ $2 == "is-active" ]] && echo inactive
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
@@ -77,30 +80,25 @@ 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" \
|
||||
HOME="$test_home" XDG_CONFIG_HOME= PATH="$mock_bin:$PATH" \
|
||||
bash "$test_tmp/remover" </dev/null >"$test_tmp/output" 2>&1
|
||||
}
|
||||
|
||||
# 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}" \
|
||||
HOME="$test_home" PATH="$mock_bin:$PATH" \
|
||||
HOME="$test_home" XDG_CONFIG_HOME= PATH="$mock_bin:$PATH" \
|
||||
script -qec "bash '$test_tmp/remover'" /dev/null >"$test_tmp/output" 2>&1
|
||||
}
|
||||
|
||||
@@ -142,12 +140,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 +160,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,16 +223,49 @@ 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.
|
||||
# The gateway unit upstream's `hermes gateway install` writes runs the runtime
|
||||
# being removed, so it is stopped first and goes with it: the unit file, the
|
||||
# .bak a forced reinstall leaves, and its enablement link.
|
||||
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"
|
||||
unit_dir="$test_home/.config/systemd/user"
|
||||
mkdir -p "$unit_dir/default.target.wants"
|
||||
printf '[Service]\nExecStart=%s/.hermes/hermes-agent/venv/bin/python %s/.hermes/hermes-agent/hermes gateway run\nEnvironment="HERMES_HOME=%s/.hermes"\n' "$test_home" "$test_home" "$test_home" >"$unit_dir/hermes-gateway.service"
|
||||
cp "$unit_dir/hermes-gateway.service" "$unit_dir/hermes-gateway.service.bak"
|
||||
ln -s "$unit_dir/hermes-gateway.service" "$unit_dir/default.target.wants/hermes-gateway.service"
|
||||
# A unit that runs a Hermes kept somewhere else is not this runtime's, even
|
||||
# when the home it serves sits under ~/.hermes.
|
||||
printf '[Service]\nExecStart=/srv/hermes/venv/bin/python /srv/hermes/hermes gateway run\nEnvironment="HERMES_HOME=%s/.hermes/profiles/work"\n' "$test_home" >"$unit_dir/hermes-gateway-other.service"
|
||||
remove || fail "remove succeeds with a gateway unit installed" "$(cat "$test_tmp/output")"
|
||||
grep -Fxq 'systemctl --user disable --now hermes-gateway.service' "$test_tmp/systemctl-log" ||
|
||||
fail "the gateway unit is stopped and disabled" "$(cat "$test_tmp/systemctl-log")"
|
||||
grep -Fxq 'systemctl --user daemon-reload' "$test_tmp/systemctl-log" || fail "systemd is told the unit is gone"
|
||||
grep -Fxq 'systemctl --user reset-failed hermes-gateway.service' "$test_tmp/systemctl-log" || fail "a failed state is reset"
|
||||
[[ ! -e $unit_dir/hermes-gateway.service && ! -e $unit_dir/hermes-gateway.service.bak && ! -L $unit_dir/default.target.wants/hermes-gateway.service ]] ||
|
||||
fail "the gateway unit, its backup and its enablement link are removed"
|
||||
[[ -f $unit_dir/hermes-gateway-other.service ]] || fail "a gateway unit for a Hermes kept elsewhere survives"
|
||||
! grep -q 'hermes-gateway-other' "$test_tmp/systemctl-log" || fail "a gateway unit for a Hermes kept elsewhere is not touched" "$(cat "$test_tmp/systemctl-log")"
|
||||
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime goes once its gateway is stopped"
|
||||
pass "removal stops the gateway unit and takes it with the runtime, leaving units for other homes alone"
|
||||
|
||||
# A user manager that will not disable a unit already down is not in the way.
|
||||
seed_install
|
||||
mkdir -p "$unit_dir"
|
||||
printf '[Service]\nExecStart=%s/.hermes/hermes-agent/venv/bin/python %s/.hermes/hermes-agent/hermes gateway run\n' "$test_home" "$test_home" >"$unit_dir/hermes-gateway.service"
|
||||
OMARCHY_TEST_UNIT_DOWN=1 remove || fail "remove succeeds when disable fails on a unit that is already inactive" "$(cat "$test_tmp/output")"
|
||||
[[ ! -e $unit_dir/hermes-gateway.service && ! -d $test_home/.hermes/hermes-agent ]] || fail "an inactive unit whose disable failed still goes with the runtime"
|
||||
pass "a unit already down goes even when systemd refuses the disable"
|
||||
|
||||
# A gateway that will not stop keeps the removal from dropping the package
|
||||
# under a live process.
|
||||
seed_install
|
||||
mkdir -p "$unit_dir"
|
||||
printf '[Service]\nExecStart=%s/.hermes/hermes-agent/venv/bin/python %s/.hermes/hermes-agent/hermes gateway run\n' "$test_home" "$test_home" >"$unit_dir/hermes-gateway.service"
|
||||
: >"$test_tmp/drop-log"
|
||||
OMARCHY_TEST_UNIT_STUCK=1 remove && fail "a gateway that will not stop aborts the removal"
|
||||
grep -q 'Could not stop hermes-gateway.service' "$test_tmp/output" || fail "a gateway that will not stop is named" "$(cat "$test_tmp/output")"
|
||||
[[ ! -s $test_tmp/drop-log ]] || fail "the package is not dropped under a gateway that will not stop"
|
||||
[[ -d $test_home/.hermes/hermes-agent && -f $unit_dir/hermes-gateway.service ]] || fail "nothing is removed under a gateway that will not stop"
|
||||
pass "a gateway unit that will not stop aborts the removal before anything goes"
|
||||
|
||||
# Real SQLite writers exercise the kernel's live/deleted file descriptors.
|
||||
# Package, service and confirmation commands remain confined to the mocks.
|
||||
@@ -274,9 +295,9 @@ def setup(name):
|
||||
runtime.mkdir(parents=True)
|
||||
(runtime / '.hermes-bootstrap-complete').touch()
|
||||
(home / '.config/Hermes').mkdir(parents=True)
|
||||
env = {**os.environ, 'HOME': str(home), 'PATH': f"{scratch / 'bin'}:/usr/bin:/bin",
|
||||
env = {**os.environ, 'HOME': str(home), 'XDG_CONFIG_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)
|
||||
@@ -306,10 +327,10 @@ def remove(env):
|
||||
|
||||
def blocked(result, home, runtime, child):
|
||||
assert result.returncode != 0 and str(child.pid) in result.stderr, result
|
||||
assert 'Close Hermes' in result.stderr, result.stderr
|
||||
assert 'other than 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):
|
||||
@@ -330,6 +351,9 @@ for deleted in (False, True):
|
||||
assert not (home / '.hermes').exists()
|
||||
print('ok - live and deleted SQLite holders block removal before any side effects; closing them allows retry')
|
||||
|
||||
# Hermes's own processes are stopped by the removal rather than reported: the
|
||||
# agent in its terminal, run by the runtime's command, and the packaged app.
|
||||
# One of the user's that merely sits in the runtime is still theirs to close.
|
||||
for kind in ('terminal', 'desktop', 'working-directory'):
|
||||
home, runtime, env = setup(kind)
|
||||
executable_name = str(scratch / 'package/Hermes') if kind == 'desktop' else str(runtime / 'hermes')
|
||||
@@ -337,11 +361,113 @@ for kind in ('terminal', 'desktop', 'working-directory'):
|
||||
child = subprocess.Popen(args, executable='/usr/bin/sleep',
|
||||
cwd=runtime if kind == 'working-directory' else scratch)
|
||||
try:
|
||||
blocked(remove(env), home, runtime, child)
|
||||
result = remove(env)
|
||||
if kind == 'working-directory':
|
||||
blocked(result, home, runtime, child)
|
||||
else:
|
||||
assert result.returncode == 0, result
|
||||
assert 'Stopping Hermes (PIDs: ' + str(child.pid) in result.stdout, result.stdout
|
||||
assert child.wait(timeout=5) != 0, 'the removal ends a Hermes process of its own'
|
||||
assert not (home / '.hermes').exists()
|
||||
finally:
|
||||
child.terminate()
|
||||
child.wait(timeout=5)
|
||||
print('ok - terminal, packaged desktop and runtime working-directory processes are detected without a database')
|
||||
if child.poll() is None:
|
||||
child.terminate()
|
||||
child.wait(timeout=5)
|
||||
print('ok - the agent and the packaged app are stopped by the removal; a process merely sitting in the runtime still blocks it')
|
||||
|
||||
# With a stranger holding Hermes's files, the refusal comes before Hermes's
|
||||
# own processes are touched: the agent is still running afterwards.
|
||||
home, runtime, env = setup('stranger-and-agent')
|
||||
unit_dir = home / '.config/systemd/user'
|
||||
unit_dir.mkdir(parents=True)
|
||||
unit = unit_dir / 'hermes-gateway.service'
|
||||
unit.write_text(f'[Service]\nExecStart={runtime}/venv/bin/python {runtime}/hermes gateway run\n')
|
||||
stranger = writer(home / '.hermes/state.db')
|
||||
agent = subprocess.Popen([str(runtime / 'hermes'), '30'], executable='/usr/bin/sleep', cwd=scratch)
|
||||
try:
|
||||
result = remove(env)
|
||||
blocked(result, home, runtime, stranger)
|
||||
assert str(agent.pid) not in result.stderr, result.stderr
|
||||
assert agent.poll() is None, 'a refusal over a stranger leaves Hermes running'
|
||||
assert 'Stopping Hermes' not in result.stdout, result.stdout
|
||||
assert unit.exists(), 'a refusal over a stranger leaves the gateway unit in place'
|
||||
finally:
|
||||
stop(stranger)
|
||||
if agent.poll() is None:
|
||||
agent.terminate()
|
||||
agent.wait(timeout=5)
|
||||
print('ok - a stranger holding Hermes files stops the removal before Hermes itself is touched')
|
||||
|
||||
# A process that merely names a runtime file on its command line, an editor
|
||||
# opened on one say, is not Hermes: neither stopped nor, holding nothing, in
|
||||
# the way.
|
||||
home, runtime, env = setup('argument-only')
|
||||
editor = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(30)', str(runtime / 'README.md')], cwd=scratch)
|
||||
try:
|
||||
result = remove(env)
|
||||
assert result.returncode == 0, result
|
||||
assert editor.poll() is None, 'a process naming a runtime file as an argument is killed'
|
||||
assert 'Stopping Hermes' not in result.stdout, result.stdout
|
||||
finally:
|
||||
editor.terminate()
|
||||
editor.wait(timeout=5)
|
||||
print('ok - a runtime path in an argument does not make a process Hermes')
|
||||
|
||||
# A gateway started by hand with the venv active is an interpreter running the
|
||||
# runtime's script: the script is the program, and it is stopped.
|
||||
home, runtime, env = setup('interpreter-script')
|
||||
script = runtime / 'hermes'
|
||||
script.write_text('import time\ntime.sleep(30)\n')
|
||||
gateway = subprocess.Popen([sys.executable, str(script), 'gateway', 'run'], cwd=scratch)
|
||||
try:
|
||||
result = remove(env)
|
||||
assert result.returncode == 0, result
|
||||
assert 'Stopping Hermes (PIDs: ' + str(gateway.pid) in result.stdout, result.stdout
|
||||
assert gateway.wait(timeout=5) != 0, 'an interpreter running the runtime script is stopped'
|
||||
finally:
|
||||
if gateway.poll() is None:
|
||||
gateway.terminate()
|
||||
gateway.wait(timeout=5)
|
||||
print('ok - an interpreter running a runtime script is Hermes and is stopped')
|
||||
|
||||
# Run as a module from an activated venv, the interpreter is bare and the
|
||||
# script is nowhere on the command line; Hermes's own package name is.
|
||||
home, runtime, env = setup('module')
|
||||
(runtime / 'hermes_cli').mkdir(parents=True)
|
||||
(runtime / 'hermes_cli/__init__.py').write_text('')
|
||||
(runtime / 'hermes_cli/main.py').write_text('import time\ntime.sleep(30)\n')
|
||||
module = subprocess.Popen([sys.executable, '-m', 'hermes_cli.main', 'gateway', 'run'], cwd=scratch,
|
||||
env={**os.environ, 'PYTHONPATH': str(runtime)})
|
||||
try:
|
||||
result = remove(env)
|
||||
assert result.returncode == 0, result
|
||||
assert 'Stopping Hermes (PIDs: ' + str(module.pid) in result.stdout, result.stdout
|
||||
assert module.wait(timeout=5) != 0, 'hermes_cli run as a module is stopped'
|
||||
finally:
|
||||
if module.poll() is None:
|
||||
module.terminate()
|
||||
module.wait(timeout=5)
|
||||
print('ok - hermes_cli run as a module from a bare interpreter is Hermes and is stopped')
|
||||
|
||||
# A program whose executable lives in the runtime is Hermes whatever it was
|
||||
# started as: the Electron helpers name themselves by path, but a copy of the
|
||||
# binary started by a bare name is found through /proc/<pid>/exe.
|
||||
home, runtime, env = setup('executable')
|
||||
import shutil
|
||||
(runtime / 'venv/bin').mkdir(parents=True)
|
||||
binary = runtime / 'venv/bin/sleeper'
|
||||
shutil.copy('/usr/bin/sleep', binary)
|
||||
helper = subprocess.Popen(['sleeper', '30'], executable=str(binary), cwd=scratch)
|
||||
try:
|
||||
result = remove(env)
|
||||
assert result.returncode == 0, result
|
||||
assert 'Stopping Hermes (PIDs: ' + str(helper.pid) in result.stdout, result.stdout
|
||||
assert helper.wait(timeout=5) != 0, 'a program whose executable is in the runtime is stopped'
|
||||
finally:
|
||||
if helper.poll() is None:
|
||||
helper.terminate()
|
||||
helper.wait(timeout=5)
|
||||
print('ok - a program whose executable is in the runtime is Hermes and is stopped')
|
||||
|
||||
home, runtime, env = setup('unrelated-writer')
|
||||
sibling = home / '.hermes-other'
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user