Files
2026-10-28 17:39:33 +08:00

290 lines
7.7 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"viewsagent/internal/llm"
)
func TestDefaultIsUsable(t *testing.T) {
cfg := Default()
cfg.Normalize()
cfg.ensureModel()
if len(cfg.Models) == 0 {
t.Fatal("首次运行应有一份空白接入点")
}
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()
cfg.ensureModel()
if len(cfg.Models) == 0 {
t.Fatal("首次运行应补上一份接入点")
}
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()
cfg.ensureModel()
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 TestRemoveModelStaysRemoved(t *testing.T) {
cfg := Default()
cfg.Models = []Profile{
{Name: "a", Model: "m-a"},
{Name: "b", Model: "m-b"},
{Name: "c", Model: "m-c"},
}
cfg.Active = "b"
cfg.Normalize()
if !cfg.RemoveModel("b") {
t.Fatal("应能删除接入点")
}
// 再归一化一次,模拟落盘后重新加载:被删的接入点不该复活。
cfg.Normalize()
if len(cfg.Models) != 2 {
t.Fatalf("删除后应只剩 2 个接入点,实际 %d: %+v", len(cfg.Models), cfg.Models)
}
for _, m := range cfg.Models {
if m.Name == "b" {
t.Fatalf("被删的接入点不该被补回来: %+v", cfg.Models)
}
}
active, ok := cfg.ActiveModel()
if !ok {
t.Fatal("应自动选中一个剩余接入点")
}
if active.Name != "a" && active.Name != "c" {
t.Fatalf("生效项应指向剩余的接入点,实际 %s", active.Name)
}
}
func TestNormalizeKeepsEmptyModelList(t *testing.T) {
// 用户把接入点删光后,Normalize 不该再偷偷补一个回来,
// 否则"删除"看起来完全没生效。
cfg := Default()
cfg.Normalize()
if len(cfg.Models) != 0 {
t.Fatalf("Normalize 不应凭空补接入点: %+v", cfg.Models)
}
if cfg.Active != "" {
t.Fatalf("没有接入点时 Active 应为空,实际 %s", cfg.Active)
}
}
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)
}
}