package config import ( "os" "path/filepath" "strings" "testing" "viewsagent/internal/llm" ) func TestDefaultIsUsable(t *testing.T) { cfg := Default() cfg.Normalize() if len(cfg.Models) == 0 { t.Fatal("Normalize 后应至少有一份接入点") } if _, ok := cfg.ActiveModel(); !ok { t.Fatal("Normalize 后应有生效接入点") } if cfg.Capture.FPS <= 0 || cfg.Capture.MaxEdge <= 0 || cfg.Capture.CooldownMs <= 0 { t.Fatalf("抓屏参数不应为零值: %+v", cfg.Capture) } if tmpl := cfg.Template("exercise"); tmpl.Key != "exercise" { t.Fatalf("应能取到习题模板,实际 %s", tmpl.Key) } if tmpl := cfg.Template("not-exist"); tmpl.Key == "" { t.Fatal("未知模板应回退到第一个模板") } } func TestNormalizeFillsZeroValues(t *testing.T) { cfg := &Config{} cfg.Normalize() if len(cfg.Models) == 0 { t.Fatal("Normalize 应补全接入点列表") } if cfg.Models[0].Format != llm.FormatOpenAI { t.Fatalf("默认接入点应为 OpenAI 兼容格式,实际 %s", cfg.Models[0].Format) } if cfg.Models[0].MaxTokens <= 0 || cfg.Models[0].Temperature <= 0 { t.Fatalf("Normalize 应补全采样默认值: %+v", cfg.Models[0]) } if len(cfg.Templates) == 0 { t.Fatal("Normalize 应补全模板") } if cfg.Capture.JPEGQuality == 0 || cfg.Capture.StableFrames == 0 || cfg.Capture.HistoryTurns == 0 { t.Fatalf("Normalize 应补全抓屏参数: %+v", cfg.Capture) } } func TestNormalizeKeepsCustomModel(t *testing.T) { cfg := Default() cfg.Models = []Profile{{ Name: "我的网关", Format: llm.FormatAnthropic, BaseURL: "https://my-gateway.example.com", Model: "my-vl-model", }} cfg.Normalize() p := cfg.Models[0] if p.BaseURL != "https://my-gateway.example.com" || p.Model != "my-vl-model" { t.Fatalf("Normalize 不应覆盖手填的模型配置: %+v", p) } if p.Format != llm.FormatAnthropic { t.Fatalf("Normalize 不应改写接口格式: %s", p.Format) } } func TestNormalizeForcesUniqueNames(t *testing.T) { cfg := &Config{Models: []Profile{ {Name: "同名", Format: llm.FormatOpenAI}, {Name: "同名", Format: llm.FormatOpenAI}, }} cfg.Normalize() if cfg.Models[0].Name == cfg.Models[1].Name { t.Fatalf("重名接入点应被自动改名: %+v", cfg.Models) } if _, ok := cfg.ActiveModel(); !ok { t.Fatal("改名后仍应有生效接入点") } } func TestNormalizeTheme(t *testing.T) { cases := map[string]string{ "": "system", "system": "system", "contrast": "contrast", "light": "light", "dark": "system", "Contrast": "system", "nonsense": "system", } for in, want := range cases { cfg := &Config{Theme: in} cfg.Normalize() if cfg.Theme != want { t.Fatalf("主题 %q 应归一化为 %q,实际 %q", in, want, cfg.Theme) } } } func TestCloneIsIndependent(t *testing.T) { cfg := Default() cfg.Normalize() cp := cfg.Clone() cp.Models[0].Model = "changed" cp.Models[0].ExtraHeaders = map[string]string{"X-Test": "1"} cp.Templates[0].System = "changed" cp.Capture.FPS = 9 if cfg.Models[0].Model == "changed" { t.Fatal("Clone 后修改模型不应影响原配置") } if len(cfg.Models[0].ExtraHeaders) != 0 { t.Fatal("Clone 后修改请求头不应影响原配置") } if cfg.Templates[0].System == "changed" { t.Fatal("Clone 后修改模板不应影响原配置") } if cfg.Capture.FPS == 9 { t.Fatal("Clone 后修改抓屏参数不应影响原配置") } } func TestAddProfileMakesUniqueName(t *testing.T) { cfg := Default() cfg.Normalize() added := cfg.AddProfile(NewProfile("接入点", llm.FormatAnthropic)) if cfg.ProfileIndex(added.Name) < 0 { t.Fatalf("新增的接入点应能在列表里找到: %+v", cfg.Models) } again := cfg.AddProfile(NewProfile("接入点", llm.FormatOpenAI)) if again.Name == added.Name { t.Fatalf("重名接入点应自动改名: %s", again.Name) } } func TestSetActive(t *testing.T) { cfg := Default() cfg.Models = []Profile{{Name: "a"}, {Name: "b"}} cfg.Normalize() if !cfg.SetActive("b") { t.Fatal("应能切换到存在的接入点") } if p, ok := cfg.ActiveModel(); !ok || p.Name != "b" { t.Fatalf("生效接入点应为 b,实际 %+v", cfg) } if cfg.SetActive("不存在") { t.Fatal("不存在的接入点不应被设为生效") } } func TestRemoveModelClearsActive(t *testing.T) { cfg := Default() cfg.Models = []Profile{{Name: "a"}, {Name: "b"}} cfg.Active = "a" cfg.Normalize() if !cfg.RemoveModel("a") { t.Fatal("应能删除接入点") } if cfg.Active != "" { t.Fatalf("删掉生效接入点后应清空 Active,实际 %s", cfg.Active) } if _, ok := cfg.ActiveModel(); ok { t.Fatal("删掉生效接入点后不应再能取到它") } } func TestMigrateLegacyProfiles(t *testing.T) { cfg := Default() cfg.migrateLegacy(legacyConfig{ ActiveProfile: "doubao", Profiles: []Profile{ {Name: "qwen", BaseURL: "https://qwen.example/v1", Model: "qwen-vl"}, {Name: "doubao", BaseURL: "https://doubao.example/v1", Model: "doubao-vl", APIKey: "sk-old"}, }, }) cfg.Normalize() active, ok := cfg.ActiveModel() if !ok { t.Fatal("应迁移出原启用接入点") } if active.Model != "doubao-vl" { t.Fatalf("应迁移原启用接入点的模型,实际 %s", active.Model) } if active.APIKey != "sk-old" { t.Fatal("应迁移原 API Key") } if active.Format != llm.FormatOpenAI { t.Fatalf("旧格式一律按 OpenAI 兼容迁移,实际 %s", active.Format) } } func TestStoreLoadMigratesLegacyFile(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") legacy := `{ "activeProfile": "doubao", "profiles": [ {"name": "qwen", "baseURL": "https://qwen.example/v1", "model": "qwen-vl"}, {"name": "doubao", "baseURL": "https://doubao.example/v1", "model": "doubao-vl", "apiKey": "sk-legacy"} ] }` if err := os.WriteFile(path, []byte(legacy), 0o644); err != nil { t.Fatal(err) } s := &Store{path: path} cfg := s.Load() active, ok := cfg.ActiveModel() if !ok { t.Fatal("应迁移出生效接入点") } if active.Model != "doubao-vl" || active.APIKey != "sk-legacy" { t.Fatalf("应迁移原模型配置: %+v", active) } if err := s.Save(cfg); err != nil { t.Fatal(err) } raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if strings.Contains(string(raw), "activeProfile") || strings.Contains(string(raw), "profiles") { t.Fatalf("保存后不应再写入旧接入点字段: %s", raw) } if !strings.Contains(string(raw), `"models"`) { t.Fatalf("保存后应写入新的接入点列表: %s", raw) } }