From 652e4a230bd83631b8ba29f1fbb7f91ac84791b7 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:49:40 +0800 Subject: [PATCH] test(config): pin that every key survives a write and a read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Config` is `#[serde(default)]`, which is what makes this worth asserting. A key whose serialized and deserialized names have drifted apart does not fail to load — it silently resolves to the default, so the setting is forgotten the next time the file is read, with nothing on any error path to say so. Nothing was checking that the names still agree. The test nudges every bool and number off its default, writes the config, reads it back, and requires the value to return rather than the default. Strings are skipped on purpose: most are enums in kebab-case, and `de_lenient` deliberately resolves a spelling it does not know to the default, so an arbitrary string would report that intended behaviour as a failure. Confirmed to fail for the case it exists for, rather than assumed: an injected `rename(serialize = "cursor_blink", deserialize = "cursor_blink_x")` trips it. A *symmetric* rename does not, and should not — that changes the file format consistently and loses nothing. Floats compare loosely because several fields are `f32` and a value nudged through `f64` returns a fraction off; an exact compare reported arithmetic as a lost setting on the first run. `checked > 20` guards the loop against silently testing nothing. No key fails today. --- crates/tty7-core/src/core/config.rs | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index 67a1c2eb..a79585da 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -1765,6 +1765,63 @@ mod tests { assert_eq!(clamp(100_000), 3600); } + #[test] + fn every_config_key_survives_being_written_and_read_back() { + // `#[serde(default)]` is what makes this worth asserting: a key whose + // serialized name and deserialized name have drifted apart does not + // fail to load, it silently resolves to the default — the setting is + // simply forgotten the next time the file is read. + // + // Only bools and numbers are nudged. A string is usually an enum + // spelled in kebab-case, and `de_lenient` deliberately falls back to + // the default for a spelling it does not know, so an arbitrary string + // would report that deliberate behaviour as a failure. + let base = serde_json::to_value(Config::default()).expect("Config serializes"); + let obj = base.as_object().expect("a config is a JSON object").clone(); + let mut checked = 0; + for (key, value) in &obj { + let nudged = match value { + serde_json::Value::Bool(b) => serde_json::Value::Bool(!b), + serde_json::Value::Number(n) if n.is_u64() => { + serde_json::Value::from(n.as_u64().unwrap() + 1) + } + serde_json::Value::Number(n) if n.is_f64() => { + serde_json::Value::from(n.as_f64().unwrap() + 1.0) + } + _ => continue, + }; + let mut doc = obj.clone(); + doc.insert(key.clone(), nudged.clone()); + let parsed: Config = serde_json::from_value(serde_json::Value::Object(doc)) + .unwrap_or_else(|e| panic!("`{key}` set to {nudged} did not parse: {e}")); + let back = serde_json::to_value(parsed).expect("Config serializes"); + let got = back.get(key).unwrap_or_else(|| panic!("`{key}` vanished")); + // The property that matters is that the written value came back + // rather than reverting to the default. Compared loosely for + // floats: several of these are `f32`, so a value nudged through + // `f64` comes back a fraction off and an exact compare would + // report arithmetic as a lost setting. + match (got.as_f64(), nudged.as_f64()) { + (Some(a), Some(b)) => assert!( + (a - b).abs() < 1e-3 * b.abs().max(1.0), + "`{key}` did not survive a write and read back: wrote {nudged}, read {got}" + ), + _ => assert_eq!( + got, &nudged, + "`{key}` did not survive a write and read back" + ), + } + assert_ne!( + got, value, + "`{key}` read back as its default, so the write was dropped" + ); + checked += 1; + } + // A guard against this test quietly checking nothing if the shape of + // the config changes. + assert!(checked > 20, "only {checked} keys were nudged"); + } + #[test] fn sanitize_normalizes_blank_http_proxy_to_none() { let normalize = |proxy: Option<&str>| {