Files
omarchy/test/shell.d/hermes-remove-test.sh
T
14eb9430c3 Close Hermes on removal instead of asking the user to close it
Remove > AI refused whenever anything had Hermes's files open and told the user to close it and try again, and the thing open was Hermes: the agent in the terminal that choosing it as the default agent leaves running, the desktop app, a gateway. Those are the removal's to close. It now stops the gateway unit that upstream's `hermes gateway install` wrote, since that unit starts the runtime about to be deleted and would start it again the moment it was killed, then ends every process whose program lives in that runtime or in the package, and only then refuses over whatever is left, which is somebody else's: an editor on a skill, a shell sitting in ~/.hermes, a writer on the state database. Both are judged by the program, never by a later argument or by the home served, so an editor opened on a runtime file is the user's and a unit running a Hermes kept elsewhere is left alone whatever home it serves; for an interpreter the program is the script it runs, so a gateway started by hand as `python .../hermes gateway run` is found too. The refusal comes before anything is touched, so a run that stops leaves Hermes running as it was. A unit that will not stop still aborts the removal, as dropping the package would strand a live gateway on deleted code.

Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 21:35:46 -05:00

492 lines
23 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
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"
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
# very data loss the default-no exists to prevent.
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
# 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"/*
seed_install() {
rm -rf "$test_home"
mkdir -p "$test_home/.hermes/hermes-agent" "$test_home/.hermes/bootstrap-cache" \
"$test_home/.hermes/bin" "$test_home/.hermes/node/bin" \
"$test_home/.hermes/memories" "$test_home/.hermes/sessions" \
"$test_home/.config/Hermes" "$test_home/.local/bin"
printf 'chat\n' >"$test_home/.hermes/sessions/one.json"
printf 'memory\n' >"$test_home/.hermes/memories/one.md"
printf 'soul\n' >"$test_home/.hermes/SOUL.md"
printf 'uv\n' >"$test_home/.hermes/bin/uv"
ln -sf "$test_home/.hermes/node/bin/node" "$test_home/.local/bin/node"
ln -sf "$test_home/.hermes/node/bin/npm" "$test_home/.local/bin/npm"
ln -sf /usr/bin/npx "$test_home/.local/bin/npx"
printf 'node\n' >"$test_home/.hermes/node/bin/node"
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
}
# </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/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
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/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-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" XDG_CONFIG_HOME= PATH="$mock_bin:$PATH" \
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.
seed_install
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds"
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime checkout is removed"
[[ ! -d $test_home/.hermes/bin ]] || fail "the uv the app installed is removed"
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
pass "removal takes the whole runtime the app installed"
grep -Fxq 'systemctl --user stop omarchy-hermes-theme.service' "$test_tmp/systemctl-log" ||
fail "the unit the installer left waiting to hand over the theme is stopped" "$(cat "$test_tmp/systemctl-log")"
pass "removal stops the installer's theme hand-over"
[[ -d $test_home/.config/Hermes ]] ||
fail "gateway connections, tokens and settings survive removal"
pass "removal keeps the app's connections and settings"
# -L, not -e: a dangling symlink fails -e while very much still being there.
[[ ! -L $test_home/.local/bin/node ]] || fail "a node symlink into ~/.hermes is removed"
[[ ! -L $test_home/.local/bin/npm ]] || fail "an npm symlink into ~/.hermes is removed"
[[ -L $test_home/.local/bin/npx ]] || fail "an npx symlink pointing elsewhere survives"
pass "removal clears only the managed Node links it stranded"
[[ -f $test_home/.hermes/sessions/one.json ]] || fail "chats survive removal"
[[ -f $test_home/.hermes/memories/one.md ]] || fail "memories survive removal"
[[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal"
pass "removal keeps what belongs to the user"
# Without a terminal there is nobody to ask, so gum must not even be reached:
# a gum that answered "yes" on its own would be a data loss.
[[ ! -s $test_tmp/gum-log ]] ||
fail "removal does not ask about the user's data without a terminal"
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"
# A hermes command the app did not write survives even when the app did install
# a runtime of its own.
seed_install
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/my-own-hermes \"\$@\"" \
>"$test_home/.local/bin/hermes"
remove || fail "remove succeeds with a foreign hermes present"
[[ -f $test_home/.local/bin/hermes ]] ||
fail "a hermes command the app did not write survives removal"
pass "removal leaves a hermes it does not own"
# Installed but never launched. The app provisions its runtime on first launch
# and marks it complete when it lands, so without that marker everything under
# ~/.hermes predates the app -- an official install, or one built by hand -- and
# the paths are identical either way. Dropping the package is the whole job.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
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"
[[ -d $test_home/.hermes/hermes-agent ]] ||
fail "a Hermes runtime the app never installed survives removal"
[[ -f $test_home/.hermes/hermes-agent/PATCH ]] ||
fail "local changes to a runtime the app never installed survive removal"
[[ -d $test_home/.hermes/bin && -d $test_home/.hermes/node ]] ||
fail "the rest of a runtime the app never installed survives removal"
[[ -f $test_home/.local/bin/hermes ]] ||
fail "the command a runtime the app never installed put on PATH survives removal"
[[ -L $test_home/.local/bin/node ]] ||
fail "node links belonging to a runtime the app never installed survive removal"
pass "removal leaves a Hermes the app never installed"
# ~/.hermes carries a dot, so a pattern rather than a plain string would also
# claim a wrapper pointing at a sibling directory that merely looks like it.
seed_install
mkdir -p "$test_home/xhermes/bin"
sibling_body="#!/bin/bash
exec $test_home/xhermes/bin/hermes \"\$@\""
printf '%s\n' "$sibling_body" >"$test_home/.local/bin/hermes"
remove || fail "remove succeeds with a wrapper pointing at a sibling directory"
[[ -f $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$sibling_body" ]] ||
fail "a wrapper pointing at ~/xhermes is not mistaken for one pointing into ~/.hermes"
pass "removal matches the runtime path as a plain string"
# On a terminal the user is asked, default no: declining leaves every piece of
# data where it was.
seed_install
remove_tty || fail "remove succeeds when the data question is declined"
tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' ||
fail "removal asks about the user's data on a terminal"
[[ -f $test_home/.hermes/sessions/one.json && -d $test_home/.config/Hermes ]] ||
fail "declining the question keeps the user's data"
pass "removal asks on a terminal and declining keeps the data"
# An explicit yes is the one path that takes the data too.
seed_install
OMARCHY_TEST_GUM_STATUS=0 remove_tty || fail "remove succeeds when the data goes too"
[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] ||
fail "a yes deletes ~/.hermes and ~/.config/Hermes"
pass "removal deletes the user's data only on an explicit yes"
# Without the bootstrap marker the runtime is not the app's to take unasked,
# but the data question is still the user's to answer: declining keeps the
# whole tree -- runtime included -- untouched.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
remove_tty || fail "remove succeeds when the app never installed Hermes"
tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' ||
fail "removal still asks about the data without the bootstrap marker"
[[ -d $test_home/.hermes/hermes-agent && -d $test_home/.config/Hermes ]] ||
fail "declining keeps a Hermes the app never installed"
pass "removal asks without the marker and declining keeps everything"
# The prompt names ~/.hermes itself, so a yes takes the whole tree there too,
# unowned runtime and all -- that is what was asked and answered.
seed_install
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_GUM_STATUS=0 remove_tty ||
fail "remove succeeds when the data goes too without the marker"
[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] ||
fail "a yes takes ~/.hermes whole when the marker never appeared"
pass "removal honors a yes on the named paths without the marker"
# 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
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.
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), 'XDG_CONFIG_HOME': '', 'PATH': f"{scratch / 'bin'}:/usr/bin:/bin",
'OMARCHY_TEST_GUM_STATUS': '0'}
for key in ('DROP', '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 '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', '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')
# 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')
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:
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:
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')
# 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'
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