Files

91 lines
3.2 KiB
Python

# requires: fastapi, httpx
import json
from collections.abc import AsyncIterator
from fastapi.testclient import TestClient
from app import Message, app, get_llm
AUTH = {"X-API-Key": "demo-key"}
BODY = {"messages": [{"role": "user", "content": "你好"}]}
class ScriptedLLM:
"""测试替身:按脚本返回 token,可以模拟中途失败。"""
def __init__(self, tokens: list[str], fail_at: int | None = None) -> None:
self.tokens, self.fail_at = tokens, fail_at
self.calls: list[list[Message]] = []
async def stream(self, messages: list[Message], temperature: float) -> AsyncIterator[str]:
self.calls.append(messages)
for i, t in enumerate(self.tokens):
if i == self.fail_at:
raise RuntimeError("上游模型 500")
yield t
def client_with(llm) -> TestClient:
app.dependency_overrides[get_llm] = lambda: llm
return TestClient(app, raise_server_exceptions=False)
def parse_sse(text: str) -> list[tuple[str, str]]:
events = []
for block in text.strip().split("\n\n"):
name, data = "message", ""
for line in block.splitlines():
if line.startswith("event: "):
name = line[7:]
elif line.startswith("data: "):
data = line[6:]
events.append((name, data))
return events
def teardown_function():
app.dependency_overrides.clear()
def test_auth_required():
r = client_with(ScriptedLLM(["x"])).post("/v1/chat", json=BODY)
assert r.status_code == 401 and r.json()["code"] == "HTTP_401" and r.json()["request_id"]
def test_validation_error_format():
r = client_with(ScriptedLLM(["x"])).post("/v1/chat", headers=AUTH,
json={"messages": [{"role": "robot", "content": ""}], "temperature": 5})
assert r.status_code == 422
fields = {d["field"] for d in r.json()["details"]}
assert {"messages.0.role", "messages.0.content", "temperature"} <= fields
def test_non_stream():
llm = ScriptedLLM(["你", "好"])
r = client_with(llm).post("/v1/chat", headers={**AUTH, "X-Request-ID": "rid-1"}, json={**BODY, "stream": False})
assert r.status_code == 200
assert r.json() == {"request_id": "rid-1", "content": "你好"}
assert r.headers["X-Request-ID"] == "rid-1"
assert llm.calls[0][0].content == "你好"
def test_stream_sse():
r = client_with(ScriptedLLM(["A", "B", "C"])).post("/v1/chat", headers=AUTH, json=BODY)
assert r.status_code == 200 and r.headers["content-type"].startswith("text/event-stream")
events = parse_sse(r.text)
assert [json.loads(d)["delta"] for n, d in events[:-1]] == ["A", "B", "C"]
assert events[-1] == ("message", "[DONE]")
def test_stream_error_event():
r = client_with(ScriptedLLM(["A", "B"], fail_at=1)).post("/v1/chat", headers=AUTH, json=BODY)
events = parse_sse(r.text)
assert events[0][0] == "message" and events[-1][0] == "error"
assert json.loads(events[-1][1])["message"] == "生成失败"
def test_internal_error_hides_details():
r = client_with(ScriptedLLM([])).get("/boom")
assert r.status_code == 500
assert r.json()["message"] == "服务器内部错误" and "postgres" not in r.text