|
3 | 3 |
|
4 | 4 | from rich.console import Console |
5 | 5 |
|
| 6 | +from src.runtime.contracts import SessionConfig |
| 7 | +from src.utils.config import ModelDetail, RetrievalMethod |
6 | 8 | from src.chat.core import Chatbot, start_chat_session_async |
7 | 9 |
|
8 | 10 |
|
@@ -40,6 +42,9 @@ def __init__(self, _console: Console): |
40 | 42 | def reload_llm(self): |
41 | 43 | return True |
42 | 44 |
|
| 45 | + def apply_config_update(self, updated_config, _llm_needs_reload): |
| 46 | + self.chat_config = updated_config |
| 47 | + |
43 | 48 | async def chat_async(self, _user_input): |
44 | 49 | if False: |
45 | 50 | yield "" |
@@ -143,3 +148,113 @@ async def consume(): |
143 | 148 |
|
144 | 149 | assert retrieval_queries == ["清除浏览器缓存的操作步骤"] |
145 | 150 | assert chunks == ["回答"] |
| 151 | + |
| 152 | + |
| 153 | +def test_reload_llm_keeps_existing_model_on_failure(monkeypatch): |
| 154 | + class OldModel: |
| 155 | + async def ainvoke(self, *_args, **_kwargs): |
| 156 | + if False: |
| 157 | + yield "" |
| 158 | + |
| 159 | + bot = object.__new__(Chatbot) |
| 160 | + bot.console = Console() |
| 161 | + bot.session_config = SimpleNamespace(active_llm_configuration="new-model") |
| 162 | + bot.retrieval_service = object() |
| 163 | + bot.llm_model = old_model = OldModel() |
| 164 | + bot.chat_service = old_chat_service = object() |
| 165 | + |
| 166 | + monkeypatch.setattr( |
| 167 | + "src.chat.core.ModelProviderFactory.get_llm_provider", |
| 168 | + lambda _llm_key: (_ for _ in ()).throw(RuntimeError("reload failed")), |
| 169 | + ) |
| 170 | + |
| 171 | + assert bot.reload_llm() is False |
| 172 | + assert bot.llm_model is old_model |
| 173 | + assert bot.chat_service is old_chat_service |
| 174 | + |
| 175 | + |
| 176 | +def test_apply_config_update_reverts_llm_key_when_reload_fails(monkeypatch): |
| 177 | + class DummySessionConfig: |
| 178 | + def __init__(self, active_llm_configuration: str): |
| 179 | + self.active_llm_configuration = active_llm_configuration |
| 180 | + |
| 181 | + def __setitem__(self, key, value): |
| 182 | + setattr(self, key, value) |
| 183 | + |
| 184 | + messages = [] |
| 185 | + bot = object.__new__(Chatbot) |
| 186 | + bot.console = SimpleNamespace(print=lambda message: messages.append(str(message))) |
| 187 | + bot.session_config = DummySessionConfig(active_llm_configuration="old-model") |
| 188 | + bot.llm_model = object() |
| 189 | + bot.chat_service = object() |
| 190 | + monkeypatch.setattr(bot, "reload_llm", lambda: False) |
| 191 | + |
| 192 | + bot.apply_config_update({"active_llm_configuration": "new-model"}, llm_needs_reload=True) |
| 193 | + |
| 194 | + assert bot.session_config.active_llm_configuration == "old-model" |
| 195 | + assert any("LLM 切换失败" in message for message in messages) |
| 196 | + |
| 197 | + |
| 198 | +def test_start_chat_session_async_reverts_llm_after_editor_mutation(monkeypatch): |
| 199 | + class RealishChatbot(FakeChatbot): |
| 200 | + last_instance = None |
| 201 | + |
| 202 | + def __init__(self, _console: Console): |
| 203 | + RealishChatbot.last_instance = self |
| 204 | + self.console = _console |
| 205 | + self.llm_model = object() |
| 206 | + self.session_config = SessionConfig( |
| 207 | + retrieval_method=RetrievalMethod.HYBRID_SEARCH, |
| 208 | + vector_weight=0.3, |
| 209 | + keyword_weight=0.7, |
| 210 | + hybrid_fusion_strategy="rrf", |
| 211 | + retrieval_candidate_multiplier=3, |
| 212 | + rerank_enabled=False, |
| 213 | + top_k=5, |
| 214 | + score_threshold=0.4, |
| 215 | + active_llm_configuration="old-model", |
| 216 | + active_rerank_configuration="siliconflow", |
| 217 | + llm_configurations={"old-model": ModelDetail(provider="openai", model_name="qwen3-max")}, |
| 218 | + rerank_configurations={"siliconflow": ModelDetail(provider="siliconflow", model_name="rerank")}, |
| 219 | + chat_temperature=0.7, |
| 220 | + ) |
| 221 | + self.chat_service = object() |
| 222 | + |
| 223 | + @property |
| 224 | + def chat_config(self): |
| 225 | + return self.session_config |
| 226 | + |
| 227 | + @chat_config.setter |
| 228 | + def chat_config(self, value): |
| 229 | + if isinstance(value, SessionConfig): |
| 230 | + self.session_config = value |
| 231 | + return |
| 232 | + for key, item in value.items(): |
| 233 | + self.session_config[key] = item |
| 234 | + |
| 235 | + def reload_llm(self): |
| 236 | + return False |
| 237 | + |
| 238 | + def apply_config_update(self, updated_config, llm_needs_reload): |
| 239 | + return Chatbot.apply_config_update(self, updated_config, llm_needs_reload) |
| 240 | + |
| 241 | + monkeypatch.setattr("src.chat.core.Chatbot", RealishChatbot) |
| 242 | + monkeypatch.setattr( |
| 243 | + "src.chat.core.PromptSession", |
| 244 | + lambda: FakePromptSession(["/config", "/quit"]), |
| 245 | + ) |
| 246 | + monkeypatch.setattr("src.chat.core.display_chat_config", lambda *_args, **_kwargs: None) |
| 247 | + |
| 248 | + edited_configs = [] |
| 249 | + |
| 250 | + def fake_launch(chat_config): |
| 251 | + edited_configs.append(chat_config) |
| 252 | + chat_config["active_llm_configuration"] = "new-model" |
| 253 | + return True, chat_config |
| 254 | + |
| 255 | + monkeypatch.setattr("src.chat.core.launch_config_editor", fake_launch) |
| 256 | + |
| 257 | + asyncio.run(start_chat_session_async()) |
| 258 | + |
| 259 | + assert edited_configs[0]["active_llm_configuration"] == "new-model" |
| 260 | + assert RealishChatbot.last_instance.session_config.active_llm_configuration == "old-model" |
0 commit comments