Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/tests/update_section.rs
T
AskaEth 1c8f6aa1b3 feat(m159): 自更新 GUI 客户端接线 + CLI update check/apply 同一内核
D50 的「检查 → 提示 → 可拒绝 → 下载校验 → 下一次启动生效」整环接线,GUI 与
CLI 共用 core 的一条内核(`update::Job`),并顺带修掉 M157 最后一轮复审的三处 P3。

内核(core::update)
- 新增 job.rs:`Job { source, target, staging, dest }` + `check` / `apply`(复查 →
  取包 → 长度/SHA256 双检 → 原子替换),`default_source`(GitHub,`ULL_UPDATE_DIR`
  可切本地目录夹具)、`process_dest`、`staging_dir`。
- `mod.rs` 导出 job 一族;`source.rs` 为 `Box<dyn UpdateSource>` 补 trait 转发
  (壳层要运行期选源,`UpdateChecker` 是泛型的)。

presenter(宿主通道)
- `dto/update.rs`:`UpdateView` / `UpdateOutcome`(Idle/Checking/UpToDate/Available/
  MissingArtifact/Failed 六态,**失败与最新不折叠**)/ `UpdateBanner` 优先级判据。
- `host/update.rs`:Factory(生产 GitHub / 单测本地目录)+ core 结论 → DTO 的唯一映射
  + 更新域偏好(`cache/update/prefs.json`)。
- `host/update_state.rs`:Runtime(会话状态机:begin_check/adopt/begin_apply/dismiss/
  set_auto_check)+ `CoreHost` 派发与后台线程。
- `AppCommand::{CheckUpdate, ApplyUpdate, DismissUpdate, SetUpdateAutoCheck}`、
  `CoreEvent::Update`、`AppState.update`;`refresh_all` 按开关派发静默检查。

app-iced
- 设置页新增第 5 组「更新」(当前版本 / 渠道 / 自动检查开关 / 立即检查 / 上次检查与
  结果 / 下载并安装 / 就位后立即重启),走 `settings::view` 的 `update` 形参 + 按下标
  分发(与 M106 语言行同一取舍)。
- 顶部提示条(每页可见):有新版本(版本号 + 体积 + 说明摘要,可安装/可忽略)、下载中、
  已就位(立即重启);静默检查失败**不**弹条(原因进设置页)。
- `pages::Message::Update(UpdateAction)` → `app/update/update_action.rs`;「立即重启」
  拉同参数的新进程后退出(截图通道不重启)。

CLI
- `ull update check` / `ull update apply [--dest <path>]`:复用同一 `Job`;四种结论分开
  报(`--json` 的 `status`),失败退出码 1(`update_check_failed` / `update_apply_failed`)。

i18n
- 新增 `catalog/update.rs`(30 条,链尾追加)+ 金表对应 30 行。

M157 复审 P3 三件
- `app/update/instances.rs` 模块文档改成「清脏的唯一时机是回程快照等于表单」。
- `pages/instances/demo.rs` 的 `demo_form` 注释改成现状(两个参数框早已是单行输入)。
- `pages/instances/launch_args.rs`:门限不满足时只灰 `opengl`/`vulkan`,`default` 仍可点
  (它是「不下发」的安全档);判据抽成 `backend_chip_enabled` 并补 3 条用例。浅色 + 灰化
  两张图:`C:/Temp/m159-shots/instances-launch-args-{light,gated}.png`。

顺带:harness 支持 `ULL_SETTINGS_GROUP=<下标>`(拍第 5 组的验收图需要它)。

验证:cargo fmt --check 绿;cargo clippy --workspace --all-targets -D warnings 绿;
cargo test --workspace --all-targets 51 个测试二进制全绿(唯一例外:core::addon::pack
一族要造 4 GiB 稀疏文件,本机磁盘 99% 满 ⇒ StorageFull,与本次改动无关);CLI 用本地
目录夹具跑通「发现 9.9.9 → 下载 → 校验 → 替换运行中的 exe → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
2026-11-07 12:09:42 +08:00

124 lines
4.8 KiB
Rust

//! 设置页「更新」分组(M159)的断言:注册表位置、四条状态各有各的句子。
//!
//! 为什么只测「词表 + 判据」而不测控件:iced 的 `Element` 没有内省接口(`tests/mod.rs` 顶部那条
//! 写过理由),上屏文本只能从**取词的那一个入口**取。这里守的是「四种结论的句子不是同一句」
//! ——正是 mission 里「检查失败不等于已是最新」那条硬要求的最小可测形态。
use unlucky_i18n::{Catalog, Key, Lang};
use super::super::update::{GROUP as UPDATE_GROUP_META, UPDATE_GROUP};
use super::super::{GROUPS, SELECTED};
use crate::pages::update::UpdateStatus;
use crate::pages::{Screen, Update};
/// 更新组**必须是最后一组**:加组只追加不重排,插到中间会让 `APPEARANCE_GROUP` / `GENERAL_GROUP`
/// 两个下标一起失效。
#[test]
fn the_update_group_sits_at_the_end_of_the_registry() {
assert_eq!(GROUPS.len(), UPDATE_GROUP + 1, "更新组之后不该还有别的组");
assert_eq!(GROUPS[UPDATE_GROUP].name, UPDATE_GROUP_META.name);
assert_eq!(GROUPS[UPDATE_GROUP].name, "Updates");
assert!(SELECTED < GROUPS.len());
}
/// 四种结论的句子**互不相同**,且「失败」那一句必须把原因带出来。
///
/// 抓得住:把「检查失败」写成「已是最新」(那是 mission 明令禁止的撒谎:断网时会说「你是最新」)。
/// 抓不住:句子翻得顺不顺、颜色对不对(那是人读与截图的事)。
#[test]
fn the_four_outcomes_read_differently() {
let catalog = Lang::ZhCn.catalog();
let up_to_date = catalog.render(Key::SettingsUpdateUpToDate, &[("version", "0.2.2")]);
let available = catalog.render(
Key::SettingsUpdateAvailable,
&[("version", "0.2.3"), ("size", "21.0 MB")],
);
let missing = catalog.render(Key::SettingsUpdateMissingArtifact, &[("version", "0.2.3")]);
let failed = catalog.render(
Key::SettingsUpdateFailed,
&[("reason", "取更新清单失败:连接被重置")],
);
let all = [&up_to_date, &available, &missing, &failed];
for (index, left) in all.iter().enumerate() {
for right in all.iter().skip(index + 1) {
assert_ne!(left, right, "两条结论说的是同一句话:{left} / {right}");
}
}
assert!(
failed.contains("连接被重置"),
"失败那一句必须带出原因,实际:{failed}"
);
assert!(
!failed.contains("0.2.2"),
"失败那一句不该冒出当前版本号(读起来像「已是最新」):{failed}"
);
}
/// 页面视图模型与 presenter 的判据同序:就位 > 进行中 > 失败(仅主动查) > 有新版本。
///
/// 两处各算一次(presenter 判据 + 页面视图),顺序必须一致,否则同一份状态在提示条与设置页上
/// 会各说一套。这里只钉页面这一侧的顺序;presenter 那一侧在 `presenter` 的 `dto::update` 单测里。
#[test]
fn the_banner_priority_is_ready_then_applying_then_failure_then_available() {
let mut update = Update {
current_version: "0.2.2".to_string(),
channel: "stable".to_string(),
auto_check: true,
status: UpdateStatus::Failed {
reason: "连不上".to_string(),
retryable: true,
},
checked_at: String::new(),
applying: true,
ready: None,
dismissed: false,
silent: false,
};
assert!(matches!(
update.banner(),
Some(crate::pages::Banner::Applying)
));
update.applying = false;
assert!(matches!(
update.banner(),
Some(crate::pages::Banner::Failed { .. })
));
update.status = UpdateStatus::Available {
version: "0.2.3".to_string(),
size: 1024,
notes: String::new(),
forced: false,
};
assert!(matches!(
update.banner(),
Some(crate::pages::Banner::Available { .. })
));
update.ready = Some("0.2.3".to_string());
assert!(matches!(
update.banner(),
Some(crate::pages::Banner::Ready { .. })
));
}
/// 演示模型里更新组能画(`pages_demo` 的 harness 路径):`Model.update` 有默认值,不是 `None` 才画。
#[test]
fn the_demo_model_carries_an_update_snapshot() {
let model = crate::pages::demo_model(Screen::Settings, 1180.0);
assert_eq!(model.update, crate::pages::update::demo());
assert!(
model.update.available().is_some(),
"演示快照要带一个可装的版本"
);
// 其它页不掺演示更新状态(实例页/下载页的基线图不该多出一条提示条)。
assert_eq!(
crate::pages::demo_model(Screen::Instances, 1180.0).update,
Update::default()
);
let catalog: Catalog = Lang::En.catalog();
assert!(!catalog.text(Key::SettingsGroupUpdate).is_empty());
}