fix(m195): 复审 r2 —— 取消的真实结局必须到达界面(三档 + 终态穿透代次)
复审 r2 P2-1:界面无条件断言「已停手并回滚:没有留下半包,也没有覆盖已有文件」,而两条路径下
这句话是假的——①取消来得太晚(落在最后一个检查点之后)时管线已经跑完,用户的同名文件被覆盖、
实例已登记;②回滚本身没做完(`rolled_back=false`)。core 一直是诚实的,是 presenter 把报告丢了:
`cancel.load()` 早退 + `cancel_pack` bump 代次后 `handle` 按代次丢弃。
按"接通真相"做(不是改软话):
- **终态一定发出**:`import::run` 去掉 `cancel` 的无条件早退,收尾抽成 `finish`(清自建空目录 +
无条件 `send(PackEvent::Report)`);取消标记在管线返回后**再读一次**,作为"太晚"的唯一证据。
- **终态穿透代次**:`HostPackState::terminal_epochs` 记下 `cancel_pack` 作废的那个代次,
`handle` 对它**只放行一条 `PackEvent::Report`**(进度事件照旧按代次丢,`fresh_cancel` 起新
一轮时清空)。这是路径 A 的成因与修法。
- **DTO 三档**(tower 批准扩 scope):`ImportStatusView` 加 `Cancelled`;新增
`ImportCancelView { NotRequested, RolledBack, RollbackIncomplete, TooLate }` 挂在
`ImportReportView::cancel` 上——已回滚 / 回滚没做完 / 取消太晚,三档各自可判。
`PackEvent::Report` 到达时清掉 `ImportState.cancelled`(它现在只是"取消后还没有结果"的占位)。
- **界面按结局分支**:标题 `PackStatusCancelled`(已取消 — 已回滚)/ `PackCancelIncomplete`
(已取消 — 回滚没做完)/ 取消太晚时保留真实状态标题并另起一句 `PackCancelTooLate`
(实例会保留、同名文件可能已被覆盖);占位的 `PackCancelled` 改成"正在停手,真实结果随后给出"。
图标:`Close`(描边,页面对填充字形有守门)/ 回滚不完整用 `Warn`,一档都不用 `Role::Error`。
守门与变异:
- 新增路径 A 用例 `a_late_cancel_still_delivers_the_real_outcome_to_the_ui`:真管线把取消置在
**最后一条 Commit 进度**上(之后没有检查点)⇒ core 报 `Success`/未回滚、用户的 `mods/a.jar`
确实被覆盖;再 `cancel_pack`(代次 +1 并记终态)→ `finish` → `handle`,断言界面收到
`status == Success` + `rolled_back == false` + `cancel == TooLate`,且同代次的进度事件仍被丢。
- 变异①:`handle` 恢复"代次不符一律丢" ⇒ 该用例红("取消太晚时,真实结果必须到达界面")。
- 变异②:`finish` 恢复"取消就早退" ⇒ 该用例红(另带 P2-2 那条也红)。
- app-iced 新增 `model/tests/cancel.rs`:三档标题互不相同 + 「太晚」必须有那句补充说明 +
已回滚不许显示成「部分成功」。
验证(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-194):
`cargo test -p unlucky-core -p unlucky-presenter -p unlucky-app-iced` = 2185 passed / 0 failed;
`-p unlucky-i18n` = 31 / 0;clippy 四 crate = 0 warning / 0 error。
This commit is contained in:
1 parent
db5b08b9ab
commit
b1524635ba
23 files changed
+627
-120
No files matched your search
@@ -8,6 +8,8 @@
|
||||
//! 于是「翻译表」本身就是一份可审计的清单:core 加了新变体,`from_slug` 返回 `None`
|
||||
//! 时这里会落到一个有名字的兜底,而不是悄悄画成别的意思。
|
||||
|
||||
mod cancel;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use unlucky_core::addon::export::{ExportReport, FileClass, FileReason};
|
||||
@@ -101,20 +103,18 @@ const fn reason_of(reason: ConflictReason) -> ConflictReasonView {
|
||||
|
||||
/// 一次导入结果 → DTO。
|
||||
///
|
||||
/// # `Cancelled` 为什么在这里没有对应档(M195)
|
||||
///
|
||||
/// DTO 的 `ImportStatusView` 只有三档,取消走的是 [`crate::dto::PackEvent::Cancelled`]
|
||||
/// (`import::run` 拿到 `ImportStatus::Cancelled` 就不发报告事件了)。所以本函数只会在
|
||||
/// "取消恰好落在两个检查点之间、报告仍然是成功/部分成功"时被调用。真收到 `Cancelled`
|
||||
/// 时按 `Partial` 落地:**绝不是** `Failed`——取消不是失败,不许画成红的。
|
||||
pub(super) fn report_view(report: &ImportReport, target: &Path) -> ImportReportView {
|
||||
/// `cancel_requested` = 这次运行**被点过取消**(presenter 手里的那个开关)。它只用来区分
|
||||
/// "取消太晚"那一档:管线已经跑完时报告是 `Success`/`Partial`,但那不是一次正常导入——
|
||||
/// 用户的取消没赶上,实例会保留、同名文件可能已被覆盖(M195 r3)。
|
||||
pub(super) fn report_view(
|
||||
report: &ImportReport,
|
||||
target: &Path,
|
||||
cancel_requested: bool,
|
||||
) -> ImportReportView {
|
||||
ImportReportView {
|
||||
status: match report.status {
|
||||
ImportStatus::Cancelled => ImportStatusView::Partial,
|
||||
other => {
|
||||
ImportStatusView::from_slug(other.as_str()).unwrap_or(ImportStatusView::Failed)
|
||||
}
|
||||
},
|
||||
status: ImportStatusView::from_slug(report.status.as_str())
|
||||
.unwrap_or(ImportStatusView::Failed),
|
||||
cancel: cancel::cancel_view(report, cancel_requested),
|
||||
format: report.format.as_deref().and_then(PackFormatView::from_slug),
|
||||
pack_name: report.pack_name.clone(),
|
||||
pack_version: report.pack_version.clone(),
|
||||
@@ -330,6 +330,8 @@ fn failed_report_with(target: PathBuf, pack: &Path, error: ImportErrorView) -> I
|
||||
rolled_back: true,
|
||||
// 预检/装配阶段就失败了:目标目录一个字节都没写(不是「写完又回滚」)。
|
||||
failure_stage: Some(ImportFailureStageView::NotStarted),
|
||||
// 这一路根本没进管线,谈不上取消。
|
||||
cancel: crate::dto::ImportCancelView::NotRequested,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user