From e6ad500a4341c367b4e511c75e0810bc2bb54f65 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sun, 23 Aug 2026 18:27:07 +0700 Subject: [PATCH] Fix UI test instability and clarify contract boundaries --- docs/UI_STATE_CONTRACT.md | 63 +++++++++++++++++++++++--------- tests/conftest.py | 12 ++++++ tests/test_oauth_lifecycle.py | 8 ++-- tests/test_ui_contract_v11.py | 4 +- tests/test_ui_mockup_redesign.py | 4 +- tests/test_ui_phase2_6.py | 4 +- tests/test_ui_wizard_finish.py | 4 +- 7 files changed, 70 insertions(+), 29 deletions(-) diff --git a/docs/UI_STATE_CONTRACT.md b/docs/UI_STATE_CONTRACT.md index aa7eab9..007f123 100644 --- a/docs/UI_STATE_CONTRACT.md +++ b/docs/UI_STATE_CONTRACT.md @@ -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` в обеих системах и автоматическое связывание одинаковых аккаунтов. | ~3–4 часа | Не требует ручной настройки от пользователя. | Не работает для профилей с API-ключами без email. | -| **Б. Чтение профилей Hermes как Single Source of Truth** | Hub отказывается от собственного каталога слотов `router_profiles.yaml` и напрямую отображает/редактирует `$HERMES_HOME/profiles/`. | ~8–12 часов | Единая учётная система, отсутствие рассинхронизации. | Высокая сложность, привязка структуры Hub к внутренностям Hermes. | -| **В. Явная таблица соответствия (Profile Mapping Table)** | В `router_profiles.yaml` и UI Hub добавляется секция `hermes_profile_map` (например, `ag-w1` ↔ `agy-01`). | ~4–5 часов | Полный контроль пользователя, устойчивость к изменениям в 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 по предоставлению идентичности. | ~3–4 дня. Требует извлечения identity на стороне Hermes и передачи в Hub. | +| **2. Чтение профилей Hermes (Single Source of Truth)** | Единый источник истины: Hub перестает вести свой список профилей и полностью отражает конфигурацию Hermes. | Теряются сущности, специфичные для Hub: цепочки отказоустойчивости (failover chains), распределение ролей Hub. Квоты сложнее привязывать к профилям. | ~8–12 дней. Требует глубокого рефакторинга конфигурации и движка роутинга Hub. | +| **3. Явная таблица соответствия (Profile Mapping Table)** | Полный контроль и предсказуемость. Владелец может вручную связать любой профиль Hermes (например, gy-05) с профилем Hub (g-w2). | Требует ручной настройки от пользователя в UI Hub. | ~4–5 дней. Требует добавления конфигурации hermes_profile_map в +outer_profiles.yaml и поддержки в UI. | + +**Рекомендация:** +Наиболее безопасным и предсказуемым является **Вариант 3 (Явная таблица соответствия)**. Он сохраняет независимость систем (сохраняются цепочки отказоустойчивости Hub) и позволяет обрабатывать профили без email. Вариант 1 можно добавить позже как механизм автозаполнения для Варианта 3, чтобы упростить ручную настройку. diff --git a/tests/conftest.py b/tests/conftest.py index e149fc9..8e913bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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}") diff --git a/tests/test_oauth_lifecycle.py b/tests/test_oauth_lifecycle.py index c334519..f465695 100644 --- a/tests/test_oauth_lifecycle.py +++ b/tests/test_oauth_lifecycle.py @@ -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) diff --git a/tests/test_ui_contract_v11.py b/tests/test_ui_contract_v11.py index 562f177..67e31cf 100644 --- a/tests/test_ui_contract_v11.py +++ b/tests/test_ui_contract_v11.py @@ -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() diff --git a/tests/test_ui_mockup_redesign.py b/tests/test_ui_mockup_redesign.py index 9298a9f..358f155 100644 --- a/tests/test_ui_mockup_redesign.py +++ b/tests/test_ui_mockup_redesign.py @@ -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() diff --git a/tests/test_ui_phase2_6.py b/tests/test_ui_phase2_6.py index a9c39b7..2518990 100644 --- a/tests/test_ui_phase2_6.py +++ b/tests/test_ui_phase2_6.py @@ -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() diff --git a/tests/test_ui_wizard_finish.py b/tests/test_ui_wizard_finish.py index df4af74..1b3b1f6 100644 --- a/tests/test_ui_wizard_finish.py +++ b/tests/test_ui_wizard_finish.py @@ -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: