Fix UI test instability and clarify contract boundaries

This commit is contained in:
Hermes Team 2026-08-23 18:27:07 +07:00
parent a18de5d468
commit e6ad500a43
7 changed files with 70 additions and 29 deletions

View file

@ -293,23 +293,52 @@ Accessible at `HubSnapshot.metrics["host"]`:
## 10. Граница между учётными системами Hub и Hermes (System Boundaries)
### 10.1 Что Hub видит от Hermes
- При вызове `antigravity_llm_execution` (middleware `llm_execution`) Hub получает kwargs:
`task_id`, `turn_id`, `api_request_id`, `session_id`, `platform`, `model`, `provider`, `base_url`, `api_mode`, `api_call_count`, `request` payload (список messages, temperature и т.д.).
- Поле `role` передаётся только если вызывающая сторона явно указала его в вызове или метаданных (`request["role"]` или `request["metadata"]["role"]`).
Hub и Hermes используют разные множества профилей и настроек. Чтобы интерфейс корректно отображал происходящее и не вводил пользователя в заблуждение, важно понимать границу между ними.
### 10.2 Чего Hub НЕ видит от Hermes
- Hermes ведёт собственную независимую систему профилей в `$HERMES_HOME/profiles/` (`agy-01`…`agy-06`, `worker-fast`, `worker-research`, `worker-review`, `worker-code`, `worker-code-2`, `deepseek`).
- Конфигурация под-агентов (`delegate_task`, `max_concurrent_children`, `provider=opencode-go`, `model=kimi-k2.7-code`) настраивается внутри Hermes и не передаётся в middleware.
- Профили Hub (`ag-w1`…`ag-w10`, `codex-orch`, `opengo-*`) — это отдельное пространство имён, независимое от профилей Hermes.
### 10.1 Что Hub получает от Hermes на каждом вызове
При вызове ntigravity_llm_execution (через middleware) Hub фактически получает от Hermes только следующие данные:
- ask_id
- urn_id
- pi_request_id
- session_id
- platform
- model
- provider
- ase_url
- pi_mode
- pi_call_count
-
equest (payload: список сообщений, temperature и т.д.)
### 10.3 Правило перехвата и прозрачного пропуска (Pass-Through Principle)
1. **Без достоверной роли Hub не претендует на вызов:** Если в запросе нет явно указанной роли, Hub не угадывает роль по тексту промпта и не подменяет вызов ролью `orchestrator` по умолчанию. Запрос мгновенно передаётся вниз родному провайдеру Hermes (`next_call`) без расхода попыток роутера и без задержек.
2. **Отказоустойчивость не ломает Hermes:** Если роль задана явно, но вся цепочка маршрутизации для этой роли исчерпана, Hub не возвращает ошибку роутера как текст ответа ассистента. Сбой логируется на уровне `warning`, а вызов прозрачно уходит дальше через `next_call`.
**Явно:** роли агента среди передаваемых данных нет.
### 10.4 Варианты связывания профилей Hub и Hermes
| Вариант | Описание | Трудоёмкость | Плюсы | Минусы |
|---|---|---|---|---|
| **А. Авто-сопоставление по email / JWT Identity** | Считывание identity из `id_token`/`access_token` в обеих системах и автоматическое связывание одинаковых аккаунтов. | ~34 часа | Не требует ручной настройки от пользователя. | Не работает для профилей с API-ключами без email. |
| **Б. Чтение профилей Hermes как Single Source of Truth** | Hub отказывается от собственного каталога слотов `router_profiles.yaml` и напрямую отображает/редактирует `$HERMES_HOME/profiles/`. | ~812 часов | Единая учётная система, отсутствие рассинхронизации. | Высокая сложность, привязка структуры Hub к внутренностям Hermes. |
| **В. Явная таблица соответствия (Profile Mapping Table)** | В `router_profiles.yaml` и UI Hub добавляется секция `hermes_profile_map` (например, `ag-w1``agy-01`). | ~45 часов | Полный контроль пользователя, устойчивость к изменениям в Hermes. | Требует настройки в UI или мастере. |
### 10.2 Чего Hub не видит
Hub не имеет доступа к внутреннему контексту Hermes. В частности, Hub не видит:
- Профиль Hermes, которым выполняется текущий вызов (например, gy-05, worker-fast, deepseek и др.).
- Настройки конфигурации задач (delegate_task, max_concurrent_children и т.д.).
- Состав и иерархию субагентов Hermes.
### 10.3 Чем Hub управляет
Hub является независимой системой и полностью управляет:
- Собственными профилями (например, g-w1, g-orch-fallback, codex-orch, opengo-*, claude-*, grok-*).
- Цепочками отказоустойчивости (failover), привязанными к его собственным профилям.
- Квотами и авторизацией своих аккаунтов.
### 10.4 Чем Hub не управляет
Hub **не управляет ничем** из перечисленного в пункте 10.2. Он не может изменять состав агентов Hermes, перенастраивать профили Hermes или управлять делегированием задач.
---
## 11. Варианты связывания профилей Hub и Hermes
Поскольку один и тот же аккаунт пользователя может существовать в двух системах под разными именами (например, gy-05 в Hermes и g-w2 в Hub), существуют следующие варианты их связывания. **Внимание:** ни один из вариантов не должен реализовываться без явного решения владельца продукта.
| Вариант | Что становится возможным | Что ломается / Риски | Объем работы |
|---|---|---|---|
| **1. Сопоставление по идентичности аккаунта (email)** | Автоматическое связывание большинства профилей без ручной настройки. | Профили без email (например, worker-fast, deepseek, API-ключи OpenCode) не могут быть сопоставлены. Надежность зависит от гарантий Hermes по предоставлению идентичности. | ~34 дня. Требует извлечения identity на стороне Hermes и передачи в Hub. |
| **2. Чтение профилей Hermes (Single Source of Truth)** | Единый источник истины: Hub перестает вести свой список профилей и полностью отражает конфигурацию Hermes. | Теряются сущности, специфичные для Hub: цепочки отказоустойчивости (failover chains), распределение ролей Hub. Квоты сложнее привязывать к профилям. | ~812 дней. Требует глубокого рефакторинга конфигурации и движка роутинга Hub. |
| **3. Явная таблица соответствия (Profile Mapping Table)** | Полный контроль и предсказуемость. Владелец может вручную связать любой профиль Hermes (например, gy-05) с профилем Hub (g-w2). | Требует ручной настройки от пользователя в UI Hub. | ~45 дней. Требует добавления конфигурации hermes_profile_map в
outer_profiles.yaml и поддержки в UI. |
**Рекомендация:**
Наиболее безопасным и предсказуемым является **Вариант 3 (Явная таблица соответствия)**. Он сохраняет независимость систем (сохраняются цепочки отказоустойчивости Hub) и позволяет обрабатывать профили без email. Вариант 1 можно добавить позже как механизм автозаполнения для Варианта 3, чтобы упростить ручную настройку.

View file

@ -56,3 +56,15 @@ def pytest_collection_modifyitems(config, items):
for item in items:
if "ui" in item.keywords:
item.add_marker(skip_ui)
@pytest.fixture(scope="session")
def tk_root():
"""Shared Tkinter root for all UI tests to avoid Tcl resource exhaustion."""
try:
import customtkinter as ctk
root = ctk.CTk()
root.withdraw()
yield root
root.destroy()
except Exception as e:
pytest.skip(f"Tkinter could not be initialized: {e}")

View file

@ -151,14 +151,14 @@ def test_d_oauth_error_callback(tmp_path, monkeypatch):
@pytest.mark.unit
def test_e_repeated_open_browser_invariance(tmp_path, monkeypatch):
def test_e_repeated_open_browser_invariance(tmp_path, monkeypatch, tk_root):
"""TEST E: Repeated 'Открыть в браузере' does NOT change session, state, verifier, or URL."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
pytest.importorskip("customtkinter")
import customtkinter as ctk
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
root = ctk.CTk()
root = ctk.CTkToplevel(tk_root)
root.withdraw()
try:
wizard = AddAccountWizard(root)
@ -195,14 +195,14 @@ def test_e_repeated_open_browser_invariance(tmp_path, monkeypatch):
@pytest.mark.unit
def test_f_copy_before_open_browser(tmp_path, monkeypatch):
def test_f_copy_before_open_browser(tmp_path, monkeypatch, tk_root):
"""TEST F: Copy button works immediately upon entering Step 2 without opening browser."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
pytest.importorskip("customtkinter")
import customtkinter as ctk
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
root = ctk.CTk()
root = ctk.CTkToplevel(tk_root)
root.withdraw()
try:
wizard = AddAccountWizard(root)

View file

@ -29,8 +29,8 @@ from antigravity_provider.router.unified_health import (
@pytest.fixture(scope="module")
def ui_root():
root = ctk.CTk()
def ui_root(tk_root):
root = ctk.CTkToplevel(tk_root)
root.withdraw()
yield root
root.destroy()

View file

@ -19,8 +19,8 @@ from antigravity_provider.router import hermes_hub_app as app_module
@pytest.fixture(scope="module")
def ui_root():
root = ctk.CTk()
def ui_root(tk_root):
root = ctk.CTkToplevel(tk_root)
root.withdraw()
yield root
root.destroy()

View file

@ -23,8 +23,8 @@ from antigravity_provider.router.unified_health import ProfileViewModel, SystemR
@pytest.fixture(scope="module")
def ui_root():
root = ctk.CTk()
def ui_root(tk_root):
root = ctk.CTkToplevel(tk_root)
root.withdraw()
yield root
root.destroy()

View file

@ -19,8 +19,8 @@ from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
@pytest.fixture(scope="module")
def ui_root():
app = ctk.CTk()
def ui_root(tk_root):
app = ctk.CTkToplevel(tk_root)
app.withdraw()
yield app
try: