diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 11a3c0758..0ad7a0d4b 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; @@ -159,15 +160,34 @@ pub fn read_json_file Deserialize<'a>>(path: &Path) -> Result Value { + match value { + Value::Object(map) => { + let mut sorted_map = Map::new(); + let mut keys: Vec<_> = map.keys().collect(); + keys.sort(); + for key in keys { + sorted_map.insert(key.clone(), sort_json_keys(&map[key])); + } + Value::Object(sorted_map) + } + Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()), + other => other.clone(), + } +} + +/// 写入 JSON 配置文件(键按字母排序,确保确定性输出) pub fn write_json_file(path: &Path, data: &T) -> Result<(), AppError> { // 确保目录存在 if let Some(parent) = path.parent() { 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 value = serde_json::to_value(data).map_err(|e| AppError::JsonSerialize { source: e })?; + let sorted_value = sort_json_keys(&value); + let json = serde_json::to_string_pretty(&sorted_value) + .map_err(|e| AppError::JsonSerialize { source: e })?; atomic_write(path, json.as_bytes()) } @@ -271,6 +291,103 @@ mod tests { let override_dir = PathBuf::from("/"); assert!(derive_mcp_path_from_override(&override_dir).is_none()); } + + #[test] + fn sort_json_keys_sorts_top_level_object() { + let input = serde_json::json!({ + "z": 1, + "a": 2, + "m": 3, + }); + let sorted = sort_json_keys(&input); + let serialized = serde_json::to_string(&sorted).unwrap(); + assert_eq!(serialized, r#"{"a":2,"m":3,"z":1}"#); + } + + #[test] + fn sort_json_keys_recurses_into_nested_objects() { + let input = serde_json::json!({ + "outer_b": {"z": 1, "a": 2}, + "outer_a": {"y": 3, "b": 4}, + }); + let sorted = sort_json_keys(&input); + let serialized = serde_json::to_string(&sorted).unwrap(); + assert_eq!( + serialized, + r#"{"outer_a":{"b":4,"y":3},"outer_b":{"a":2,"z":1}}"# + ); + } + + #[test] + fn sort_json_keys_preserves_array_order() { + let input = serde_json::json!([3, 1, 2]); + let sorted = sort_json_keys(&input); + let serialized = serde_json::to_string(&sorted).unwrap(); + assert_eq!(serialized, "[3,1,2]"); + } + + #[test] + fn sort_json_keys_sorts_objects_inside_arrays_but_keeps_array_order() { + let input = serde_json::json!([ + {"z": 1, "a": 2}, + {"y": 3, "b": 4}, + ]); + let sorted = sort_json_keys(&input); + let serialized = serde_json::to_string(&sorted).unwrap(); + assert_eq!(serialized, r#"[{"a":2,"z":1},{"b":4,"y":3}]"#); + } + + #[test] + fn sort_json_keys_passes_through_primitives() { + let cases = vec![ + serde_json::json!("hello"), + serde_json::json!(42), + serde_json::json!(3.14), + serde_json::json!(true), + serde_json::json!(null), + ]; + for value in cases { + let sorted = sort_json_keys(&value); + assert_eq!(sorted, value); + } + } + + #[test] + fn sort_json_keys_handles_empty_collections() { + let empty_obj = serde_json::json!({}); + assert_eq!( + serde_json::to_string(&sort_json_keys(&empty_obj)).unwrap(), + "{}" + ); + + let empty_arr = serde_json::json!([]); + assert_eq!( + serde_json::to_string(&sort_json_keys(&empty_arr)).unwrap(), + "[]" + ); + } + + #[test] + fn sort_json_keys_produces_identical_output_for_different_insertion_orders() { + // 核心保证:同一逻辑配置无论键的插入顺序如何,写出的字节序列必须一致。 + let mut a = Map::new(); + a.insert("env".to_string(), serde_json::json!({"PATH": "/usr/bin"})); + a.insert("model".to_string(), serde_json::json!("claude-sonnet-4-5")); + a.insert("permissions".to_string(), serde_json::json!({"allow": []})); + + let mut b = Map::new(); + b.insert("permissions".to_string(), serde_json::json!({"allow": []})); + b.insert("model".to_string(), serde_json::json!("claude-sonnet-4-5")); + b.insert("env".to_string(), serde_json::json!({"PATH": "/usr/bin"})); + + let sorted_a = sort_json_keys(&Value::Object(a)); + let sorted_b = sort_json_keys(&Value::Object(b)); + + assert_eq!( + serde_json::to_string(&sorted_a).unwrap(), + serde_json::to_string(&sorted_b).unwrap(), + ); + } } /// 复制文件