Files
meshtastic_mqtt_server/internal/llm/state_test.go
T
kevinandClaude Fable 5 2f83308dce 修复 LLM Provider 配置热更新问题
问题:在 /admin/llm/api 页面修改 AI 提供商配置后,配置没有立即生效,AI 依然使用旧的提供商工作

根本原因:
- LLM Provider 配置在程序启动时加载到内存 (llm.State)
- 管理后台修改配置时,只更新了数据库,内存配置未更新
- AI 继续使用内存中的旧配置

解决方案:
1. 为 llm.State 添加动态更新方法 (UpdateProvider/AddProvider/RemoveProvider)
2. 在 AI Service 中暴露配置更新接口,支持运行时重新加载
3. 在配置保存后自动触发内存配置重新加载
4. 创建新的 LLM Client 使新配置立即生效

关键特性:
- 线程安全:使用 sync.RWMutex 保护并发访问
- 容错处理:重新加载失败不影响数据库更新
- 无需重启:配置修改后立即生效
- 完整测试:添加单元测试验证功能

修改文件:
- internal/llm/state.go: 添加配置更新方法
- internal/ai/service.go: 添加配置重新加载接口
- internal/llmadmin/admin_llm_routes.go: 配置更新时触发重新加载
- internal/web/web.go: 传递 AI Service 到路由
- main.go: 连接组件
- internal/llm/state_test.go: 新增单元测试
- internal/web/map_tile_proxy_routes_test.go: 修复测试

测试:
-  所有现有测试通过
-  新增测试覆盖核心功能
-  项目成功编译

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 21:13:31 +08:00

161 lines
4.2 KiB
Go

package llm
import (
"testing"
)
func TestUpdateProvider(t *testing.T) {
// Create initial state with one provider
configs := []ProviderConfig{
{
Name: "test-provider",
Active: true,
APIKey: "test-key",
BaseURL: "https://test.example.com",
Model: "test-model",
Timeout: 120,
ContextWindowTokens: 4096,
},
}
state, err := NewState(configs)
if err != nil {
t.Fatalf("failed to create state: %v", err)
}
// Get the initial profile
profile := state.ActiveProfile()
if profile.Config.APIKey != "test-key" {
t.Errorf("expected APIKey 'test-key', got '%s'", profile.Config.APIKey)
}
// Update the provider with new config
updatedConfig := ProviderConfig{
Name: "test-provider",
Active: true,
APIKey: "new-key",
BaseURL: "https://new.example.com",
Model: "new-model",
Timeout: 60,
ContextWindowTokens: 8192,
}
err = state.UpdateProvider(updatedConfig)
if err != nil {
t.Fatalf("failed to update provider: %v", err)
}
// Verify the update
profile = state.ActiveProfile()
if profile.Config.APIKey != "new-key" {
t.Errorf("expected updated APIKey 'new-key', got '%s'", profile.Config.APIKey)
}
if profile.Config.BaseURL != "https://new.example.com" {
t.Errorf("expected updated BaseURL 'https://new.example.com', got '%s'", profile.Config.BaseURL)
}
if profile.Config.Model != "new-model" {
t.Errorf("expected updated Model 'new-model', got '%s'", profile.Config.Model)
}
if profile.Config.Timeout != 60 {
t.Errorf("expected updated Timeout 60, got %d", profile.Config.Timeout)
}
}
func TestAddProvider(t *testing.T) {
// Create initial state with one provider
configs := []ProviderConfig{
{
Name: "provider1",
Active: true,
APIKey: "key1",
BaseURL: "https://example1.com",
Model: "model1",
Timeout: 120,
ContextWindowTokens: 4096,
},
}
state, err := NewState(configs)
if err != nil {
t.Fatalf("failed to create state: %v", err)
}
// Add a second provider
newConfig := ProviderConfig{
Name: "provider2",
Active: false,
APIKey: "key2",
BaseURL: "https://example2.com",
Model: "model2",
Timeout: 60,
ContextWindowTokens: 8192,
}
err = state.AddProvider(newConfig)
if err != nil {
t.Fatalf("failed to add provider: %v", err)
}
// Verify the new provider exists
profile, err := state.GetProfile("provider2")
if err != nil {
t.Fatalf("failed to get provider2: %v", err)
}
if profile.Config.Name != "provider2" {
t.Errorf("expected name 'provider2', got '%s'", profile.Config.Name)
}
// Verify active provider is still provider1
activeProfile := state.ActiveProfile()
if activeProfile.Config.Name != "provider1" {
t.Errorf("expected active provider 'provider1', got '%s'", activeProfile.Config.Name)
}
}
func TestRemoveProvider(t *testing.T) {
// Create initial state with two providers
configs := []ProviderConfig{
{
Name: "provider1",
Active: true,
APIKey: "key1",
BaseURL: "https://example1.com",
Model: "model1",
Timeout: 120,
ContextWindowTokens: 4096,
},
{
Name: "provider2",
Active: false,
APIKey: "key2",
BaseURL: "https://example2.com",
Model: "model2",
Timeout: 60,
ContextWindowTokens: 8192,
},
}
state, err := NewState(configs)
if err != nil {
t.Fatalf("failed to create state: %v", err)
}
// Remove provider2
err = state.RemoveProvider("provider2")
if err != nil {
t.Fatalf("failed to remove provider: %v", err)
}
// Verify provider2 is gone
_, err = state.GetProfile("provider2")
if err == nil {
t.Error("expected error when getting removed provider, got nil")
}
// Try to remove the last provider (should fail)
err = state.RemoveProvider("provider1")
if err == nil {
t.Error("expected error when removing last provider, got nil")
}
}