hermes-hub/tests/test_adapter_error_parsing.py
Hermes Team ad08c17d83 fix(adapters): обработчик ошибок падал сам, скрывая настоящую причину
Найдено при проверке 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>
2026-08-24 14:54:39 +07:00

54 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Обработчик ошибок не должен падать сам.
Найдено на живом 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}"