fix(app-iced): 复审 r1 三条(在飞守卫补 Back/Target、预检按版本过期、shell 文档如实)

P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派
零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉——
实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在
改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。

P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选
1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。
现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句
「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、
不发命令),改成如实说明过期判定只能在这里做。

P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、
页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。

用例(两条都是先写红再修,红时 panic 信息就是错在哪):
- back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派
  Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`;
- fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期
  (否则画的是 1.20.1 的后果原文)`。

顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态),
出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
This commit is contained in:
AskaEth committed 2026-11-10 19:37:45 +08:00
1 parent 6ffbd51f60
commit fa3fab9086
5 files changed
+183 -8

No files matched your search

+110 -1
View File
@@ -48,6 +48,12 @@ impl App {
}
AdoptMessage::Name(name) => self.adopt_form.name = name,
AdoptMessage::Target(target) => {
// 登记在飞时不吃「换档」:它会派一条零写盘的预览把 `adopt_epoch` +1,
// 于是在飞的 Confirm 交卷时 Done 被代次丢掉——实例写进去了,界面什么都没说。
// 连本地档位也不改:界面上显示 A 档、盘上写的是 B 档,是最糟的一种不一致。
if self.adopt_form.busy == Busy::Importing {
return Task::none();
}
self.adopt_form.target = target;
// 换了目标档 ⇒ 上一档的预检作废(确认按钮不该继续亮着)
self.adopt_form.checked = false;
@@ -74,6 +80,11 @@ impl App {
}
}
AdoptMessage::Back => {
// 同 Close / Target:登记在飞时「返回」会把 `adopt_epoch` +1(它派 Scan),
// 在飞 Confirm 的 Done 就再也到不了界面。
if self.adopt_form.busy == Busy::Importing {
return Task::none();
}
// 退回候选列表 = 重新扫一次:`scan` 会把上一轮的预检/失败一起作废,
// 于是界面回到 Pick 段,不需要再给 core 加一个「清失败」的命令。
self.adopt_form.checked = false;
@@ -124,8 +135,13 @@ impl App {
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use unlucky_design::color::SchemeFlavor;
use unlucky_presenter::dto::{AdoptEvent, AdoptTargetView};
use unlucky_presenter::dto::{
AdoptCommand, AdoptEvent, AdoptTargetView, AppCommand, AppEvent, Backend,
};
use unlucky_presenter::mock::MockBackend;
use unlucky_presenter::settings_bridge::SettingsBridge;
@@ -150,6 +166,52 @@ mod tests {
)
}
/// 记账后端:把 `dispatch` 收到的命令留在台账里(`MockBackend` 只认自己那几条,
/// 「有没有派出去」根本看不见——而 P2-1 的判据正是「不许派」)。
#[derive(Debug, Default)]
struct RecordingBackend {
dispatched: Rc<RefCell<Vec<AppCommand>>>,
}
impl Backend for RecordingBackend {
fn drain_events(&mut self) -> Vec<AppEvent> {
Vec::new()
}
fn dispatch(&mut self, command: AppCommand) {
self.dispatched.borrow_mut().push(command);
}
}
/// 带台账的 App(返回台账句柄,测试直接查「派了什么」)。
fn recording() -> (App, Rc<RefCell<Vec<AppCommand>>>) {
let dispatched: Rc<RefCell<Vec<AppCommand>>> = Rc::new(RefCell::new(Vec::new()));
let app = App::with_presenter(
Options::default(),
FAMILIES,
SchemeFlavor::Light,
Box::new(RecordingBackend {
dispatched: Rc::clone(&dispatched),
}),
SettingsBridge::memory_only(),
);
(app, dispatched)
}
/// 台账里有没有派过某条 adopt 命令(只认命令种类,不看载荷内容)。
fn dispatched_adopt(ledger: &Rc<RefCell<Vec<AppCommand>>>, want: &str) -> bool {
ledger.borrow().iter().any(|command| match command {
AppCommand::Adopt(command) => match (&**command, want) {
(AdoptCommand::Scan, "scan") => true,
(AdoptCommand::Preview(_), "preview") => true,
(AdoptCommand::Confirm(_), "confirm") => true,
(AdoptCommand::Dismiss, "dismiss") => true,
_ => false,
},
_ => false,
})
}
/// 打开向导 = 表单进入「在等扫描」+ 派一条 `Scan`。
///
/// 抓得住:`Open` 没派命令(面板永远停在「正在扫描」)、或忘了置 `Busy::Scanning`(没有等待态)。
@@ -241,4 +303,51 @@ mod tests {
let _ = app.on_adopt(AdoptMessage::Close);
assert!(app.adopt_form.open, "登记在飞时不许把面板关掉");
}
/// **P2-1**:登记在飞时,除了 Close,「返回」与「换目标档」也必须早退。
///
/// 三条都是同一条病:它们都会派一条新命令把 `adopt_epoch` +1,于是在飞 Confirm 的
/// `Done` 回来时被代次丢掉 —— 实例**写进去了**,界面却什么都没说(Close 那条早就守住了,
/// 唯独这两支漏了)。
///
/// 抓得住:`Back` 派了 `Scan`(旧 `Done` 被丢)、`Target` 派了 `Preview`(同上),
/// 以及「守卫写了但写在改本地状态之后」(表单被改成新档位,而盘上写的是旧档位)。
/// 抓不住:host 侧代次比较的真实时序(那在 presenter 的 `host/adopt` 那层)。
#[test]
fn back_and_target_are_ignored_while_the_registration_is_in_flight() {
let (mut app, ledger) = recording();
let _ = app.on_adopt(AdoptMessage::Open);
let _ = app.on_adopt(AdoptMessage::Select("1.20.1".to_string()));
let _ = app.on_adopt(AdoptMessage::Target(AdoptTargetView::InstanceDir));
// 走到「确认」那一步:真的派一条 Confirm 出去
let _ = app.on_adopt(AdoptMessage::Check);
let _ = app.on_adopt(AdoptMessage::Confirm);
assert!(
dispatched_adopt(&ledger, "confirm"),
"前提:确认要真的派出去"
);
assert_eq!(app.adopt_form.busy, Busy::Importing);
ledger.borrow_mut().clear();
// 在飞期间点「返回」:不许派 Scan
let _ = app.on_adopt(AdoptMessage::Back);
assert!(
!dispatched_adopt(&ledger, "scan"),
"登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)"
);
assert_eq!(app.adopt_form.busy, Busy::Importing, "等待态不许被改掉");
// 在飞期间换目标档:不许派 Preview,也不许改本地档位
let _ = app.on_adopt(AdoptMessage::Target(AdoptTargetView::Shared));
assert!(
!dispatched_adopt(&ledger, "preview"),
"登记在飞时换档不许派 Preview(会把 Done 的代次顶掉)"
);
assert_eq!(
app.adopt_form.target,
AdoptTargetView::InstanceDir,
"挡下来的点击不该改出界面与盘上不一致的档位"
);
assert_eq!(app.adopt_form.busy, Busy::Importing);
}
}