Files
ai-contract-platform/backend/tests/test_chat_api.py
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

338 lines
15 KiB
Python

import json
import unittest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.api.chat import (
get_chat_legal_article_service,
get_chat_prompt_runtime_service,
get_chat_review_service,
get_shared_ai_platform_client,
get_tenant_service,
)
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
class StubChatService:
def __init__(self):
self.stream_calls = []
def stream_answer(self, current_user, conversation_id, message, review_context=None, template_key=None):
self.stream_calls.append((current_user, conversation_id, message, review_context, template_key))
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 StubReviewService:
def get_review_result(self, current_user, review_id):
return {
"risk_items": [
{
"title": "违约金过高",
"legal_article_refs": [
{
"id": "lart-civil-585",
"source_id": "LAW-CIVIL-CODE-2020",
"source_title": "中华人民共和国民法典",
"article_no": "第五百八十五条",
"article_excerpt": "当事人可以约定一方违约时应当根据违约情况支付违约金。",
"authority": "全国人民代表大会",
"knowledge_base_id": "kb-phase14",
"knowledge_document_id": "doc-lart-civil-585",
"detail_path": "/legal-articles?article_id=lart-civil-585",
"match_reason": "命中风险关键词:违约金",
"tags": ["违约金"],
}
],
}
]
}
class StubLegalArticleService:
def __init__(self, sources=None):
self.sources = sources or []
self.calls = []
def search_runtime_sources(self, current_user, query, limit=5):
self.calls.append((current_user, query, limit))
return self.sources[:limit]
class StubTenantService:
def __init__(self, blocked=False):
self.blocked = blocked
self.calls = []
def assert_billing_allows(self, current_user, action, requested_file_size_bytes=0):
self.calls.append((action, requested_file_size_bytes))
if self.blocked:
raise HTTPException(status_code=402, detail="试用已到期,请升级套餐后继续使用 Chat 法律问答。")
class StubPromptRuntimeService:
def __init__(self):
self.record_calls = []
def resolve_published_prompt(self, current_user, prompt_type, *, preferred_keys=None, fallback_prompt_version=None):
return {
"available": True,
"prompt_asset_id": "prompt-001",
"prompt_asset_version_id": "promptver-001",
"prompt_key": "legal_chat_default",
"prompt_type": prompt_type,
"version_label": "v1.0",
"prompt_version": "legal_chat_default.v1.0",
"template_key": "legal_chat_default",
"source": "published_prompt_asset",
}
def to_trace(self, prompt_runtime):
return {
key: prompt_runtime.get(key)
for key in [
"available",
"prompt_asset_id",
"prompt_asset_version_id",
"prompt_key",
"prompt_type",
"version_label",
"prompt_version",
"template_key",
"source",
]
}
def record_run_link(self, current_user, prompt_runtime, **kwargs):
self.record_calls.append((current_user, prompt_runtime, kwargs))
return {"id": "promptrun-001"}
class ChatApiTestCase(unittest.TestCase):
def setUp(self):
self.chat_service = StubChatService()
self.tenant_service = StubTenantService()
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
app.dependency_overrides[get_chat_review_service] = lambda: None
app.dependency_overrides[get_chat_legal_article_service] = lambda: StubLegalArticleService()
app.dependency_overrides[get_chat_prompt_runtime_service] = lambda: None
app.dependency_overrides[get_tenant_service] = lambda: self.tenant_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")
self.assertEqual(self.tenant_service.calls[0][0], "chat")
def test_stream_chat_returns_billing_block_message(self):
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
app.dependency_overrides[get_tenant_service] = lambda: StubTenantService(blocked=True)
response = self.client.post("/api/v1/chat/stream", json={"message": "违约金条款有没有问题"})
self.assertEqual(response.status_code, 402)
self.assertIn("试用已到期", response.json()["message"])
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_stream_chat_passes_published_prompt_template_key(self):
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
prompt_runtime_service = StubPromptRuntimeService()
app.dependency_overrides[get_chat_prompt_runtime_service] = lambda: prompt_runtime_service
response = self.client.post("/api/v1/chat/stream", json={"message": "check clause"})
self.assertEqual(response.status_code, 200)
review_context = self.chat_service.stream_calls[0][3]
self.assertEqual(self.chat_service.stream_calls[0][4], "legal_chat_default")
self.assertEqual(review_context["promptRuntime"]["prompt_version"], "legal_chat_default.v1.0")
self.assertEqual(prompt_runtime_service.record_calls[0][2]["run_type"], "legal_chat")
def test_stream_chat_adds_review_legal_article_sources(self):
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
app.dependency_overrides[get_chat_review_service] = lambda: StubReviewService()
response = self.client.post(
"/api/v1/chat/stream",
json={
"message": "请解释违约金风险",
"review_context": {
"reviewId": "review-123",
"metadata": {"focusLabel": "违约金"},
},
},
)
self.assertEqual(response.status_code, 200)
events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")]
payloads = [json.loads(item) for item in events]
sources_event = next(item for item in payloads if item["type"] == "sources")
legal_source = next(item for item in sources_event["sources"] if item.get("source_type") == "legal_article")
self.assertEqual(legal_source["source_id"], "lart-civil-585")
self.assertEqual(legal_source["detail_path"], "/legal-articles?article_id=lart-civil-585")
self.assertEqual(self.chat_service.stream_calls[0][3]["legalArticleRefs"][0]["id"], "lart-civil-585")
def test_stream_chat_adds_runtime_legal_article_sources(self):
app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"}
legal_article_service = StubLegalArticleService(
[
{
"title": "中华人民共和国网络安全法 第一条",
"content": "为了保障网络安全,维护网络空间主权和国家安全、社会公共利益,制定本法。",
"score": 12.5,
"source_type": "legal_article",
"source_id": "lart-cyber-001",
"detail_path": "/legal-articles?article_id=lart-cyber-001",
"article_no": "第一条",
"authority": "全国人民代表大会常务委员会",
"metadata": {
"source_id": "FLK-CYBER-SECURITY",
"source_title": "中华人民共和国网络安全法",
"match_reason": "命中条号;命中法源",
"tags": ["网络安全"],
},
}
]
)
app.dependency_overrides[get_chat_legal_article_service] = lambda: legal_article_service
response = self.client.post("/api/v1/chat/stream", json={"message": "网络安全法第一条是什么"})
self.assertEqual(response.status_code, 200)
events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")]
payloads = [json.loads(item) for item in events]
sources_event = next(item for item in payloads if item["type"] == "sources")
legal_source = next(item for item in sources_event["sources"] if item.get("source_type") == "legal_article")
self.assertEqual(legal_source["source_id"], "lart-cyber-001")
self.assertEqual(legal_source["detail_path"], "/legal-articles?article_id=lart-cyber-001")
self.assertEqual(legal_article_service.calls[0][1], "网络安全法第一条是什么")
review_context = self.chat_service.stream_calls[0][3]
self.assertEqual(review_context["legalArticleSearchSources"][0]["source_id"], "lart-cyber-001")
self.assertEqual(review_context["metadata"]["legalArticleSearchSourceCount"], 1)
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()