Files
omarchy-pkgs/test/hermes-desktop-test.py
T
2026-09-06 01:57:42 -05:00

230 lines
10 KiB
Python

#!/usr/bin/env python3
"""Exercise the packaged entry point with a disposable HOME and installer."""
import json
import os
import pty
import signal
import time
from pathlib import Path
import subprocess
import tempfile
import unittest
SOURCE = Path(__file__).resolve().parents[1] / 'pkgbuilds/hermes-desktop/hermes-desktop.sh'
INSTALLER = r'''#!/bin/bash
set -eu
while (( $# )); do
case $1 in --dir) root=$2; shift;; --stage) stage=$2; shift;; esac
shift
done
printf '%s\n' "$stage" >> "$TEST_LOG"
[[ ${FAIL_STAGE:-} != "$stage" ]] || exit 42
case $stage in
repository)
mkdir -p "$root/venv/bin"
git init -q "$root"
git -C "$root" remote add origin https://github.com/NousResearch/hermes-agent.git
touch "$root/hermes"
cp "$TEST_PYTHON" "$root/venv/bin/python"
git -C "$root" add hermes venv
git -C "$root" -c user.email=test@example.com -c user.name=Test commit -qm initial
;;
desktop)
mkdir -p "$root/apps/desktop/release/linux-unpacked"
cp "$TEST_GUI" "$root/apps/desktop/release/linux-unpacked/Hermes"
printf '/apps/\n/.hermes-bootstrap-complete\n' >> "$root/.git/info/exclude"
;;
complete) touch "$root/.hermes-bootstrap-complete";;
path)
mkdir -p "$HOME/.local/bin"
for command in hermes hermes-agent hermes-acp; do
entry=hermes; suffix=''
[[ $command != hermes-agent ]] || entry=run_agent.py
[[ $command != hermes-acp ]] || suffix=' acp'
printf '#!/usr/bin/env bash\nunset PYTHONPATH\nunset PYTHONHOME\nexec "%s/venv/bin/python" "%s/%s"%s "$@"\n' "$root" "$root" "$entry" "$suffix" > "$HOME/.local/bin/$command"
chmod +x "$HOME/.local/bin/$command"
done
[[ ${FAIL_AFTER_PATH:-} != 1 ]] || exit 43
;;
esac
'''
class LauncherTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='hermes-package-')
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.home = self.base / 'home with spaces'
self.home.mkdir()
self.root = self.home / '.hermes/hermes-agent'
self.bin = self.base / 'bin'
self.bin.mkdir()
self.log = self.base / 'stages'
self.output = self.base / 'launch.json'
self.env = dict(os.environ, HOME=str(self.home), HERMES_HOME=str(self.home / '.hermes'),
PATH=f'{self.bin}:/usr/bin:/bin', TEST_LOG=str(self.log),
TEST_GUI=str(self.base / 'gui'), TEST_PYTHON=str(self.base / 'python'),
TEST_OUTPUT=str(self.output), GIT_CONFIG_NOSYSTEM='1',
GIT_CONFIG_GLOBAL='/dev/null')
for k in ['FAIL_STAGE', 'FAIL_AFTER_PATH', 'PYTHONHOME', 'PYTHONPATH']:
self.env.pop(k, None)
self.write(self.base / 'installer', INSTALLER)
self.write(self.base / 'python', '#!/bin/bash\n[[ ${FAIL_READY:-} != 1 ]]\n')
self.write(self.base / 'gui', '''#!/usr/bin/env python3
import json,os,sys,time
json.dump({'pid':os.getpid(),'args':sys.argv[1:],'root':os.environ['HERMES_DESKTOP_HERMES_ROOT'],'node':os.environ.get('ELECTRON_RUN_AS_NODE'),'password':os.environ['HERMES_DESKTOP_PASSWORD_STORE']},open(os.environ['TEST_OUTPUT'],'w'))
if os.environ.get('TEST_GUI_WAIT'): time.sleep(30)
''')
self.write(self.bin / 'unshare', '#!/bin/bash\nexit 0\n')
self.write(self.bin / 'xdg-terminal-exec', '#!/bin/bash\nprintf "%s\\n" "$@" > "$TEST_OUTPUT"\n')
source = SOURCE.read_text().replace('/usr/share/hermes-desktop/install.sh', str(self.base / 'installer'))
source = source.replace('/usr/bin/hermes-desktop', str(self.base / 'launcher'))
self.write(self.base / 'launcher', source)
def write(self, path, text):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
path.chmod(0o755)
def run_launcher(self, *args, ok=True, **env):
result = subprocess.run(['bash', str(self.base / 'launcher'), *args], env=self.env | env,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.assertEqual(result.returncode == 0, ok, result.stdout)
return result
def install(self):
self.run_launcher('--install')
self.run_launcher('--check')
def test_fresh_install_publishes_cli_after_desktop(self):
self.install()
stages = self.log.read_text().splitlines()
self.assertLess(stages.index('desktop'), stages.index('path'))
self.assertEqual((self.root / '.omarchy-hermes-desktop').read_text(), 'ready\n')
self.assertEqual(subprocess.check_output(['git','-C',str(self.root),'status','--porcelain'],env=self.env),b'')
def test_warm_launch_and_package_reinstall_leave_runtime_untouched(self):
self.install()
before = self.log.read_bytes()
self.run_launcher('--install')
self.run_launcher('hermes://test?a=b', 'two words', WAYLAND_DISPLAY='wayland-1', ELECTRON_RUN_AS_NODE='1')
observed = json.loads(self.output.read_text())
self.assertEqual(observed['args'], ['--ozone-platform=wayland','--disable-setuid-sandbox','hermes://test?a=b','two words'])
self.assertEqual(observed['root'],str(self.root))
self.assertIsNone(observed['node'])
self.assertEqual(observed['password'],'gnome-libsecret')
self.assertEqual(before,self.log.read_bytes())
def test_explicit_platform_and_password_are_preserved(self):
self.install()
self.run_launcher('--ozone-platform=x11', WAYLAND_DISPLAY='wayland-1', HERMES_DESKTOP_PASSWORD_STORE='kwallet6')
result=json.loads(self.output.read_text())
self.assertNotIn('--ozone-platform=wayland',result['args'])
self.assertEqual(result['password'],'kwallet6')
def test_foreign_command_and_symlink_are_preserved(self):
wrapper=self.home/'.local/bin/hermes'
self.write(wrapper,'#!/bin/bash\necho custom\n')
before=wrapper.read_bytes()
self.run_launcher('--install',ok=False)
self.assertEqual(before,wrapper.read_bytes())
self.assertFalse(self.log.exists())
wrapper.unlink()
wrapper.symlink_to(self.base/'missing')
self.run_launcher('--install',ok=False)
self.assertTrue(wrapper.is_symlink())
def test_customized_native_wrapper_is_preserved(self):
self.install()
wrapper=self.home/'.local/bin/hermes'
wrapper.write_text(wrapper.read_text().replace('unset PYTHONPATH','export CUSTOM=yes\nunset PYTHONPATH'))
before=wrapper.read_bytes()
self.run_launcher('--install',ok=False)
self.assertEqual(wrapper.read_bytes(),before)
def test_failed_build_preserves_old_cli_and_retry_succeeds(self):
wrapper=self.home/'.local/bin/hermes'
self.write(wrapper,'#!/bin/bash\n# Written by omarchy-install-hermes-cli.\necho old\n')
before=wrapper.read_bytes()
self.run_launcher('--install',ok=False,FAIL_STAGE='desktop')
self.assertEqual(wrapper.read_bytes(),before)
self.run_launcher('--check',ok=False)
self.install()
self.assertNotEqual(wrapper.read_bytes(),before)
def test_late_path_failure_restores_old_cli(self):
wrapper=self.home/'.local/bin/hermes'
self.write(wrapper,'#!/bin/bash\n# Written by omarchy-install-hermes-cli.\necho old\n')
before=wrapper.read_bytes()
self.run_launcher('--install',ok=False,FAIL_AFTER_PATH='1')
self.assertEqual(wrapper.read_bytes(),before)
self.run_launcher('--check',ok=False)
self.install()
def test_dirty_pending_checkout_is_preserved(self):
self.run_launcher('--install',ok=False,FAIL_STAGE='desktop')
(self.root/'hermes').write_text('user changes')
before=self.log.read_bytes()
self.run_launcher('--install',ok=False)
self.assertEqual((self.root/'hermes').read_text(),'user changes')
self.assertEqual(self.log.read_bytes(),before)
def test_cold_graphical_launch_opens_visible_setup(self):
self.run_launcher('hermes://open')
self.assertEqual(self.output.read_text().splitlines(),[str(self.base/'launcher'),'--setup','hermes://open'])
self.assertFalse(self.log.exists())
def test_unexecutable_native_cli_is_repaired(self):
self.install()
wrapper=self.home/'.local/bin/hermes'
wrapper.chmod(0o644)
self.run_launcher('--check',ok=False)
self.install()
self.assertTrue(os.access(wrapper,os.X_OK))
def test_old_cli_ownership_survives_package_only_setup(self):
wrapper=self.home/'.local/bin/hermes'
self.write(wrapper,'#!/bin/bash\n# Written by omarchy-install-hermes-cli.\necho old\n')
self.install()
receipt=self.root/'.git/omarchy-mise-predecessor'
self.assertEqual(receipt.read_text(),'pipx:hermes-agent[extras=all]\n')
def test_graphical_setup_detaches_and_releases_install_lock(self):
started=time.monotonic()
self.run_launcher('--setup', 'hermes://open', TEST_GUI_WAIT='1')
self.assertLess(time.monotonic()-started,5)
for _ in range(100):
if self.output.exists(): break
time.sleep(.02)
observed=json.loads(self.output.read_text())
self.addCleanup(lambda: os.kill(observed['pid'],signal.SIGTERM))
self.assertIn('hermes://open',observed['args'])
self.run_launcher('--install')
lock=self.home/'.hermes/.omarchy-hermes-desktop.lock'
self.assertEqual(subprocess.run(['flock','--nonblock',str(lock),'true']).returncode,0)
def test_cold_terminal_launch_releases_lock_before_exec(self):
master,slave=pty.openpty()
process=subprocess.Popen(['bash',str(self.base/'launcher')],stdin=slave,stdout=slave,stderr=slave,env=self.env | {'TEST_GUI_WAIT':'1'})
os.close(slave)
def cleanup():
if process.poll() is None: process.terminate()
process.wait(timeout=5)
os.close(master)
self.addCleanup(cleanup)
for _ in range(100):
if self.output.exists(): break
time.sleep(.02)
self.assertTrue(self.output.exists())
self.assertIsNone(process.poll())
lock=self.home/'.hermes/.omarchy-hermes-desktop.lock'
self.assertEqual(subprocess.run(['flock','--nonblock',str(lock),'true']).returncode,0)
def test_pending_marker_overrides_old_completion(self):
self.install()
(self.root/'.omarchy-hermes-desktop').write_text('pending\n')
self.run_launcher('--check',ok=False)
if __name__=='__main__':
unittest.main()