From f188a181366e23ac795bf446f25f0f1a8afb33f0 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 1 Sep 2026 14:03:56 +0700 Subject: [PATCH] =?UTF-8?q?fix(accounts):=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BB=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B5=20=D1=81=D0=B2=D0=B5=D0=B6=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В карточке владельца рядом стояли «Проверен: не работает — Please sign in» и «Получено 11 моделей · 13:25:53». Источники разные: красная строка берётся из состояния проверки, список — из кэша каталога, и обновляются они независимо. Каталог был получен позже отказа, то есть провайдер с тех пор ответил, а карточка продолжала утверждать обратное. Теперь, если каталог получен без ошибки и позже неудачной проверки, вердикт показывается как устаревший с предложением проверить заново. Объявлять аккаунт рабочим не за что — проверка после этого не выполнялась, и выдумывать её результат нельзя. Отдельно установлено: поле commit в /api/health читается из файла на диске при каждом запросе, поэтому устаревший процесс рапортует свежий коммит. Различать сборки по нему нельзя. Co-Authored-By: Claude Opus 5 --- installer/HermesHubSetup.cs | 2 +- launcher/HermesHub.exe | Bin 6144 -> 6144 bytes launcher/HermesHubWeb.exe | Bin 20992 -> 20992 bytes .../router/unified_health.py | 20 ++- tests/test_stale_verdict_vs_catalog.py | 118 ++++++++++++++++++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 tests/test_stale_verdict_vs_catalog.py diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index eb017a0..ac8a6bf 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -18,7 +18,7 @@ namespace HermesHubSetup // Подставляется сборщиком из фактического git-коммита. Раньше здесь // жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из // какого кода собран установщик. - public const string BuildCommit = "001cd1f"; + public const string BuildCommit = "0ad946e"; public const string MIN_HERMES_VERSION = "0.20.0"; public const string MAX_TESTED_HERMES = "0.20.4"; diff --git a/launcher/HermesHub.exe b/launcher/HermesHub.exe index 38f6ade59e46698fa3791abc676b6a3f2bcff834..0d5345926c7ee192c79e9125edc1662902261575 100644 GIT binary patch delta 35 rcmZoLXfT-2!PJ+(vFj9%!0QY9&Kz8|)1j67T)W-x&eY8*ymPn!G6E48 delta 35 rcmZoLXfT-2!K9bDvFj9%fcGki^*8G;yEkt%>Ga*-%(gj&cMcZ-2BQwK diff --git a/launcher/HermesHubWeb.exe b/launcher/HermesHubWeb.exe index b4a1cff7232b7c882d3ba41f450da273a8b456c5..71bd5857e23e02d43d9b943130a4e5051be4fc60 100644 GIT binary patch delta 79 zcmV-V0I>gnqyd1W0g#9Sj&HGw`XD4jHA6u#IXE#bMMgqGEi^?#Gc7_iGe#{iF)=qd lL_|h4GB7r?s3FiJ5P=EG#k=prPP9J(7meRi2D6Aocnl8&8w>yd delta 79 zcmZoz!q~8caY6@^UgpNGUkWP5mPXD7j*c$6ZYItyx+X@(2D(me2F|*s76wjcW)>!< jhHjRdS14Xk7Ff%rV|n(Afzy7T2=26&z@M8tTq}40)zul5 diff --git a/src/antigravity_provider/router/unified_health.py b/src/antigravity_provider/router/unified_health.py index 19fb431..5c764cd 100644 --- a/src/antigravity_provider/router/unified_health.py +++ b/src/antigravity_provider/router/unified_health.py @@ -543,7 +543,25 @@ class UnifiedHealthService: if check.get("state") == "checking": health_state, health_lbl = "checking", "Проверяется…" elif check.get("state") == "failed": - health_state, health_lbl = STATUS_UNHEALTHY, "Проверен: не работает — " + check.get("message", "Причина Н/Д") + # Вердикт проверки и каталог моделей обновляются + # независимо, и в карточке владельца они оказались + # рядом: «не работает — 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", "Причина Н/Д") elif check.get("state") == "working" and health_state in (STATUS_NOT_TESTED, STATUS_HEALTHY): health_state, health_lbl = STATUS_HEALTHY, "Проверен: работает" assigned = role_assignments.get(pid, []) diff --git a/tests/test_stale_verdict_vs_catalog.py b/tests/test_stale_verdict_vs_catalog.py new file mode 100644 index 0000000..29957ea --- /dev/null +++ b/tests/test_stale_verdict_vs_catalog.py @@ -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