import pytest from app.core.contracts import RequestContext from app.core.knowledge_contracts import KnowledgeHit, KnowledgeQuery, KnowledgeSearchResult from app.service import knowledge_tool_service from app.service.knowledge_tool_service import DatabaseEmbeddingAdapter, query_knowledge_tool class FakeGateway: def __init__(self) -> None: self.calls: list[tuple[str, str, int]] = [] async def embed(self, *, endpoint_code: str, text: str, timeout_ms: int) -> list[float]: self.calls.append((endpoint_code, text, timeout_ms)) return [0.1] * 1024 @pytest.mark.asyncio async def test_embedding_adapter_uses_single_text_gateway_contract() -> None: gateway = FakeGateway() adapter = DatabaseEmbeddingAdapter("knowledge-embedding", 15000, gateway=gateway) vector = await adapter.embed("基金开户") assert len(vector) == 1024 assert gateway.calls == [("knowledge-embedding", "基金开户", 15000)] @pytest.mark.asyncio async def test_query_tool_degrades_when_embedding_endpoint_is_unconfigured(monkeypatch) -> None: class Settings: knowledge_embedding_endpoint_code = "" monkeypatch.setattr("app.service.knowledge_tool_service.get_settings", lambda: Settings()) result = await query_knowledge_tool( KnowledgeQuery(query="基金开户", intents=("faq",)), RequestContext( user_id="visitor-1", trace_id="trace", roles=("visitor",), data_scope="public" ), ) assert result.degraded is True assert result.degradation_reason == "embedding_endpoint_unconfigured" @pytest.mark.asyncio async def test_query_tool_uses_configured_embedding_endpoint_and_read_only_dependencies( monkeypatch, ) -> None: class Settings: knowledge_embedding_endpoint_code = "knowledge-embedding" knowledge_embedding_timeout_ms = 15000 milvus_uri = "http://milvus:19530" milvus_token = "" class FakeGateway: calls: list[tuple[str, str, int]] = [] async def embed( self, *, endpoint_code: str, text: str, timeout_ms: int ) -> list[float]: self.calls.append((endpoint_code, text, timeout_ms)) return [0.1] * 1024 class FakeMilvus: def __init__(self, uri: str, token: str | None) -> None: self.uri = uri self.token = token class FakeSession: async def __aenter__(self) -> object: return object() async def __aexit__(self, exc_type, exc, traceback) -> None: return None class FakeAuthority: def __init__(self, session: object) -> None: self.session = session class FakeRetrievalService: def __init__(self, embedder, vector_store, config, authority) -> None: self.embedder = embedder self.vector_store = vector_store self.config = config self.authority = authority async def search( self, query: KnowledgeQuery, context: RequestContext ) -> KnowledgeSearchResult: vector = await self.embedder.embed(query.query) assert len(vector) == 1024 assert isinstance(self.vector_store, FakeMilvus) assert isinstance(self.authority, FakeAuthority) assert self.config.routes["faq"] == ("fin_faq_collection", 3) assert context.data_scope == "public" return KnowledgeSearchResult( hits=( KnowledgeHit( knowledge_id="1", collection="fin_faq_collection", snippet="snippet", answer="answer", ), ), searched_collections=("fin_faq_collection",), ) gateway = FakeGateway() monkeypatch.setattr(knowledge_tool_service, "get_settings", lambda: Settings()) monkeypatch.setattr(knowledge_tool_service, "DatabaseModelGateway", lambda: gateway) monkeypatch.setattr(knowledge_tool_service, "MilvusKnowledgeClient", FakeMilvus) monkeypatch.setattr(knowledge_tool_service, "KnowledgeMysqlAuthority", FakeAuthority) monkeypatch.setattr( knowledge_tool_service, "KnowledgeRetrievalService", FakeRetrievalService ) monkeypatch.setattr(knowledge_tool_service, "SessionFactory", FakeSession) result = await query_knowledge_tool( KnowledgeQuery(query="基金开户", intents=("faq",)), RequestContext( user_id="visitor-1", trace_id="trace", roles=("visitor",), data_scope="public", ), ) assert result.hits[0].answer == "answer" assert gateway.calls == [("knowledge-embedding", "基金开户", 15000)]