package config import ( "os" "path/filepath" "testing" ) // ==================== DefaultAppConfig 测试 ==================== func TestDefaultAppConfig(t *testing.T) { cfg := DefaultAppConfig() if cfg.LogLevel != "info" { t.Errorf("默认 LogLevel 应为 info, got: %s", cfg.LogLevel) } if cfg.LogFile != "" { t.Errorf("默认 LogFile 应为空, got: %s", cfg.LogFile) } if cfg.DefaultProfile != "standard" { t.Errorf("默认 DefaultProfile 应为 standard, got: %s", cfg.DefaultProfile) } if cfg.Restore.ConflictStrategy != "prompt" { t.Errorf("默认 ConflictStrategy 应为 prompt, got: %s", cfg.Restore.ConflictStrategy) } if !cfg.Restore.CreateRestorePoint { t.Error("默认 CreateRestorePoint 应为 true") } if cfg.Restore.DryRunFirst { t.Error("默认 DryRunFirst 应为 false") } } // ==================== LoadConfig 测试 ==================== func TestLoadConfig_FileNotExist(t *testing.T) { cfg, err := LoadConfig("/not/exist/config.yaml") if err != nil { t.Fatalf("文件不存在时不应报错, got: %v", err) } // 应返回默认配置 if cfg.LogLevel != "info" { t.Errorf("应返回默认 LogLevel, got: %s", cfg.LogLevel) } if cfg.DefaultProfile != "standard" { t.Errorf("应返回默认 DefaultProfile, got: %s", cfg.DefaultProfile) } } func TestLoadConfig_ValidFile(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "config.yaml") content := `log_level: debug log_file: /tmp/devpack.log default_profile: golang-dev restore: conflict_strategy: overwrite create_restore_point: false dry_run_first: true ` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("写入测试文件失败: %v", err) } cfg, err := LoadConfig(path) if err != nil { t.Fatalf("加载配置不应报错, got: %v", err) } if cfg.LogLevel != "debug" { t.Errorf("LogLevel 应为 debug, got: %s", cfg.LogLevel) } if cfg.LogFile != "/tmp/devpack.log" { t.Errorf("LogFile 应为 /tmp/devpack.log, got: %s", cfg.LogFile) } if cfg.DefaultProfile != "golang-dev" { t.Errorf("DefaultProfile 应为 golang-dev, got: %s", cfg.DefaultProfile) } if cfg.Restore.ConflictStrategy != "overwrite" { t.Errorf("ConflictStrategy 应为 overwrite, got: %s", cfg.Restore.ConflictStrategy) } if cfg.Restore.CreateRestorePoint { t.Error("CreateRestorePoint 应为 false") } if !cfg.Restore.DryRunFirst { t.Error("DryRunFirst 应为 true") } } func TestLoadConfig_PartialFile(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "config.yaml") // 只设置部分字段,其余应保留默认值 content := `log_level: warn ` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("写入测试文件失败: %v", err) } cfg, err := LoadConfig(path) if err != nil { t.Fatalf("加载配置不应报错, got: %v", err) } // 显式设置的字段 if cfg.LogLevel != "warn" { t.Errorf("LogLevel 应为 warn, got: %s", cfg.LogLevel) } // 未设置的字段应保留默认值 if cfg.DefaultProfile != "standard" { t.Errorf("未设置的 DefaultProfile 应保留默认值 standard, got: %s", cfg.DefaultProfile) } if cfg.Restore.ConflictStrategy != "prompt" { t.Errorf("未设置的 ConflictStrategy 应保留默认值 prompt, got: %s", cfg.Restore.ConflictStrategy) } if !cfg.Restore.CreateRestorePoint { t.Error("未设置的 CreateRestorePoint 应保留默认值 true") } } func TestLoadConfig_InvalidYAML(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "config.yaml") content := `log_level: [invalid yaml this is broken` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("写入测试文件失败: %v", err) } _, err := LoadConfig(path) if err == nil { t.Fatal("无效 YAML 应返回错误") } } func TestLoadConfig_EmptyFile(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "config.yaml") if err := os.WriteFile(path, []byte(""), 0o644); err != nil { t.Fatalf("写入测试文件失败: %v", err) } cfg, err := LoadConfig(path) if err != nil { t.Fatalf("空文件不应报错, got: %v", err) } // 空文件应返回默认配置 if cfg.LogLevel != "info" { t.Errorf("空文件应保留默认 LogLevel, got: %s", cfg.LogLevel) } } // ==================== SaveConfig 测试 ==================== func TestSaveConfig(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "config.yaml") cfg := &AppConfig{ LogLevel: "debug", LogFile: "/var/log/devpack.log", DefaultProfile: "full", Restore: RestoreConfig{ ConflictStrategy: "skip", CreateRestorePoint: true, DryRunFirst: true, }, } if err := SaveConfig(path, cfg); err != nil { t.Fatalf("保存配置不应报错, got: %v", err) } // 验证文件已创建 if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatal("配置文件应已被创建") } // 重新加载验证内容一致 loaded, err := LoadConfig(path) if err != nil { t.Fatalf("重新加载不应报错, got: %v", err) } if loaded.LogLevel != cfg.LogLevel { t.Errorf("LogLevel 不匹配: want %s, got %s", cfg.LogLevel, loaded.LogLevel) } if loaded.LogFile != cfg.LogFile { t.Errorf("LogFile 不匹配: want %s, got %s", cfg.LogFile, loaded.LogFile) } if loaded.DefaultProfile != cfg.DefaultProfile { t.Errorf("DefaultProfile 不匹配: want %s, got %s", cfg.DefaultProfile, loaded.DefaultProfile) } if loaded.Restore.ConflictStrategy != cfg.Restore.ConflictStrategy { t.Errorf("ConflictStrategy 不匹配: want %s, got %s", cfg.Restore.ConflictStrategy, loaded.Restore.ConflictStrategy) } if loaded.Restore.CreateRestorePoint != cfg.Restore.CreateRestorePoint { t.Errorf("CreateRestorePoint 不匹配: want %v, got %v", cfg.Restore.CreateRestorePoint, loaded.Restore.CreateRestorePoint) } if loaded.Restore.DryRunFirst != cfg.Restore.DryRunFirst { t.Errorf("DryRunFirst 不匹配: want %v, got %v", cfg.Restore.DryRunFirst, loaded.Restore.DryRunFirst) } } func TestSaveConfig_InvalidPath(t *testing.T) { cfg := DefaultAppConfig() // 写入不存在的目录应报错 err := SaveConfig("/not/exist/dir/config.yaml", cfg) if err == nil { t.Fatal("写入不存在的目录应返回错误") } } // ==================== Save → Load 往返测试 ==================== func TestSaveAndLoad_RoundTrip(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "roundtrip.yaml") original := &AppConfig{ LogLevel: "trace", LogFile: "logs/app.log", DefaultProfile: "minimal", Restore: RestoreConfig{ ConflictStrategy: "newest", CreateRestorePoint: false, DryRunFirst: true, }, } // Save if err := SaveConfig(path, original); err != nil { t.Fatalf("SaveConfig 失败: %v", err) } // Load loaded, err := LoadConfig(path) if err != nil { t.Fatalf("LoadConfig 失败: %v", err) } // 逐字段对比 if *loaded != *original { t.Errorf("往返后配置不一致:\n 原始: %+v\n 加载: %+v", original, loaded) } } // ==================== DefaultPaths 测试 ==================== func TestDefaultPaths(t *testing.T) { paths, err := DefaultPaths() if err != nil { t.Fatalf("DefaultPaths 不应报错, got: %v", err) } home, _ := os.UserHomeDir() expected := filepath.Join(home, ".devpack") if paths.Home != expected { t.Errorf("Home 应为 %s, got: %s", expected, paths.Home) } if paths.Config != filepath.Join(expected, "config.yaml") { t.Errorf("Config 路径不正确, got: %s", paths.Config) } if paths.Profiles != filepath.Join(expected, "profiles") { t.Errorf("Profiles 路径不正确, got: %s", paths.Profiles) } if paths.Packs != filepath.Join(expected, "packs") { t.Errorf("Packs 路径不正确, got: %s", paths.Packs) } if paths.Logs != filepath.Join(expected, "logs") { t.Errorf("Logs 路径不正确, got: %s", paths.Logs) } if paths.Temp != filepath.Join(expected, "tmp") { t.Errorf("Temp 路径不正确, got: %s", paths.Temp) } } func TestEnsureDirs(t *testing.T) { tmpDir := t.TempDir() paths := &Paths{ Home: filepath.Join(tmpDir, ".devpack"), Profiles: filepath.Join(tmpDir, ".devpack", "profiles"), Packs: filepath.Join(tmpDir, ".devpack", "packs"), Logs: filepath.Join(tmpDir, ".devpack", "logs"), Temp: filepath.Join(tmpDir, ".devpack", "tmp"), } if err := paths.EnsureDirs(); err != nil { t.Fatalf("EnsureDirs 不应报错, got: %v", err) } // 验证所有目录都已创建 dirs := []string{paths.Home, paths.Profiles, paths.Packs, paths.Logs, paths.Temp} for _, dir := range dirs { info, err := os.Stat(dir) if os.IsNotExist(err) { t.Errorf("目录应已创建: %s", dir) } if err == nil && !info.IsDir() { t.Errorf("应为目录而非文件: %s", dir) } } } func TestEnsureDirs_Idempotent(t *testing.T) { tmpDir := t.TempDir() paths := &Paths{ Home: filepath.Join(tmpDir, ".devpack"), Profiles: filepath.Join(tmpDir, ".devpack", "profiles"), Packs: filepath.Join(tmpDir, ".devpack", "packs"), Logs: filepath.Join(tmpDir, ".devpack", "logs"), Temp: filepath.Join(tmpDir, ".devpack", "tmp"), } // 调用两次应幂等 if err := paths.EnsureDirs(); err != nil { t.Fatalf("第一次 EnsureDirs 失败: %v", err) } if err := paths.EnsureDirs(); err != nil { t.Fatalf("第二次 EnsureDirs 应幂等, got: %v", err) } }