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 cd5b237..38f6ade 100644
Binary files a/launcher/HermesHub.exe and b/launcher/HermesHub.exe differ
diff --git a/launcher/HermesHubWeb.exe b/launcher/HermesHubWeb.exe
index c395499..b4a1cff 100644
Binary files a/launcher/HermesHubWeb.exe and b/launcher/HermesHubWeb.exe differ
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 = `
⏳ ${escapeHtml(providerId)}: сохранение аккаунта и запуск проверки. Опрос провайдера может занять до минуты на этап.
`;
+ // Обещать «до минуты на этап» было неправдой: проверка Antigravity через
+ // CLI занимала до четырёх минут, и мастер выглядел зависшим. Теперь
+ // действие возвращается сразу, а проверка идёт в фоне.
+ feedbackArea.innerHTML = `⏳ ${escapeHtml(providerId)}: сохраняем аккаунт…
`;
}
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"