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:
parent
98b6e68617
commit
a40bcac3c3
3 changed files with 77 additions and 1 deletions
|
|
@ -248,6 +248,17 @@ class ActionExecutor:
|
||||||
"""
|
"""
|
||||||
pid = data.get('profile_id', '')
|
pid = data.get('profile_id', '')
|
||||||
prov = data.get('provider', '')
|
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 был готов давно, но
|
# Device-flow для Grok и Codex через веб. Backend был готов давно, но
|
||||||
# наружу не выведен: веб-мастер показывал заглушку «не реализовано», и
|
# наружу не выведен: веб-мастер показывал заглушку «не реализовано», и
|
||||||
|
|
|
||||||
|
|
@ -1371,7 +1371,7 @@ function openAccountDetailsModal(profileId) {
|
||||||
elements.modalFooter.innerHTML = `
|
elements.modalFooter.innerHTML = `
|
||||||
<button class="btn btn-secondary" onclick="handleTestProfile('${escapeHtml(profileId)}')">⚡ Проверить подключение</button>
|
<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('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>
|
<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>`;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
52
tests/test_delete_without_provider.py
Normal file
52
tests/test_delete_without_provider.py
Normal 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, "экран не обновляется после удаления"
|
||||||
Loading…
Reference in a new issue