From 0ad946eccd977ff2499f7c0a370271a543c1884d Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 1 Sep 2026 13:23:55 +0700 Subject: [PATCH] =?UTF-8?q?fix(accounts):=20=D0=BC=D0=B0=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D1=80=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BC=D0=B8=D1=80=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=88=D0=B0=D0=B3=D0=B5=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Владелец видел «сохранение аккаунта и запуск проверки» и ждал. Зависанием это не было: действие честно дожидалось проверки у провайдера. Для Antigravity она идёт через CLI и в худшем случае складывается из 90 с на захват замка профиля, 65 на каталог моделей и 90 на пробный вызов — около четырёх минут молчания при обещанной в интерфейсе «минуте на этап». Сохранение учётных данных и назначение роли занимают миллисекунды. Теперь действие возвращается сразу, а опрос провайдера ставится в фон; карточка обновляется, когда он закончится, — снапшот и так опрашивается по таймеру. Провайдеры с ключом поведения не меняют: их подключение проверяется предварительной проверкой до сохранения и возвращается сразу, как требует A54. Если фоновая служба не работает, проверка по-прежнему выполняется на месте — иначе результата не будет вовсе. Надпись в мастере исправлена: обещание «до минуты на этап» не соответствовало действительности. 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/action_handler.py | 22 ++++- .../router/web/static/app.js | 5 +- tests/test_add_account_does_not_block.py | 77 ++++++++++++++++++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/test_add_account_does_not_block.py diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index f4f4521..eb017a0 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -18,7 +18,7 @@ namespace HermesHubSetup // Подставляется сборщиком из фактического git-коммита. Раньше здесь // жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из // какого кода собран установщик. - public const string BuildCommit = "9641957"; + public const string BuildCommit = "001cd1f"; 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 cd5b237ed3cbfd03cb611f8f4a070ecb2f3903a6..38f6ade59e46698fa3791abc676b6a3f2bcff834 100644 GIT binary patch delta 35 rcmZoLXfT-2!K9bDvFj9%fcGki^*8G;yEkt%>Ga*-%(gj&cMcZ-2BQwK delta 35 tcmV+;0Nnq8Fn}!< jhHjRdS14Xk7Ff%rV|n(Afzy7T2=26&z@M8tTq}40)zul5 delta 79 zcmV-V0I>gnqyd1W0g#9Sd1JAP`XD4RMKn1!IYKurMKv@-Ei^SkI4w9uLNF~wGeSf- lLNzrsGc-7}s3FiJ5VvVb9!%x9MUJrJy^B>tNVAAVcntn>8!`X@ diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 1c1e794..c8b3961 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -900,7 +900,27 @@ class ActionExecutor: if validation: AccountProbeService.get().record_validation(prov_norm, slot, validation) return {'ok': True, 'message': validation['message'], 'data': {'profile_id': slot, 'models': validation['data']['models']}} - result = AccountProbeService.get().check_now(prov_norm, slot) + # Проверку у провайдера не ждём в самом действии. Для + # Antigravity она идёт через CLI и в худшем случае занимает до + # 90 с на захват замка, до 65 на каталог моделей и до 90 на + # пробный вызов — около четырёх минут молчания при обещанной + # «минуте на этап». Владелец видит это как зависший мастер. + # + # Провайдеры с ключом сюда не попадают: их подключение уже + # проверено предварительной проверкой выше и возвращается + # сразу, как того требует A54. + probe = AccountProbeService.get() + started = probe.schedule(prov_norm, slot, force=True) + if started or probe.state(slot).get('state') == 'checking': + return { + 'ok': True, + 'message': 'Аккаунт подключён. Проверка у провайдера идёт в фоне, ' + 'результат появится в карточке.', + 'data': {'profile_id': slot, 'check': 'running'}, + } + # Фоновая служба не работает — проверяем здесь, иначе результата + # не будет вовсе. Ручная проверка обязана работать и без неё. + result = probe.check_now(prov_norm, slot) result.setdefault('data', {})['profile_id'] = slot return result except Exception as e: diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 86199a9..ca16a8d 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -3251,7 +3251,10 @@ async function finishAddAccount(providerId) { const feedbackArea = document.getElementById('modal-feedback-area'); if (feedbackArea) { - feedbackArea.innerHTML = ``; + // Обещать «до минуты на этап» было неправдой: проверка Antigravity через + // CLI занимала до четырёх минут, и мастер выглядел зависшим. Теперь + // действие возвращается сразу, а проверка идёт в фоне. + feedbackArea.innerHTML = ``; } const payload = { diff --git a/tests/test_add_account_does_not_block.py b/tests/test_add_account_does_not_block.py new file mode 100644 index 0000000..3984fc9 --- /dev/null +++ b/tests/test_add_account_does_not_block.py @@ -0,0 +1,77 @@ +"""Подключение аккаунта не должно ждать опроса провайдера. + +Мастер владельца замирал на шаге 3 с надписью «сохранение аккаунта и запуск +проверки». Зависанием это не было: действие честно ждало проверку Antigravity +через CLI — до 90 с на захват замка профиля, до 65 на каталог моделей и до 90 +на пробный вызов. Около четырёх минут молчания при обещанной «минуте на этап». + +Сохранение и назначение роли занимают миллисекунды. Опрос провайдера идёт в +фоне, а карточка обновляется, когда он закончится. +""" +from __future__ import annotations + +import time +from unittest.mock import patch + +from antigravity_provider.router.account_probe_service import AccountProbeService +from antigravity_provider.router.action_handler import ActionExecutor +from antigravity_provider.router.auto_assigner import AutoAssigner +from antigravity_provider.router.profile_manager import ProfileAuthManager + + +def _connect_authenticated_antigravity(): + with patch.object(ProfileAuthManager, "get_profile_status", return_value={"authenticated": True}), \ + patch.object(ProfileAuthManager, "load_profile_auth", return_value={"email": "x@gmail.com"}), \ + patch.object(ProfileAuthManager, "save_profile_auth", return_value="/tmp/auth.json"), \ + patch.object(AutoAssigner, "ensure_profile_definition", return_value=(True, "ok")), \ + patch.object(AutoAssigner, "assign_profile_to_role", return_value=(True, "ok")): + return ActionExecutor.execute( + "add_account", + {"provider": "antigravity", "profile_id": "ag-1", "target_role": "orchestrator"}, + ) + + +def test_connect_returns_without_waiting_for_provider(): + slow_call = [] + + def _slow_check_now(self, provider, profile_id, models_only=False): + slow_call.append(profile_id) + time.sleep(5) # изображаем опрос провайдера + return {"ok": True, "message": "проверено"} + + with patch.object(AccountProbeService, "schedule", return_value=True) as scheduled, \ + patch.object(AccountProbeService, "check_now", _slow_check_now): + started = time.monotonic() + res = _connect_authenticated_antigravity() + elapsed = time.monotonic() - started + + assert res["ok"], res + assert not slow_call, "действие не должно ждать опрос провайдера" + assert scheduled.called, "проверка обязана быть поставлена в фон" + assert elapsed < 2, f"ответ занял {elapsed:.1f} с" + assert res["data"]["check"] == "running" + + +def test_result_reaches_owner_when_background_service_is_down(): + """Служба не работает — проверяем здесь, иначе результата не будет вовсе.""" + with patch.object(AccountProbeService, "schedule", return_value=False), \ + patch.object(AccountProbeService, "state", return_value={"state": "never_checked"}), \ + patch.object( + AccountProbeService, + "check_now", + return_value={"ok": False, "message": "провайдер отказал", "data": {}}, + ) as checked: + res = _connect_authenticated_antigravity() + + assert checked.called, "без фоновой службы проверка выполняется на месте" + assert res["message"] == "провайдер отказал" + + +def test_check_already_running_is_not_awaited(): + with patch.object(AccountProbeService, "schedule", return_value=False), \ + patch.object(AccountProbeService, "state", return_value={"state": "checking"}), \ + patch.object(AccountProbeService, "check_now") as checked: + res = _connect_authenticated_antigravity() + + assert not checked.called, "уже идущую проверку не ждём и не дублируем" + assert res["ok"] and res["data"]["check"] == "running"