Files
Unlucky-Launcher/crates/app-iced/src/pages/content/packui/model/tests/cancel.rs
T
AskaEth b1524635ba 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。
2026-11-09 05:06:55 +08:00

141 lines
4.6 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.
//! 取消的**三档真实结局**在界面上的分支(M195 r3)。
//!
//! 口径:一句「已停手并回滚」打天下是假话——取消可能停在提交前(已回滚)、可能停在回滚没收尾
//! (目标目录可能变了)、也可能**太晚**(管线已经跑完,实例保留、同名文件可能已被覆盖)。
//! 这一族用例钉的就是"三档各说各的话",以及"占位文案不再替结局下结论"。
use std::path::PathBuf;
use unlucky_i18n::{Catalog, Key, Lang};
use unlucky_presenter::dto::{
ImportCancelView, ImportReportView, ImportStatusView, PackFormatView,
};
use super::super::report::report_of;
use super::super::PackPageState;
fn catalog() -> Catalog {
Lang::En.catalog()
}
fn report(
status: ImportStatusView,
cancel: ImportCancelView,
rolled_back: bool,
) -> ImportReportView {
ImportReportView {
status,
format: Some(PackFormatView::Multimc),
pack_name: Some("Better MC".to_string()),
pack_version: None,
game_version: Some("1.20.1".to_string()),
loader: None,
version_id: Some("1.20.1".to_string()),
target: PathBuf::from("C:/tmp/instance"),
files: Vec::new(),
conflicts: Vec::new(),
missing: Vec::new(),
rejected: Vec::new(),
warnings: Vec::new(),
error: None,
registered: None,
bytes_written: 0,
rolled_back,
failure_stage: None,
cancel,
}
}
#[test]
fn a_rolled_back_cancel_says_it_is_cancelled_not_partially_installed() {
// 取消停在提交前:已回滚 ⇒ 标题是「已取消 —— 已回滚」,**不是**「部分成功」(那等于说装了一半),
// 也不能出现「取消太晚」那句补充说明。
let view = report(
ImportStatusView::Cancelled,
ImportCancelView::RolledBack,
true,
);
let block = report_of(&view, &PackPageState::default(), catalog());
assert_eq!(block.title, catalog().text(Key::PackStatusCancelled));
assert_ne!(
block.title,
catalog().text(Key::PackStatusPartial),
"已回滚的取消不许显示成「部分成功」"
);
assert!(!block.installed(), "取消不是「装上了」");
assert!(block.cancel_note.is_none(), "已回滚不需要「太晚」那句");
}
#[test]
fn an_incomplete_cancel_rollback_says_the_directory_may_differ() {
let view = report(
ImportStatusView::Cancelled,
ImportCancelView::RollbackIncomplete,
false,
);
let block = report_of(&view, &PackPageState::default(), catalog());
assert_eq!(block.title, catalog().text(Key::PackCancelIncomplete));
assert_ne!(
block.title,
catalog().text(Key::PackStatusCancelled),
"回滚没做完 ≠ 已回滚(这两句话不能混)"
);
}
#[test]
fn a_too_late_cancel_says_the_import_already_finished() {
// 取消来得太晚:状态是真实的 `Success`,另加一句「实例会保留、同名文件可能已被覆盖」。
let view = report(ImportStatusView::Success, ImportCancelView::TooLate, false);
let block = report_of(&view, &PackPageState::default(), catalog());
assert_eq!(block.title, catalog().text(Key::PackStatusSuccess));
assert_eq!(block.status, ImportStatusView::Success);
assert_ne!(
block.status,
ImportStatusView::Cancelled,
"太晚 ≠ 已回滚(状态是真的 Success/Partial)"
);
assert_eq!(
block.cancel_note,
Some(catalog().text(Key::PackCancelTooLate).to_owned()),
"必须另外说清「取消没赶上」"
);
}
/// 三档的标题**互不相同**(把任意两档并成一句,这里就红)。
#[test]
fn the_three_cancel_outcomes_have_three_different_titles() {
let titles = [
report_of(
&report(
ImportStatusView::Cancelled,
ImportCancelView::RolledBack,
true,
),
&PackPageState::default(),
catalog(),
)
.title,
report_of(
&report(
ImportStatusView::Cancelled,
ImportCancelView::RollbackIncomplete,
false,
),
&PackPageState::default(),
catalog(),
)
.title,
report_of(
&report(ImportStatusView::Success, ImportCancelView::TooLate, false),
&PackPageState::default(),
catalog(),
)
.title,
];
let unique: std::collections::BTreeSet<&String> = titles.iter().collect();
assert_eq!(unique.len(), 3, "三档必须各说各的话:{titles:?}");
}