hermes-hub/tests/test_web_snapshot_freshness.py
Hermes Team c75b4e42ac fix(web): квоты не подтягивались — прогрев кэша и пересбор снапшота
/api/snapshot отдавал квоты с source="baseline" и нулём измеренных
корзин всегда. Две независимые причины, и лечение одной из них ничего
не давало.

1. state_store наполняет квоты через quota_service.get_snapshot, который
   читает кэш и при промахе отдаёт пустую заглушку, живой опрос НЕ
   запуская. Кэш никто не грел: в десктопе это делал
   _refresh_quotas_on_startup, в вебе аналога не было. Штатный
   планировщик службы не спасает — его цикл сначала спит интервал
   (300 с по умолчанию) и только потом опрашивает.

2. HubStateStore.get_snapshot() возвращает КЭШИРОВАННЫЙ снапшот и
   пересобирает его только при первом вызове. Даже после прогрева квот
   ответ оставался прежним. В десктопе пересбор делал _refresh_data.

Добавлен фоновый цикл: прогрев квот при старте, затем пересбор снапшота
каждые 30 секунд. Порядок важен — снапшот, собранный до прогрева,
зафиксировал бы пустые корзины.

Проверено исполнением: квоты появляются через ~10 секунд после старта,
24 измеренных корзины, source=provider_api, ag-w2 Gemini неделя 80.5% —
совпадает с прямым опросом провайдера.

Регрессия закрыта tests/test_web_snapshot_freshness.py, включая проверку
порядка «прогрев перед пересбором».

Тесты: 327 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:32:26 +07:00

81 lines
3.3 KiB
Python
Raw Permalink 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.

"""Веб-сервер обязан прогревать квоты и пересобирать снапшот.
Дефект, ради которого написан файл: /api/snapshot отдавал квоты с
source="baseline" и нулём измеренных корзин ВСЕГДА. Две независимые
причины, и лечение только одной из них ничего не давало:
1. state_store наполняет квоты через quota_service.get_snapshot, который
читает кэш и при промахе отдаёт пустую заглушку, живой опрос не
запуская. Кэш никто не грел: в десктопе это делал
_refresh_quotas_on_startup, в вебе аналога не было.
2. HubStateStore.get_snapshot() возвращает кэшированный снапшот и
пересобирает его только при первом вызове. Даже после прогрева квот
ответ оставался прежним.
"""
from __future__ import annotations
import pytest
pytest.importorskip("fastapi")
def test_server_declares_startup_refresh():
"""Фоновое обновление должно быть подключено к старту приложения."""
from antigravity_provider.router.web import server
assert hasattr(server, "_background_refresh_loop"), (
"нет фонового цикла: квоты останутся пустыми навсегда"
)
handlers = [
getattr(h, "__name__", "") for h in server.app.router.on_startup
]
assert any("refresh" in name for name in handlers), (
f"обработчик старта не зарегистрирован, найдено: {handlers}"
)
def test_refresh_loop_warms_quota_cache_then_rebuilds_snapshot(monkeypatch):
"""Прогрев квот обязан предшествовать пересбору снапшота.
Порядок важен: снапшот, собранный до прогрева, зафиксирует пустые
корзины, и следующий пересбор случится только через интервал.
"""
from antigravity_provider.router.web import server
order: list[str] = []
class _Quota:
@staticmethod
def get():
return _Quota()
def fetch_all_configured(self, force: bool = False):
order.append("quota_fetch")
return {}
def start_background_scheduler(self):
return None
class _Store:
@staticmethod
def get():
return _Store()
def refresh(self, force_scan: bool = True):
order.append("snapshot_refresh")
raise KeyboardInterrupt # прерываем бесконечный цикл
import antigravity_provider.router.quota_collector as qc
monkeypatch.setattr(qc, "AccountQuotaService", _Quota)
monkeypatch.setattr(server, "HubStateStore", _Store)
with pytest.raises(KeyboardInterrupt):
server._background_refresh_loop()
assert order == ["quota_fetch", "snapshot_refresh"], (
f"нарушен порядок прогрева и пересбора: {order}"
)