hermes-hub/agents/inbox/2026-08-21-A-antigravity-state-layer.md
Hermes Team f171a8069d test(conftest): skip GUI tests by marker instead of by test name
The headless guard matched any test whose name contained "ui", "view" or
"wizard", so static checks that never touch the toolkit were skipped too. That
is how four real failures stayed invisible: they lived in modules the name
filter silently removed. Selection is now the `ui` marker alone; modules that
import the GUI stack already guard themselves with pytest.importorskip, which
tests/test_import_invariants.py enforces.

Headless goes from 152 to 156 passed — four tests that were being skipped by
accident now actually run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:41:48 +07:00

164 lines
17 KiB
Markdown
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.

# Задание A (Antigravity): слой состояния и данных
## Дата поступления
2026-08-21
## База
Проверочный HEAD на момент выдачи: **`50fde5f`**. Перед началом выполнить `git fetch`, зафиксировать фактический `BASE_SHA` и не считать этот SHA актуальным автоматически.
## Ветка
`antigravity/state-layer`
---
## ГРАНИЦА РАБОТ — читать первым
Параллельно выполняется **Задание B (Codex)** — переработка интерфейса. Чтобы задания не конфликтовали, разделение проходит **по файлам**, а не по смыслу.
**Ваша зона (можно менять):**
```
src/antigravity_provider/router/state_store.py
src/antigravity_provider/router/unified_health.py
src/antigravity_provider/router/scheduler.py
src/antigravity_provider/router/event_bus.py
src/antigravity_provider/router/quota_collector.py
src/antigravity_provider/router/model_registry.py
src/antigravity_provider/router/account_identity.py
src/antigravity_provider/router/router_engine.py
src/antigravity_provider/router/router_config.py
src/antigravity_provider/router/session_affinity.py
src/antigravity_provider/router/health_tracker.py
src/antigravity_provider/router/profile_manager.py
src/antigravity_provider/router/auto_assigner.py
src/antigravity_provider/router/adapters/**
src/antigravity_provider/router/*_oauth.py
src/antigravity_provider/*.py
scripts/**, installer/**, config/**
tests/** (кроме tests/test_ui_*.py)
```
**Чужая зона (НЕ трогать):**
```
src/antigravity_provider/router/ui/** ← весь UI, включая views, components, theme, wizard
src/antigravity_provider/router/hermes_hub_app.py
tests/test_ui_*.py
```
Если для вашей задачи потребовалось изменить файл из чужой зоны — **это сигнал, что контракт спроектирован неверно**. Вместо правки UI расширьте ViewModel или событие.
---
## P0. Контракт ViewModel публикуется первым
До любых других изменений зафиксировать и запушить контракт, против которого Codex будет писать интерфейс: `docs/UI_STATE_CONTRACT.md`.
Описать точно, по фактическому коду:
- `HubSnapshot` — состав, гарантии консистентности, поле версии/seq;
- `ProfileViewModel` — все поля, какие опциональны, что означает каждое состояние `auth_state`/`health_state`;
- `QuotaSnapshot` и `QuotaBucket` — полная схема (`id`, `label`, `used_percent`, `remaining_percent`, `used`, `limit`, `reset_at`, `unit`, `scope`, `model_family`), какие поля реально заполняются **для каждого провайдера отдельно**, и признак `is_estimated`;
- `AgentViewModel`, `RolePipeline`, `PipelineNode`, `ProviderSummary`, `SystemReadiness`;
- перечень событий `event_bus` с полезной нагрузкой: что именно приходит при изменении квоты одного аккаунта, при добавлении аккаунта, при смене маршрута.
**Критично:** для каждого поля указать, реально ли backend его отдаёт, или это заглушка. Codex обязан отличать «данных нет» от «данные есть». Раздел «Backend gaps» обязателен.
Этот документ — интерфейс между двумя заданиями. После публикации менять его только с явной пометкой в отчёте.
---
## Область задачи
### 1. Единый источник состояния
Завершить `HubSnapshot` как единственный источник для UI. Ни один view не должен иметь возможности самостоятельно инициировать сканирование — не потому, что это запрещено правилом, а потому, что данные ему приходят готовыми.
Убрать вызовы `scan_all()` из кода, доступного UI: сервис отдаёт снапшот, обновляет его сам.
### 2. Централизованный планировщик обновлений
На базе существующего `HermesRefreshScheduler`:
- интервалы на провайдера;
- дедупликация одновременных запросов;
- защита от устаревшего ответа (поздний ответ не перезаписывает более свежее состояние — использовать `seq`);
- раздельные операции: обновить один аккаунт / одного провайдера / всё.
### 3. Событийная модель вместо полного пересбора
Изменение квоты одного аккаунта обязано порождать точечное событие с идентификатором аккаунта, а не сигнал «всё изменилось». То же для добавления/удаления аккаунта, смены авторизации, смены активного маршрута.
**Отдельно:** OAuth — самостоятельная state machine. Не подключать OAuth listener, таймауты авторизации и жизненный цикл PKCE к общему планировщику. После успешной авторизации: сохранить аккаунт → событие `ACCOUNT_ADDED` → точечное обновление, **без** глобального пересканирования.
### 4. Мульти-корзинные квоты
Довести `quota_collector` до реального сбора там, где провайдер это отдаёт. Требования:
- не сводить разные лимиты к одному проценту;
- корзины привязаны к семейству моделей, если у провайдера лимиты раздельные (Antigravity: Gemini и не-Gemini — проверить фактическим кодом, что именно доступно);
- каждый снапшот честно помечен `is_estimated` и источником; запрет на `*_api` для локально вычисленных значений остаётся в силе;
- если провайдер не отдаёт данных — отдавать отсутствие данных, а не число.
Роутер должен понимать, что переключение модели может сменить квотный пул.
### 5. Реестр моделей и маршрутизация
Довести `model_registry` и capability-роутинг: выбор модели с учётом возможностей, стоимости и остатка квоты. Учитывать `preferred_models` профиля и приоритет роли.
### 6. Долги, оставшиеся открытыми
- **Мёртвые модули.** `capability_matrix`, `lifecycle_supervisor`, `skill_registry` сейчас лишь реэкспортируются из `router/__init__.py` — ни один кодовый путь их не использует. Реэкспорт не равен интеграции. Либо подключить по-настоящему, либо удалить и записать причину.
- **`HKCU` в тестах.** `APPDATA`/`USERPROFILE` теперь в песочнице, но `HermesHubSetup.cs` пишет в реестр двумя вызовами `Registry.CurrentUser`, а реестр переменными окружения не перенаправляется. Вынести реальный запуск установщика в явный integration-режим либо параметризовать ключ реестра.
- **Комментарии `router_profiles.yaml`** стираются при сохранении (5 строк → 0). Оценить `ruamel.yaml`; если замена YAML-стека велика — зафиксировать отдельным долгом и не расширять scope.
- **Сериализация Antigravity.** `_AGY_INVOCATION_LOCK` вернул корректность ценой параллелизма: вызовы профилей со своим auth снова идут по одному. Оценить отказ от глобальной записи `gemini:antigravity` в пользу изоляции только через `USERPROFILE` — это снимет и лок, и гонку. Если не делать — записать как осознанный долг.
### 7. Релизная инфраструктура
- выложить ассет `hermes-hub-0.1.1.zip` с суммой из манифеста (сейчас `package_url` → HTTP 404) и `HermesHubSetup.exe` в GitHub Release;
- `dist/checksums.txt` — только из `scripts/build_dist.py`, вручную не править;
- порядок публикации: build → checksum → upload → verify → publish manifest → verify feed. Манифест не должен рекламировать несуществующий артефакт.
**Тег `v0.1.1` не создавать.**
---
## P0-bis. Найдено при прогоне с установленными UI-зависимостями
Обнаружено 2026-08-21 при проверке PHASE 1 Codex. Эти дефекты **не видны в обычном прогоне**: без `customtkinter` соответствующие тесты пропускаются, поэтому suite зелёный, а код не исполняется.
### 1. Подмена `sys.path` заставляет тесты грузить УСТАНОВЛЕННУЮ копию плагина
`hermes_hub_app.py:41` при импорте делает `sys.path.insert(0, ...)` для `%LOCALAPPDATA%/hermes/plugins/antigravity-provider/src`. Как только любой тест импортирует этот модуль, путь установленной копии оказывается **впереди** репозитория, и последующие импорты `antigravity_provider.*` резолвятся в развёрнутую — то есть более старую — версию.
Проявление: `tests/test_p0_release_gate.py::test_p0_5_b1_non_router_error_fallback` и `::test_n2_error_formatter_deduplication` падают с `ImportError: format_antigravity_error` **только в полном прогоне**, а по отдельности проходят. Проверено: функция есть в `src/antigravity_provider/runtime.py` и отсутствует в установленной копии.
Последствия шире тестов: в проде приложение может исполнять развёрнутый плагин вместо ожидаемого кода, а после обновления — смесь версий. Пакет `antigravity_provider` — namespace-типа (нет `__init__.py`), поэтому его `__path__` спокойно объединяет оба каталога.
**Нужно:** определить единственный источник импорта. Либо приложение не трогает `sys.path` и полагается на установленный пакет, либо repo-режим явно вытесняет установленный. Смешанное состояние недопустимо. Добавить тест, фиксирующий, из какого каталога загружен `antigravity_provider`.
### 2. `test_oauth_lifecycle.py` — два теста падают при реальном исполнении
```
FAILED test_e_repeated_open_browser_invariance
FAILED test_f_copy_before_open_browser
assert wizard.oauth_url is not None
E assert None is not None
```
Падают и по отдельности, и в полном прогоне — от порядка не зависят. То есть заявленная в `249a888` «immediate URL readiness» этими тестами не подтверждена. Разобраться: дефект в продукте или в тесте (например, мок `start_profile_oauth` не покрывает путь `_init_antigravity_oauth`). Пока не выяснено — считать функцию непроверенной.
### 3. Заглушка в `conftest.py` — ✅ уже исправлено ревьюером
Отбор шёл по имени теста (`ui`, `view`, `wizard`), из-за чего пропускались и статические проверки, не касающиеся GUI, — именно поэтому оба дефекта выше жили незамеченными. Теперь отбор только по маркеру `ui`; модули, импортирующие GUI, защищают себя `pytest.importorskip` на уровне модуля, и это требование закреплено `tests/test_import_invariants.py`.
Действий не требуется — пункт оставлен для контекста. После правки headless-прогон даёт 156 passed вместо 152: четыре теста, которые раньше молча пропускались, теперь реально выполняются.
**Критерий приёмки:** полный `pytest` зелёный **и** в окружении без UI-зависимостей, **и** в окружении с установленными `customtkinter`/`pillow`/`psutil`. Сейчас второй вариант даёт 4 падения.
## Критерии приёмки
1. `docs/UI_STATE_CONTRACT.md` опубликован до остальных изменений, содержит раздел «Backend gaps» и для каждого поля — признак реальности данных.
2. Ни один файл из чужой зоны не изменён (`git diff --name-only BASE_SHA..HEAD -- src/antigravity_provider/router/ui src/antigravity_provider/router/hermes_hub_app.py` пуст).
3. `scan_all()` недоступен из UI-слоя; сервис обновляет снапшот сам.
4. Изменение квоты одного аккаунта порождает событие с идентификатором аккаунта; тест это проверяет.
5. Поздний ответ не перезаписывает более свежее состояние; тест на гонку `seq`.
6. OAuth-сессия не управляется общим планировщиком; успешная авторизация даёт точечное обновление, тест это фиксирует.
7. Мульти-корзинные квоты: для каждого провайдера в контракте указано, какие корзины реально доступны; ни одна локально вычисленная величина не помечена источником провайдера.
8. Мёртвые модули либо интегрированы (есть вызывающий код вне `__init__.py`), либо удалены.
9. Тесты установщика не оставляют записей в `HKCU` при обычном прогоне.
10. `package_url` из манифеста отдаёт HTTP 200, sha256 совпадает с загруженным артефактом.
11. Полный `pytest` зелёный **в окружении без UI-зависимостей**; `ruff check .` чисто; release gate PASSED на финальном коммите.
12. Отчёт содержит `BASE_SHA`, `FINAL_SHA`, `origin/main`, `git status`, точный результат тестов (`X passed / Y skipped / Z failed`) и список того, что осталось.
## Порядок сдачи
Никаких функциональных коммитов после последнего запуска release gate. Ревьюеру передать точный `FINAL_COMMIT_SHA` и не продолжать разработку поверх него до вердикта.