fix(presenter,app-iced): 删除确认绑定到对话框身份 + 快照不再吃掉目标(M247 r2 复审)

复审 r1 P1-1(安全):`SavesState::confirm` 的「关闭即清」在 Esc 这条路径上没有落实,
而主按钮分支无条件先取目标 —— 「删存档 → Esc → 崩溃卡片 → 导出诊断包」会静默删掉那个世界。

两层修,各自有独立用例钉住:

1. **认框不认人**:`confirm` 改成 `Option<(DialogId, String)>`,`take_confirm(current)`
   只在 id 相等时交付;`DialogMessage::Primary` 传 `dialogs.current().id`。即使别的路径漏清,
   也核销不了别人那个框。用例 `a_leaked_target_cannot_be_redeemed_by_another_dialog`
   (去掉 id 校验即红:实测「漏清的目标被别的框兑现了:世界被静默删除」)。
2. **关闭路径补全**:`update/dialog.rs::escape()` 补 `clear_confirm()`(次按钮/遮罩早就清了,
   Esc 漏了)。用例 `escaping_the_delete_dialog_disarms_it_for_the_next_one` 复现整条事故链,
   断言没有下发 `DeleteWorld`、框走自己的路关掉(去掉这行即红)。

P2-1:`changed()` 不再无条件清 `confirm`(一拍无关快照会让主按钮变成静默空操作)——
只在「被确认的世界已经从快照里消失」时清,并登记 `closed_confirm` 让 app 层**收起那个框**
(`refresh_view → settle_saves_confirm`),避免留一个点了没反应的按钮。`failed()` 同理不动 confirm。
用例:`an_unrelated_snapshot_keeps_the_confirmation_redeemable`、`a_vanished_world_closes_its_confirmation`、
`an_unrelated_snapshot_no_longer_drops_the_confirmation`、`the_confirmation_is_dropped_and_reported_when_the_world_vanishes`。

P2-2:`catalog/instances.rs` 那段「这一版只读 / InstancesSavesReadOnly」的陈旧说明改成现状
(写操作已接上,边界由 core 的 `write::boundary` 守)。

拆分(400 行):`presenter/src/state/saves.rs` 的交互态方法 → `state/saves/interaction.rs`、
判据 → `state/saves/tests.rs`;`app/update/dialog.rs` 的接线用例内联在该文件(5 条)。
This commit is contained in:
AskaEth committed 2026-11-11 13:51:07 +08:00
1 parent 8c67e4eca6
commit 6c87e2ad8a
8 files changed
+682 -229

No files matched your search

+24
View File
@@ -68,6 +68,9 @@ impl App {
self.sync_saves();
// M224:启停失败的提示语(presenter 只发结构,文案在这里按 i18n 现求)。
self.note_mods_failure();
// M247(复审 r1 P2-1):被确认的世界已经从快照里消失 ⇒ 收起那个确认框。
// 不这样的话主按钮就是一次静默空操作(点了没删、也不说一句)。
self.settle_saves_confirm();
// 分类是**页面自己持有的过滤条件**(真 host 丢弃 `ListVersions` 的分类参数):
// 重建视图模型时带回去,否则每次事件都会把 chip 打回「全部」(复审 P2-1)。
@@ -100,6 +103,27 @@ impl App {
self.refreshes += 1;
}
/// 收掉「被确认的世界已经不在快照里」的那个删除确认框(M247,复审 r1 P2-1)。
///
/// 判据由 presenter 给(`SavesState::take_closed_confirm`:那个世界从快照里消失了),
/// **框的 id 也由它带着**——所以只收我们自己那个框,崩溃卡片/进度框一概不碰。
///
/// 为什么必须收:目标已经作废,主按钮就成了一次**静默空操作**(用户点了「删除世界」,
/// 框关了、什么也没发生、也没一句解释)。少删是安全的,但"看起来坏了"不是可接受的结果。
fn settle_saves_confirm(&mut self) {
let Some(dialog) = self.state.saves.take_closed_confirm() else {
return;
};
if self
.state
.dialogs
.current()
.is_some_and(|current| current.id == dialog)
{
self.close_dialog();
}
}
/// 把详情页当前实例的启动参数快照灌进表单(唯一的重播种点,见 `pages/instances/state.rs`)。
///
/// 不在详情页(或实例已不在快照里)时解绑:下次进详情页会重新播种,