refactor(backend): phase 4 - add test hooks and extend service layer

- Extract internal functions in commands/mcp.rs and commands/provider.rs
  to enable unit testing without Tauri context
- Add test hooks: set_mcp_enabled_test_hook, import_mcp_from_claude_test_hook,
  import_mcp_from_codex_test_hook, import_default_config_test_hook
- Migrate error types from String to AppError for precise error matching in tests
- Extend ProviderService with delete() method to unify Codex/Claude cleanup logic
- Add comprehensive test coverage:
  - tests/mcp_commands.rs: command-level tests for MCP operations
  - tests/provider_service.rs: service-level tests for switch/delete operations
- Run cargo fmt to fix formatting issues (EOF newlines)
- Update BACKEND_REFACTOR_PLAN.md to mark phase 3 complete
This commit is contained in:
Jason
2025-10-28 11:58:57 +08:00
parent c2e8855a0f
commit 7e27f88154
20 changed files with 1005 additions and 415 deletions

View File

@@ -150,10 +150,7 @@ pub fn get_provider_config_path(provider_id: &str, provider_name: Option<&str>)
/// 读取 JSON 配置文件
pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, AppError> {
if !path.exists() {
return Err(AppError::Config(format!(
"文件不存在: {}",
path.display()
)));
return Err(AppError::Config(format!("文件不存在: {}", path.display())));
}
let content = fs::read_to_string(path).map_err(|e| AppError::io(path, e))?;
@@ -168,8 +165,8 @@ pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), AppErr
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let json = serde_json::to_string_pretty(data)
.map_err(|e| AppError::JsonSerialize { source: e })?;
let json =
serde_json::to_string_pretty(data).map_err(|e| AppError::JsonSerialize { source: e })?;
atomic_write(path, json.as_bytes())
}
@@ -204,8 +201,7 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
tmp.push(format!("{}.tmp.{}", file_name, ts));
{
let mut f =
fs::File::create(&tmp).map_err(|e| AppError::io(&tmp, e))?;
let mut f = fs::File::create(&tmp).map_err(|e| AppError::io(&tmp, e))?;
f.write_all(data).map_err(|e| AppError::io(&tmp, e))?;
f.flush().map_err(|e| AppError::io(&tmp, e))?;
}
@@ -226,11 +222,7 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
let _ = fs::remove_file(path);
}
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
context: format!(
"原子替换失败: {} -> {}",
tmp.display(),
path.display()
),
context: format!("原子替换失败: {} -> {}", tmp.display(), path.display()),
source: e,
})?;
}
@@ -238,11 +230,7 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
#[cfg(not(windows))]
{
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
context: format!(
"原子替换失败: {} -> {}",
tmp.display(),
path.display()
),
context: format!("原子替换失败: {} -> {}", tmp.display(), path.display()),
source: e,
})?;
}
@@ -287,11 +275,7 @@ mod tests {
/// 复制文件
pub fn copy_file(from: &Path, to: &Path) -> Result<(), AppError> {
fs::copy(from, to).map_err(|e| AppError::IoContext {
context: format!(
"复制文件失败 ({} -> {})",
from.display(),
to.display()
),
context: format!("复制文件失败 ({} -> {})", from.display(), to.display()),
source: e,
})?;
Ok(())