hermes-hub/tests/test_installer.py
ochenstarik-ui 377c567b85 fix(installer,release): находки agy с живой Windows-машины (A61)
Ветка installer/a61-live-verification (agy, коммит b0644a3) частично
пересекалась с HUB-1, частично добавляла то, чего в HUB-1 не было. Пункты
взяты по одному, дубли — нет.

## Взято

1. verify_multi_provider_router.py проверял изоляцию пути на ID "ag-w2" —
   на живой машине владельца это существующий подключённый профиль, и
   "assert not pdir.exists()" падал не из-за бага, а потому что каталог
   реального аккаунта и так был на месте (код возврата 12). ID заменён на
   заведомо не боевой "ag-probe-isolation-test".

2. HermesHubSetup.cs: CreateStartMenuShortcut/RemoveStartMenuShortcut не
   уважали HERMES_HUB_NO_REGISTRY — переменная гасила запись в реестр (A4),
   но ярлык в настоящем меню Пуск изолированные тесты всё равно писали.
   Добавлена та же проверка, что уже стоит перед записью в реестр. Заодно
   LOCALAPPDATA читается из окружения раньше SpecialFolder — расхождение
   найдено живым прогоном.

3. test_installer.py: /silent-тест линковался на venv настоящей машины
   junction'ом (Windows) или symlink'ом вместо пустых touch-файлов — раньше
   проверка живых Win32-зависимостей ничего по сути не проверяла.

4. update_manager.py: запасной перебор известных имён установщика
   (hermes-hub-setup.sh/install-linux.sh/HermesHubSetup.exe) до отката на
   .zip — подстраховка на случай расхождения определения платформы.

5. release_gate.py: --assets проверял только присутствие файлов. Добавлена
   нижняя граница размера (усечённая сборка, найдено вживую) и сверка
   SHA-256 каждого установщика с локальным checksums.txt — до всякой
   публикации. Своя реализация (agy: только HermesHubSetup.exe, только
   argparse-обвязка, несовместимая с --publication-only из HUB-1), но идея
   и обе живые находки — его. Проверено полным циклом: собран настоящий
   dist/hermes-hub-setup.sh, посчитаны настоящие контрольные суммы,
   --assets прошёл на них 12588887 байт, SHA-256 сошёлся.

## Не взято — уже есть шире в HUB-1

- security_guard.py: точечный "$HOME" в тексте команды вместо конвейера
  (диалект по команде, ${HOME}, %USERPROFILE%, fail-closed) — версия HUB-1
  шире и уже зелёная на настоящем Windows CI.
- test_a59: заглушка os.getuid без проверки ветки Windows (taskkill/wmic) —
  версия HUB-1 параметризована на обе ветки.
- Скомпилированные .exe — не переношу: пересборка на Windows после этого
  коммита, здесь compилятора нет.

Тесты: 780 -> 781 passed, 2 skipped, 4 deselected. ruff check . чисто.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 00:57:08 +07:00

139 lines
6.3 KiB
Python
Raw 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
import sys
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 pointing to the active venv.
#
# Пустые touch-файлы python.exe/hermes.exe раньше делали живую проверку
# зависимостей на установленной машине бессмысленной: файл существовал,
# но не был исполняемым питоном, и по-настоящему установщик его не
# запускал ни разу. Найдено прогоном на настоящей машине (A61): junction
# (или symlink) на реальный venv делает эту проверку осмысленной —
# запускается настоящий интерпретатор.
agent_dir = tmp_path / "hermes" / "hermes-agent"
agent_dir.mkdir(parents=True, exist_ok=True)
real_venv = Path(sys.prefix)
target_venv = agent_dir / "venv"
try:
import _winapi
_winapi.CreateJunction(str(real_venv), str(target_venv))
except Exception:
try:
os.symlink(str(real_venv), str(target_venv), target_is_directory=True)
except Exception:
venv_scripts = target_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}. Stdout: {res.stdout}"
@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, где зависимостей нет"
)