fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债

① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
   - 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
     SettingSchema 加 with_all 供域键表批量登记。
   - 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
     四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
     主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
   - presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
     auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
     保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
   - CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。

② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
   (记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。

③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
   设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
   (忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。

④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
   用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
   ≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。

⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
   改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
   Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
   InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。

⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
   由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。

顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
This commit is contained in:
AskaEth committed 2026-11-07 15:02:13 +08:00
1 parent b2d9f3927f
commit 03741ecd2d
69 files changed
+2653 -707

No files matched your search

@@ -81,21 +81,32 @@ fn dismissing_without_a_candidate_does_nothing() {
}
/// 关掉「启动时检查」之后:状态上是 no-op、启动路径上根本不派发。
///
/// 开关的落点是**主配置**(`update.auto_check`),这里顺带把 M161 的迁移走一遍:
/// 旧 `cache/update/prefs.json` 里的 `false` 必须是新会话看到的 `false`(不是默认的 `true`)。
#[test]
fn auto_check_off_means_no_request_on_start() {
let tmp = TempDir::new("autocheck");
let env = HostEnv::at_root(tmp.join("root"));
let prefs_path = env.layout.cache().join("update").join("prefs.json");
Prefs { auto_check: false }
.save(&prefs_path)
.expect("写偏好");
std::fs::create_dir_all(prefs_path.parent().expect("父目录")).expect("建 cache");
std::fs::write(&prefs_path, r#"{"auto_check": false}"#).expect("写旧偏好");
let runtime = Runtime::with_factory(&env, factory(&tmp));
assert!(!runtime.view().auto_check, "开关要读进快照");
assert!(!runtime.auto_check(), "关掉的值一路迁进主配置");
assert!(
!runtime.should_check_on_start(),
"关掉开关后启动不许派发检查"
);
// 迁移确实发生了:旧文件退役 + 收据落盘(收据可查)。
assert!(!prefs_path.exists(), "旧文件必须退役");
let receipt = runtime
.migration()
.receipt()
.expect("收据路径")
.to_path_buf();
assert!(receipt.exists(), "收据要落盘:{}", receipt.display());
// 结构性判据:`refresh_all` 里那次静默检查必须挂在 `should_check_on_start` 上,
// 否则单测绿着而启动仍然联网(这条读源码,理由同 `instances/tests.rs` 的源码断言)。
@@ -106,18 +117,30 @@ fn auto_check_off_means_no_request_on_start() {
);
}
/// 偏好能落盘、能读回(换一份 Runtime 仍记得上次的选择)。
/// 开关能写进主配置、能读回(换一份 Runtime 仍记得上次的选择)。
#[test]
fn the_auto_check_preference_survives_a_restart() {
let tmp = TempDir::new("prefs");
let env = HostEnv::at_root(tmp.join("root"));
let mut runtime = Runtime::with_factory(&env, factory(&tmp));
assert!(runtime.prefs().auto_check, "缺省开");
assert!(runtime.auto_check(), "缺省开");
runtime.set_auto_check(false).expect("落盘");
let restarted = Runtime::with_factory(&env, factory(&tmp));
assert!(!restarted.prefs().auto_check, "重启后仍记得关掉");
assert!(!restarted.auto_check(), "重启后仍记得关掉");
assert!(!restarted.view().auto_check);
// 落点是主配置的 schema 键(CLI 读到的就是这一份),不是更新域自己的文件。
let text = std::fs::read_to_string(env.launcher_config()).expect("读主配置");
assert!(text.contains("\"auto_check\": false"), "{text}");
assert!(
!env.layout
.cache()
.join("update")
.join("prefs.json")
.exists(),
"新写入不许再落到旧文件"
);
}
/// GUI 与 CLI 共用**同一个** `Job::apply`(mission:不许两套逻辑)。
@@ -145,7 +168,33 @@ fn the_factory_names_its_source_and_target() {
let factory = factory(&tmp);
assert_eq!(factory.dir(), Some(tmp.join("dist").as_path()));
assert_eq!(factory.dest(), tmp.join("bin/unlucky-launcher.exe"));
assert_eq!(CHANNEL.as_str(), "stable", "D50:先只发 stable");
}
/// 渠道来自主配置的 `update.channel`(M161:不再是编译期常量)。
///
/// 抓得住:把渠道写回硬编码常量而键里是别的值;也抓得住「认不出的渠道 panic 而不是回出厂值」。
#[test]
fn the_channel_is_read_from_the_schema_key() {
let tmp = TempDir::new("channel-key");
let env = HostEnv::at_root(tmp.join("root"));
let runtime = Runtime::with_factory(&env, factory(&tmp));
assert_eq!(
runtime.channel().as_str(),
unlucky_core::setting::update_prefs::DEFAULT_CHANNEL,
"缺键 ⇒ 出厂渠道"
);
assert_eq!(runtime.view().channel, "stable", "快照里的渠道同源");
// 手改成一个 schema 不允许的值(越过 `config set` 的校验)⇒ 回出厂渠道,不许 panic。
std::fs::create_dir_all(env.layout.config()).expect("建 config");
std::fs::write(
env.launcher_config(),
r#"{"$schema":"https://schemas.unlucky-launcher.dev/ull/launcher/1.6.0","schemaVersion":1,
"update":{"channel":"hand-edited"}}"#,
)
.expect("写配置");
let runtime = Runtime::with_factory(&env, factory(&tmp));
assert_eq!(runtime.channel().as_str(), "stable", "认不出的渠道回出厂值");
}
/// CLI 与 GUI 用的是**同一个** core Job:源码锚点,防止有人另起一条流程。