hermes-hub/tests/test_installer.py
Hermes Team 001cd1f91d fix(installer): установка на Linux падала на защищённом системном Python
Установка на сервере владельца прервалась на проверке: «No module named
'fastapi'». Причин две, и обе в установщике.

Первая: запуск через sudo. Установка пользовательская — всё ложится в
$HOME/.hermes и $HOME/.local/bin, root не нужен. Под sudo домашним каталогом
становится /root, венв Hermes там не находится, и установщик уходит на
системный python. Копия при этом ложится в /root/.hermes, где владелец её не
видит. Теперь запуск через sudo распознаётся и отклоняется с объяснением;
осознанный обход остаётся через HERMES_ALLOW_ROOT=1.

Вторая: системный python в Ubuntu 24.04 помечен EXTERNALLY-MANAGED (PEP 668) и
отклоняет pip install — и обычный, и с --user. Установщик обе неудачи проглатывал
(|| true) и продолжал работу до отказа на проверке. Теперь при защищённом
системном python создаётся собственное окружение $HERMES_HOME/venv, а неудача
установки зависимостей прекращает установку с внятным кодом возврата.
--break-system-packages не применяется: имя флага не преувеличивает.

Пусковик научен той же ветке — иначе запуск уходил бы на системный python,
где зависимостей нет и быть не может.

Проверено исполнением на сервере под непривилегированным пользователем:
окружение создано, зависимости установлены, HERMES_HUB_LINUX_VERIFY_OK,
запуск через sudo отклонён с кодом 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:56:47 +07:00

121 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for Hermes Hub Installer (HermesHubSetup.exe) and pre-flight logic."""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
import pytest
from antigravity_provider.version import __version__
from antigravity_provider import paths
REPO_ROOT = paths.get_repo_root()
SETUP_EXE = REPO_ROOT / "dist" / "HermesHubSetup.exe"
COMPATIBILITY_JSON = REPO_ROOT / "config" / "compatibility.json"
@pytest.mark.unit
def test_compatibility_json():
"""Verify compatibility manifest structure and fields."""
assert COMPATIBILITY_JSON.is_file()
data = json.loads(COMPATIBILITY_JSON.read_text(encoding="utf-8"))
assert data["hub_version"] == __version__
assert "0.20.4" in data["tested_versions"]
assert "min_hermes_version" in data
@pytest.mark.installer
def test_setup_exe_exists():
"""Verify that HermesHubSetup.exe is built and present."""
if not SETUP_EXE.is_file():
pytest.skip("HermesHubSetup.exe not built yet")
assert SETUP_EXE.stat().st_size > 0
@pytest.mark.installer
def test_silent_installer_execution_with_hermes(tmp_path):
"""Run HermesHubSetup.exe /silent on system where Hermes is installed (Expect 0)."""
if not SETUP_EXE.is_file():
pytest.skip("HermesHubSetup.exe not built yet")
# Set up mock Hermes Agent structure in temp home
agent_dir = tmp_path / "hermes" / "hermes-agent"
venv_scripts = agent_dir / "venv" / "Scripts"
venv_scripts.mkdir(parents=True, exist_ok=True)
(venv_scripts / "python.exe").touch()
(venv_scripts / "hermes.exe").touch()
env = dict(os.environ)
env["HERMES_HOME"] = str(tmp_path / "hermes")
env["LOCALAPPDATA"] = str(tmp_path / "localappdata")
env["APPDATA"] = str(tmp_path / "appdata")
env["USERPROFILE"] = str(tmp_path / "user")
env["HERMES_HUB_NO_REGISTRY"] = "1"
res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True)
assert res.returncode == 0, f"Expected returncode 0, got {res.returncode}. Stderr: {res.stderr}"
@pytest.mark.installer
def test_silent_installer_fails_without_hermes(tmp_path):
"""Run HermesHubSetup.exe /silent when HERMES_HOME points to empty dir."""
if not SETUP_EXE.is_file():
pytest.skip("HermesHubSetup.exe not built yet")
fake_home = tmp_path / "non_existent_hermes"
env = dict(os.environ)
env["HERMES_HOME"] = str(fake_home)
env["LOCALAPPDATA"] = str(tmp_path / "localappdata")
env["APPDATA"] = str(tmp_path / "appdata")
env["USERPROFILE"] = str(tmp_path / "user")
res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True)
assert res.returncode != 0, f"Expected failure for missing Hermes, got {res.returncode}"
# ── Установка на Linux: sudo и защищённый системный Python ──
#
# Установка на сервере владельца упала так: запуск через sudo увёл всё в
# /root/.hermes, венв Hermes там не нашёлся, установщик взял /usr/bin/python3,
# а он в Ubuntu 24.04 помечен EXTERNALLY-MANAGED и отклоняет pip. Проверка
# после установки честно упала на «No module named 'fastapi'».
def _linux_installer_text() -> str:
root = Path(__file__).resolve().parent.parent
return (root / "installer" / "install-linux.sh").read_text(encoding="utf-8")
def test_linux_installer_refuses_sudo():
text = _linux_installer_text()
assert "SUDO_USER" in text, "запуск через sudo обязан распознаваться"
assert "HERMES_ALLOW_ROOT" in text, "должен быть осознанный способ обойти отказ"
assert "exit 3" in text
def test_linux_installer_creates_own_venv_when_system_python_is_managed():
text = _linux_installer_text()
assert "EXTERNALLY-MANAGED" in text, "PEP 668 должен распознаваться"
assert "-m venv" in text, "ответ на PEP 668 — собственное окружение"
# --break-system-packages ломает питон всей машины: имя флага не
# преувеличивает. Упоминание в пояснении допустимо, применение — нет,
# поэтому смотрим только на исполняемые строки.
code_lines = [
line for line in text.splitlines() if not line.lstrip().startswith("#")
]
assert not any("--break-system-packages" in line for line in code_lines)
def test_linux_installer_fails_loudly_on_missing_deps():
"""Раньше pip падал, а установка продолжалась — до отказа на проверке."""
text = _linux_installer_text()
assert "exit 12" in text and "exit 13" in text
assert "|| true" not in text.split("Installing required packages")[1][:400]
def test_linux_launcher_knows_about_installer_venv():
root = Path(__file__).resolve().parent.parent
launcher = (root / "launcher" / "hermes-hub-web.sh").read_text(encoding="utf-8")
assert '$HERMES_HOME/venv/bin/python3' in launcher, (
"иначе запуск уйдёт на системный python, где зависимостей нет"
)