fix(web): удаление аккаунта не работало из интерфейса

Владелец: «удалить так и не могу ненужный». Backend был починен в
d755a07, но кнопка по-прежнему не работала.

Причина в клиенте: он отправлял только profile_id, без provider. Сервер
не знал, в каком каталоге искать auth.json, строил неверный путь и снова
отвечал «удалять нечего».

Идентификатор профиля однозначен, поэтому провайдер теперь берётся из
конфигурации, когда его не передали. Действие работает независимо от
того, кто его вызвал.

Заодно в клиенте: подтверждение перед необратимым удалением и обновление
экрана после — раньше карточка оставалась в прежнем виде, и было
непонятно, сработало ли.

Проверено через веб-API без параметра provider: до — авторизован,
после — нет.

Тесты: 426 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-24 20:19:58 +07:00
parent 98b6e68617
commit a40bcac3c3
3 changed files with 77 additions and 1 deletions

View file

@ -248,6 +248,17 @@ class ActionExecutor:
"""
pid = data.get('profile_id', '')
prov = data.get('provider', '')
# Провайдер часто не передают: веб-клиент шлёт только profile_id, и
# раньше удаление из-за этого искало файл не в том каталоге и отвечало
# «удалять нечего». Идентификатор профиля однозначен — берём провайдера
# из конфигурации, чтобы работал любой вызывающий.
if not prov and pid:
try:
_pcfg = load_router_config().get_profile(pid)
if _pcfg:
prov = _pcfg.provider
except Exception:
pass
# Device-flow для Grok и Codex через веб. Backend был готов давно, но
# наружу не выведен: веб-мастер показывал заглушку «не реализовано», и

View file

@ -1371,7 +1371,7 @@ function openAccountDetailsModal(profileId) {
elements.modalFooter.innerHTML = `
<button class="btn btn-secondary" onclick="handleTestProfile('${escapeHtml(profileId)}')"> Проверить подключение</button>
<button class="btn btn-secondary" onclick="executeAction('set_main', { profile_id: '${escapeHtml(profileId)}' })"> Сделать основным</button>
<button class="btn btn-secondary" onclick="executeAction('delete_credentials', { profile_id: '${escapeHtml(profileId)}' })">Удалить ключ</button>
<button class="btn btn-secondary" onclick="handleDeleteCredentials('${escapeHtml(profileId)}')">Удалить ключ</button>
<button class="btn btn-primary" onclick="closeModal()">Закрыть</button>
`;
@ -2060,3 +2060,16 @@ async function pollDeviceAuth(providerId) {
status.innerHTML = `<span style="color:var(--status-error); font-weight:600;">${escapeHtml(res.message || 'Авторизация не завершена')}</span>`;
}
}
async function handleDeleteCredentials(profileId) {
// Подтверждение обязательно: действие необратимо, аккаунт придётся
// подключать заново.
if (!confirm(`Удалить учётные данные профиля ${profileId}? Аккаунт придётся подключить заново.`)) {
return;
}
const res = await executeAction('delete_credentials', { profile_id: profileId });
if (res && res.ok) {
closeModal();
fetchSnapshot();
}
}

View file

@ -0,0 +1,52 @@
"""Удаление должно работать, когда провайдер не передан.
Владелец: «удалить так и не могу ненужный». Backend был уже починен, но
веб-клиент отправлял только profile_id, без provider. Сервер не знал, в
каком каталоге искать файл, и снова отвечал «удалять нечего» кнопка
по-прежнему выглядела неработающей.
Идентификатор профиля однозначен, поэтому провайдер берётся из
конфигурации, и действие работает независимо от того, кто его вызвал.
"""
from __future__ import annotations
import json
import pytest
from antigravity_provider.router.action_handler import ActionExecutor
from antigravity_provider.router.profile_manager import get_profile_auth_path
@pytest.mark.parametrize("profile_id,provider", [
("grok-worker-2", "grok"),
("ag-spare-2", "antigravity"),
("opengo-2", "opencode-go"),
])
def test_delete_works_without_provider_argument(profile_id, provider, monkeypatch, tmp_path):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
auth_path = get_profile_auth_path(provider, profile_id)
auth_path.parent.mkdir(parents=True, exist_ok=True)
auth_path.write_text(json.dumps({"api_key": "x"}), encoding="utf-8")
# Ровно то, что шлёт веб-клиент: без provider.
result = ActionExecutor().execute("delete_credentials", {"profile_id": profile_id})
assert result.get("ok"), f"{profile_id}: {result.get('message')}"
assert not auth_path.is_file(), f"{profile_id}: файл остался, а действие отчиталось об успехе"
def test_client_confirms_before_deleting():
"""Необратимое действие обязано спрашивать подтверждение."""
import pathlib
app_js = pathlib.Path(
"src/antigravity_provider/router/web/static/app.js"
).read_text(encoding="utf-8")
assert "handleDeleteCredentials" in app_js
block = app_js.split("function handleDeleteCredentials", 1)[1][:600]
assert "confirm(" in block, "удаление выполняется без подтверждения"
assert "fetchSnapshot" in block, "экран не обновляется после удаления"