Files
Unlucky-Launcher/crates/presenter/src/host/addon/pack/convert/cancel.rs
T
AskaEth 59011b2d56 fix(m195): 复审 r3 —— 失败不再被说成「取消太晚」,并给三档补推导用例
P2-1(界面撒谎):`cancel_view` 的 `_ if cancel_requested => TooLate` 把 `Failed` 也吞进去了。
「点取消的同时摘要不符」这类失败(取消置位之后、下一个检查点之前的非容忍失败)会得到
`status=Failed cancel=TooLate rolled_back=true`,于是同一屏上红卡说「已回滚 —— 目标目录零残留」,
下面还有一句「取消来得太晚:这次导入已经跑完,实例会保留,同名文件可能已被覆盖」——后半句是假的,
而且与红卡直接对立。改成**只有** `Success | Partial + cancel_requested` 才判 `TooLate`;
`Failed` 一律 `NotRequested`(失败卡自己用 `failure_stage` 分「没写过 / 回滚干净 / 回滚没做完」,
真相已经说全)。
另外 app-iced 的 `cancel_note` 加一道双保险:只有 `status` 是 `Success/Partial` 时才出那句话,
推导万一回归也不会贴到红卡上。

P2-2(推导零覆盖):此前只有「给定这一档、文案怎么画」的用例,把 `RollbackIncomplete` 的推导
删掉也全绿。现在 `convert/cancel.rs` 带三条推导用例:`Cancelled + !rolled_back ⇒ 回滚没做完`
(含对照 `rolled_back ⇒ 已回滚`)、`Failed + 取消 ⇒ NotRequested`(含对照 `Success/Partial ⇒ TooLate`)、
没点过取消 ⇒ `NotRequested`。

变异实测:
- P2-1:把 `Failed` 也判 `TooLate` ⇒ `a_failed_run_is_never_dressed_up_as_a_late_cancel` 红
  (`left: TooLate / right: NotRequested`,"失败的导入没有「跑完」这一说")。
- P2-2:删掉「回滚没做完」的推导(`Cancelled` 一律按已回滚)⇒ `an_incomplete_rollback_is_its_own_bucket` 红
  (`left: RolledBack / right: RollbackIncomplete`)。

验证:`cargo test -p unlucky-core -p unlucky-presenter -p unlucky-app-iced` = 2188 passed / 0 failed;
`-p unlucky-i18n` = 31 / 0;clippy 四 crate = 0 warning / 0 error。
2026-11-09 05:06:56 +08:00

123 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 「取消的**真实结局**」→ DTO 三档(M195 r3)。
//!
//! 从父模块拆出来只为行数(父文件已到 400 硬限附近),判据与文案口径原样。
use unlucky_core::addon::import::report::ImportStatus;
use unlucky_core::addon::import::ImportReport;
use crate::dto::ImportCancelView;
/// core 的「取消真实结局」→ DTO 的三档(M195 r3:**不许**把三档混成一句)。
///
/// - core 说 `Cancelled` ⇒ 管线真的停手了;回滚完整与否看 `rolled_back`(两档分开说);
/// - core 说 `Success`/`Partial` 而这次被点过取消 ⇒ **取消太晚**:报告是真结局,但它不是
/// "停下来了",界面必须照实说「实例会保留、文件可能已被覆盖」;
/// - core 说 `Failed` ⇒ **一律 `NotRequested`**(复审 r3 P2-1):失败卡自己已经把真相说全了
/// (`failure_stage` 分「没写过 / 回滚干净 / 回滚没做完」),再贴一句「取消太晚、已经跑完、
/// 文件可能被覆盖」就是与红卡**直接对立**的第二句假话——失败的导入没有"跑完"这一说。
pub(super) fn cancel_view(report: &ImportReport, cancel_requested: bool) -> ImportCancelView {
match report.status {
ImportStatus::Cancelled => {
if report.rolled_back {
ImportCancelView::RolledBack
} else {
ImportCancelView::RollbackIncomplete
}
}
ImportStatus::Success | ImportStatus::Partial if cancel_requested => {
ImportCancelView::TooLate
}
ImportStatus::Success | ImportStatus::Partial | ImportStatus::Failed => {
ImportCancelView::NotRequested
}
}
}
#[cfg(test)]
mod tests {
//! 三档的**推导**(不是"给定这一档、文案怎么画"):什么输入会推出哪一档。
//!
//! 这一族用例是复审 r3 P2-1/P2-2 的直接产物:此前只有下半段(文案)有用例,把推导删掉
//! 也全绿。
use unlucky_core::addon::import::report::ImportStatus;
use unlucky_core::addon::import::ImportReport;
use crate::dto::ImportCancelView;
use super::super::report_view;
fn core_report(status: ImportStatus, rolled_back: bool) -> ImportReport {
let mut report = ImportReport::empty(std::path::PathBuf::from("instance"));
report.status = status;
report.rolled_back = rolled_back;
report
}
fn cancel_of(
status: ImportStatus,
rolled_back: bool,
cancel_requested: bool,
) -> ImportCancelView {
report_view(
&core_report(status, rolled_back),
std::path::Path::new("instance"),
cancel_requested,
)
.cancel
}
#[test]
fn an_incomplete_rollback_is_its_own_bucket() {
// 推导:`Cancelled` + `rolled_back == false` ⇒ **回滚没做完**(不是"已回滚")。
// 把这条推导删掉(一律按已回滚)这里就红——那正是复审 r3 P2-2 说的零覆盖。
assert_eq!(
cancel_of(ImportStatus::Cancelled, false, true),
ImportCancelView::RollbackIncomplete
);
// 对照:回滚完整才是「已回滚」——两档不许并成一个。
assert_eq!(
cancel_of(ImportStatus::Cancelled, true, true),
ImportCancelView::RolledBack
);
}
#[test]
fn a_failed_run_is_never_dressed_up_as_a_late_cancel() {
// P2-1:失败 + 点过取消(例如"点取消的同时摘要不符")⇒ 失败卡自己说全了真相,
// 不许再贴「取消太晚、已经跑完、文件可能被覆盖」——那两句直接对立。
assert_eq!(
cancel_of(ImportStatus::Failed, false, true),
ImportCancelView::NotRequested,
"失败的导入没有「跑完」这一说"
);
assert_eq!(
cancel_of(ImportStatus::Failed, true, true),
ImportCancelView::NotRequested
);
// 对照:成功/部分成功 + 点过取消,才是「太晚」。
for status in [ImportStatus::Success, ImportStatus::Partial] {
assert_eq!(
cancel_of(status, false, true),
ImportCancelView::TooLate,
"{status:?} + 取消 ⇒ 太晚"
);
}
}
#[test]
fn an_untouched_run_carries_no_cancel_verdict() {
for status in [
ImportStatus::Success,
ImportStatus::Partial,
ImportStatus::Failed,
] {
assert_eq!(
cancel_of(status, false, false),
ImportCancelView::NotRequested,
"没点过取消就不该有取消结论"
);
}
}
}