CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
143 lines
5.0 KiB
Rust
143 lines
5.0 KiB
Rust
//! 键废弃机制:`ui.theme` → `appearance.theme_mode`(tower 键冲突裁决)。
|
||
//!
|
||
//! 规则:**读兼容**(旧值按 `THEME_ALIASES` 映射,新键已在则新键优先)+ **写永远走新键**
|
||
//! (`set` 旧键直接报错,落盘不含旧键,AI 白名单与工具 JSON Schema 也不含旧键)。
|
||
|
||
use serde_json::{json, Value};
|
||
use unlucky_core::setting::document::{LoadProblem, SettingDocument};
|
||
use unlucky_core::setting::keys::{launcher_schema, LEGACY_THEMES, THEME_ALIASES, THEME_MODES};
|
||
use unlucky_core::setting::schema::{SchemaId, SchemaRef, SchemaVersion, SCHEMA_KEY};
|
||
use unlucky_core::setting::store::SettingStore;
|
||
|
||
use crate::common::TempDir;
|
||
|
||
fn legacy_file(raw_ui: Value) -> Value {
|
||
json!({
|
||
SCHEMA_KEY: SchemaRef::new(SchemaId::LAUNCHER, SchemaVersion::new(1, 0, 0)).url(),
|
||
"ui": raw_ui
|
||
})
|
||
}
|
||
|
||
fn deprecated_problem(problems: &[LoadProblem]) -> Option<&'static str> {
|
||
problems.iter().find_map(|problem| match problem {
|
||
LoadProblem::DeprecatedKey {
|
||
key, replaced_by, ..
|
||
} if key == "ui.theme" => Some(*replaced_by),
|
||
_ => None,
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn legacy_theme_values_are_translated_on_read() {
|
||
assert_eq!(THEME_MODES.len(), 3);
|
||
assert!(THEME_MODES.contains(&"auto") && THEME_MODES.contains(&"light"));
|
||
assert!(THEME_MODES.contains(&"dark"));
|
||
assert_eq!(
|
||
LEGACY_THEMES.len(),
|
||
THEME_ALIASES.len(),
|
||
"三个旧值都要有映射"
|
||
);
|
||
|
||
for (legacy, expected) in [
|
||
("system", "auto"),
|
||
("metro_light", "light"),
|
||
("metro_dark", "dark"),
|
||
] {
|
||
let (doc, problems) =
|
||
SettingDocument::load(&launcher_schema(), &legacy_file(json!({ "theme": legacy })));
|
||
|
||
assert_eq!(
|
||
doc.effective_str("appearance.theme_mode"),
|
||
Some(expected),
|
||
"旧值 {legacy} 应映射成 {expected}"
|
||
);
|
||
assert_eq!(
|
||
doc.legacy_value("ui.theme").and_then(Value::as_str),
|
||
Some(legacy),
|
||
"旧键原文要留着给 UI 提示/回滚"
|
||
);
|
||
assert_eq!(deprecated_problem(&problems), Some("appearance.theme_mode"));
|
||
assert!(!doc.is_dirty(), "读兼容不算用户改动");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn new_key_wins_when_both_present() {
|
||
let raw = json!({
|
||
SCHEMA_KEY: SchemaRef::new(SchemaId::LAUNCHER, SchemaVersion::new(1, 0, 0)).url(),
|
||
"ui": { "theme": "metro_dark" },
|
||
"appearance": { "theme_mode": "light" }
|
||
});
|
||
let (doc, _) = SettingDocument::load(&launcher_schema(), &raw);
|
||
assert_eq!(doc.effective_str("appearance.theme_mode"), Some("light"));
|
||
}
|
||
|
||
#[test]
|
||
fn unmapped_legacy_value_is_reported() {
|
||
let (doc, problems) =
|
||
SettingDocument::load(&launcher_schema(), &legacy_file(json!({ "theme": "neon" })));
|
||
assert_eq!(doc.effective_str("appearance.theme_mode"), Some("neon"));
|
||
assert!(
|
||
problems.iter().any(|problem| matches!(
|
||
problem,
|
||
LoadProblem::InvalidValue { key, .. } if key == "appearance.theme_mode"
|
||
)),
|
||
"映射不过去的旧值要报出来: {problems:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn writing_the_deprecated_key_is_refused() {
|
||
let (mut doc, _) = SettingDocument::load(&launcher_schema(), &json!({}));
|
||
let err = doc
|
||
.set("ui.theme", json!("metro_dark"))
|
||
.expect_err("旧键禁止写");
|
||
assert!(err.to_string().contains("appearance.theme_mode"), "{err}");
|
||
assert!(!doc.is_dirty());
|
||
}
|
||
|
||
#[test]
|
||
fn save_drops_the_old_key_and_writes_the_new_one() {
|
||
let temp = TempDir::new("deprecated-save");
|
||
temp.write(
|
||
"config/launcher.json",
|
||
&legacy_file(json!({ "theme": "metro_dark", "language": "en_us" })).to_string(),
|
||
);
|
||
|
||
let mut store = SettingStore::load(temp.child("config/launcher.json"), launcher_schema());
|
||
assert_eq!(
|
||
store.effective("appearance.theme_mode"),
|
||
Some(&json!("dark"))
|
||
);
|
||
|
||
// 随便改一个别的键触发一次保存:旧键消失、新键(映射值)落盘。
|
||
store.set("ui.scale", json!(1.25)).expect("改缩放");
|
||
store.save().expect("保存");
|
||
|
||
let saved: Value = serde_json::from_str(&temp.read("config/launcher.json")).expect("JSON 合法");
|
||
assert_eq!(saved["appearance"]["theme_mode"], json!("dark"));
|
||
assert!(
|
||
saved["ui"].get("theme").is_none(),
|
||
"废弃键不再落盘: {saved}"
|
||
);
|
||
assert_eq!(saved["ui"]["language"], json!("en_us"));
|
||
}
|
||
|
||
#[test]
|
||
fn whitelist_and_tool_schema_exclude_the_old_key() {
|
||
let schema = launcher_schema();
|
||
assert_eq!(schema.deprecated_keys(), vec!["ui.theme"]);
|
||
assert_eq!(
|
||
schema.replacement_of("ui.theme"),
|
||
Some("appearance.theme_mode")
|
||
);
|
||
assert_eq!(schema.replacement_of("appearance.theme_mode"), None);
|
||
assert!(!schema.accepts_ai_write("ui.theme"), "废弃键不许 AI 写");
|
||
assert!(!schema.ai_writable_keys().contains(&"ui.theme"));
|
||
|
||
let projected = schema.to_json_schema();
|
||
let properties = projected["properties"].as_object().expect("properties");
|
||
assert!(properties.contains_key("appearance.theme_mode"));
|
||
assert!(!properties.contains_key("ui.theme"));
|
||
}
|