Merge pull request #10613 from spencerbull/hermes-desktop-in-app-updates
Enable Hermes in-app updates and guard removal
This commit is contained in:
6 files changed
+706
-10
No files matched your search
@@ -5,28 +5,155 @@
|
||||
|
||||
set -e
|
||||
|
||||
# No CLI is installed here on purpose. Hermes Desktop only runs against a
|
||||
# runtime built from its own commit, so it provisions one itself under
|
||||
# ~/.hermes on first launch, which takes a few minutes and shows its own
|
||||
# progress. Handing it the mise CLI instead fails: PyPI trails the tags, and
|
||||
# the version gap fails the app's readiness probe with a 401.
|
||||
if (( EUID == 0 )); then
|
||||
echo "Run this command as your desktop user, without sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Hermes Desktop..."
|
||||
omarchy-pkg-add hermes-desktop
|
||||
|
||||
if [[ ! -r /usr/share/hermes-desktop/install.sh || ! -r /usr/share/hermes-desktop/runtime.patch ]] ||
|
||||
! release_commit=$(jq -er 'select(.branch == "main") | .commit | select(test("^[0-9a-f]{40}$"))' /opt/hermes-desktop/resources/install-stamp.json 2>/dev/null); then
|
||||
echo "The installed Hermes package cannot prepare in-app updates. Run 'omarchy update', then try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If Hermes was already installed for the terminal, the app supersedes it: one
|
||||
# machine, one Hermes. This drops that copy so the terminal, the default agent
|
||||
# and the app all end up on the app's installation.
|
||||
omarchy-install-hermes-cli || true
|
||||
|
||||
# Keep the runtime at the root even when invoked from a Hermes profile.
|
||||
HERMES_HOME=$(realpath -ms -- "${HERMES_HOME:-$HOME/.hermes}")
|
||||
home_parent=$(dirname -- "$HERMES_HOME")
|
||||
if [[ ${home_parent##*/} == [Pp][Rr][Oo][Ff][Ii][Ll][Ee][Ss] ]]; then
|
||||
HERMES_HOME=$(dirname -- "$home_parent")
|
||||
fi
|
||||
export HERMES_HOME
|
||||
|
||||
runtime="$HERMES_HOME/hermes-agent"
|
||||
native_app="$runtime/apps/desktop/release/linux-unpacked"
|
||||
|
||||
runtime_ready() {
|
||||
[[ -f $runtime/.hermes-bootstrap-complete && -f $runtime/venv/bin/hermes && -x $runtime/venv/bin/hermes && -f $runtime/venv/bin/python && -x $runtime/venv/bin/python ]] &&
|
||||
timeout 15 "$runtime/venv/bin/hermes" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_main() {
|
||||
local main_commit
|
||||
main_commit=$(git -C "$runtime" rev-parse --verify refs/heads/main 2>/dev/null || true)
|
||||
if [[ -n $main_commit && $main_commit != "$release_commit" && $main_commit != "$(git -C "$runtime" rev-parse --verify refs/remotes/origin/main 2>/dev/null)" ]]; then
|
||||
echo "Hermes main has local commits. Keep that work and prepare the desktop with 'hermes desktop --build-only'." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if ! runtime_ready; then
|
||||
# The upstream installer can reset an existing checkout. Do not pin a newer
|
||||
# or modified runtime back to the package release while repairing setup.
|
||||
if [[ -e $runtime || -L $runtime ]]; then
|
||||
if [[ $(git -C "$runtime" rev-parse HEAD 2>/dev/null) != "$release_commit" ]] ||
|
||||
[[ -n $(git -C "$runtime" status --porcelain --untracked-files=all) ]]; then
|
||||
echo "Hermes setup is incomplete at $runtime. Repair that installation before trying again; existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
check_main
|
||||
fi
|
||||
|
||||
# Upstream replaces these commands, including foreign files and symlinks.
|
||||
# Keep their original bytes/links before handing the names to the desktop.
|
||||
command_backup=""
|
||||
for command in hermes hermes-agent hermes-acp; do
|
||||
command_path="$HOME/.local/bin/$command"
|
||||
if [[ -e $command_path || -L $command_path ]]; then
|
||||
if [[ ! -f $command_path && ! -L $command_path ]]; then
|
||||
echo "Cannot replace $command_path: move it aside before installing Hermes Desktop." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z $command_backup ]]; then
|
||||
command_backup=$(mktemp -d "$HOME/.local/bin/.hermes-before-desktop.XXXXXX")
|
||||
echo "Saving existing Hermes commands in $command_backup"
|
||||
fi
|
||||
cp -a -- "$command_path" "$command_backup/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Setting up the Hermes runtime..."
|
||||
bash /usr/share/hermes-desktop/install.sh --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$HERMES_HOME"
|
||||
if ! runtime_ready; then
|
||||
echo "Hermes runtime setup did not complete. Re-run this command after resolving the installer error." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
runtime_commit=$(git -C "$runtime" rev-parse HEAD)
|
||||
if [[ $runtime_commit == "$release_commit" ]]; then
|
||||
# The updater switches to main before checking for changes. Start main at
|
||||
# the packaged release, with enough history for its first fast-forward.
|
||||
check_main
|
||||
if [[ $(git -C "$runtime" rev-parse --is-shallow-repository) == "true" ]]; then
|
||||
git -C "$runtime" fetch --unshallow origin main
|
||||
fi
|
||||
git -C "$runtime" switch -C main "$release_commit"
|
||||
|
||||
if git -C "$runtime" apply --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
||||
git -C "$runtime" apply /usr/share/hermes-desktop/runtime.patch
|
||||
elif ! git -C "$runtime" apply --reverse --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
||||
echo "The Hermes Linux runtime patch conflicts with local changes. Existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e $native_app || -L $native_app ]]; then
|
||||
if [[ ! -f $native_app/Hermes || ! -x $native_app/Hermes || ! -f $native_app/resources/app.asar || ! -f $native_app/resources/install-stamp.json ]]; then
|
||||
echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [[ $runtime_commit != "$release_commit" ]]; then
|
||||
echo "The Hermes runtime has moved beyond the packaged desktop release. Run 'hermes desktop --build-only', then try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
desktop_changes=$(git -C "$runtime" status --porcelain --untracked-files=all -- apps/desktop package.json package-lock.json)
|
||||
if [[ -n $desktop_changes ]]; then
|
||||
echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p -- "${native_app%/*}"
|
||||
staging=$(mktemp -d "${native_app%/*}/.linux-unpacked.XXXXXX")
|
||||
trap 'rm -rf -- "$staging"' EXIT
|
||||
cp -a /opt/hermes-desktop/. "$staging/"
|
||||
chmod 0755 "$staging/chrome-sandbox"
|
||||
mv -T --no-clobber -- "$staging" "$native_app"
|
||||
if [[ -e $staging ]]; then
|
||||
echo "A Hermes desktop app appeared during setup. It has been kept; please try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap - EXIT
|
||||
|
||||
# Record this matching prebuilt app using the CLI's own content hash, so
|
||||
# subsequent menu launches do not rebuild an app that is already current.
|
||||
env -u PYTHONPATH -u PYTHONHOME "$runtime/venv/bin/python" - "$runtime" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
from hermes_cli.main import _write_desktop_build_stamp
|
||||
|
||||
_write_desktop_build_stamp(Path(sys.argv[1]), source_mode=False)
|
||||
PY
|
||||
fi
|
||||
|
||||
echo "Opening Hermes Desktop..."
|
||||
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
|
||||
|
||||
# Only a running Hermes can be told which skin to show, and the first launch
|
||||
# takes minutes; a unit outlives this terminal and reports to the journal.
|
||||
# Only a running Hermes can be told which skin to show; a unit outlives this
|
||||
# terminal and reports to the journal.
|
||||
echo "Matching Hermes to the current theme once it is set up..."
|
||||
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
|
||||
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme omarchy-theme-set-hermes --wait
|
||||
|
||||
echo ""
|
||||
echo "Hermes Desktop has been installed."
|
||||
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
|
||||
@@ -6,6 +6,52 @@
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
ensure_hermes_stopped() {
|
||||
python3 - "$HOME" <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
home = Path(sys.argv[1])
|
||||
roots = [str((home / relative).resolve()) for relative in ('.hermes', '.config/Hermes')]
|
||||
roots.append('/opt/hermes-desktop')
|
||||
|
||||
def belongs_to_hermes(target):
|
||||
target = target.removesuffix(' (deleted)')
|
||||
return any(target == root or target.startswith(root + '/') for root in roots)
|
||||
|
||||
holders = []
|
||||
for process in Path('/proc').iterdir():
|
||||
if not process.name.isdigit() or int(process.name) == os.getpid():
|
||||
continue
|
||||
try:
|
||||
if process.stat().st_uid != os.getuid():
|
||||
continue
|
||||
targets = [os.fsdecode(arg) for arg in (process / 'cmdline').read_bytes().split(b'\0')]
|
||||
entries = [process / 'exe', process / 'cwd']
|
||||
try:
|
||||
entries.extend((process / 'fd').iterdir())
|
||||
except PermissionError:
|
||||
pass
|
||||
for entry in entries:
|
||||
try:
|
||||
targets.append(os.readlink(entry))
|
||||
except OSError:
|
||||
pass
|
||||
if any(belongs_to_hermes(target) for target in targets):
|
||||
holders.append(process.name)
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
||||
continue
|
||||
|
||||
if holders:
|
||||
print('Close Hermes and processes using its files before removing it (PIDs: '
|
||||
+ ', '.join(holders) + '). Then try again.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
# Removing an open SQLite WAL leaves a live writer on a deleted generation.
|
||||
ensure_hermes_stopped
|
||||
omarchy-pkg-drop hermes-desktop
|
||||
|
||||
# The installer leaves a unit waiting to hand the app the Omarchy theme.
|
||||
@@ -19,6 +65,7 @@ systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
|
||||
# Tolerated here rather than fatal, so the ~/.hermes handling below still runs;
|
||||
# the failure is answered for at the end instead of being swallowed.
|
||||
cli_removed=true
|
||||
ensure_hermes_stopped
|
||||
omarchy-install-hermes-cli --remove || cli_removed=false
|
||||
|
||||
# The app writes this when the runtime it provisions under ~/.hermes has landed,
|
||||
@@ -78,6 +125,7 @@ if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && omarchy-cm
|
||||
# turn that into an aborted removal; the size is worth no such thing.
|
||||
size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true)
|
||||
if gum confirm --default=false "Also delete ~/.hermes and ~/.config/Hermes ($size: chats, memories, skills, connections and tokens)?"; then
|
||||
ensure_hermes_stopped
|
||||
rm -rf "$HOME/.hermes" "$HOME/.config/Hermes"
|
||||
data_removed=true
|
||||
fi
|
||||
|
||||
@@ -12,6 +12,7 @@ fcitx5-configtool
|
||||
fcitx5-wayland-launcher
|
||||
foot-server
|
||||
footclient
|
||||
hermes
|
||||
java-java-openjdk
|
||||
jconsole-java-openjdk
|
||||
jshell-java-openjdk
|
||||
|
||||
@@ -55,6 +55,14 @@ const entries = [
|
||||
}
|
||||
]
|
||||
|
||||
// Keep the packaged launcher when upstream rebuilds register their own entry.
|
||||
const configuredHides = new Set(fs.readFileSync(path.join(root, 'default/omarchy/launcher.hides'), 'utf8').trim().split(/\n/))
|
||||
const hermesEntries = [{ name: 'Hermes', id: 'hermes' }, { name: 'Hermes', id: 'hermes-desktop' }]
|
||||
for (const query of ['', 'hermes']) {
|
||||
const visible = search.sortedEntries(hermesEntries, query, entry => configuredHides.has(entry.id))
|
||||
assertDeepEqual(visible.map(row => row.entry.id), ['hermes-desktop'], 'only the packaged Hermes launcher is visible')
|
||||
}
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
for command in git jq python3; do require_command "$command"; done
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf -- "$test_tmp"' EXIT
|
||||
export OMARCHY_TEST_ROOT="$test_tmp"
|
||||
mkdir -p "$test_tmp/bin" "$test_tmp/package/resources" "$test_tmp/share" "$test_tmp/seed"
|
||||
|
||||
# Real Git exercises patch checks and preservation; all package, desktop and
|
||||
# service commands are mocks. No command reaches the live user installation.
|
||||
git -C "$test_tmp/seed" init -q -b main
|
||||
printf 'venv/\n.hermes-bootstrap-complete\napps/desktop/release/\n__pycache__/\n' >"$test_tmp/seed/.gitignore"
|
||||
printf 'before\n' >"$test_tmp/seed/runtime.txt"
|
||||
mkdir -p "$test_tmp/seed/apps/desktop/src"
|
||||
printf 'desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
|
||||
mkdir -p "$test_tmp/seed/hermes_cli"
|
||||
cat >"$test_tmp/seed/hermes_cli/main.py" <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def _write_desktop_build_stamp(project_root, *, source_mode):
|
||||
home = Path(os.environ['HERMES_HOME'])
|
||||
assert project_root == home / 'hermes-agent'
|
||||
assert source_mode is False
|
||||
assert (project_root / 'apps/desktop/release/linux-unpacked/resources/app.asar').is_file()
|
||||
(home / 'desktop-build-stamp.json').write_text('upstream build stamp')
|
||||
with (Path(os.environ['OMARCHY_TEST_ROOT']) / 'events').open('a') as log:
|
||||
log.write('build-stamp\n')
|
||||
PY
|
||||
git -C "$test_tmp/seed" add .
|
||||
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm fixture
|
||||
release_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
|
||||
printf 'after\n' >"$test_tmp/seed/runtime.txt"
|
||||
git -C "$test_tmp/seed" diff >"$test_tmp/share/runtime.patch"
|
||||
printf 'before\n' >"$test_tmp/seed/runtime.txt"
|
||||
printf 'newer desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
|
||||
git -C "$test_tmp/seed" add apps/desktop/src/main.js
|
||||
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm newer-main
|
||||
origin_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
|
||||
export OMARCHY_TEST_RELEASE_COMMIT="$release_commit"
|
||||
printf '{"branch":"main","commit":"%s"}\n' "$release_commit" >"$test_tmp/package/resources/install-stamp.json"
|
||||
printf 'packaged app\n' >"$test_tmp/package/resources/app.asar"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/package/Hermes"
|
||||
touch "$test_tmp/package/chrome-sandbox"
|
||||
chmod 755 "$test_tmp/package/Hermes"
|
||||
chmod 4755 "$test_tmp/package/chrome-sandbox"
|
||||
|
||||
cat >"$test_tmp/share/install.sh" <<'MOCK'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
printf 'bootstrap\n' >>"$OMARCHY_TEST_ROOT/events"
|
||||
printf '%s\n' "$@" >"$OMARCHY_TEST_ROOT/install-args"
|
||||
[[ ${OMARCHY_TEST_INSTALL_FAIL:-0} != 1 ]] || exit 7
|
||||
commit=$OMARCHY_TEST_RELEASE_COMMIT
|
||||
force=false
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--dir) runtime=$2; shift ;;
|
||||
--commit) commit=$2; shift ;;
|
||||
--force-commit) force=true ;;
|
||||
--hermes-home) [[ $2 == "$HERMES_HOME" ]] ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
mkdir -p -- "${runtime%/*}"
|
||||
if [[ ! -d $runtime ]]; then
|
||||
git clone -q --depth 1 "file://$OMARCHY_TEST_ROOT/seed" "$runtime"
|
||||
else
|
||||
git -C "$runtime" checkout -q main
|
||||
git -C "$runtime" pull -q --ff-only origin main
|
||||
fi
|
||||
git -C "$runtime" fetch -q origin "$commit"
|
||||
if [[ $force == true ]] || ! git -C "$runtime" merge-base --is-ancestor "$commit" HEAD; then
|
||||
git -C "$runtime" checkout -q --detach "$commit"
|
||||
fi
|
||||
mkdir -p "$runtime/venv/bin"
|
||||
git -C "$runtime" rev-parse HEAD >"$runtime/venv/dependency-commit"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$runtime/venv/bin/hermes"
|
||||
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
|
||||
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 ]]
|
||||
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 "$@"
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/cp" <<'MOCK'
|
||||
#!/bin/bash
|
||||
if [[ ${OMARCHY_TEST_COPY_FAIL:-0} == 1 ]]; then
|
||||
touch "${@: -1}/partial-copy"
|
||||
exit 9
|
||||
fi
|
||||
exec /usr/bin/cp "$@"
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/mv" <<'MOCK'
|
||||
#!/bin/bash
|
||||
if [[ ${OMARCHY_TEST_COPY_RACE:-0} == 1 && $1 == -T ]]; then
|
||||
mkdir -p "${@: -1}"
|
||||
fi
|
||||
exec /usr/bin/mv "$@"
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/uwsm-app" <<'MOCK'
|
||||
#!/bin/bash
|
||||
[[ $1 == -- ]] || exit 1
|
||||
shift
|
||||
exec "$@"
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/hermes-desktop" <<'MOCK'
|
||||
#!/bin/bash
|
||||
sleep 0.05
|
||||
native="$HERMES_HOME/hermes-agent/apps/desktop/release/linux-unpacked"
|
||||
if [[ -x $native/Hermes && -f $native/resources/app.asar ]]; then
|
||||
printf 'launch\n' >>"$OMARCHY_TEST_ROOT/events"
|
||||
else
|
||||
printf 'launch-before-copy\n' >>"$OMARCHY_TEST_ROOT/events"
|
||||
fi
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/systemctl" <<'MOCK'
|
||||
#!/bin/bash
|
||||
printf 'theme-stop\n' >>"$OMARCHY_TEST_ROOT/events"
|
||||
MOCK
|
||||
cat >"$test_tmp/bin/systemd-run" <<'MOCK'
|
||||
#!/bin/bash
|
||||
printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events"
|
||||
# Join the mock asynchronous launch so every test owns its full lifetime.
|
||||
for (( attempt=0; attempt<100; attempt++ )); do
|
||||
if grep -q '^launch' "$OMARCHY_TEST_ROOT/events"; then exit 0; fi
|
||||
sleep 0.01
|
||||
done
|
||||
exit 1
|
||||
MOCK
|
||||
chmod +x "$test_tmp/bin/"*
|
||||
|
||||
# Substitute only system package paths in a scratch copy of the actual script.
|
||||
python3 - "$ROOT/bin/omarchy-install-ai-hermes" "$test_tmp" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
source, scratch = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
script = source.read_text()
|
||||
for original, replacement in {
|
||||
'/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)
|
||||
PY
|
||||
|
||||
new_home() {
|
||||
test_home="$test_tmp/$1"
|
||||
hermes_home="$test_home/.hermes"
|
||||
runtime="$hermes_home/hermes-agent"
|
||||
native="$runtime/apps/desktop/release/linux-unpacked"
|
||||
mkdir -p "$test_home"
|
||||
: >"$test_tmp/events"
|
||||
}
|
||||
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
|
||||
}
|
||||
assert_stopped() {
|
||||
if grep -Eq '^(launch|theme-|build-stamp)' "$test_tmp/events"; then fail "$1"; fi
|
||||
}
|
||||
|
||||
new_home fresh
|
||||
run_installer || fail "fresh setup succeeds" "$(cat "$test_tmp/output")"
|
||||
expected=$(printf '%s\n' --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$hermes_home")
|
||||
[[ $(cat "$test_tmp/install-args") == "$expected" ]] || fail "upstream installer receives the pinned main arguments"
|
||||
[[ $(head -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"
|
||||
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "the upstream helper records the completed packaged build"
|
||||
[[ $(cat "$runtime/runtime.txt") == after ]] || fail "the release runtime receives its patch"
|
||||
[[ $(stat -c %a "$native/chrome-sandbox") == 755 ]] || fail "the user sandbox is not setuid"
|
||||
[[ $(stat -c %a "$test_tmp/package/chrome-sandbox") == 4755 ]] || fail "package sandbox permissions remain unchanged"
|
||||
[[ $(git -C "$runtime" symbolic-ref --short HEAD) == main && $(git -C "$runtime" rev-parse main) == "$release_commit" ]] || fail "main starts at the release rather than the clone tip"
|
||||
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "dependencies are installed for the release"
|
||||
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "first update has connected history"
|
||||
# Reproduce the updater's checkout/count/pull sequence while origin stays put.
|
||||
git clone -q "$runtime" "$test_tmp/first-update"
|
||||
git -C "$test_tmp/first-update" remote set-url origin "file://$test_tmp/seed"
|
||||
git -C "$test_tmp/first-update" fetch -q origin main
|
||||
git -C "$test_tmp/first-update" checkout -q main
|
||||
[[ $(git -C "$test_tmp/first-update" rev-list HEAD..origin/main --count) == 1 ]] || fail "first update detects work even when origin has not moved since install"
|
||||
git -C "$test_tmp/first-update" pull -q --ff-only origin main
|
||||
[[ $(git -C "$test_tmp/first-update" rev-parse HEAD) == "$origin_commit" ]] || fail "first update fast-forwards to origin"
|
||||
pass "fresh setup pins main, patches the matching runtime and copies the complete app before launch"
|
||||
|
||||
printf 'user app\n' >"$native/resources/app.asar"
|
||||
printf 'user build stamp\n' >"$hermes_home/desktop-build-stamp.json"
|
||||
: >"$test_tmp/events"
|
||||
run_installer || fail "repeat setup succeeds" "$(cat "$test_tmp/output")"
|
||||
! grep -qx bootstrap "$test_tmp/events" || fail "repeat setup does not bootstrap again"
|
||||
! grep -qx build-stamp "$test_tmp/events" || fail "existing app never reruns the build stamp writer"
|
||||
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'user build stamp' ]] || fail "existing native build stamp remains unchanged"
|
||||
[[ $(cat "$native/resources/app.asar") == 'user app' ]] || fail "existing native app remains unchanged"
|
||||
pass "repeat setup accepts the applied patch and preserves the existing native app"
|
||||
|
||||
# Advancing the runtime must never reinstall the release or reapply its patch.
|
||||
printf 'new main\n' >"$runtime/runtime.txt"
|
||||
git -C "$runtime" add runtime.txt
|
||||
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm update
|
||||
: >"$test_tmp/events"
|
||||
run_installer || fail "a complete updated runtime and native app are reused"
|
||||
[[ $(cat "$runtime/runtime.txt") == 'new main' ]] || fail "updated runtime is not release-patched"
|
||||
mv "$native" "$test_tmp/saved-native"
|
||||
: >"$test_tmp/events"
|
||||
run_installer && fail "a newer runtime cannot receive an older native app"
|
||||
[[ ! -e $native ]] || fail "no mismatched native app was copied"
|
||||
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
|
||||
assert_stopped "a missing updated app prevents launch and theme setup"
|
||||
pass "updated runtimes are preserved and never seeded with the old packaged app"
|
||||
|
||||
new_home dirty-desktop
|
||||
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
|
||||
printf 'local desktop edit\n' >"$runtime/apps/desktop/src/main.js"
|
||||
: >"$test_tmp/events"
|
||||
run_installer && fail "modified desktop sources cannot be certified as the packaged build"
|
||||
[[ ! -e $native && ! -e $hermes_home/desktop-build-stamp.json ]] || fail "modified desktop sources receive neither packaged app nor build stamp"
|
||||
[[ $(cat "$runtime/apps/desktop/src/main.js") == 'local desktop edit' ]] || fail "desktop source edits are preserved"
|
||||
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "modified desktop sources have build guidance"
|
||||
assert_stopped "modified desktop sources prevent stamping, launch and theme setup"
|
||||
pass "a matching commit with modified desktop sources is preserved without seeding or stamping"
|
||||
|
||||
for failure in package install marker; do
|
||||
new_home "$failure-failure"
|
||||
case "$failure" in
|
||||
package) OMARCHY_TEST_PACKAGE_FAIL=1 run_installer && fail "package failure stops setup" ;;
|
||||
install) OMARCHY_TEST_INSTALL_FAIL=1 run_installer && fail "installer failure stops setup" ;;
|
||||
marker) OMARCHY_TEST_NO_MARKER=1 run_installer && fail "missing marker stops setup" ;;
|
||||
esac
|
||||
[[ ! -e $native ]] || fail "failed setup does not seed the app"
|
||||
assert_stopped "failed setup prevents launch and theme setup"
|
||||
done
|
||||
pass "package, upstream installer and readiness failures stop before launch"
|
||||
|
||||
for failure in copy race; do
|
||||
new_home "$failure-failure"
|
||||
if [[ $failure == "copy" ]]; then
|
||||
OMARCHY_TEST_COPY_FAIL=1 run_installer && fail "copy failure stops setup"
|
||||
[[ ! -e $native ]] || fail "partial copy is never published"
|
||||
else
|
||||
OMARCHY_TEST_COPY_RACE=1 run_installer && fail "concurrent native app stops publication"
|
||||
[[ -d $native && -z $(ls -A "$native") ]] || fail "concurrent empty app directory is preserved"
|
||||
fi
|
||||
[[ -z $(find "${native%/*}" -maxdepth 1 -name '.linux-unpacked.*' -print) ]] || fail "owned staging directory is cleaned up"
|
||||
assert_stopped "publication failure prevents launch"
|
||||
done
|
||||
pass "failed copies and concurrent app creation preserve existing work and clean only staging"
|
||||
|
||||
new_home incomplete-native
|
||||
run_installer || fail "incomplete native fixture sets up"
|
||||
rm "$native/resources/app.asar"
|
||||
: >"$test_tmp/events"
|
||||
run_installer && fail "incomplete existing app requires repair"
|
||||
[[ ! -e $native/resources/app.asar ]] || fail "incomplete existing app is not overwritten"
|
||||
assert_stopped "incomplete native app prevents launch"
|
||||
pass "an incomplete existing native app is preserved"
|
||||
|
||||
new_home patch-conflict
|
||||
run_installer || fail "patch conflict fixture sets up"
|
||||
printf 'local edit\n' >"$runtime/runtime.txt"
|
||||
: >"$test_tmp/events"
|
||||
run_installer && fail "unexpected patch conflict stops setup"
|
||||
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "conflicting runtime changes are preserved"
|
||||
assert_stopped "patch conflict prevents launch"
|
||||
rm "$runtime/.hermes-bootstrap-complete"
|
||||
: >"$test_tmp/events"
|
||||
run_installer && fail "incomplete modified runtime cannot be reset by upstream installer"
|
||||
! grep -qx bootstrap "$test_tmp/events" || fail "modified runtime never reaches upstream installer"
|
||||
pass "patch conflicts and incomplete modified runtimes retain local changes and stop safely"
|
||||
|
||||
new_home full-history-retry
|
||||
git clone -q "$test_tmp/seed" "$runtime"
|
||||
git -C "$runtime" checkout -q --detach "$release_commit"
|
||||
run_installer || fail "clean incomplete full-history release checkout is repaired" "$(cat "$test_tmp/output")"
|
||||
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "full-history retry pins before installing dependencies"
|
||||
[[ $(git -C "$runtime" rev-parse HEAD) == "$release_commit" && -f $native/resources/app.asar ]] || fail "full-history retry seeds the matching release"
|
||||
pass "full-history retries force the guarded release pin before dependency setup"
|
||||
|
||||
new_home local-main
|
||||
git clone -q "$test_tmp/seed" "$runtime"
|
||||
printf 'local branch work\n' >"$runtime/keep"
|
||||
git -C "$runtime" add keep
|
||||
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm local-work
|
||||
local_main=$(git -C "$runtime" rev-parse main)
|
||||
git -C "$runtime" checkout -q --detach "$release_commit"
|
||||
run_installer && fail "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"
|
||||
|
||||
new_home deepen-retry
|
||||
OMARCHY_TEST_FETCH_FAIL=1 run_installer && fail "history fetch failure stops setup"
|
||||
[[ ! -e $native ]] || fail "failed history fetch does not seed the app"
|
||||
assert_stopped "failed history fetch prevents launch"
|
||||
: >"$test_tmp/events"
|
||||
run_installer || fail "history fetch can be retried after runtime setup" "$(cat "$test_tmp/output")"
|
||||
! grep -qx bootstrap "$test_tmp/events" || fail "history retry does not repeat upstream installation"
|
||||
pass "a history fetch failure can be retried without reinstalling the ready runtime"
|
||||
|
||||
new_home existing-commands
|
||||
mkdir -p "$test_home/.local/bin"
|
||||
printf 'foreign wrapper\n' >"$test_home/.local/bin/hermes"
|
||||
printf 'symlink target\n' >"$test_home/target"
|
||||
ln -s "$test_home/target" "$test_home/.local/bin/hermes-agent"
|
||||
ln -s "$test_home/missing" "$test_home/.local/bin/hermes-acp"
|
||||
run_installer || fail "existing commands are preserved before upstream replaces them" "$(cat "$test_tmp/output")"
|
||||
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
|
||||
[[ ${#backups[@]} == 1 && -d ${backups[0]} ]] || fail "one backup directory preserves existing command names"
|
||||
[[ $(cat "${backups[0]}/hermes") == 'foreign wrapper' ]] || fail "foreign wrapper bytes are saved"
|
||||
[[ $(readlink "${backups[0]}/hermes-agent") == "$test_home/target" && $(readlink "${backups[0]}/hermes-acp") == "$test_home/missing" ]] || fail "working and broken symlinks are saved as links"
|
||||
[[ $(cat "$test_home/target") == 'symlink target' ]] || fail "upstream does not overwrite the original symlink target"
|
||||
grep -qF "${backups[0]}" "$test_tmp/output" || fail "backup location is reported"
|
||||
pass "pre-existing command files and symlinks are backed up before replacement"
|
||||
|
||||
new_home old-package
|
||||
mv "$test_tmp/package/resources/install-stamp.json" "$test_tmp/saved-install-stamp.json"
|
||||
run_installer && fail "an old installed package cannot bootstrap"
|
||||
grep -q 'omarchy update' "$test_tmp/output" || fail "old package has actionable upgrade guidance"
|
||||
! grep -qx 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"
|
||||
|
||||
new_home custom-profile
|
||||
hermes_home="$test_home/custom home"
|
||||
runtime="$hermes_home/hermes-agent"
|
||||
OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail "profile setup succeeds"
|
||||
[[ -x $runtime/apps/desktop/release/linux-unpacked/Hermes ]] || fail "profile uses the canonical root runtime"
|
||||
grep -qxF "$hermes_home" "$test_tmp/install-args" || fail "canonical custom home reaches upstream installer"
|
||||
pass "custom profile paths normalize to the shared Hermes home"
|
||||
@@ -11,6 +11,14 @@ mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
# Keep package-path checks scoped to the fixture, even with a live app open.
|
||||
python3 - "$ROOT/bin/omarchy-remove-ai-hermes" "$test_tmp" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
source, scratch = map(Path, sys.argv[1:])
|
||||
(scratch / 'remover').write_text(source.read_text().replace('/opt/hermes-desktop', str(scratch / 'package')))
|
||||
PY
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
|
||||
@@ -32,6 +40,14 @@ SH
|
||||
cat >"$mock_bin/gum" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_GUM_LOG"
|
||||
if [[ -n ${OMARCHY_TEST_PROMPT_GATE:-} ]]; then
|
||||
touch "$OMARCHY_TEST_PROMPT_GATE.started"
|
||||
for (( attempt=0; attempt<500; attempt++ )); do
|
||||
[[ ! -e $OMARCHY_TEST_PROMPT_GATE.continue ]] || exit 0
|
||||
sleep 0.01
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
exit "${OMARCHY_TEST_GUM_STATUS:-1}"
|
||||
SH
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
@@ -70,7 +86,7 @@ remove() {
|
||||
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
|
||||
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
|
||||
HOME="$test_home" PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-remove-ai-hermes" </dev/null >/dev/null 2>&1
|
||||
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
|
||||
@@ -85,7 +101,7 @@ remove_tty() {
|
||||
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
|
||||
OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \
|
||||
HOME="$test_home" PATH="$mock_bin:$PATH" \
|
||||
script -qec "bash '$ROOT/bin/omarchy-remove-ai-hermes'" /dev/null >/dev/null 2>&1
|
||||
script -qec "bash '$test_tmp/remover'" /dev/null >"$test_tmp/output" 2>&1
|
||||
}
|
||||
|
||||
# The app brings its own uv and its own node; both are runtime, not data.
|
||||
@@ -229,3 +245,140 @@ OMARCHY_TEST_INSTALLER_STATUS=1 remove && fail "a failed CLI teardown surfaces i
|
||||
[[ ! -d $test_home/.hermes/hermes-agent ]] ||
|
||||
fail "a failed CLI teardown does not stop the runtime removal"
|
||||
pass "a failed CLI teardown is reported after the runtime is handled"
|
||||
|
||||
# Real SQLite writers exercise the kernel's live/deleted file descriptors.
|
||||
# Package, service and confirmation commands remain confined to the mocks.
|
||||
python3 - "$test_tmp" <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pty
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
scratch = Path(sys.argv[1])
|
||||
writer_code = '''import os, sqlite3, sys
|
||||
c = sqlite3.connect(os.environ['TEST_DB'])
|
||||
c.execute('pragma journal_mode=wal')
|
||||
c.execute('create table fixture(value)')
|
||||
c.execute("insert into fixture values ('keep')")
|
||||
c.commit()
|
||||
print('ready', flush=True)
|
||||
sys.stdin.readline()
|
||||
c.close()
|
||||
'''
|
||||
|
||||
def setup(name):
|
||||
home = scratch / name
|
||||
runtime = home / '.hermes/hermes-agent'
|
||||
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",
|
||||
'OMARCHY_TEST_GUM_STATUS': '0'}
|
||||
for key in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'):
|
||||
log = home / (key + '.log')
|
||||
log.touch()
|
||||
env['OMARCHY_TEST_' + key + '_LOG'] = str(log)
|
||||
return home, runtime, env
|
||||
|
||||
def writer(db):
|
||||
child = subprocess.Popen([sys.executable, '-u', '-c', writer_code],
|
||||
env={**os.environ, 'TEST_DB': str(db)},
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
||||
assert child.stdout.readline().strip() == 'ready'
|
||||
return child
|
||||
|
||||
def stop(child):
|
||||
if child.poll() is None:
|
||||
child.stdin.write('\n')
|
||||
child.stdin.flush()
|
||||
child.wait(timeout=5)
|
||||
|
||||
def remove(env):
|
||||
master, slave = pty.openpty()
|
||||
try:
|
||||
return subprocess.run(['bash', str(scratch / 'remover')], env=env,
|
||||
stdin=slave, capture_output=True, text=True, timeout=10)
|
||||
finally:
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
|
||||
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 (runtime / '.hermes-bootstrap-complete').exists()
|
||||
assert all((home / (name + '.log')).stat().st_size == 0
|
||||
for name in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'))
|
||||
assert child.poll() is None, 'remover must not kill sessions'
|
||||
|
||||
for deleted in (False, True):
|
||||
home, runtime, env = setup('deleted-writer' if deleted else 'live-writer')
|
||||
db = home / '.hermes/state.db'
|
||||
child = writer(db)
|
||||
try:
|
||||
if deleted:
|
||||
for suffix in ('', '-wal', '-shm'):
|
||||
Path(str(db) + suffix).unlink()
|
||||
db.write_bytes(b'new database generation')
|
||||
blocked(remove(env), home, runtime, child)
|
||||
if deleted:
|
||||
assert db.read_bytes() == b'new database generation'
|
||||
finally:
|
||||
stop(child)
|
||||
assert remove(env).returncode == 0, 'removal succeeds once the writer closes'
|
||||
assert not (home / '.hermes').exists()
|
||||
print('ok - live and deleted SQLite holders block removal before any side effects; closing them allows retry')
|
||||
|
||||
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')
|
||||
args = ['sleep', '30'] if kind == 'working-directory' else [executable_name, '30']
|
||||
child = subprocess.Popen(args, executable='/usr/bin/sleep',
|
||||
cwd=runtime if kind == 'working-directory' else scratch)
|
||||
try:
|
||||
blocked(remove(env), home, runtime, child)
|
||||
finally:
|
||||
child.terminate()
|
||||
child.wait(timeout=5)
|
||||
print('ok - terminal, packaged desktop and runtime working-directory processes are detected without a database')
|
||||
|
||||
home, runtime, env = setup('unrelated-writer')
|
||||
sibling = home / '.hermes-other'
|
||||
sibling.mkdir()
|
||||
child = writer(sibling / 'state.db')
|
||||
try:
|
||||
assert remove(env).returncode == 0, 'a sibling database does not block Hermes removal'
|
||||
assert child.poll() is None
|
||||
finally:
|
||||
stop(child)
|
||||
print('ok - unrelated database holders are left alone')
|
||||
|
||||
home, runtime, env = setup('prompt-race')
|
||||
gate = home / 'prompt'
|
||||
env['OMARCHY_TEST_PROMPT_GATE'] = str(gate)
|
||||
master, slave = pty.openpty()
|
||||
remover = subprocess.Popen(['bash', str(scratch / 'remover')], env=env, stdin=slave,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
os.close(slave)
|
||||
child = None
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while not Path(str(gate) + '.started').exists():
|
||||
assert remover.poll() is None and time.monotonic() < deadline, 'prompt was not reached'
|
||||
time.sleep(0.01)
|
||||
child = writer(home / '.hermes/state.db')
|
||||
Path(str(gate) + '.continue').touch()
|
||||
stdout, stderr = remover.communicate(timeout=10)
|
||||
assert remover.returncode != 0 and str(child.pid) in stderr, (stdout, stderr)
|
||||
assert (home / '.hermes/state.db-wal').exists()
|
||||
assert (home / '.config/Hermes').exists()
|
||||
finally:
|
||||
if child is not None:
|
||||
stop(child)
|
||||
if remover.poll() is None:
|
||||
remover.terminate()
|
||||
remover.wait(timeout=5)
|
||||
os.close(master)
|
||||
print('ok - a writer started during confirmation blocks data deletion')
|
||||
PY
|
||||
Reference in new issue
Block a user