import json import unittest from unittest.mock import Mock from fastapi import HTTPException from fastapi.testclient import TestClient from app.api.chat import get_shared_ai_platform_client from app.api.conversations import get_shared_ai_platform_client as get_conversations_shared_ai_platform_client from app.api.reviews import require_current_user from app.main import app from app.services.chat_service import ChatService class StubChatService: def __init__(self): self.stream_calls = [] def stream_answer(self, current_user, conversation_id, message, review_context=None): self.stream_calls.append((current_user, conversation_id, message, review_context)) if not message.strip(): yield 'data: {"type":"error","message":"message 不能为空"}\n\n' return active_id = conversation_id or "conv-123" yield 'data: {"type":"text","content":"第一段"}\n\n' yield 'data: {"type":"text","content":"重点摘要:\\n- 关注违约金条款"}\n\n' yield 'data: {"type":"sources","sources":[{"title":"民法典第585条","content":"...","score":1.2}]}\n\n' yield f'data: {{"type":"end","conversation_id":"{active_id}"}}\n\n' def list_conversations(self, current_user, page, page_size): return { "items": [ { "id": "conv-123", "title": "违约金条款咨询", "message_count": 2, "last_message_at": "2026-04-15T11:00:00+00:00", } ], "page": page, "page_size": page_size, "total": 1, } def get_conversation_detail(self, current_user, conversation_id): if conversation_id == "missing": raise HTTPException(status_code=404, detail="conversation not found") return { "id": conversation_id, "title": "违约金条款咨询", "messages": [ { "id": "msg-1", "role": "user", "content": "这个违约金条款有没有问题?", "sources": [], "created_at": "2026-04-15T11:00:00+00:00", }, { "id": "msg-2", "role": "assistant", "content": "存在偏高风险。", "sources": [{"title": "民法典第585条", "content": "..."}], "created_at": "2026-04-15T11:00:03+00:00", }, ], } class ChatServiceUnitTestCase(unittest.TestCase): def setUp(self): self.service = ChatService(Mock()) self.service.conversation_repository = Mock() self.service.message_repository = Mock() self.service.rag_service = Mock() def test_build_title_prefers_review_context(self): title = self.service._build_title( "继续分析这个条款", {"metadata": {"reviewTitle": "采购合同审查", "focusLabel": "自动续约"}}, ) self.assertEqual(title, "采购合同审查 · 自动续约") def test_build_effective_query_includes_review_context(self): query = self.service._build_effective_query( "请继续解释", { "metadata": { "reviewTitle": "采购合同审查", "contractType": "采购合同", "overallRisk": "high", "reviewOpinion": "存在自动续约风险", "focusLabel": "自动续约", "actionLabel": "解释风险", } }, ) self.assertIn("采购合同审查", query) self.assertIn("自动续约", query) self.assertIn("解释风险", query) self.assertTrue(query.endswith("请继续解释")) def test_build_default_chunks_uses_review_context(self): chunks = self.service._build_default_chunks( "请继续解释", {"metadata": {"reviewTitle": "采购合同审查", "focusLabel": "自动续约", "actionLabel": "解释风险"}}, ) self.assertIn("采购合同审查", chunks[0]) self.assertIn("当前问题:请继续解释", chunks[1]) self.assertIn("追问对象:自动续约", chunks[2]) def test_build_payload_passes_review_context_to_rag_service(self): self.service.rag_service.search_relevant_documents.return_value = [ {"id": "doc-1", "content": "自动续约风险", "file_name": "doc.txt", "score": 1.0} ] self.service.rag_service.build_chat_payload.return_value = { "chunks": ["审查结论:\n继续分析"], "sources": [], } review_context = { "reviewId": "review-123", "metadata": { "focusLabel": "自动续约", "actionLabel": "解释风险", "focusType": "risk_item", }, } payload = self.service._build_payload( {"tenant_id": "tenant-1", "id": "user-1"}, "请继续解释", review_context, ) self.assertEqual(payload["chunks"][0], "审查结论:\n继续分析") self.service.rag_service.build_chat_payload.assert_called_once_with( unittest.mock.ANY, self.service.rag_service.search_relevant_documents.return_value, review_context=review_context, ) class ChatApiTestCase(unittest.TestCase): def setUp(self): self.chat_service = StubChatService() app.dependency_overrides[get_shared_ai_platform_client] = lambda: self.chat_service app.dependency_overrides[get_conversations_shared_ai_platform_client] = lambda: self.chat_service self.client = TestClient(app) def tearDown(self): app.dependency_overrides.clear() def test_stream_chat_requires_authentication(self): response = self.client.post("/api/v1/chat/stream", json={"message": "违约金条款有没有问题"}) self.assertEqual(response.status_code, 401) def test_stream_chat_returns_sse_events(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} response = self.client.post("/api/v1/chat/stream", json={"message": "违约金条款有没有问题"}) self.assertEqual(response.status_code, 200) self.assertIn("text/event-stream", response.headers["content-type"]) events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")] payloads = [json.loads(item) for item in events] self.assertEqual(payloads[0]["type"], "text") self.assertEqual(payloads[1]["type"], "text") self.assertEqual(payloads[2]["type"], "sources") self.assertEqual(payloads[2]["sources"][0]["score"], 1.2) self.assertEqual(payloads[3]["type"], "end") self.assertEqual(payloads[3]["conversation_id"], "conv-123") self.assertEqual(self.chat_service.stream_calls[0][0]["id"], "user-1") def test_stream_chat_accepts_review_context(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} response = self.client.post( "/api/v1/chat/stream", json={ "message": "请继续解释这个风险项", "review_context": { "reviewId": "review-123", "metadata": { "reviewTitle": "采购合同审查", "focusLabel": "自动续约", "actionLabel": "解释风险", }, }, }, ) self.assertEqual(response.status_code, 200) self.assertEqual(self.chat_service.stream_calls[0][3]["reviewId"], "review-123") self.assertEqual(self.chat_service.stream_calls[0][3]["metadata"]["focusLabel"], "自动续约") def test_list_conversations_returns_paginated_payload(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} response = self.client.get("/api/v1/conversations?page=1&page_size=20") self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["total"], 1) self.assertEqual(payload["items"][0]["id"], "conv-123") def test_get_conversation_detail_returns_messages(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} response = self.client.get("/api/v1/conversations/conv-123") self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["id"], "conv-123") self.assertEqual(len(payload["messages"]), 2) self.assertEqual(payload["messages"][1]["sources"][0]["title"], "民法典第585条") def test_get_conversation_detail_returns_404_when_missing(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} response = self.client.get("/api/v1/conversations/missing") self.assertEqual(response.status_code, 404) if __name__ == "__main__": unittest.main()