Unlucky Launcher v0.2.0
CI / runtime smoke (macos arm64, probe) (push) Skipped
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
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
This commit is contained in:
AskaEth committed 2026-11-05 14:22:11 +08:00
commit 8a8e5ca0c0
1079 files changed
+208035

No files matched your search

@@ -0,0 +1,142 @@
//! 键废弃机制:`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"));
}