gmn2a / tests /test_integration.py
longxing's picture
会话自愈:Cookie 初始化 + 后台持续轮换 __Secure-1PSIDTS
4ebb74e
Raw History Blame Contribute Delete
7.01 kB
"""端到端集成验证:真实 FastAPI 应用 + 假 Gemini 后端,不联网。"""
import asyncio
import os
import sys
import tempfile
import time
from types import SimpleNamespace
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TMP = tempfile.mkdtemp(prefix="gs-e2e-")
os.environ["SECURE_1PSID"] = "PSID-E2E"
os.environ["SECURE_1PSIDTS"] = "ENV-TS-E2E"
os.environ["GEMINI_COOKIE_PATH"] = TMP
os.environ["SESSION_HEALTH_INTERVAL"] = "30"
os.environ["AUTO_DELETE_CHAT"] = "true"
import gemini_session as gs # noqa: E402
PASS, FAIL = [], []
def check(name, cond, extra=""):
(PASS if cond else FAIL).append(name)
print(("[OK] " if cond else "[FAIL] ") + name + (f" {extra}" if extra and not cond else ""))
class FakeCookies:
def __init__(self, values):
self.jar = [SimpleNamespace(name=k, value=v) for k, v in values.items()]
def get(self, name, default=None):
for c in self.jar:
if c.name == name:
return c.value
return default
class FakeResponse:
def __init__(self, text, cid, delta=None):
self.text = text
self.text_delta = delta
self.thoughts = None
self.images = []
self.metadata = [cid]
class FakeClient:
def __init__(self, psid=None, psidts=None, **kwargs):
self.psid = psid
self.psidts = psidts
self._running = False
self.client = None
self.auto_refresh = False
self.refresh_task = None
self.activity_task = None
self.account_status = SimpleNamespace(name="AVAILABLE")
self.cookies = FakeCookies({"__Secure-1PSID": psid, "__Secure-1PSIDTS": psidts})
self.deleted = []
self.fail_next_generate = False
async def init(self, **kwargs):
self._running = True
self.client = object()
self.auto_refresh = kwargs.get("auto_refresh", False)
# 对齐真实 GeminiClient.init():开启 auto_refresh 时立即拉起后台任务
if self.auto_refresh:
self.refresh_task = asyncio.create_task(self.start_auto_refresh())
self.activity_task = asyncio.create_task(self.start_activity_watchdog())
async def generate_content(self, prompt, **kwargs):
if self.fail_next_generate:
self.fail_next_generate = False
self.account_status = SimpleNamespace(name="UNAUTHENTICATED")
raise gs_gemini_error()
return FakeResponse("OK", "cid-generate")
async def generate_content_stream(self, prompt, **kwargs):
yield FakeResponse("Hel", "cid-stream-1", delta="Hel")
yield FakeResponse("lo", "cid-stream-2", delta="lo")
async def fetch_latest_chat_response(self, cid):
return FakeResponse("OK", cid)
async def delete_chat(self, cid):
self.deleted.append(cid)
async def start_auto_refresh(self):
while self._running:
await asyncio.sleep(3600)
async def start_activity_watchdog(self):
while self._running:
await asyncio.sleep(3600)
async def close(self):
self._closed_marker = True
self._running = False
self.client = None
def gs_gemini_error():
from gemini_webapi import GeminiError
return GeminiError("Failed to generate contents. No output data found in response.")
def main():
gs.GeminiClient = FakeClient
import main as app_main # 导入时才读取环境变量
from fastapi.testclient import TestClient
with TestClient(app_main.app) as client:
# ---------------------------------------------------------- 生命周期
check("E1 lifespan 启动了监督协程", app_main.session_manager.status()["supervisor_alive"] is True)
# 初始化是后台任务,等它跑完再断言
deadline = time.time() + 10
while time.time() < deadline and not app_main.session_manager.status()["initialized"]:
time.sleep(0.1)
check("E1b 后台初始化已完成", app_main.session_manager.status()["initialized"] is True)
# ---------------------------------------------------------- /api/session
r = client.get("/api/session")
check("E2 /api/session 返回 200", r.status_code == 200, str(r.status_code))
data = r.json()
check("E3 基于 Cookie 初始化成功", data["initialized"] is True)
check("E4 凭据来源为 environment", data["credential_source"] == "environment")
check("E5 轮换任务存活", data["rotation_task_alive"] is True)
check("E6 1PSIDTS 已脱敏", "*" in data["live_1psidts_masked"] and data["live_1psidts_masked"] != "ENV-TS-E2E")
check("E7 缓存已落盘", data["cached_1psidts_masked"] != "")
# ---------------------------------------------------------- 非流式对话
payload = {"model": "gemini-2.5-flash", "messages": [{"role": "user", "content": "hi"}]}
r = client.post("/v1/chat/completions", json=payload)
check("E8 非流式请求 200", r.status_code == 200, f"{r.status_code} {r.text[:200]}")
body = r.json()
check("E9 返回内容正确", body["choices"][0]["message"]["content"].strip() == "OK", str(body)[:200])
# ---------------------------------------------------------- 流式对话
r = client.post("/v1/chat/completions", json={**payload, "stream": True})
check("E10 流式请求 200", r.status_code == 200, str(r.status_code))
check("E11 流式返回 SSE 数据", "data: [DONE]" in r.text and "Hel" in r.text, r.text[:200])
# ---------------------------------------------------------- 失效自愈(非流式)
bad = app_main.session_manager.client
bad.fail_next_generate = True
r = client.post("/v1/chat/completions", json=payload)
check("E12 会话失效后请求仍成功(自动重建重试)", r.status_code == 200, f"{r.status_code} {r.text[:200]}")
check("E13 确实重建了客户端", app_main.session_manager.client is not bad)
check("E14 recycle_count 递增", app_main.session_manager.status()["recycle_count"] >= 1)
# ---------------------------------------------------------- 轮换 -> 缓存 -> 状态
live = app_main.session_manager.client
live.cookies = FakeCookies({"__Secure-1PSID": "PSID-E2E", "__Secure-1PSIDTS": "ROTATED-E2E"})
app_main.session_manager._observe_rotation(live)
r = client.get("/api/session")
data = r.json()
check("E15 轮换被记录", data["rotation_count"] >= 1)
check("E16 轮换值已落盘", data["cached_1psidts_masked"].startswith("ROTA"))
check("E17 换值年龄已更新", data["last_1psidts_change_age_sec"] is not None and data["last_1psidts_change_age_sec"] < 60)
# ---------------------------------------------------------- 管理面板状态
# 没有 API_KEY 时管理面板不可用,这里直接调用函数绕过 token
import admin
status = asyncio.run(admin.get_status(token=""))
check("E18 管理面板状态含 session 块", "session" in status and "rotation_count" in status)
check("E19 管理面板透出实时 1PSIDTS", status["live_1psidts_masked"] != "")
# ---------------------------------------------------------- 关闭
check("E20 退出 lifespan 后监督协程停止", app_main.session_manager.status()["supervisor_alive"] is False)
print(f"\n通过 {len(PASS)} / {len(PASS) + len(FAIL)}")
if FAIL:
print("失败项:")
for name in FAIL:
print(" -", name)
return 1 if FAIL else 0
if __name__ == "__main__":
sys.exit(main())