hermes-hub/tests/test_web_client_contract.py
Hermes Team da962c7866 Merge antigravity/agy-native-login (A22) — принято, работает
Проверено исполнением, все три требуемых доказательства получены.

1. agy models через профиль Hub: обнаружено 14 моделей, включая
   gemini-3.1-pro-high и gemini-3.1-pro-low.
2. Реальный вызов adapter.invoke(ag-w1): модель ответила «ОК».
3. route_request(coder-primary): переключение с отказавшего
   codex-worker-1 на ag-w1, ответ получен, router_error отсутствует.

Подход из A20 (синтез oauth_creds.json) действительно был тупиковым;
родной вход agy с подменой HOME решает задачу. Реализация аккуратная:
видимая консоль на Windows через CREATE_NEW_CONSOLE, терминалы на Linux,
HOMEDRIVE выставляется корректно.

P0-4 подтверждён: пересохранение профиля не изменяет ни одного файла в
глобальном ~/.gemini.

Исправлено при слиянии:

1. Инструкция в веб-клиенте вела на НЕСУЩЕСТВУЮЩИЙ файл
   launcher/main.py. Заменена на реальный вызов launch_native_agy_login
   с указанием профиля.
2. test_headless_server_auth_matrix проверял дословную формулировку и
   падал при её правке, хотя поведение оставалось верным. Приведён к
   проверке сути, добавлена защита от возврата несуществующего пути.

Конфигурация владельца: обоим кодерам поставлена gemini-3.1-pro-high по
его прямой просьбе — теперь это возможно.

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

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

130 lines
6.2 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.

"""
Hermes Hub — Web Client Invariants & Contract Verification Suite
Tests adherence to docs/web-api/CONTRACT.md and A16 requirements.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
STATIC_DIR = REPO_ROOT / "src" / "antigravity_provider" / "router" / "web" / "static"
SNAPSHOT_EXAMPLE = REPO_ROOT / "docs" / "web-api" / "snapshot.example.json"
def test_static_assets_exist_and_no_build_dependencies():
"""Verify that index.html, style.css, app.js exist and have zero build / npm dependencies."""
index_html = STATIC_DIR / "index.html"
style_css = STATIC_DIR / "style.css"
app_js = STATIC_DIR / "app.js"
assert index_html.is_file(), f"Missing {index_html}"
assert style_css.is_file(), f"Missing {style_css}"
assert app_js.is_file(), f"Missing {app_js}"
html_content = index_html.read_text(encoding="utf-8")
# No React, Webpack, Vite, npm or external bundle references
assert "react" not in html_content.lower()
assert "webpack" not in html_content.lower()
assert "vite" not in html_content.lower()
assert "<script src=\"app.js\"></script>" in html_content
assert "<link rel=\"stylesheet\" href=\"style.css\">" in html_content
def test_snapshot_fixture_validity_and_completeness():
"""Verify snapshot.example.json conforms to HubSnapshot contract."""
assert SNAPSHOT_EXAMPLE.is_file(), f"Missing {SNAPSHOT_EXAMPLE}"
with open(SNAPSHOT_EXAMPLE, "r", encoding="utf-8") as f:
data = json.load(f)
# Top level keys
required_keys = [
"generation", "seq", "timestamp", "profiles_by_provider",
"all_profiles", "readiness", "agents", "providers",
"routing", "quotas", "metrics", "is_stale"
]
for key in required_keys:
assert key in data, f"Missing required top-level key: {key}"
# Verify monotonic seq structure
assert isinstance(data["seq"], int)
assert data["seq"] >= 1
# Verify profiles count
assert len(data["all_profiles"]) >= 16, "Must contain real profiles fixture"
# Zero leaked tokens / secrets in snapshot
raw_text = json.dumps(data)
forbidden_tokens = ["access_token", "refresh_token", "api_key", "client_secret"]
for tok in forbidden_tokens:
# Key shouldn't exist as actual secret payload
assert f'"{tok}": "sk-' not in raw_text
assert f'"{tok}": "gho_' not in raw_text
def test_monotonic_seq_logic_in_app_js():
"""Verify app.js contains strict monotonic seq checking to prevent stale response overwrites."""
app_js_content = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
assert "lastAppliedSeq" in app_js_content
assert "snapshot.seq < lastAppliedSeq" in app_js_content
assert "Stale snapshot" in app_js_content
def test_account_card_compact_height_and_quota_rendering():
"""Verify CSS has 164px compact fixed height and app.js renders multi-pool quota cells."""
style_css = (STATIC_DIR / "style.css").read_text(encoding="utf-8")
assert "164px" in style_css
assert ".account-card" in style_css
assert "overflow: hidden" in style_css
app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
assert "renderQuotaCell" in app_js
assert "remaining_percent" in app_js
assert "unavailable_reason" in app_js
assert "Н" in app_js
def test_headless_server_auth_matrix():
"""Verify headless server honesty in Add Account Wizard (Grok/Codex device-code vs Antigravity/Claude redirect warning)."""
app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
assert "Device Code OAuth" in app_js
assert "https://x.ai/device" in app_js
assert "https://auth.openai.com/device" in app_js
# Проверяем суть, а не дословную формулировку: текст правился уже дважды,
# и тест на точную фразу падал, хотя поведение оставалось верным.
assert "Headless" in app_js, "нет предупреждения про сервер без экрана"
assert "agy" in app_js, "не сказано, что вход идёт через консоль agy"
assert "launcher/main.py" not in app_js, "инструкция ведёт на несуществующий файл"
assert "ssh -L 8085:localhost:8085" in app_js
def test_actions_contract_handling():
"""Verify POST /api/action handles ok: false as valid 200 business response and displays feedback in-place."""
app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
assert "POST" in app_js
assert "/api/action" in app_js
assert "executeAction" in app_js
assert "modal-feedback-area" in app_js
def test_client_distinguishes_loading_from_missing_data():
"""Загрузка не должна выглядеть как отсутствие данных.
Опрос провайдера идёт в фоне и занимает секунды. Пока он не завершился,
корзины квот пусты. Клиент показывал в этот момент «Н/Д» — то же самое,
что при подключённом аккаунте без лимитов, — и владелец решил, что
лимиты не подтягиваются вовсе. Сервер отдаёт признак is_loading;
клиент обязан его учитывать.
"""
app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
assert "is_loading" in app_js, "клиент игнорирует признак загрузки из снапшота"
assert "Загрузка" in app_js, "нет отдельного текста для состояния загрузки"
# Причина отказа важнее флага: если провайдер уже ответил «лимитов не
# даю», это не загрузка, и показывать «Загрузка…» бесконечно нельзя.
assert "!unavailableReason" in app_js or "! unavailableReason" in app_js, (
"состояние загрузки не подавляется при известной причине отказа"
)