"""Tests for Knowledge Base Management API routes""" from __future__ import annotations import io from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient from agentkit.llm.gateway import LLMGateway from agentkit.server.app import create_app from agentkit.server.routes.kb_management import KnowledgeSourceStore, KnowledgeSource from agentkit.skills.base import Skill, SkillConfig from agentkit.skills.registry import SkillRegistry from agentkit.tools.registry import ToolRegistry # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def mock_llm_gateway(): gateway = LLMGateway() return gateway @pytest.fixture def skill_registry(): return SkillRegistry() @pytest.fixture def tool_registry(): return ToolRegistry() @pytest.fixture def app(mock_llm_gateway, skill_registry, tool_registry): return create_app( llm_gateway=mock_llm_gateway, skill_registry=skill_registry, tool_registry=tool_registry, ) @pytest.fixture def client(app): return TestClient(app) # --------------------------------------------------------------------------- # KnowledgeSourceStore unit tests # --------------------------------------------------------------------------- class TestKnowledgeSourceStore: def test_add_source(self): store = KnowledgeSourceStore() source = store.add_source("测试知识库", "local", {}) assert source.id is not None assert source.name == "测试知识库" assert source.type == "local" assert source.status == "active" def test_get_source(self): store = KnowledgeSourceStore() source = store.add_source("飞书知识库", "feishu", {"app_id": "test"}) retrieved = store.get_source(source.id) assert retrieved is not None assert retrieved.name == "飞书知识库" def test_get_source_not_found(self): store = KnowledgeSourceStore() assert store.get_source("nonexistent") is None def test_remove_source(self): store = KnowledgeSourceStore() source = store.add_source("待删除", "local", {}) assert store.remove_source(source.id) is True assert store.get_source(source.id) is None def test_remove_source_not_found(self): store = KnowledgeSourceStore() assert store.remove_source("nonexistent") is False def test_list_sources(self): store = KnowledgeSourceStore() store.add_source("源1", "local", {}) store.add_source("源2", "feishu", {}) sources = store.list_sources() assert len(sources) == 2 def test_list_sources_empty(self): store = KnowledgeSourceStore() assert store.list_sources() == [] def test_add_document_updates_source(self): store = KnowledgeSourceStore() source = store.add_source("本地文档", "local", {}) from agentkit.server.routes.kb_management import UploadedDocument doc = UploadedDocument( document_id="doc-1", filename="test.pdf", source_id=source.id, chunks=5, status="indexed", ) store.add_document(doc) updated_source = store.get_source(source.id) assert updated_source.document_count == 1 assert updated_source.last_synced is not None # --------------------------------------------------------------------------- # GET /kb-management/sources # --------------------------------------------------------------------------- class TestListSources: def test_list_sources_empty(self, client): response = client.get("/api/v1/kb-management/sources") assert response.status_code == 200 data = response.json() assert "sources" in data assert isinstance(data["sources"], list) def test_list_sources_after_add(self, client): # Add a source first client.post( "/api/v1/kb-management/sources", json={"name": "测试源", "type": "local", "config": {}}, ) response = client.get("/api/v1/kb-management/sources") assert response.status_code == 200 data = response.json() assert len(data["sources"]) >= 1 # --------------------------------------------------------------------------- # POST /kb-management/sources # --------------------------------------------------------------------------- class TestAddSource: def test_add_local_source(self, client): response = client.post( "/api/v1/kb-management/sources", json={"name": "本地文档", "type": "local", "config": {}}, ) assert response.status_code == 201 data = response.json() assert data["name"] == "本地文档" assert data["type"] == "local" assert data["status"] == "active" def test_add_feishu_source(self, client): response = client.post( "/api/v1/kb-management/sources", json={ "name": "飞书知识库", "type": "feishu", "config": {"app_id": "test", "app_secret": "secret"}, }, ) assert response.status_code == 201 data = response.json() assert data["type"] == "feishu" def test_add_confluence_source(self, client): response = client.post( "/api/v1/kb-management/sources", json={ "name": "Confluence", "type": "confluence", "config": {"base_url": "https://wiki.example.com"}, }, ) assert response.status_code == 201 def test_add_http_source(self, client): response = client.post( "/api/v1/kb-management/sources", json={ "name": "HTTP API", "type": "http", "config": {"url": "https://api.example.com/kb"}, }, ) assert response.status_code == 201 def test_add_source_invalid_type(self, client): response = client.post( "/api/v1/kb-management/sources", json={"name": "无效类型", "type": "invalid", "config": {}}, ) assert response.status_code == 422 # --------------------------------------------------------------------------- # DELETE /kb-management/sources/{source_id} # --------------------------------------------------------------------------- class TestRemoveSource: def test_remove_source(self, client): add_resp = client.post( "/api/v1/kb-management/sources", json={"name": "待删除", "type": "local", "config": {}}, ) source_id = add_resp.json()["id"] response = client.delete(f"/api/v1/kb-management/sources/{source_id}") assert response.status_code == 200 assert response.json()["status"] == "removed" def test_remove_source_not_found(self, client): response = client.delete("/api/v1/kb-management/sources/nonexistent") assert response.status_code == 404 # --------------------------------------------------------------------------- # POST /kb-management/documents/upload # --------------------------------------------------------------------------- class TestUploadDocument: def test_upload_document(self, client): file_content = b"This is a test document content for upload." response = client.post( "/api/v1/kb-management/documents/upload", files={"file": ("test.txt", io.BytesIO(file_content), "text/plain")}, ) assert response.status_code == 200 data = response.json() assert data["filename"] == "test.txt" assert data["document_id"] is not None assert data["status"] == "indexed" assert data["chunks"] >= 1 def test_upload_document_with_source_id(self, client): # Create a source first add_resp = client.post( "/api/v1/kb-management/sources", json={"name": "上传源", "type": "local", "config": {}}, ) source_id = add_resp.json()["id"] file_content = b"Test content with source ID." response = client.post( "/api/v1/kb-management/documents/upload", files={"file": ("doc.txt", io.BytesIO(file_content), "text/plain")}, data={"source_id": source_id}, ) assert response.status_code == 200 data = response.json() assert data["filename"] == "doc.txt" # --------------------------------------------------------------------------- # POST /kb-management/search # --------------------------------------------------------------------------- class TestSearchKnowledge: def test_search_returns_results(self, client): response = client.post( "/api/v1/kb-management/search", json={"query": "测试查询", "top_k": 5}, ) assert response.status_code == 200 data = response.json() assert "results" in data assert isinstance(data["results"], list) def test_search_with_sources_filter(self, client): response = client.post( "/api/v1/kb-management/search", json={"query": "测试", "sources": ["local"], "top_k": 3}, ) assert response.status_code == 200 def test_search_default_top_k(self, client): response = client.post( "/api/v1/kb-management/search", json={"query": "默认参数"}, ) assert response.status_code == 200 # --------------------------------------------------------------------------- # GET /kb-management/sources/{source_id}/health # --------------------------------------------------------------------------- class TestSourceHealth: def test_health_local_source(self, client): add_resp = client.post( "/api/v1/kb-management/sources", json={"name": "本地", "type": "local", "config": {}}, ) source_id = add_resp.json()["id"] response = client.get(f"/api/v1/kb-management/sources/{source_id}/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" assert data["source_id"] == source_id def test_health_external_source(self, client): add_resp = client.post( "/api/v1/kb-management/sources", json={"name": "外部", "type": "feishu", "config": {}}, ) source_id = add_resp.json()["id"] response = client.get(f"/api/v1/kb-management/sources/{source_id}/health") assert response.status_code == 200 data = response.json() assert data["status"] == "unknown" def test_health_source_not_found(self, client): response = client.get("/api/v1/kb-management/sources/nonexistent/health") assert response.status_code == 404