Keep build-time registration private, verify the published launcher, and then let the native command register the final desktop entry. Keep that launcher first on the desktop environment PATH for subsequent registrations. Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
285 lines
13 KiB
Python
285 lines
13 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"
|
|
[[ ${FAIL_PARTIAL_CLONE:-} != 1 ]] || exit 44
|
|
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
|
|
if [[ ${RACE_CLONE:-} == 1 ]]; then
|
|
mkdir -p "$HOME/.hermes/hermes-agent"
|
|
printf 'foreign checkout\n' > "$HOME/.hermes/hermes-agent/keep"
|
|
fi
|
|
;;
|
|
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'),
|
|
XDG_DATA_HOME=str(self.home / '.local/share'),
|
|
XDG_CONFIG_HOME=str(self.home / '.config'), XDG_CACHE_HOME=str(self.home / '.cache'),
|
|
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
|
|
set -eu
|
|
if [[ ${2:-} == desktop ]]; then
|
|
root=$(dirname "$1")
|
|
if [[ ${3:-} == --skip-build ]]; then
|
|
[[ ${4:-} == --build-only && -x $root/apps/desktop/release/linux-unpacked/Hermes ]]
|
|
[[ ${FAIL_FINAL_REGISTER:-} != 1 ]] || exit 45
|
|
else
|
|
[[ ${3:-} == --build-only ]]
|
|
printf 'desktop\\n' >> "$TEST_LOG"
|
|
[[ ${FAIL_STAGE:-} != desktop ]] || exit 42
|
|
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"
|
|
fi
|
|
entry_dir="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
|
mkdir -p "$entry_dir"
|
|
printf 'Exec=%s desktop\\n' "$(command -v hermes || printf '%s/venv/bin/python -m hermes_cli.main' "$root")" > "$entry_dir/hermes.desktop"
|
|
else
|
|
[[ ${FAIL_READY:-} != 1 ]]
|
|
fi
|
|
''')
|
|
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_interrupted_clone_stays_aside_and_retry_succeeds(self):
|
|
self.run_launcher('--install', ok=False, FAIL_PARTIAL_CLONE='1')
|
|
self.assertFalse(self.root.exists())
|
|
partials=list((self.home/'.hermes').glob('.omarchy-hermes-repository.*/hermes-agent/.git'))
|
|
self.assertEqual(len(partials),1)
|
|
self.install()
|
|
self.assertTrue(partials[0].is_dir())
|
|
|
|
def test_clone_publication_preserves_concurrent_checkout(self):
|
|
self.run_launcher('--install', ok=False, RACE_CLONE='1')
|
|
self.assertEqual((self.root/'keep').read_text(),'foreign checkout\n')
|
|
self.assertFalse((self.root/'.omarchy-hermes-desktop').exists())
|
|
self.assertFalse((self.home/'.local/bin/hermes').exists())
|
|
|
|
def test_desktop_registration_uses_published_cli(self):
|
|
self.write(self.bin/'hermes', '#!/bin/bash\necho foreign\n')
|
|
self.install()
|
|
entry=self.home/'.local/share/applications/hermes.desktop'
|
|
self.assertEqual(entry.read_text(),f'Exec={self.home}/.local/bin/hermes desktop\n')
|
|
|
|
def test_failed_registration_preserves_previous_launcher(self):
|
|
wrapper=self.home/'.local/bin/hermes'
|
|
self.write(wrapper,'#!/bin/bash\n# Written by omarchy-install-hermes-cli.\necho old\n')
|
|
entry=self.home/'.local/share/applications/hermes.desktop'
|
|
self.write(entry,'previous desktop entry\n')
|
|
before=wrapper.read_bytes()
|
|
self.run_launcher('--install',ok=False,FAIL_FINAL_REGISTER='1')
|
|
self.assertEqual(wrapper.read_bytes(),before)
|
|
self.assertEqual(entry.read_text(),'previous desktop entry\n')
|
|
self.run_launcher('--check',ok=False)
|
|
self.install()
|
|
|
|
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()
|