agy 2.0 читает учётные данные не из .gemini/oauth_creds.json — это формат
Gemini CLI. Свой токен он берёт из .gemini/antigravity-cli/antigravity-oauth-token
в виде {"auth_method": ..., "token": {...}}. Хаб этот файл никогда не создавал,
поэтому «Авторизация успешно завершена» соседствовала с ответом
«Please sign in to view available models».
Установлено сравнением рабочего профиля владельца с неработающим: оба имели
oauth_creds.json одинакового размера, различие было только в этом файле.
Значение auth_method («consumer») взято из рабочего профиля, а не выведено
из общих соображений.
Срок годности пишется в обоих видах: expiry_date в миллисекундах для формата
Node и expiry строкой RFC3339 для Go-шного oauth2.Token.
Заодно устранён рост номеров профилей: слот выбирался до входа, когда почта
ещё неизвестна, и повторный вход тем же аккаунтом занимал очередной свободный
слот — один аккаунт владельца расползся на ag-2, ag-3, ag-4. Теперь после
опознания почты учётные данные возвращаются в слот, который этот аккаунт уже
занимает. Функция проверки двойников в проекте была, но её никто не вызывал.
Версия поднята до 0.1.3, чтобы отличать сборки на глаз. Проверка версии в
тестах сверяется с исходником, а не с записанным числом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
4 KiB
Python
96 lines
4 KiB
Python
"""agy 2.0 читает вход не из oauth_creds.json.
|
||
|
||
Сравнение рабочего профиля владельца с неработающим показало единственное
|
||
значимое различие: у рабочего есть `.gemini/antigravity-cli/antigravity-oauth-token`,
|
||
у неработающего — только `.gemini/oauth_creds.json`. Оба файла с токенами были
|
||
на месте и одинакового размера, но `agy models` отвечал «Please sign in to view
|
||
available models».
|
||
|
||
Формат подтверждён на машине владельца: верхний уровень — ключи `auth_method`
|
||
и `token`, значение `auth_method` — `consumer`.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
|
||
from antigravity_provider.router.profile_manager import ProfileAuthManager
|
||
|
||
|
||
def _write(tmp_path):
|
||
return ProfileAuthManager.write_agy_oauth_creds(
|
||
tmp_path,
|
||
{
|
||
"access_token": "ya29.TEST-ACCESS",
|
||
"refresh_token": "1//TEST-REFRESH",
|
||
"scope": "https://www.googleapis.com/auth/cloud-platform",
|
||
"token_type": "Bearer",
|
||
"id_token": "eyJTEST",
|
||
"expiry_date": int((time.time() + 3600) * 1000),
|
||
},
|
||
)
|
||
|
||
|
||
def test_antigravity_cli_token_written(tmp_path):
|
||
_write(tmp_path)
|
||
target = tmp_path / ".gemini" / "antigravity-cli" / "antigravity-oauth-token"
|
||
assert target.is_file(), "agy читает вход именно отсюда"
|
||
|
||
payload = json.loads(target.read_text(encoding="utf-8"))
|
||
assert sorted(payload) == ["auth_method", "token"]
|
||
assert payload["auth_method"] == "consumer"
|
||
assert payload["token"]["access_token"] == "ya29.TEST-ACCESS"
|
||
assert payload["token"]["refresh_token"] == "1//TEST-REFRESH"
|
||
|
||
|
||
def test_expiry_written_in_both_formats(tmp_path):
|
||
_write(tmp_path)
|
||
target = tmp_path / ".gemini" / "antigravity-cli" / "antigravity-oauth-token"
|
||
token = json.loads(target.read_text(encoding="utf-8"))["token"]
|
||
# Node-формат — миллисекунды, Go-шный oauth2.Token — строка RFC3339.
|
||
assert isinstance(token["expiry_date"], int)
|
||
assert token["expiry"].endswith("Z")
|
||
|
||
|
||
def test_gemini_creds_still_written(tmp_path):
|
||
"""Прежний файл остаётся: его читают другие части agy и сам хаб."""
|
||
target_file = _write(tmp_path)
|
||
assert target_file == tmp_path / ".gemini" / "oauth_creds.json"
|
||
creds = json.loads(target_file.read_text(encoding="utf-8"))
|
||
assert creds["access_token"] == "ya29.TEST-ACCESS"
|
||
assert "auth_method" not in creds
|
||
|
||
|
||
def test_auth_method_taken_from_working_neighbour(tmp_path):
|
||
"""Значение способа входа берётся у уже работающего профиля, если он есть."""
|
||
neighbour = tmp_path / "ag-working" / ".gemini" / "antigravity-cli"
|
||
neighbour.mkdir(parents=True)
|
||
(neighbour / "antigravity-oauth-token").write_text(
|
||
json.dumps({"auth_method": "workforce", "token": {}}), encoding="utf-8"
|
||
)
|
||
|
||
profile = tmp_path / "ag-new"
|
||
profile.mkdir()
|
||
_write(profile)
|
||
|
||
payload = json.loads(
|
||
(profile / ".gemini" / "antigravity-cli" / "antigravity-oauth-token").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
)
|
||
assert payload["auth_method"] == "workforce"
|
||
|
||
|
||
def test_empty_login_writes_nothing(tmp_path):
|
||
"""Вход без токена не должен оставлять ни одного файла учётных данных."""
|
||
try:
|
||
ProfileAuthManager.write_agy_oauth_creds(tmp_path, {"email": "x@y.z"})
|
||
except ValueError:
|
||
pass
|
||
else: # pragma: no cover - защита от возврата прежнего поведения
|
||
raise AssertionError("пустой вход обязан отказывать")
|
||
|
||
assert not (tmp_path / ".gemini" / "oauth_creds.json").exists()
|
||
assert not (
|
||
tmp_path / ".gemini" / "antigravity-cli" / "antigravity-oauth-token"
|
||
).exists()
|