fix(accounts): мастер подключения замирал на шаге 3
Владелец видел «сохранение аккаунта и запуск проверки» и ждал. Зависанием это не было: действие честно дожидалось проверки у провайдера. Для Antigravity она идёт через CLI и в худшем случае складывается из 90 с на захват замка профиля, 65 на каталог моделей и 90 на пробный вызов — около четырёх минут молчания при обещанной в интерфейсе «минуте на этап». Сохранение учётных данных и назначение роли занимают миллисекунды. Теперь действие возвращается сразу, а опрос провайдера ставится в фон; карточка обновляется, когда он закончится, — снапшот и так опрашивается по таймеру. Провайдеры с ключом поведения не меняют: их подключение проверяется предварительной проверкой до сохранения и возвращается сразу, как требует A54. Если фоновая служба не работает, проверка по-прежнему выполняется на месте — иначе результата не будет вовсе. Надпись в мастере исправлена: обещание «до минуты на этап» не соответствовало действительности. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
001cd1f91d
commit
0ad946eccd
6 changed files with 103 additions and 3 deletions
|
|
@ -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.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
77
tests/test_add_account_does_not_block.py
Normal file
77
tests/test_add_account_does_not_block.py
Normal 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"
|
||||
Loading…
Reference in a new issue