import json import httpx import pytest from fastapi import FastAPI from src.agent.collect import validated_reply from src.agent.llm import LLMAPIError, LLMAuthError, LLMRateLimitError from src.api.endpoints import router from src.backends.langgraph import LangGraphBackend from src.core.config import Config from src.core.fastgpt_client import get_chat_backend class FailingLLM: def __init__(self, error_type, partial): self.error_type = error_type self.partial = partial async def complete(self, *args, **kwargs): raise self.error_type('private provider detail') async def stream_tokens(self, *args, **kwargs): if self.partial: yield '1002' yield 'partial reply' raise self.error_type('private provider detail') class Graph: async def ainvoke(self, data, *, config, context): await validated_reply(context.llm, 'system', 'user', '1002') raise AssertionError('failed turn must not complete') def sse_events(body): events = [] for block in body.strip().split('\n\n'): lines = block.splitlines() kind = next(line[7:] for line in lines if line.startswith('event: ')) data = json.loads(next(line[6:] for line in lines if line.startswith('data: '))) events.append((kind, data)) return events @pytest.mark.asyncio @pytest.mark.parametrize('partial', [False, True], ids=['before-text', 'after-text']) @pytest.mark.parametrize('error_type,code,message', [ (LLMAuthError, '401', '认证失败'), (LLMRateLimitError, '429', '请求过于频繁,请稍后重试'), (LLMAPIError, '500', '大模型服务器无响应'), ]) async def test_http_sse_errors_match_non_stream_codes_and_terminate( monkeypatch, partial, error_type, code, message, ): # 独立协议测试 app:实际 LangGraphBackend/LLM 错误映射,不启动真实 provider。 monkeypatch.setattr(Config, 'AGENT_BACKEND', 'fastgpt') app = FastAPI() app.include_router(router) backend = LangGraphBackend(graph=Graph(), llm=FailingLLM(error_type, partial)) app.dependency_overrides[get_chat_backend] = lambda: backend payload = {'sessionId': 'error-audit', 'timeStamp': '20261022120000', 'text': 'hello'} async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url='http://test') as client: response = await client.post('/chat?stream=true', json=payload) assert response.status_code == 200 # 错误分类在 SSE data.code,不改已发送的 HTTP 头 assert response.headers['content-type'].startswith('text/event-stream') events = sse_events(response.text) assert events[-1] == ('error', {'code': code, 'msg': message}) assert sum(kind == 'error' for kind, _ in events) == 1 assert all(kind != 'done' for kind, _ in events) assert any(kind == 'text_delta' for kind, _ in events) is partial assert 'private provider detail' not in response.text result = await client.post('/chat', json=payload) assert result.json()['code'] == code assert result.json()['msg'] == message