Fix UI test instability and clarify contract boundaries
This commit is contained in:
parent
a18de5d468
commit
e6ad500a43
7 changed files with 70 additions and 29 deletions
|
|
@ -293,23 +293,52 @@ Accessible at `HubSnapshot.metrics["host"]`:
|
||||||
|
|
||||||
## 10. Граница между учётными системами Hub и Hermes (System Boundaries)
|
## 10. Граница между учётными системами Hub и Hermes (System Boundaries)
|
||||||
|
|
||||||
### 10.1 Что Hub видит от Hermes
|
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"]`).
|
|
||||||
|
|
||||||
### 10.2 Чего Hub НЕ видит от Hermes
|
### 10.1 Что Hub получает от Hermes на каждом вызове
|
||||||
- Hermes ведёт собственную независимую систему профилей в `$HERMES_HOME/profiles/` (`agy-01`…`agy-06`, `worker-fast`, `worker-research`, `worker-review`, `worker-code`, `worker-code-2`, `deepseek`).
|
При вызове ntigravity_llm_execution (через middleware) Hub фактически получает от Hermes только следующие данные:
|
||||||
- Конфигурация под-агентов (`delegate_task`, `max_concurrent_children`, `provider=opencode-go`, `model=kimi-k2.7-code`) настраивается внутри Hermes и не передаётся в middleware.
|
- ask_id
|
||||||
- Профили Hub (`ag-w1`…`ag-w10`, `codex-orch`, `opengo-*`) — это отдельное пространство имён, независимое от профилей Hermes.
|
- 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
|
### 10.2 Чего Hub не видит
|
||||||
| Вариант | Описание | Трудоёмкость | Плюсы | Минусы |
|
Hub не имеет доступа к внутреннему контексту Hermes. В частности, Hub не видит:
|
||||||
|---|---|---|---|---|
|
- Профиль Hermes, которым выполняется текущий вызов (например, gy-05, worker-fast, deepseek и др.).
|
||||||
| **А. Авто-сопоставление по email / JWT Identity** | Считывание identity из `id_token`/`access_token` в обеих системах и автоматическое связывание одинаковых аккаунтов. | ~3–4 часа | Не требует ручной настройки от пользователя. | Не работает для профилей с API-ключами без email. |
|
- Настройки конфигурации задач (delegate_task, max_concurrent_children и т.д.).
|
||||||
| **Б. Чтение профилей Hermes как Single Source of Truth** | Hub отказывается от собственного каталога слотов `router_profiles.yaml` и напрямую отображает/редактирует `$HERMES_HOME/profiles/`. | ~8–12 часов | Единая учётная система, отсутствие рассинхронизации. | Высокая сложность, привязка структуры Hub к внутренностям Hermes. |
|
- Состав и иерархию субагентов Hermes.
|
||||||
| **В. Явная таблица соответствия (Profile Mapping Table)** | В `router_profiles.yaml` и UI Hub добавляется секция `hermes_profile_map` (например, `ag-w1` ↔ `agy-01`). | ~4–5 часов | Полный контроль пользователя, устойчивость к изменениям в Hermes. | Требует настройки в UI или мастере. |
|
|
||||||
|
### 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, чтобы упростить ручную настройку.
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,15 @@ def pytest_collection_modifyitems(config, items):
|
||||||
for item in items:
|
for item in items:
|
||||||
if "ui" in item.keywords:
|
if "ui" in item.keywords:
|
||||||
item.add_marker(skip_ui)
|
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}")
|
||||||
|
|
|
||||||
|
|
@ -151,14 +151,14 @@ def test_d_oauth_error_callback(tmp_path, monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@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."""
|
"""TEST E: Repeated 'Открыть в браузере' does NOT change session, state, verifier, or URL."""
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||||
pytest.importorskip("customtkinter")
|
pytest.importorskip("customtkinter")
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
||||||
|
|
||||||
root = ctk.CTk()
|
root = ctk.CTkToplevel(tk_root)
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
try:
|
try:
|
||||||
wizard = AddAccountWizard(root)
|
wizard = AddAccountWizard(root)
|
||||||
|
|
@ -195,14 +195,14 @@ def test_e_repeated_open_browser_invariance(tmp_path, monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@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."""
|
"""TEST F: Copy button works immediately upon entering Step 2 without opening browser."""
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||||
pytest.importorskip("customtkinter")
|
pytest.importorskip("customtkinter")
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
||||||
|
|
||||||
root = ctk.CTk()
|
root = ctk.CTkToplevel(tk_root)
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
try:
|
try:
|
||||||
wizard = AddAccountWizard(root)
|
wizard = AddAccountWizard(root)
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ from antigravity_provider.router.unified_health import (
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def ui_root():
|
def ui_root(tk_root):
|
||||||
root = ctk.CTk()
|
root = ctk.CTkToplevel(tk_root)
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
yield root
|
yield root
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ from antigravity_provider.router import hermes_hub_app as app_module
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def ui_root():
|
def ui_root(tk_root):
|
||||||
root = ctk.CTk()
|
root = ctk.CTkToplevel(tk_root)
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
yield root
|
yield root
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ from antigravity_provider.router.unified_health import ProfileViewModel, SystemR
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def ui_root():
|
def ui_root(tk_root):
|
||||||
root = ctk.CTk()
|
root = ctk.CTkToplevel(tk_root)
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
yield root
|
yield root
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def ui_root():
|
def ui_root(tk_root):
|
||||||
app = ctk.CTk()
|
app = ctk.CTkToplevel(tk_root)
|
||||||
app.withdraw()
|
app.withdraw()
|
||||||
yield app
|
yield app
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue