feat: add governed knowledge lifecycle
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem
|
||||
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):
|
||||
@@ -23,7 +34,7 @@ def test_create_and_list_knowledge_base(admin_client, db):
|
||||
assert response.json()[0]["name"] == "税务采集知识库"
|
||||
|
||||
|
||||
def test_create_knowledge_item_builds_chunks(admin_client, db):
|
||||
def test_create_knowledge_item_always_creates_governed_draft(admin_client, db):
|
||||
base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集")
|
||||
|
||||
response = admin_client.post(
|
||||
@@ -45,8 +56,33 @@ def test_create_knowledge_item_builds_chunks(admin_client, db):
|
||||
|
||||
assert response.status_code == 200
|
||||
item = KnowledgeItem.objects.get(id=response.json()["id"])
|
||||
assert item.status == "published"
|
||||
assert KnowledgeChunk.objects.filter(item=item).count() >= 1
|
||||
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):
|
||||
@@ -67,3 +103,223 @@ def test_ingest_document_text_creates_document_and_chunks(admin_client, db):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user