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