306 lines
13 KiB
Python
306 lines
13 KiB
Python
import unittest
|
|
|
|
from fastapi import HTTPException
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.api.knowledge_bases import get_audit_log_service, get_shared_ai_platform_client, require_current_user
|
|
from app.main import app
|
|
|
|
|
|
class StubAuditLogService:
|
|
def __init__(self):
|
|
self.events = []
|
|
|
|
def record_event(self, **kwargs):
|
|
self.events.append(kwargs)
|
|
return kwargs
|
|
|
|
|
|
class StubKnowledgeBaseService:
|
|
def list_knowledge_bases(self, current_user, page, page_size):
|
|
if current_user["role"] != "admin":
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": "kb-123",
|
|
"name": "劳动合同知识库",
|
|
"description": "劳动用工条款",
|
|
"status": "active",
|
|
"document_count": 1,
|
|
"created_at": "2026-04-16T00:00:00+00:00",
|
|
"updated_at": "2026-04-16T00:00:00+00:00",
|
|
}
|
|
],
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"total": 1,
|
|
}
|
|
|
|
def create_knowledge_base(self, current_user, name, description):
|
|
if current_user["role"] != "admin":
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
return {
|
|
"id": "kb-123",
|
|
"name": name,
|
|
"description": description,
|
|
"status": "active",
|
|
"document_count": 0,
|
|
"created_at": "2026-04-16T00:00:00+00:00",
|
|
"updated_at": "2026-04-16T00:00:00+00:00",
|
|
}
|
|
|
|
def list_documents(self, current_user, knowledge_base_id, page, page_size):
|
|
if current_user["role"] != "admin":
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": "kbdoc-123",
|
|
"knowledge_base_id": knowledge_base_id,
|
|
"file_name": "rules.txt",
|
|
"mime_type": "text/plain",
|
|
"file_size": 12,
|
|
"status": "indexed",
|
|
"content_excerpt": "合同解除规则",
|
|
"extracted_text_length": 6,
|
|
"preview_truncated": False,
|
|
"created_at": "2026-04-16T00:00:00+00:00",
|
|
"current_stage": "indexed",
|
|
"current_stage_label": "已完成索引",
|
|
"status_detail": "文本提取与索引已完成,可直接用于检索和问答。",
|
|
"parse_status": "completed",
|
|
"index_status": "completed",
|
|
"failure_reason": None,
|
|
"next_action": "可直接进入问答,或继续补充更多制度与条款文档。",
|
|
"retryable": False,
|
|
},
|
|
{
|
|
"id": "kbdoc-456",
|
|
"knowledge_base_id": knowledge_base_id,
|
|
"file_name": "draft.docx",
|
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
"file_size": 18,
|
|
"status": "processing",
|
|
"content_excerpt": None,
|
|
"extracted_text_length": 0,
|
|
"preview_truncated": False,
|
|
"created_at": "2026-04-16T00:10:00+00:00",
|
|
"current_stage": "parse",
|
|
"current_stage_label": "文本提取中",
|
|
"status_detail": "系统正在提取文本并写入检索索引。",
|
|
"parse_status": "processing",
|
|
"index_status": "blocked",
|
|
"failure_reason": "文本提取尚未完成,索引阶段暂时阻塞。",
|
|
"next_action": "请等待文本提取完成;提取成功后系统会继续进入索引阶段。",
|
|
"retryable": False,
|
|
}
|
|
],
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"total": 2,
|
|
}
|
|
|
|
def upload_document(self, current_user, knowledge_base_id, file_name, mime_type, content):
|
|
if current_user["role"] != "admin":
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
return {
|
|
"id": "kbdoc-123",
|
|
"knowledge_base_id": knowledge_base_id,
|
|
"file_name": file_name,
|
|
"mime_type": mime_type,
|
|
"file_size": len(content),
|
|
"status": "indexed",
|
|
"content_excerpt": content.decode("utf-8"),
|
|
"extracted_text_length": len(content.decode("utf-8")),
|
|
"preview_truncated": False,
|
|
"created_at": "2026-04-16T00:00:00+00:00",
|
|
"current_stage": "indexed",
|
|
"current_stage_label": "已完成索引",
|
|
"status_detail": "文本提取与索引已完成,可直接用于检索和问答。",
|
|
"parse_status": "completed",
|
|
"index_status": "completed",
|
|
"failure_reason": None,
|
|
"next_action": "可直接进入问答,或继续补充更多制度与条款文档。",
|
|
"error_message": None,
|
|
}
|
|
|
|
def retry_document(self, current_user, knowledge_base_id, document_id, file_name=None, mime_type=None, content=None):
|
|
if current_user["role"] != "admin":
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
indexed = content is not None and len(content) > 0
|
|
return {
|
|
"id": document_id,
|
|
"knowledge_base_id": knowledge_base_id,
|
|
"file_name": file_name or "draft.docx",
|
|
"mime_type": mime_type or "text/plain",
|
|
"file_size": len(content or b""),
|
|
"status": "indexed" if indexed else "failed",
|
|
"content_excerpt": content.decode("utf-8") if indexed else None,
|
|
"extracted_text_length": len(content.decode("utf-8")) if indexed else 0,
|
|
"preview_truncated": False,
|
|
"created_at": "2026-04-16T00:00:00+00:00",
|
|
"current_stage": "indexed" if indexed else "failed",
|
|
"current_stage_label": "已完成索引" if indexed else "文本提取失败",
|
|
"status_detail": "文本提取与索引已完成,可直接用于检索和问答。" if indexed else "重试需要重新上传可复制文本版本,当前记录未保留可重新索引的原文。",
|
|
"parse_status": "completed" if indexed else "failed",
|
|
"index_status": "completed" if indexed else "blocked",
|
|
"failure_reason": None if indexed else "重试需要重新上传可复制文本版本,当前记录未保留可重新索引的原文。",
|
|
"next_action": "可直接进入问答,或继续补充更多制度与条款文档。" if indexed else "请检查文件格式、内容编码或改为可复制文本后重新上传。",
|
|
"retryable": not indexed,
|
|
"error_message": None if indexed else "重试需要重新上传可复制文本版本,当前记录未保留可重新索引的原文。",
|
|
}
|
|
|
|
|
|
class KnowledgeBasesApiTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.audit_log_service = StubAuditLogService()
|
|
app.dependency_overrides[get_shared_ai_platform_client] = lambda: StubKnowledgeBaseService()
|
|
app.dependency_overrides[get_audit_log_service] = lambda: self.audit_log_service
|
|
self.client = TestClient(app)
|
|
|
|
def tearDown(self):
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_admin_can_list_knowledge_bases(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-admin",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "admin",
|
|
}
|
|
|
|
response = self.client.get("/api/v1/knowledge-bases")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json()["data"]["items"][0]["name"], "劳动合同知识库")
|
|
|
|
def test_admin_can_list_documents_with_blocked_index_stage(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-admin",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "admin",
|
|
}
|
|
|
|
response = self.client.get("/api/v1/knowledge-bases/kb-123/documents")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
items = response.json()["data"]["items"]
|
|
self.assertEqual(len(items), 2)
|
|
blocked_item = items[1]
|
|
self.assertEqual(blocked_item["current_stage"], "parse")
|
|
self.assertEqual(blocked_item["index_status"], "blocked")
|
|
self.assertEqual(blocked_item["failure_reason"], "文本提取尚未完成,索引阶段暂时阻塞。")
|
|
self.assertIn("继续进入索引阶段", blocked_item["next_action"])
|
|
|
|
def test_admin_can_create_knowledge_base(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-admin",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "admin",
|
|
}
|
|
|
|
response = self.client.post(
|
|
"/api/v1/knowledge-bases",
|
|
data={"name": "采购合同知识库", "description": "采购模板与制度"},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
body = response.json()
|
|
self.assertEqual(body["data"]["name"], "采购合同知识库")
|
|
self.assertEqual(self.audit_log_service.events[0]["action"], "create_knowledge_base")
|
|
self.assertEqual(
|
|
self.audit_log_service.events[0]["detail_json"],
|
|
{
|
|
"name": "采购合同知识库",
|
|
"description": "采购模板与制度",
|
|
"status": "active",
|
|
"document_count": 0,
|
|
},
|
|
)
|
|
|
|
def test_admin_can_upload_document(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-admin",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "admin",
|
|
}
|
|
|
|
response = self.client.post(
|
|
"/api/v1/knowledge-bases/kb-123/documents",
|
|
data={"note": "首批规则"},
|
|
files={"file": ("rules.txt", "合同解除规则".encode("utf-8"), "text/plain")},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
body = response.json()
|
|
self.assertEqual(body["data"]["file_name"], "rules.txt")
|
|
self.assertEqual(self.audit_log_service.events[0]["action"], "upload_knowledge_document")
|
|
self.assertEqual(
|
|
self.audit_log_service.events[0]["detail_json"],
|
|
{
|
|
"knowledge_base_id": "kb-123",
|
|
"file_name": "rules.txt",
|
|
"mime_type": "text/plain",
|
|
"file_size": len("合同解除规则".encode("utf-8")),
|
|
"status": "indexed",
|
|
"current_stage": "indexed",
|
|
"parse_status": "completed",
|
|
"index_status": "completed",
|
|
"error_message": None,
|
|
"note": "首批规则",
|
|
},
|
|
)
|
|
|
|
def test_admin_can_retry_document_with_replacement_file(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-admin",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "admin",
|
|
}
|
|
|
|
response = self.client.post(
|
|
"/api/v1/knowledge-bases/kb-123/documents/kbdoc-456/retry",
|
|
data={"note": "替换为空文档"},
|
|
files={"file": ("rules-v2.txt", "补充合同审查规则".encode("utf-8"), "text/plain")},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
body = response.json()
|
|
self.assertEqual(body["data"]["id"], "kbdoc-456")
|
|
self.assertEqual(body["data"]["status"], "indexed")
|
|
self.assertEqual(self.audit_log_service.events[0]["action"], "retry_knowledge_document")
|
|
self.assertEqual(
|
|
self.audit_log_service.events[0]["detail_json"],
|
|
{
|
|
"knowledge_base_id": "kb-123",
|
|
"original_document_id": "kbdoc-456",
|
|
"file_name": "rules-v2.txt",
|
|
"mime_type": "text/plain",
|
|
"file_size": len("补充合同审查规则".encode("utf-8")),
|
|
"status": "indexed",
|
|
"current_stage": "indexed",
|
|
"parse_status": "completed",
|
|
"index_status": "completed",
|
|
"error_message": None,
|
|
"replacement_file": True,
|
|
"note": "替换为空文档",
|
|
},
|
|
)
|
|
|
|
def test_member_cannot_access_knowledge_bases(self):
|
|
app.dependency_overrides[require_current_user] = lambda: {
|
|
"id": "user-member",
|
|
"tenant_id": "tenant-demo-001",
|
|
"role": "member",
|
|
}
|
|
|
|
response = self.client.get("/api/v1/knowledge-bases")
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertEqual(response.json()["message"], "forbidden")
|
|
self.assertEqual(self.audit_log_service.events, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|