Files
AskaEth fa2c6560fd fix(appearance): M91 r2 —— 真交叉断言 + unsafe 门禁跨行扫描 + α 上取整
P2-1「与 design 码值一致」的守门此前不存在
- 新增 crates/app-iced/src/platform/mod.rs::palette_source_codes_match_design_level_codes:
  三个格子对照 PaletteSource::as_str() 与 unlucky_design::palette::Level::code(),
  并覆盖反方向(from_key_value(level.code()) 必须还原同一档)与「码值在允许值表里」
- core 侧那条测试改名为 value_tables_are_self_consistent,注释改成事实:
  core 不依赖 design,只能钉自己的字面量;一致性由 app-iced 的那条对照断言守
- 实测:把 design 的 Level::Wallpaper 码值改成 "desktop" → 新断言红(design 自己 178 个测试仍全绿)

P2-2 unsafe-scope 门禁:行级正则 → 跨行扫描
- 去注释 + 按方括号闭合把续行并成逻辑行,再逐个属性组判断 allow(...unsafe_code)
- 抓得住原来两种绕过:多行属性 #![allow(\n unsafe_code\n)]、同行两属性 #[inline] #[allow(unsafe_code)]
- 门禁注释补全已知边界(不再声称「依赖 fmt job 兜底」——已不依赖)

O-1 α 量化方向:round → ceil
- 0.08×255=20.4,round 得 20 → 实际下发 20/255=0.0784,低于 D36 名义下限
- ceil 得 21 → 0.0824;新增 applied_alpha_is_never_below_the_floor 断言 下发字节/255 ≥ 0.08

O-7 契约测试补三个新键
- appearance_keys_are_all_ai_writable_and_registered 的 audited 清单加 window_opacity /
  titlebar_mode / palette_source
2026-11-02 14:28:36 +08:00

201 lines
7.7 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 「外观(高级)」键簇(`setting::appearance` + `keys::launcher_schema` 的 appearance.* 段)。
//!
//! 守三条设计约束(09-ui-layout §111):
//! 1. 一切高级特性**默认关闭**(出厂 Metro 纯色)——默认值即出厂态;
//! 2. 全部走 D6 schema 且 **AI 可写**(白名单内,可被 agent 调节);
//! 3. 值域有硬边界(滑条夹取),坏值/手改配置不崩(回落默认)。
use serde_json::json;
use unlucky_core::setting::appearance::{
hex_to_rgb, normalize_hex, relative_luminance, Appearance, BgFill, DEFAULT_ACCENT,
PANEL_ALPHA_MIN,
};
use unlucky_core::setting::document::SettingDocument;
use unlucky_core::setting::keys::{launcher_schema, launcher_version};
use unlucky_core::setting::spec::ValueKind;
fn doc(raw: serde_json::Value) -> SettingDocument {
SettingDocument::load(&launcher_schema(), &raw).0
}
#[test]
fn factory_defaults_are_all_advanced_features_off() {
let appearance = Appearance::from_store(&unlucky_core::setting::store::SettingStore::load(
std::env::temp_dir().join(format!("ull-appearance-none-{}.json", std::process::id())),
launcher_schema(),
));
assert_eq!(appearance.theme_mode, "auto", "D17:出厂跟随系统");
assert!(!appearance.bg_image_enabled, "背景图默认关");
assert!(!appearance.bg_image_active());
assert_eq!(appearance.bg_fill, BgFill::Cover);
assert_eq!(appearance.panel_alpha, 1.0, "出厂完全不透明(Metro 纯色)");
assert!(!appearance.glass, "玻璃拟态默认关");
assert_eq!(appearance.accent_color, "", "空串 = 未自定义");
assert_eq!(appearance.accent_or_default(), DEFAULT_ACCENT);
}
#[test]
fn reading_a_configured_document_reflects_each_key() {
let document = doc(json!({
"appearance": {
"theme_mode": "dark",
"bg_image_enabled": true,
"bg_image": "D:/wall/paper.png",
"bg_fill": "tile",
"bg_dim": 0.6,
"panel_alpha": 0.8,
"glass": true,
"accent_color": "#ff5722",
}
}));
let appearance = Appearance::read(&document);
assert_eq!(appearance.theme_mode, "dark");
assert!(appearance.bg_image_active());
assert_eq!(appearance.bg_image, "D:/wall/paper.png");
assert_eq!(appearance.bg_fill, BgFill::Tile);
assert!((appearance.bg_dim - 0.6).abs() < 1e-9);
assert!((appearance.panel_alpha - 0.8).abs() < 1e-9);
assert!(appearance.glass);
assert_eq!(appearance.accent_color, "#FF5722", "读入即规范化大写");
assert_eq!(appearance.accent_or_default(), "#FF5722");
}
#[test]
fn bad_values_fall_back_instead_of_panicking() {
// 手改坏的配置:枚举拼错 / 开关类型错 / accent 不是 #RRGGBB。
let document = doc(json!({
"appearance": {
"bg_fill": "stretch",
"bg_image_enabled": "yes",
"accent_color": "rebeccapurple",
}
}));
let appearance = Appearance::read(&document);
assert_eq!(appearance.bg_fill, BgFill::Cover);
assert!(!appearance.bg_image_enabled);
assert_eq!(appearance.accent_color, "");
assert_eq!(appearance.accent_or_default(), DEFAULT_ACCENT);
}
#[test]
fn panel_alpha_is_clamped_to_the_contrast_floor() {
// schema 下限 0.4;读侧再夹一次(挡手改文件绕过 schema 的情况)。
let document = doc(json!({ "appearance": { "panel_alpha": 0.05 } }));
assert!((Appearance::read(&document).panel_alpha - PANEL_ALPHA_MIN).abs() < 1e-9);
}
#[test]
fn appearance_keys_are_all_ai_writable_and_registered() {
let schema = launcher_schema();
// 主题三态是 D17 已定案的最轻量键(silent/Low),其余高级键留审计痕迹(audited/Medium)。
let audited_keys = [
"appearance.bg_image_enabled",
"appearance.bg_image",
"appearance.bg_fill",
"appearance.bg_dim",
"appearance.panel_alpha",
"appearance.glass",
"appearance.accent_color",
// M91(D36/D37/D38)的外观下层三键,同样 audited。
"appearance.window_opacity",
"appearance.titlebar_mode",
"appearance.palette_source",
];
for key in audited_keys {
let spec = schema.spec(key).unwrap_or_else(|| panic!("{key} 必须登记"));
assert!(spec.ai.writable, "{key} 必须在 AI 可写白名单内");
// 纯视觉、可一键复原 → 不要求弹窗确认(High 级留给凭据/数据根/代理)。
assert!(
!spec.ai.sensitivity.requires_confirmation(),
"{key} 不该要确认"
);
assert_eq!(
spec.ai.sensitivity.as_str(),
"medium",
"{key} 要留审计痕迹(AI 改过什么可回看)"
);
}
// 主题三态仍是 silent(D17 已定案的最轻量键)。
assert_eq!(
schema
.spec("appearance.theme_mode")
.unwrap()
.ai
.sensitivity
.as_str(),
"low"
);
}
#[test]
fn schema_registration_shape_is_right() {
assert_eq!(launcher_version().minor, 4, "1.4.0 登记外观下层三键");
let schema = launcher_schema();
assert_eq!(
schema.spec("appearance.bg_fill").unwrap().kind,
ValueKind::Enum(&["cover", "contain", "tile"])
);
assert!(matches!(
schema.spec("appearance.panel_alpha").unwrap().kind,
ValueKind::Float { min, max } if (min - PANEL_ALPHA_MIN).abs() < 1e-9 && (max - 1.0).abs() < 1e-9
));
assert_eq!(
schema.spec("appearance.accent_color").unwrap().kind,
ValueKind::Text { max_len: 9 },
"#RRGGBB 最多 7 字符,留 9 容错带井号与空白"
);
// 默认值 = 出厂态
assert_eq!(
schema.spec("appearance.panel_alpha").unwrap().default,
serde_json::Value::from(1.0)
);
assert_eq!(
schema.spec("appearance.glass").unwrap().default,
serde_json::Value::from(false)
);
}
#[test]
fn write_round_trips_through_the_store() {
let temp =
std::env::temp_dir().join(format!("ull-appearance-{}-{}", std::process::id(), line!()));
std::fs::create_dir_all(temp.join("config")).expect("临时目录");
let path = temp.join("config/launcher.json");
let _ = std::fs::remove_file(&path);
let mut store = unlucky_core::setting::store::SettingStore::load(&path, launcher_schema());
let mut appearance = Appearance::from_store(&store);
appearance.theme_mode = "dark".to_owned();
appearance.panel_alpha = 0.8;
appearance.bg_image_enabled = true;
appearance.bg_image = "D:/wall/p.png".to_owned();
appearance.accent_color = "#0f6cbd".to_owned();
appearance.write(&mut store).expect("写外观键");
store.save().expect("落盘");
let reloaded = Appearance::from_store(&unlucky_core::setting::store::SettingStore::load(
&path,
launcher_schema(),
));
assert_eq!(reloaded.theme_mode, "dark");
assert!((reloaded.panel_alpha - 0.8).abs() < 1e-9);
assert!(reloaded.bg_image_active());
assert_eq!(reloaded.accent_color, "#0F6CBD");
std::fs::remove_dir_all(&temp).ok();
}
#[test]
fn hex_helpers_validate_and_convert() {
assert_eq!(normalize_hex(" 0f6cbd "), Some("#0F6CBD".to_owned()));
assert_eq!(normalize_hex("#0F6CBD"), Some("#0F6CBD".to_owned()));
assert_eq!(normalize_hex("#0F6CB"), None);
assert_eq!(normalize_hex("#GGGGGG"), None);
assert_eq!(hex_to_rgb("#0078D4"), Some((0x00, 0x78, 0xD4)));
// WCAG 相对亮度:纯黑 0、纯白 1、Metro 蓝居中偏暗。
assert!(relative_luminance("#000000").unwrap().abs() < 1e-9);
assert!((relative_luminance("#FFFFFF").unwrap() - 1.0).abs() < 1e-9);
let metro = relative_luminance(DEFAULT_ACCENT).unwrap();
assert!(metro > 0.05 && metro < 0.3, "Metro 蓝亮度 {metro}");
}