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。
This commit is contained in:
1 parent
b1524635ba
commit
59011b2d56
2 files changed
+108
-6
No files matched your search
@@ -208,8 +208,15 @@ pub(super) fn report_of(
|
||||
overwrite_note: (!report.conflicts.is_empty())
|
||||
.then(|| catalog.text(Key::PackOverwriteNote).to_owned()),
|
||||
cancel: report.cancel,
|
||||
cancel_note: (report.cancel == ImportCancelView::TooLate)
|
||||
.then(|| catalog.text(Key::PackCancelTooLate).to_owned()),
|
||||
// 「取消太晚」那句只说给**真的跑完**的导入(复审 r3 P2-1):推导那边已经保证
|
||||
// `Failed` 不会落到 `TooLate`,这里再看一眼状态是双保险——万一推导回归,也不会让
|
||||
// 红卡("零残留")与"已经跑完、文件可能被覆盖"同时出现在一屏上。
|
||||
cancel_note: (report.cancel == ImportCancelView::TooLate
|
||||
&& matches!(
|
||||
report.status,
|
||||
ImportStatusView::Success | ImportStatusView::Partial
|
||||
))
|
||||
.then(|| catalog.text(Key::PackCancelTooLate).to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,11 @@ use crate::dto::ImportCancelView;
|
||||
/// core 的「取消真实结局」→ DTO 的三档(M195 r3:**不许**把三档混成一句)。
|
||||
///
|
||||
/// - core 说 `Cancelled` ⇒ 管线真的停手了;回滚完整与否看 `rolled_back`(两档分开说);
|
||||
/// - core 说别的(`Success`/`Partial`/`Failed`)但这次被点过取消 ⇒ **取消太晚**:报告是真结局,
|
||||
/// 但它不是"停下来了",界面必须照实说「实例会保留、文件可能已被覆盖」。
|
||||
/// - 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 => {
|
||||
@@ -21,7 +24,99 @@ pub(super) fn cancel_view(report: &ImportReport, cancel_requested: bool) -> Impo
|
||||
ImportCancelView::RollbackIncomplete
|
||||
}
|
||||
}
|
||||
_ if cancel_requested => ImportCancelView::TooLate,
|
||||
_ => ImportCancelView::NotRequested,
|
||||
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,
|
||||
"没点过取消就不该有取消结论"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user