fix(launch): M157 r3 周快照门限按继承根版本 + 保存失败保留草稿(复审 P2-1/P2-2)

P2-1 周快照 + 加载器的 --graphicsBackend 静默丢失:presenter host::launch::build_request 与 cli snapshot::prepare 给 SnapshotInput.game_version 传 None,装配按带加载器后缀的 version_id(26w14a-fabric-0.16.5)判门限,GameVersion::is_weekly_snapshot 的 6 字符形态认不出来。两个调用点改为传继承链的 MC 根版本(25 行注释说明「门槛与拼装必须同一口径」)。回归:core launch::overrides 的周快照实际装配用例(正/负两档)、presenter host::launch::tests、cli snapshot::tests 各一条根版本断言。

P2-2 保存失败丢草稿:app/update/instances.rs 的 Save 分支派发前就把 launch_form.dirty 清掉,失败事件后 pump 用旧快照重建视图并覆盖表单。改为派发不清脏;LaunchForm::sync 只在「回程快照已等于表单」(= 落盘确认)时清脏并重播种,否则保留草稿。回归:failed_save_keeps_the_draft / confirmed_save_clears_dirty_and_reseeds / save_dispatch_keeps_the_dirty_flag。

顺带:launch::overrides 测试拆到 overrides/tests.rs(守 400 行)。
This commit is contained in:
AskaEth committed 2026-11-07 10:36:37 +08:00
1 parent c76a1437e6
commit 211e07b470
9 files changed
+640 -311

No files matched your search

+17 -5
View File
@@ -53,8 +53,11 @@ impl App {
self.launch_form.reset();
}
InstanceMessage::Save => {
// **不在这里清脏**:`dispatch` 只是把命令送出去,落盘成败要等回程事件。
// 失败时(内存 `4g` / 写盘失败)pump 会带**旧快照**重建视图,脏标记若已清掉,
// `LaunchForm::sync` 就会用旧值覆盖用户刚敲的草稿(M157 r3 复审 P2-2)。
// 清脏的唯一时机是「回程快照已经等于表单」——见 `LaunchForm::sync`。
let patch: LaunchArgsPatch = self.launch_form.patch();
self.launch_form.dirty = false;
self.dispatch(AppCommand::set_launch_args(&instance, patch));
}
}
@@ -149,13 +152,22 @@ mod tests {
assert!(!app.launch_form.dirty);
}
/// 保存之后脏标记清掉(否则下一次快照刷新不敢重播种,界面会一直显示旧值)。
/// **保存只派发、不清脏**(M157 r3 复审 P2-2):落盘成败要等回程事件,
/// 派发前清掉脏标记会让失败后的重播覆盖用户草稿。
///
/// 抓得住:Save 分支里又写回 `dirty = false`(那条路径在失败时丢草稿)。
/// 抓不住:回程事件的真实时序——清脏判据在 `LaunchForm::sync` 的
/// `confirmed_save_clears_dirty_and_reseeds` / `failed_save_keeps_the_draft` 里。
#[test]
fn save_clears_the_dirty_flag() {
fn save_dispatch_keeps_the_dirty_flag() {
let mut app = app_with_form();
let _ = app.on_instance_message(InstanceMessage::Java("D:/jdk21/bin/java.exe".to_string()));
let _ = app.on_instance_message(InstanceMessage::Memory("4g".to_string()));
assert!(app.launch_form.dirty);
let _ = app.on_instance_message(InstanceMessage::Save);
assert!(!app.launch_form.dirty);
assert!(
app.launch_form.dirty,
"派发后仍要保留脏标记,等回程快照确认"
);
assert_eq!(app.launch_form.memory, "4g", "派发不改表单内容");
}
}
@@ -68,14 +68,36 @@ impl LaunchForm {
}
/// 重播种(见模块文档的三档)。
///
/// **脏草稿只在「落盘已确认」时被替换**:保存把命令派发出去之后,回程可能是一条错误事件
/// (内存写了 `4g`、实例文件写不进去),那时 pump 会带着**旧快照**重建视图——如果这里无条件
/// 覆盖,用户刚敲的东西就没了(M157 r3 复审 P2-2)。判据是「快照已经等于表单」:
/// 相等 ⇒ 这次保存落了盘,清脏并重播种;不相等 ⇒ 保留草稿等用户改。
pub fn sync(&mut self, view: &LaunchArgsView) {
let same_instance = self.bound_to(&view.instance_id);
if same_instance && self.dirty {
if self.matches(view) {
self.dirty = false;
*self = Self::from_view(Some(&view.instance_id), view);
}
return;
}
*self = Self::from_view(Some(&view.instance_id), view);
}
/// 表单当前内容是否与一份快照(= 盘上已持久化的值)完全一致。
///
/// 比较的是**文本口径**:内存按「快照有值 → 十进制串,无值 → 空串」对齐,其余直接比原文。
#[must_use]
pub fn matches(&self, view: &LaunchArgsView) -> bool {
let memory = view.memory_mb.map(|mb| mb.to_string()).unwrap_or_default();
self.jvm.trim() == view.jvm_args.trim()
&& self.game.trim() == view.game_args.trim()
&& self.memory.trim() == memory
&& self.java.trim() == view.java_path.trim()
&& self.backend == view.graphics_backend
}
/// 提交补丁:**五项全给**(表单是整份可见的,不用增量语义会让「清空」无法表达)。
#[must_use]
pub fn patch(&self) -> LaunchArgsPatch {
@@ -169,6 +191,44 @@ mod tests {
assert!(form.bound_to("b"));
}
/// **保存失败不许吃掉草稿**(M157 r3 复审 P2-2):脏表单 + 旧快照回来 ⇒ 原样保留。
///
/// 抓得住:`sync` 在「同实例 + 脏」时无条件重播种(用户敲的 `4g` 被磁盘上的 4096 覆盖)。
/// 抓不住:真实事件时序(那在 `app/update/instances.rs` 的用例里按消息顺序模拟)。
#[test]
fn failed_save_keeps_the_draft() {
let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default"));
// 用户把内存改成非法值(host 会拒),此时表单是脏的。
form.memory = "4g".to_string();
form.jvm = "-Xmx3g".to_string();
form.dirty = true;
// 回程事件带来的仍是**旧**快照(保存失败 ⇒ 盘上没变)。
form.sync(&view("a", "", Some(4096), "default"));
assert_eq!(form.memory, "4g", "失败后的重播把用户正在改的内存吃掉了");
assert_eq!(form.jvm, "-Xmx3g");
assert!(form.dirty, "草稿还在,脏标记也不能掉");
}
/// **保存成功要清脏**:回程快照等于表单 ⇒ 这一步算落盘确认,之后可以跟着外部变化重播种。
#[test]
fn confirmed_save_clears_dirty_and_reseeds() {
let mut form = LaunchForm::from_view(Some("a"), &view("a", "", Some(4096), "default"));
form.memory = "8192".to_string();
form.backend = "vulkan".to_string();
form.dirty = true;
form.sync(&view("a", "", Some(8192), "vulkan"));
assert!(!form.dirty, "盘上已经是这份值了,脏标记该清");
assert_eq!(form.memory, "8192");
// 之后外部改了配置(比如 CLI 写入)⇒ 干净的表格跟着更新。
form.sync(&view("a", "-Xss4m", Some(2048), "opengl"));
assert_eq!(form.memory, "2048");
assert_eq!(form.jvm, "-Xss4m");
}
/// 补丁五项全给:空串就是「清空」,不留 `None` 让 host 猜。
#[test]
fn patch_always_covers_all_five_fields() {