Files
ai-ticket-platform/backend/apps/knowledge/tests/test_knowledge_api.py
2026-07-27 19:01:19 +08:00

326 lines
11 KiB
Python

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from apps.knowledge.documents import process_knowledge_document
from apps.knowledge.models import (
GovernedAsset,
KnowledgeBase,
KnowledgeChunk,
KnowledgeDocument,
KnowledgeItem,
)
from apps.tickets.materials import MaterialScanError
def test_create_and_list_knowledge_base(admin_client, db):
response = admin_client.post(
"/api/knowledge/bases",
data={
"name": "税务采集知识库",
"description": "税局采集、日志和常见问题",
"scope": "数据采集",
},
content_type="application/json",
)
assert response.status_code == 200
payload = response.json()
assert payload["name"] == "税务采集知识库"
assert payload["enabled"] is True
response = admin_client.get("/api/knowledge/bases")
assert response.status_code == 200
assert response.json()[0]["name"] == "税务采集知识库"
def test_create_knowledge_item_always_creates_governed_draft(admin_client, db):
base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集")
response = admin_client.post(
"/api/knowledge/items",
data={
"knowledge_base_id": base.id,
"title": "税局采集进度查询",
"problem": "客户询问采集进度长时间不更新",
"applicable_scope": "生产环境税局采集",
"error_code": "",
"root_cause": "税局服务响应不稳定",
"troubleshooting_steps": "查询 Python 日志并确认采集任务状态",
"solution": "告知客户税局不稳定,任务仍在采集中,稍后复查。",
"risk_note": "不要重复触发采集任务。",
"status": "published",
},
content_type="application/json",
)
assert response.status_code == 200
item = KnowledgeItem.objects.get(id=response.json()["id"])
assert item.status == "draft"
assert KnowledgeChunk.objects.filter(item=item).count() == 0
assert GovernedAsset.objects.filter(
asset_type="knowledge_item",
object_id=item.id,
versions__status="draft",
).exists()
def test_create_knowledge_item_rejects_unknown_category(admin_client, db):
base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集")
response = admin_client.post(
"/api/knowledge/items",
data={
"knowledge_base_id": base.id,
"title": "未知分类知识",
"problem": "分类编码不存在",
"category_code": "unknown_category",
"solution": "不应写入知识库",
},
content_type="application/json",
)
assert response.status_code == 400
assert response.json()["detail"] == "知识适用分类不存在或已停用"
assert not KnowledgeItem.objects.filter(title="未知分类知识").exists()
def test_ingest_document_text_creates_document_and_chunks(admin_client, db):
base = KnowledgeBase.objects.create(name="日志知识库", scope="技术支持")
response = admin_client.post(
"/api/knowledge/documents",
data={
"knowledge_base_id": base.id,
"file_name": "tax-log.md",
"file_type": "markdown",
"content": "税局接口不稳定时,采集任务会停留在处理中。需要查询 Python 日志确认。",
},
content_type="application/json",
)
assert response.status_code == 200
document = KnowledgeDocument.objects.get(id=response.json()["id"])
assert document.parse_status == "success"
assert KnowledgeChunk.objects.filter(document=document).count() >= 1
def test_upload_document_preserves_original_and_indexes_text(
admin_client,
db,
monkeypatch,
):
base = KnowledgeBase.objects.create(name="文件知识库", scope="技术支持")
objects = {}
monkeypatch.setattr(
"apps.knowledge.documents.put_object",
lambda path, raw, mime_type: objects.update({path: raw}),
)
monkeypatch.setattr(
"apps.knowledge.documents.get_object",
lambda path: objects[path],
)
monkeypatch.setattr(
"apps.knowledge.api.dispatch_knowledge_document_processing",
lambda document_id: process_knowledge_document(document_id),
)
response = admin_client.post(
f"/api/knowledge/documents/upload?knowledge_base_id={base.id}",
data={
"file": SimpleUploadedFile(
"support.log",
"接口超时,请检查连接池。".encode(),
content_type="text/plain",
)
},
)
assert response.status_code == 200
payload = response.json()
document = KnowledgeDocument.objects.get(public_id=payload["public_id"])
assert document.scan_status == "clean"
assert document.parse_status == "success"
assert document.storage_path in objects
assert document.content_text == "接口超时,请检查连接池。"
assert document.sha256
assert document.file_size == len(objects[document.storage_path])
assert document.processing_attempts == 1
assert document.indexed_at is not None
assert document.chunks.count() >= 1
assert payload["has_original"] is True
listed = admin_client.get(f"/api/knowledge/documents?knowledge_base_id={base.id}")
assert listed.status_code == 200
assert listed.json()[0]["public_id"] == str(document.public_id)
def test_upload_document_rejects_disguised_file(admin_client, db):
base = KnowledgeBase.objects.create(name="文件知识库")
response = admin_client.post(
f"/api/knowledge/documents/upload?knowledge_base_id={base.id}",
data={
"file": SimpleUploadedFile(
"fake.pdf",
b"not a pdf",
content_type="application/pdf",
)
},
)
assert response.status_code == 415
assert not KnowledgeDocument.objects.exists()
def test_document_scan_failure_keeps_original_and_can_retry(
admin_client,
db,
monkeypatch,
):
base = KnowledgeBase.objects.create(name="文件知识库")
objects = {}
monkeypatch.setattr(
"apps.knowledge.documents.put_object",
lambda path, raw, mime_type: objects.update({path: raw}),
)
monkeypatch.setattr(
"apps.knowledge.documents.get_object",
lambda path: objects[path],
)
monkeypatch.setattr(
"apps.knowledge.documents.scan_bytes",
lambda raw: (_ for _ in ()).throw(MaterialScanError("扫描服务不可用")),
)
monkeypatch.setattr(
"apps.knowledge.api.dispatch_knowledge_document_processing",
lambda document_id: process_knowledge_document(document_id),
)
response = admin_client.post(
f"/api/knowledge/documents/upload?knowledge_base_id={base.id}",
data={
"file": SimpleUploadedFile(
"support.txt",
"重试后可解析".encode(),
content_type="text/plain",
)
},
)
document = KnowledgeDocument.objects.get(public_id=response.json()["public_id"])
assert response.status_code == 200
assert document.scan_status == "failed"
assert document.parse_status == "failed"
assert document.storage_path in objects
assert document.chunks.count() == 0
monkeypatch.setattr(
"apps.knowledge.documents.scan_bytes",
lambda raw: {"status": "clean", "engine": "test"},
)
retried = admin_client.post(f"/api/knowledge/documents/{document.public_id}/retry")
assert retried.status_code == 200
document.refresh_from_db()
assert document.parse_status == "success"
assert document.processing_attempts == 2
assert document.chunks.count() >= 1
process_knowledge_document(document.id)
document.refresh_from_db()
assert document.processing_attempts == 2
def test_upload_document_storage_failure_does_not_create_record(
admin_client,
db,
monkeypatch,
):
base = KnowledgeBase.objects.create(name="文件知识库")
monkeypatch.setattr(
"apps.knowledge.documents.put_object",
lambda *args: (_ for _ in ()).throw(OSError("storage down")),
)
response = admin_client.post(
f"/api/knowledge/documents/upload?knowledge_base_id={base.id}",
data={
"file": SimpleUploadedFile(
"support.txt",
b"content",
content_type="text/plain",
)
},
)
assert response.status_code == 503
assert not KnowledgeDocument.objects.exists()
def test_download_uploaded_document_returns_original(
admin_client,
db,
monkeypatch,
):
base = KnowledgeBase.objects.create(name="文件知识库")
document = KnowledgeDocument.objects.create(
knowledge_base=base,
file_name="支持手册.txt",
file_type="text",
extension="txt",
mime_type="text/plain",
file_size=7,
sha256="a" * 64,
storage_path="knowledge-documents/manual.txt",
scan_status="clean",
parse_status="success",
)
monkeypatch.setattr("apps.knowledge.api.get_object", lambda path: b"content")
response = admin_client.get(f"/api/knowledge/documents/{document.public_id}/download")
assert response.status_code == 200
assert response.content == b"content"
assert response["Content-Type"] == "text/plain"
assert "attachment" in response["Content-Disposition"]
def test_download_infected_document_is_blocked(admin_client, db, monkeypatch):
base = KnowledgeBase.objects.create(name="文件知识库")
document = KnowledgeDocument.objects.create(
knowledge_base=base,
file_name="可疑文件.txt",
storage_path="knowledge-documents/infected.txt",
scan_status="infected",
parse_status="failed",
)
monkeypatch.setattr(
"apps.knowledge.api.get_object",
lambda path: (_ for _ in ()).throw(AssertionError("隔离文件不应从对象存储读取")),
)
response = admin_client.get(f"/api/knowledge/documents/{document.public_id}/download")
assert response.status_code == 423
@override_settings(KNOWLEDGE_EMBEDDING_ALLOW_HASH_FALLBACK=False)
def test_knowledge_draft_creation_does_not_require_embedding_deployment(admin_client, db):
base = KnowledgeBase.objects.create(name="正式知识库")
response = admin_client.post(
"/api/knowledge/items",
data={
"knowledge_base_id": base.id,
"title": "订单超时",
"problem": "订单接口超时",
"solution": "检查下游连接池",
"status": "published",
},
content_type="application/json",
)
assert response.status_code == 200
item = KnowledgeItem.objects.get(title="订单超时")
assert item.status == "draft"
assert item.chunks.count() == 0