File size: 7,011 Bytes
4ebb74e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """端到端集成验证:真实 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())
|