290 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|