"""U6 测试 — KB 设置模型与存储。 测试场景: 1. KBSettings 默认值 2. KBSettingsUpdate 部分更新语义 3. KBSettingsStore CRUD(get / get_or_create / update) 4. owner 校验(is_owner / set_owner) 5. KB 设置默认模式生效(与 HitProcessor 集成) """ from __future__ import annotations from agentkit.rag_platform.hit_processing import ( HIT_PROCESSING_DIRECT, HIT_PROCESSING_MODEL_OPT, ) from agentkit.rag_platform.models import QueryMode from agentkit.rag_platform.settings import ( KBSettings, KBSettingsStore, KBSettingsUpdate, ) # --------------------------------------------------------------------------- # KBSettings 模型测试 # --------------------------------------------------------------------------- class TestKBSettings: """KBSettings 模型测试。""" def test_defaults(self): """默认值正确。""" settings = KBSettings(kb_id="kb1") assert settings.kb_id == "kb1" assert settings.owner is None assert settings.default_query_mode == QueryMode.blend assert settings.default_hit_processing == HIT_PROCESSING_MODEL_OPT assert settings.caching_disabled is False assert settings.rerank_enabled is True assert settings.rerank_provider == "cohere" assert settings.rerank_api_key is None assert settings.rerank_base_url is None assert settings.data_export_warning is False def test_custom_values(self): """自定义值正确。""" settings = KBSettings( kb_id="kb1", owner="user1", default_query_mode=QueryMode.keywords, default_hit_processing=HIT_PROCESSING_DIRECT, caching_disabled=True, rerank_enabled=False, rerank_provider="bge", rerank_api_key="key-123", rerank_base_url="http://localhost:9997", data_export_warning=True, ) assert settings.owner == "user1" assert settings.default_query_mode == QueryMode.keywords assert settings.default_hit_processing == HIT_PROCESSING_DIRECT assert settings.caching_disabled is True assert settings.rerank_enabled is False assert settings.rerank_provider == "bge" assert settings.rerank_api_key == "key-123" assert settings.rerank_base_url == "http://localhost:9997" assert settings.data_export_warning is True # --------------------------------------------------------------------------- # KBSettingsUpdate 模型测试 # --------------------------------------------------------------------------- class TestKBSettingsUpdate: """KBSettingsUpdate 模型测试。""" def test_all_none_by_default(self): """默认所有字段为 None(部分更新语义)。""" update = KBSettingsUpdate() assert update.default_query_mode is None assert update.default_hit_processing is None assert update.caching_disabled is None assert update.rerank_enabled is None assert update.rerank_provider is None assert update.rerank_api_key is None assert update.rerank_base_url is None def test_partial_update(self): """部分字段赋值。""" update = KBSettingsUpdate(default_hit_processing=HIT_PROCESSING_DIRECT) assert update.default_hit_processing == HIT_PROCESSING_DIRECT assert update.default_query_mode is None def test_exclude_none_dumps(self): """model_dump(exclude_none=True) 仅包含已设置字段。""" update = KBSettingsUpdate(caching_disabled=True) data = update.model_dump(exclude_none=True) assert data == {"caching_disabled": True} def test_full_update(self): """所有字段同时赋值。""" update = KBSettingsUpdate( default_query_mode=QueryMode.embedding, default_hit_processing=HIT_PROCESSING_DIRECT, caching_disabled=True, rerank_enabled=False, rerank_provider="none", rerank_api_key="key", rerank_base_url="http://x", ) data = update.model_dump(exclude_none=True) assert len(data) == 7 # --------------------------------------------------------------------------- # KBSettingsStore CRUD 测试 # --------------------------------------------------------------------------- class TestKBSettingsStoreCRUD: """KBSettingsStore 存储测试。""" async def test_get_settings_not_exist(self): """不存在的 KB 返回 None。""" store = KBSettingsStore() assert await store.get_settings("kb1") is None async def test_get_or_create_creates_defaults(self): """get_or_create 创建默认设置。""" store = KBSettingsStore() settings = await store.get_or_create("kb1", owner="user1") assert settings.kb_id == "kb1" assert settings.owner == "user1" assert settings.default_hit_processing == HIT_PROCESSING_MODEL_OPT async def test_get_or_create_idempotent(self): """get_or_create 幂等 — 已存在时不覆盖 owner。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") second = await store.get_or_create("kb1", owner="user2") assert second.owner == "user1" # 不覆盖已有 owner async def test_update_settings_creates_if_not_exist(self): """update_settings 在设置不存在时创建。""" store = KBSettingsStore() update = KBSettingsUpdate(caching_disabled=True) result = await store.update_settings("kb1", update, owner="user1") assert result.caching_disabled is True assert result.owner == "user1" assert result.kb_id == "kb1" async def test_update_settings_partial(self): """update_settings 仅更新提供的字段,其他字段保持不变。""" store = KBSettingsStore() # 先创建默认设置 await store.get_or_create("kb1", owner="user1") # 部分更新 update = KBSettingsUpdate(default_hit_processing=HIT_PROCESSING_DIRECT) result = await store.update_settings("kb1", update) assert result.default_hit_processing == HIT_PROCESSING_DIRECT # 其他字段保持默认 assert result.default_query_mode == QueryMode.blend assert result.caching_disabled is False assert result.owner == "user1" async def test_update_settings_multiple_fields(self): """update_settings 同时更新多个字段。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") update = KBSettingsUpdate( caching_disabled=True, rerank_provider="bge", rerank_enabled=False, ) result = await store.update_settings("kb1", update) assert result.caching_disabled is True assert result.rerank_provider == "bge" assert result.rerank_enabled is False async def test_update_settings_persists(self): """update_settings 后 get_settings 返回更新后的值。""" store = KBSettingsStore() await store.update_settings("kb1", KBSettingsUpdate(caching_disabled=True), owner="user1") retrieved = await store.get_settings("kb1") assert retrieved is not None assert retrieved.caching_disabled is True async def test_update_settings_none_field_ignored(self): """update_settings 中 None 字段被忽略。""" store = KBSettingsStore() await store.update_settings( "kb1", KBSettingsUpdate(caching_disabled=True, default_hit_processing=HIT_PROCESSING_DIRECT), owner="user1", ) # 第二次更新 — 只改 caching_disabled,default_hit_processing 应保持 await store.update_settings( "kb1", KBSettingsUpdate(caching_disabled=False), ) result = await store.get_settings("kb1") assert result is not None assert result.caching_disabled is False assert result.default_hit_processing == HIT_PROCESSING_DIRECT # --------------------------------------------------------------------------- # owner 校验测试 # --------------------------------------------------------------------------- class TestOwnerCheck: """owner 校验测试。""" async def test_is_owner_true(self): """owner 匹配返回 True。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") assert store.is_owner("kb1", "user1") is True async def test_is_owner_false_wrong_user(self): """非 owner 用户返回 False。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") assert store.is_owner("kb1", "user2") is False async def test_is_owner_false_none_user(self): """None 用户返回 False。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") assert store.is_owner("kb1", None) is False async def test_is_owner_false_nonexistent_kb(self): """不存在的 KB 返回 False。""" store = KBSettingsStore() assert store.is_owner("nonexistent", "user1") is False async def test_is_owner_false_no_owner_set(self): """owner 未设置时返回 False。""" store = KBSettingsStore() await store.get_or_create("kb1") # 不传 owner assert store.is_owner("kb1", "user1") is False async def test_set_owner(self): """set_owner 设置所有者。""" store = KBSettingsStore() await store.get_or_create("kb1", owner="user1") assert store.set_owner("kb1", "user2") is True assert store.is_owner("kb1", "user2") is True assert store.is_owner("kb1", "user1") is False async def test_set_owner_nonexistent(self): """set_owner 对不存在的 KB 返回 False。""" store = KBSettingsStore() assert store.set_owner("nonexistent", "user1") is False # --------------------------------------------------------------------------- # KB 默认模式生效测试(与 HitProcessor 集成) # --------------------------------------------------------------------------- class TestKBSettingsDefaultModeIntegration: """KB 设置默认模式与 HitProcessor 集成测试。""" async def test_default_model_opt_flows_to_processor(self): """KB 默认 model_opt 模式传递给 HitProcessor。""" from unittest.mock import AsyncMock, MagicMock from agentkit.rag_platform.hit_processing import HitProcessor from agentkit.rag_platform.models import QueryResult mock_llm = MagicMock() mock_resp = MagicMock() mock_resp.content = "LLM 回答" mock_llm.chat = AsyncMock(return_value=mock_resp) store = KBSettingsStore() settings = await store.get_or_create("kb1", owner="user1") processor = HitProcessor(llm_gateway=mock_llm, cache_enabled=False) results = [ QueryResult( chunk_id="c1", content="内容", score=0.9, metadata={}, document_id="d1", kb_id="kb1", ) ] result = await processor.process("query", results, mode=settings.default_hit_processing) assert result.mode == HIT_PROCESSING_MODEL_OPT mock_llm.chat.assert_awaited_once() async def test_default_direct_flows_to_processor(self): """KB 默认 direct 模式传递给 HitProcessor。""" from agentkit.rag_platform.hit_processing import HitProcessor from agentkit.rag_platform.models import QueryResult store = KBSettingsStore() await store.update_settings( "kb1", KBSettingsUpdate(default_hit_processing=HIT_PROCESSING_DIRECT), owner="user1", ) settings = await store.get_settings("kb1") assert settings is not None processor = HitProcessor(llm_gateway=None, cache_enabled=False) results = [ QueryResult( chunk_id="c1", content="直接段落", score=0.9, metadata={}, document_id="d1", kb_id="kb1", ) ] result = await processor.process("query", results, mode=settings.default_hit_processing) assert result.mode == HIT_PROCESSING_DIRECT assert "直接段落" in result.answer