hermes-hub/tests/test_grok_device_flow.py
Hermes Team 50e4de5230 fix(grok): опрос device-flow не различал отказ, просрочку и ожидание
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен —
проверено: провайдер возвращает настоящую сессию, адрес
accounts.x.ai/oauth2/device и код.

Дефект в цикле опроса: коды 400, 403 и 404 скопом считались
«авторизация ещё не подтверждена» и опрос молча продолжался. Но в
device-flow сервер сообщает РАЗНЫЕ вещи одним кодом 400, различая их
полем error в теле: authorization_pending, slow_down, access_denied,
expired_token.

Следствие: отказ пользователя и просроченный код выглядели как ожидание.
Мастер показывал «Ожидание подтверждения...» до самого таймаута и не
говорил, что подтверждение уже отклонено или код давно истёк.

Теперь каждый исход обрабатывается по существу: ожидание продолжает
опрос, slow_down увеличивает интервал, отказ и просрочка прекращают его
с внятным сообщением.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:11:54 +07:00

52 lines
2.8 KiB
Python
Raw Permalink 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.

"""Опрос device-flow обязан различать исходы.
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен —
провайдер возвращает настоящие адрес и код.
Дефект в цикле опроса: коды 400, 403 и 404 скопом считались «ещё не
подтверждено». Но в device-flow сервер сообщает РАЗНЫЕ вещи одним кодом
400, различая их полем error в теле: authorization_pending, slow_down,
access_denied, expired_token. Отказ пользователя и просроченный код
выглядели как ожидание — мастер крутил «Ожидание подтверждения...» до
таймаута и не говорил правду.
"""
from __future__ import annotations
import io
import json
import pathlib
import urllib.error
def _error(reason: str, code: int = 400) -> urllib.error.HTTPError:
body = json.dumps({"error": reason}).encode("utf-8")
return urllib.error.HTTPError("https://x", code, "Bad Request", {}, io.BytesIO(body))
def test_poll_loop_handles_each_outcome_distinctly():
src = pathlib.Path("src/antigravity_provider/router/grok_oauth.py").read_text(encoding="utf-8")
assert "authorization_pending" in src, "ожидание больше не распознаётся явно"
assert "access_denied" in src, "отказ пользователя не отличается от ожидания"
assert "expired_token" in src, "просроченный код не отличается от ожидания"
assert "slow_down" in src, "требование сбавить темп игнорируется"
# Прежняя конструкция считала все три кода одним исходом.
assert "if http_err.code in (400, 403, 404):\n # Authorization pending" not in src
def test_error_body_is_readable():
"""Тело ответа должно разбираться — на нём и держится различение."""
err = _error("access_denied")
parsed = json.loads(err.read().decode("utf-8"))
assert parsed["error"] == "access_denied"
def test_denied_and_expired_are_terminal():
"""Оба исхода обязаны прекращать опрос, а не продолжать его."""
src = pathlib.Path("src/antigravity_provider/router/grok_oauth.py").read_text(encoding="utf-8")
tail = src.split("if reason == \"access_denied\":", 1)[1][:400]
assert "break" in tail, "отказ не прекращает опрос"
expired = src.split("if reason == \"expired_token\":", 1)[1][:400]
assert "break" in expired, "просрочка не прекращает опрос"