fix(accounts): мастер подключения замирал на шаге 3

Владелец видел «сохранение аккаунта и запуск проверки» и ждал. Зависанием это
не было: действие честно дожидалось проверки у провайдера. Для Antigravity она
идёт через CLI и в худшем случае складывается из 90 с на захват замка профиля,
65 на каталог моделей и 90 на пробный вызов — около четырёх минут молчания при
обещанной в интерфейсе «минуте на этап».

Сохранение учётных данных и назначение роли занимают миллисекунды. Теперь
действие возвращается сразу, а опрос провайдера ставится в фон; карточка
обновляется, когда он закончится, — снапшот и так опрашивается по таймеру.

Провайдеры с ключом поведения не меняют: их подключение проверяется
предварительной проверкой до сохранения и возвращается сразу, как требует A54.
Если фоновая служба не работает, проверка по-прежнему выполняется на месте —
иначе результата не будет вовсе.

Надпись в мастере исправлена: обещание «до минуты на этап» не соответствовало
действительности.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-09-01 13:23:55 +07:00
parent 001cd1f91d
commit 0ad946eccd
6 changed files with 103 additions and 3 deletions

View file

@ -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";

Binary file not shown.

Binary file not shown.

View file

@ -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:

View file

@ -3251,7 +3251,10 @@ async function finishAddAccount(providerId) {
const feedbackArea = document.getElementById('modal-feedback-area');
if (feedbackArea) {
feedbackArea.innerHTML = `<div class="modal-feedback info">⏳ ${escapeHtml(providerId)}: сохранение аккаунта и запуск проверки. Опрос провайдера может занять до минуты на этап.</div>`;
// Обещать «до минуты на этап» было неправдой: проверка Antigravity через
// CLI занимала до четырёх минут, и мастер выглядел зависшим. Теперь
// действие возвращается сразу, а проверка идёт в фоне.
feedbackArea.innerHTML = `<div class="modal-feedback info">⏳ ${escapeHtml(providerId)}: сохраняем аккаунт…</div>`;
}
const payload = {

View file

@ -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"