Найдено при проверке Grok на живых данных владельца. Вызов падал с
AttributeError: 'str' object has no attribute 'get' — grok_adapter.py:103.
Поле error провайдеры отдают то объектом {"message": ...}, то строкой.
Код безусловно звал .get у результата, и на строковой форме ОБРАБОТЧИК
ОШИБОК ПАДАЛ САМ: сбой происходил ровно там, где обрабатывался другой
сбой, маршрутизация обрывалась вместо перехода к резерву, а настоящая
причина терялась.
После правки причина видна: Grok API Error (403): The OAuth2 access token
could not be validated. То есть у Grok просто протух токен, а выглядело
как поломка кода.
Та же конструкция стояла ещё в пяти адаптерах: claude, codex, opencode,
deepseek, local. Разбор вынесен в base_adapter.extract_api_error_message,
все шесть переведены на него.
Тест проверяет обе формы ответа и отдельно следит, чтобы копии хрупкой
конструкции не вернулись.
Тесты: 411 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""Обработчик ошибок не должен падать сам.
|
||
|
||
Найдено на живом Grok владельца: поле ``error`` пришло строкой, а код
|
||
безусловно звал у результата ``.get``. Обработчик падал с AttributeError,
|
||
настоящая причина (403, токен не проходит проверку) терялась, и
|
||
маршрутизация обрывалась вместо перехода к резерву — сбой ровно там, где
|
||
обрабатывался другой сбой.
|
||
|
||
Та же конструкция стояла ещё в пяти адаптерах.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
import pytest
|
||
|
||
from antigravity_provider.router.adapters.base_adapter import extract_api_error_message
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"raw,expected_fragment",
|
||
[
|
||
(json.dumps({"error": {"message": "quota exceeded"}}), "quota exceeded"),
|
||
(json.dumps({"error": "The OAuth2 access token could not be validated."}), "OAuth2"),
|
||
# Без поля error возвращается сырой ответ целиком — это правильно:
|
||
# лучше отдать всё, что прислал провайдер, чем потерять причину.
|
||
(json.dumps({"detail": "boom"}), "boom"),
|
||
("<html>502 Bad Gateway</html>", "502"),
|
||
("", ""),
|
||
],
|
||
)
|
||
def test_error_message_extracted_from_any_shape(raw, expected_fragment):
|
||
"""Строковая и объектная формы разбираются одинаково спокойно."""
|
||
assert expected_fragment in extract_api_error_message(raw)
|
||
|
||
|
||
def test_string_error_does_not_raise():
|
||
"""Именно эта форма роняла обработчик у Grok."""
|
||
result = extract_api_error_message(json.dumps({"error": "plain string"}))
|
||
assert result == "plain string"
|
||
|
||
|
||
def test_all_adapters_use_shared_parser():
|
||
"""Копии хрупкой конструкции не должны вернуться."""
|
||
import pathlib
|
||
|
||
adapters = pathlib.Path("src/antigravity_provider/router/adapters")
|
||
offenders = [
|
||
f.name
|
||
for f in adapters.glob("*_adapter.py")
|
||
if '.get("error", {}).get(' in f.read_text(encoding="utf-8")
|
||
]
|
||
assert not offenders, f"хрупкий разбор ошибки вернулся в: {offenders}"
|