fix(accounts): отказ проверки переживал более свежий каталог моделей
В карточке владельца рядом стояли «Проверен: не работает — Please sign in» и «Получено 11 моделей · 13:25:53». Источники разные: красная строка берётся из состояния проверки, список — из кэша каталога, и обновляются они независимо. Каталог был получен позже отказа, то есть провайдер с тех пор ответил, а карточка продолжала утверждать обратное. Теперь, если каталог получен без ошибки и позже неудачной проверки, вердикт показывается как устаревший с предложением проверить заново. Объявлять аккаунт рабочим не за что — проверка после этого не выполнялась, и выдумывать её результат нельзя. Отдельно установлено: поле commit в /api/health читается из файла на диске при каждом запросе, поэтому устаревший процесс рапортует свежий коммит. Различать сборки по нему нельзя. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0ad946eccd
commit
f188a18136
5 changed files with 138 additions and 2 deletions
|
|
@ -18,7 +18,7 @@ namespace HermesHubSetup
|
||||||
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
|
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
|
||||||
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
||||||
// какого кода собран установщик.
|
// какого кода собран установщик.
|
||||||
public const string BuildCommit = "001cd1f";
|
public const string BuildCommit = "0ad946e";
|
||||||
public const string MIN_HERMES_VERSION = "0.20.0";
|
public const string MIN_HERMES_VERSION = "0.20.0";
|
||||||
public const string MAX_TESTED_HERMES = "0.20.4";
|
public const string MAX_TESTED_HERMES = "0.20.4";
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -543,6 +543,24 @@ class UnifiedHealthService:
|
||||||
if check.get("state") == "checking":
|
if check.get("state") == "checking":
|
||||||
health_state, health_lbl = "checking", "Проверяется…"
|
health_state, health_lbl = "checking", "Проверяется…"
|
||||||
elif check.get("state") == "failed":
|
elif check.get("state") == "failed":
|
||||||
|
# Вердикт проверки и каталог моделей обновляются
|
||||||
|
# независимо, и в карточке владельца они оказались
|
||||||
|
# рядом: «не работает — Please sign in» и тут же
|
||||||
|
# «Получено 11 моделей» с более поздним временем.
|
||||||
|
# Утверждать отказ, когда каталог получен ПОЗЖЕ него,
|
||||||
|
# нельзя: провайдер с тех пор ответил. Но и объявлять
|
||||||
|
# аккаунт рабочим не за что — проверка после этого не
|
||||||
|
# выполнялась. Честный ответ — что вердикт устарел.
|
||||||
|
checked_at = check.get("checked_at") or 0
|
||||||
|
catalog_at = model_meta.get("discovered_at") or 0
|
||||||
|
if not model_meta.get("error") and catalog_at > checked_at:
|
||||||
|
when = datetime.datetime.fromtimestamp(catalog_at).strftime("%H:%M:%S")
|
||||||
|
health_state = STATUS_NOT_TESTED
|
||||||
|
health_lbl = (
|
||||||
|
f"Проверка устарела: каталог моделей получен позже ({when}). "
|
||||||
|
"Нажмите «Проверить подключение»."
|
||||||
|
)
|
||||||
|
else:
|
||||||
health_state, health_lbl = STATUS_UNHEALTHY, "Проверен: не работает — " + check.get("message", "Причина Н/Д")
|
health_state, health_lbl = STATUS_UNHEALTHY, "Проверен: не работает — " + check.get("message", "Причина Н/Д")
|
||||||
elif check.get("state") == "working" and health_state in (STATUS_NOT_TESTED, STATUS_HEALTHY):
|
elif check.get("state") == "working" and health_state in (STATUS_NOT_TESTED, STATUS_HEALTHY):
|
||||||
health_state, health_lbl = STATUS_HEALTHY, "Проверен: работает"
|
health_state, health_lbl = STATUS_HEALTHY, "Проверен: работает"
|
||||||
|
|
|
||||||
118
tests/test_stale_verdict_vs_catalog.py
Normal file
118
tests/test_stale_verdict_vs_catalog.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""Вердикт проверки не должен переживать более свежий каталог моделей.
|
||||||
|
|
||||||
|
В карточке владельца рядом стояли «Проверен: не работает — Please sign in» и
|
||||||
|
«Получено 11 моделей · 13:25:53». Источники разные и обновляются независимо:
|
||||||
|
красная строка берётся из состояния проверки, список — из кэша каталога.
|
||||||
|
Каталог был получен ПОЗЖЕ отказа, то есть провайдер с тех пор ответил.
|
||||||
|
|
||||||
|
Утверждать отказ в такой ситуации нельзя. Объявлять аккаунт рабочим — тоже
|
||||||
|
не за что: проверка после этого не выполнялась. Честный ответ один: вердикт
|
||||||
|
устарел, нужна новая проверка.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from antigravity_provider.router.account_probe_service import AccountProbeService
|
||||||
|
from antigravity_provider.router.model_discovery_service import ModelDiscoveryService
|
||||||
|
from antigravity_provider.router.profile_manager import ProfileAuthManager
|
||||||
|
from antigravity_provider.router.router_config import (
|
||||||
|
RouterConfig,
|
||||||
|
RouterProfileConfig,
|
||||||
|
save_router_config,
|
||||||
|
)
|
||||||
|
from antigravity_provider.router.unified_health import (
|
||||||
|
STATUS_NOT_TESTED,
|
||||||
|
STATUS_UNHEALTHY,
|
||||||
|
UnifiedHealthService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare(failed_at: float, catalog_at: float | None, catalog_error=None):
|
||||||
|
cfg = RouterConfig()
|
||||||
|
cfg.profiles["ag-5"] = RouterProfileConfig(
|
||||||
|
profile_id="ag-5",
|
||||||
|
provider="antigravity",
|
||||||
|
account_id="ag-5",
|
||||||
|
preferred_models=[],
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
save_router_config(cfg)
|
||||||
|
ProfileAuthManager.save_profile_auth(
|
||||||
|
"antigravity",
|
||||||
|
"ag-5",
|
||||||
|
{
|
||||||
|
# Запись входа Antigravity по праву отказывает без токена доступа,
|
||||||
|
# поэтому в проверке он должен быть настоящим по форме.
|
||||||
|
"token": {
|
||||||
|
"access_token": "ya29.TEST",
|
||||||
|
"refresh_token": "1//TEST",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expiry_date": int((time.time() + 3600) * 1000),
|
||||||
|
},
|
||||||
|
"email": "owner@gmail.com",
|
||||||
|
"auth_method": "oauth",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
probe = AccountProbeService.get()
|
||||||
|
with probe._lock:
|
||||||
|
probe._states["ag-5"] = {
|
||||||
|
"state": "failed",
|
||||||
|
"provider": "antigravity",
|
||||||
|
"checked_at": failed_at,
|
||||||
|
"message": "agy models: код 1. Please sign in to view available models.",
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery = ModelDiscoveryService.get()
|
||||||
|
with discovery._cache_lock:
|
||||||
|
discovery._cache["antigravity:ag-5"] = {
|
||||||
|
"models": ["gemini-3.1-pro-high", "claude-sonnet-4-6"],
|
||||||
|
"discovered_at": catalog_at,
|
||||||
|
"error": catalog_error,
|
||||||
|
}
|
||||||
|
return UnifiedHealthService.get()
|
||||||
|
|
||||||
|
|
||||||
|
def _view(service):
|
||||||
|
profiles = service.scan_all(force=True)
|
||||||
|
return next(p for p in profiles["antigravity"] if p.profile_id == "ag-5")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_newer_catalog_makes_the_failed_verdict_stale(tmp_path):
|
||||||
|
now = time.time()
|
||||||
|
view = _view(_prepare(failed_at=now - 60, catalog_at=now - 20))
|
||||||
|
|
||||||
|
assert view.health_state == STATUS_NOT_TESTED
|
||||||
|
assert "устарела" in view.health_label_ru
|
||||||
|
assert "Please sign in" not in view.health_label_ru
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_failure_stands_when_it_is_the_newer_fact(tmp_path):
|
||||||
|
now = time.time()
|
||||||
|
view = _view(_prepare(failed_at=now - 10, catalog_at=now - 300))
|
||||||
|
|
||||||
|
assert view.health_state == STATUS_UNHEALTHY
|
||||||
|
assert "Please sign in" in view.health_label_ru
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_failure_stands_when_catalog_itself_errored(tmp_path):
|
||||||
|
now = time.time()
|
||||||
|
view = _view(
|
||||||
|
_prepare(failed_at=now - 60, catalog_at=now - 20, catalog_error="Сервер отказал")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert view.health_state == STATUS_UNHEALTHY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_failure_stands_when_catalog_was_never_obtained(tmp_path):
|
||||||
|
now = time.time()
|
||||||
|
view = _view(_prepare(failed_at=now - 60, catalog_at=None))
|
||||||
|
|
||||||
|
assert view.health_state == STATUS_UNHEALTHY
|
||||||
Loading…
Reference in a new issue