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:
Chris Chen
2026-04-30 13:53:32 +10:00
committed by GitHub
parent 151af0ffa3
commit 8084bfafa8
+120 -3
View File
@@ -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(),
);
}
}
/// 复制文件