mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(config): sort JSON keys alphabetically for deterministic output (#2469)
* fix(config): sort JSON keys alphabetically for deterministic output Ensures settings.json keys are written in sorted order, preventing non-deterministic git diffs when switching configs. * test(config): add unit tests for sort_json_keys and fix formatting Cover top-level sort, nested recursion, array order preservation, primitive pass-through, empty collections, and the core determinism guarantee (different insertion orders must yield identical output). Also fix line-length in write_json_file flagged by `cargo fmt --check`. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
+120
-3
@@ -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<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, AppE
|
||||
serde_json::from_str(&content).map_err(|e| AppError::json(path, e))
|
||||
}
|
||||
|
||||
/// 写入 JSON 配置文件
|
||||
/// 递归排序 JSON 对象的键(按字母顺序),确保序列化输出是确定性的
|
||||
fn sort_json_keys(value: &Value) -> 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<T: Serialize>(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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 复制文件
|
||||
|
||||
Reference in New Issue
Block a user