Найдено и проверено живым прогоном на этой машине (реальный GTK-рабочий стол, реальные профили agy) — не по чтению кода. 1. Значок .desktop-записи был .ico. Измерено: GdkPixbuf.Pixbuf.new_from_file на HermesHub.ico падает с "Compressed icons are not supported". .desktop-файл с нерендерящейся иконкой меню приложений и файловый менеджер просто показывают пустым — без ошибки, молча. PNG в тех же ассетах уже был и загружается (проверено: 256x256). Подставлен app_icon_256.png. 2. Хаб нельзя было остановить иначе как из терминала. На Windows сервер стартует из HermesHubWeb.exe, который держит значок в системном трее — оттуда «Exit» останавливает процесс. На Linux сервер остаётся в фоне после закрытия окна браузера (так и задумано — не переустанавливать каждый раз), но ни кнопки в интерфейсе (её нет ни на одной платформе), ни трея, ни пункта меню не было вовсе. launcher/hermes-hub-stop.sh — недостающий эквивалент «Exit из трея»: доступен из меню приложений через собственный .desktop-пункт, использует ту же функцию, что и install/uninstall. Проверено живым прогоном: сервер запущен, остановлен через новый лаунчер, curl после этого получает connection refused. 3. uninstall-linux.sh не останавливал работающий хаб перед удалением файлов — та же причина, что install уже чинил для установки: с --purge-user-data это ещё и rm -rf каталогов, на которые у живого процесса открыты файловые дескрипторы. Измерено: сервер, запущенный в песочнице, оставался в списке процессов после uninstall-linux.sh до этой правки. Добавлена та же остановка, тем же кодом. 4. stop_running_hub была вписана отдельно в install-linux.sh и uninstall-linux.sh — две копии, которые разошлись бы при правке одной незамеченной для другой. Вынесена в installer/lib_stop_running_hub.sh, источается обоими скриптами и новым лаунчером остановки. Всё проверено дважды: прямым запуском install-linux.sh/uninstall-linux.sh в изолированной песочнице (не ~/.hermes) и через собранный dist/hermes-hub-setup.sh — тот самый файл, который уходит в релиз. Тесты: 738 -> 740 passed, 2 skipped, 4 deselected. Новый тест на .ico падает на прежней версии install-linux.sh (проверено git stash) и проходит после фикса. ruff check . чисто. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
170 lines
9.1 KiB
Python
170 lines
9.1 KiB
Python
"""
|
||
Hermes Hub — Windows & Linux Installers and Launchers Test Suite.
|
||
Verifies requirements of Task A19.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from pathlib import Path
|
||
import subprocess
|
||
import pytest
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
INSTALLER_DIR = REPO_ROOT / "installer"
|
||
LAUNCHER_DIR = REPO_ROOT / "launcher"
|
||
|
||
|
||
def test_windows_csharp_launchers_and_setup_compile():
|
||
"""Verify HermesHub.cs, HermesHubWeb.cs, and HermesHubSetup.cs compile without errors on Windows."""
|
||
csc_path = r"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
|
||
if not os.path.isfile(csc_path):
|
||
csc_path = r"C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe"
|
||
|
||
if not os.path.isfile(csc_path):
|
||
pytest.skip("csc.exe compiler not found in standard .NET Framework location")
|
||
|
||
temp_out = REPO_ROOT / "artifacts" / "test_compile"
|
||
temp_out.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 1. Compile HermesHub.cs
|
||
hub_cs = LAUNCHER_DIR / "HermesHub.cs"
|
||
res1 = subprocess.run([
|
||
csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHub.exe'}",
|
||
"/r:System.Windows.Forms.dll", "/r:System.Drawing.dll", str(hub_cs)
|
||
], capture_output=True, text=True)
|
||
assert res1.returncode == 0, f"HermesHub.cs compilation failed: {res1.stdout}\n{res1.stderr}"
|
||
|
||
# 2. Compile HermesHubWeb.cs
|
||
hub_web_cs = LAUNCHER_DIR / "HermesHubWeb.cs"
|
||
res2 = subprocess.run([
|
||
csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHubWeb.exe'}",
|
||
"/r:System.Windows.Forms.dll", "/r:System.Drawing.dll", str(hub_web_cs)
|
||
], capture_output=True, text=True)
|
||
assert res2.returncode == 0, f"HermesHubWeb.cs compilation failed: {res2.stdout}\n{res2.stderr}"
|
||
|
||
# 3. Compile HermesHubSetup.cs
|
||
setup_cs = INSTALLER_DIR / "HermesHubSetup.cs"
|
||
res3 = subprocess.run([
|
||
csc_path, "/target:winexe", f"/out:{temp_out / 'HermesHubSetup.exe'}",
|
||
# Установщик несёт содержимое вшитым ресурсом и распаковывает его через
|
||
# ZipFile — сборка требует System.IO.Compression.FileSystem.
|
||
"/r:System.Windows.Forms.dll", "/r:System.Drawing.dll",
|
||
"/r:System.IO.Compression.FileSystem.dll", str(setup_cs)
|
||
], capture_output=True, text=True)
|
||
assert res3.returncode == 0, f"HermesHubSetup.cs compilation failed: {res3.stdout}\n{res3.stderr}"
|
||
|
||
|
||
def test_windows_launcher_browser_search_and_health_check():
|
||
"""Verify HermesHubWeb.cs includes Edge/Chrome detection, --app mode, and /api/health polling."""
|
||
web_cs = (LAUNCHER_DIR / "HermesHubWeb.cs").read_text(encoding="utf-8")
|
||
|
||
assert "msedge.exe" in web_cs
|
||
assert "chrome.exe" in web_cs
|
||
assert "--app=" in web_cs
|
||
assert "/api/health" in web_cs
|
||
assert "IsServerHealthy" in web_cs
|
||
assert "HermesHubWeb" in web_cs or "WebLauncher" in web_cs
|
||
|
||
|
||
def test_windows_installer_creates_standard_shortcut_and_cleans_legacy():
|
||
"""Verify HermesHubSetup.cs creates standard Hermes Hub.lnk shortcut and cleans up legacy shortcuts."""
|
||
setup_cs = (INSTALLER_DIR / "HermesHubSetup.cs").read_text(encoding="utf-8")
|
||
|
||
assert "Hermes Hub.lnk" in setup_cs
|
||
assert "Hermes Hub (Web).lnk" in setup_cs
|
||
assert "Hermes Hub (Desktop).lnk" in setup_cs
|
||
assert "HermesHubWeb.exe" in setup_cs
|
||
|
||
|
||
def test_linux_installer_script_structure():
|
||
"""Verify install-linux.sh contains prerequisite checks, mirroring, manifest, and .desktop setup."""
|
||
install_sh = (INSTALLER_DIR / "install-linux.sh").read_text(encoding="utf-8")
|
||
|
||
assert "#!/usr/bin/env bash" in install_sh
|
||
assert "deployment_manifest.json" in install_sh
|
||
assert "hermes-hub-web.desktop" in install_sh
|
||
assert "HERMES_HOME" in install_sh
|
||
assert "antigravity-provider" in install_sh
|
||
|
||
# Значок .desktop-записи должен быть растровым (PNG/SVG), не .ico.
|
||
#
|
||
# Измерено на настоящем GTK-рабочем столе: GdkPixbuf.Pixbuf.new_from_file
|
||
# на .ico падает с "Compressed icons are not supported", а .desktop-файл
|
||
# с нерендерящейся иконкой меню приложений и файловый менеджер просто
|
||
# показывают пустым — без ошибки, молча. HermesHub.ico в тех же ассетах
|
||
# существует и раньше подставлялся сюда, поэтому мало проверить, что путь
|
||
# не пуст — нужно, чтобы это не был именно .ico.
|
||
icon_line = next(line for line in install_sh.splitlines() if line.startswith("ICON_PATH="))
|
||
assert ".ico" not in icon_line, f"иконка .desktop-записи — .ico, GTK его не рендерит: {icon_line!r}"
|
||
|
||
|
||
def test_linux_launcher_script_headless_and_app_mode():
|
||
"""Verify hermes-hub-web.sh checks DISPLAY, prints SSH port forwarding on headless, and uses --app on desktop."""
|
||
launcher_sh = (LAUNCHER_DIR / "hermes-hub-web.sh").read_text(encoding="utf-8")
|
||
|
||
assert "#!/usr/bin/env bash" in launcher_sh
|
||
assert "DISPLAY" in launcher_sh
|
||
assert "WAYLAND_DISPLAY" in launcher_sh
|
||
assert "ssh -L" in launcher_sh
|
||
assert "127.0.0.1" in launcher_sh
|
||
assert "--app=" in launcher_sh
|
||
assert "/api/health" in launcher_sh
|
||
assert "google-chrome" in launcher_sh or "chromium" in launcher_sh or "microsoft-edge" in launcher_sh
|
||
|
||
|
||
def test_linux_uninstaller_preserves_user_data():
|
||
"""Verify uninstall-linux.sh preserves user config, keys, and profiles by default."""
|
||
uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8")
|
||
|
||
assert "#!/usr/bin/env bash" in uninstall_sh
|
||
assert "PURGE_USER_DATA" in uninstall_sh
|
||
assert "--purge-user-data" in uninstall_sh
|
||
assert "Preserving user data and credentials" in uninstall_sh
|
||
assert "hermes-hub-web.desktop" in uninstall_sh
|
||
|
||
|
||
def test_linux_uninstaller_stops_running_hub():
|
||
"""A61/A62: uninstall-linux.sh must stop the hub before deleting its files.
|
||
|
||
Раньше удаление не трогало работающий процесс: rm -rf уходил под живым
|
||
сервером, а с --purge-user-data ещё и сносил каталоги, на которые у
|
||
процесса были открыты файловые дескрипторы. Хаб продолжал отвечать по
|
||
старому порту после «успешного» удаления. Измерено живым прогоном:
|
||
сервер, запущенный в песочнице, оставался в списке процессов после
|
||
uninstall-linux.sh до этой правки.
|
||
"""
|
||
uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8")
|
||
assert "lib_stop_running_hub.sh" in uninstall_sh
|
||
assert "stop_running_hub" in uninstall_sh
|
||
|
||
|
||
def test_linux_stop_launcher_exists_and_reuses_shared_logic():
|
||
"""A61/A62: должен существовать способ остановить хаб не из терминала руками.
|
||
|
||
На Windows это «Exit» из системного трея HermesHubWeb.exe. На Linux до
|
||
этого не было ничего — ни кнопки в интерфейсе (её нет ни на одной
|
||
платформе), ни трея, ни пункта меню: сервер, оставленный в фоне после
|
||
закрытия окна браузера, можно было остановить только pkill'ом из
|
||
терминала. launcher/hermes-hub-stop.sh — недостающий эквивалент,
|
||
устанавливается install-linux.sh как второй пункт меню приложений.
|
||
"""
|
||
stop_sh = (LAUNCHER_DIR / "hermes-hub-stop.sh").read_text(encoding="utf-8")
|
||
assert "#!/usr/bin/env bash" in stop_sh
|
||
assert "stop_running_hub" in stop_sh
|
||
|
||
lib_sh = (INSTALLER_DIR / "lib_stop_running_hub.sh").read_text(encoding="utf-8")
|
||
assert "stop_running_hub()" in lib_sh
|
||
# Общий источник, а не третья копия той же функции: install и uninstall
|
||
# обязаны ссылаться на тот же файл, а не хранить свою версию.
|
||
install_sh = (INSTALLER_DIR / "install-linux.sh").read_text(encoding="utf-8")
|
||
uninstall_sh = (INSTALLER_DIR / "uninstall-linux.sh").read_text(encoding="utf-8")
|
||
for script_name, script_text in (("install-linux.sh", install_sh), ("uninstall-linux.sh", uninstall_sh)):
|
||
assert "lib_stop_running_hub.sh" in script_text, f"{script_name} не источает общую функцию"
|
||
assert script_text.count("stop_running_hub() {") == 0, (
|
||
f"{script_name} держит собственную копию функции вместо общего источника"
|
||
)
|
||
|
||
assert "hermes-hub-stop" in install_sh, "install-linux.sh не разворачивает лаунчер остановки"
|
||
assert "hermes-hub-stop.desktop" in install_sh, "у лаунчера остановки нет пункта меню"
|
||
assert "hermes-hub-stop" in uninstall_sh, "uninstall-linux.sh не убирает лаунчер остановки"
|