// config_test.go -- 配置系统的单元测试. // // 覆盖场景: // - defaultSettings 默认值 // - mergeSettings 多级合并 // - loadSettingsFile 从文件加载 // - SaveSettings 保存配置 // - LoadSettings 完整加载流程 package config import ( "encoding/json" "os" "path/filepath" "testing" ) // TestDefaultSettings 测试默认设置 func TestDefaultSettings(t *testing.T) { s := defaultSettings() if s.Permissions.DefaultMode != "default" { t.Errorf("默认模式应为 'default', 实际: %q", s.Permissions.DefaultMode) } if s.MCPServers == nil { t.Error("MCPServers 不应为 nil") } if s.Hooks == nil { t.Error("Hooks 不应为 nil") } } // TestMergeSettings 测试设置合并 func TestMergeSettings(t *testing.T) { dst := defaultSettings() src := &Settings{ Model: "claude-sonnet-4-6", MaxTurns: 100, CustomInstructions: "custom", Verbose: true, Permissions: PermissionSettings{ AllowedTools: []string{"Bash(prefix:npm)"}, DefaultMode: "accept_edits", }, } mergeSettings(dst, src) if dst.Model != "claude-sonnet-4-6" { t.Errorf("Model: %q", dst.Model) } if dst.MaxTurns != 100 { t.Errorf("MaxTurns: %d", dst.MaxTurns) } if dst.CustomInstructions != "custom" { t.Errorf("CustomInstructions: %q", dst.CustomInstructions) } if !dst.Verbose { t.Error("Verbose 应为 true") } if dst.Permissions.DefaultMode != "accept_edits" { t.Errorf("DefaultMode: %q", dst.Permissions.DefaultMode) } if len(dst.Permissions.AllowedTools) != 1 { t.Errorf("AllowedTools 数量: %d", len(dst.Permissions.AllowedTools)) } } // TestMergeSettings_ZeroValues 测试零值不覆盖 func TestMergeSettings_ZeroValues(t *testing.T) { dst := &Settings{ Model: "claude-opus-4-6", MaxTurns: 50, } src := &Settings{ // 零值不应覆盖 } mergeSettings(dst, src) if dst.Model != "claude-opus-4-6" { t.Errorf("零值不应覆盖 Model: %q", dst.Model) } if dst.MaxTurns != 50 { t.Errorf("零值不应覆盖 MaxTurns: %d", dst.MaxTurns) } } // TestMergeSettings_MCPServers 测试 MCP 服务器合并 func TestMergeSettings_MCPServers(t *testing.T) { dst := defaultSettings() dst.MCPServers["server1"] = MCPServerConfig{Name: "s1", Transport: "stdio"} src := &Settings{ MCPServers: map[string]MCPServerConfig{ "server2": {Name: "s2", Transport: "sse"}, }, } mergeSettings(dst, src) if len(dst.MCPServers) != 2 { t.Errorf("合并后应有 2 个服务器, 实际 %d", len(dst.MCPServers)) } } // TestLoadSettingsFile 测试从文件加载设置 func TestLoadSettingsFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.json") settings := &Settings{ Model: "claude-haiku-4-5", MaxTurns: 200, } data, _ := json.MarshalIndent(settings, "", " ") os.WriteFile(path, data, 0644) loaded, err := loadSettingsFile(path) if err != nil { t.Fatalf("加载失败: %v", err) } if loaded.Model != "claude-haiku-4-5" { t.Errorf("Model: %q", loaded.Model) } if loaded.MaxTurns != 200 { t.Errorf("MaxTurns: %d", loaded.MaxTurns) } } // TestLoadSettingsFile_NotExist 测试文件不存在 func TestLoadSettingsFile_NotExist(t *testing.T) { _, err := loadSettingsFile("/nonexistent/settings.json") if err == nil { t.Error("文件不存在应报错") } } // TestLoadSettingsFile_InvalidJSON 测试无效 JSON func TestLoadSettingsFile_InvalidJSON(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "bad.json") os.WriteFile(path, []byte("{invalid json"), 0644) _, err := loadSettingsFile(path) if err == nil { t.Error("无效 JSON 应报错") } } // TestSaveSettings 测试保存设置 func TestSaveSettings(t *testing.T) { dir := t.TempDir() settings := &Settings{Model: "claude-sonnet-4-6", MaxTurns: 100} err := SaveSettings(ScopeProject, dir, settings) if err != nil { t.Fatalf("保存失败: %v", err) } // 验证文件存在 path := filepath.Join(dir, ".flyto", "settings.json") if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatal("配置文件应存在") } // 验证内容可加载 loaded, err := loadSettingsFile(path) if err != nil { t.Fatalf("加载失败: %v", err) } if loaded.Model != "claude-sonnet-4-6" { t.Errorf("Model: %q", loaded.Model) } } // TestSaveSettings_DefaultScope 测试保存到 default scope 不执行 func TestSaveSettings_DefaultScope(t *testing.T) { err := SaveSettings(ScopeDefault, "/tmp", &Settings{}) if err != nil { t.Errorf("default scope 不应报错: %v", err) } } // TestLoadSettings_MultiLevel 测试多级设置加载 func TestLoadSettings_MultiLevel(t *testing.T) { dir := t.TempDir() // 创建项目级配置 projectDir := filepath.Join(dir, ".flyto") os.MkdirAll(projectDir, 0755) projectSettings := &Settings{ Model: "claude-sonnet-4-6", MaxTurns: 100, } data, _ := json.MarshalIndent(projectSettings, "", " ") os.WriteFile(filepath.Join(projectDir, "settings.json"), data, 0644) // 创建本地级配置(覆盖部分值) localSettings := &Settings{ MaxTurns: 200, Verbose: true, } data, _ = json.MarshalIndent(localSettings, "", " ") os.WriteFile(filepath.Join(projectDir, "settings.local.json"), data, 0644) settings, err := LoadSettings(dir) if err != nil { t.Fatalf("加载失败: %v", err) } // Model 来自项目级 if settings.Model != "claude-sonnet-4-6" { t.Errorf("Model 应来自项目级: %q", settings.Model) } // MaxTurns 来自本地级(覆盖项目级) if settings.MaxTurns != 200 { t.Errorf("MaxTurns 应被本地级覆盖: %d", settings.MaxTurns) } // Verbose 来自本地级 if !settings.Verbose { t.Error("Verbose 应来自本地级") } }