fix(m161-r1): 崩溃/静默失败不再冒充「启动进程失败」;补三处假绿的守卫
复审 P1-1(用户可见假文案):`LaunchEnd` 在 `failure == None`(跑起来之后崩的)时
临时造了 `Stage::Spawn + format!("exit {:?}")`,于是磁贴写「启动失败 · 启动游戏进程」、
详情页漏出 `exit Some(1)`、静默失败丢掉「exit 0 却没起来」,而同一时刻 Toast 说「崩溃」。
- 新增 `LaunchFailureCause{Stage|Crashed|SilentFailure}`(dto/launch_failure.rs),
`LaunchFailureCause::of(outcome, failure, exit_code)` 是唯一组装点;`InstanceRuntime::Failed{cause}`。
`LaunchFailureStage` 保持「只装装配阶段」(文档那句「崩溃是结局不是阶段」现在成立)。
- 界面:`InstancesFailed`(框架)+ `InstancesCrashed` + `InstancesSilentFailed`;
删掉 `InstancesFailedStage`。退出码按数字出词,不再有 Debug 文本。
- 用例:presenter 断言**枚举变体**(三种形状逐条)、app 断言**具体句子**
(「不许出现 Some(」「进程起来了不许说启动游戏进程」「静默失败要点出没起来」)。
mock 夹具与 launch_state_tests 的期望值同步;launch-error 三张图重拍。
复审 P2-1(`convert::failure_stage` 零覆盖):新增 `convert/stage_tests.rs` 5 条(三档 +
「既没回滚也没宣称干净」+ 非致命无阶段 + `failed_report_with` ⇒ NotStarted)。
变异实测:改成恒 `RolledBack` ⇒ 3 条红。
复审 P2-2(矮窗口用例的算式三个数全错、`body_height` 零测试):用例改成直接调
`frame::body_height`,chrome 从 `Ui::metrics` + `WidthClass::gutter` 现取,并把门槛与真实
chrome 绑死(chrome+MIN ⇒ Split、-1px ⇒ ScrollWhole);新增 `frame` 自己的 `body_height` 用例。
变异实测:`2.0 * gutter` → `gutter` ⇒ 2 条红。
复审 P2-3(源指纹闸门 / `Refused` 零覆盖):新增两条 core 用例——闸门(原地留着 prefs.json
+ 预先写好的同源收据 ⇒ Nothing 且收据未被重写,反证:源变了允许重放)、`Refused`
(`$schema` 1.7.0 ⇒ ReadOnly ⇒ 不动旧文件、不写收据、理由含「不可写」)。
变异实测:删闸门 ⇒ 1 条红;`can_save` 守卫改 `if false` ⇒ 1 条红。
顺手收:
- CLI `pack import` 那句「已整包回滚」也按三档出词(`RollbackStage` + `rollback_line`),
加 2 条单测。CLI 不依赖 presenter,判定暂与 presenter 各一份 → 已 file finding 建议下沉 core。
- 设置页 `status_text()` 抽出 applying 档,用例断言「正在安装…」且非错误色。
金表:新 key 加在 `golden.rs`(81 行),未动 `golden/tail.rs`(211 行),避免与 M162 的拆分冲突。
This commit is contained in:
1 parent
6804b0ad1d
commit
79b796fd58
21 files changed
+894
-168
No files matched your search
@@ -375,3 +375,7 @@ pub(super) fn slug(text: &str) -> String {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert/stage_tests.rs"]
|
||||
mod stage_tests;
|
||||
@@ -0,0 +1,108 @@
|
||||
//! M161 复审 P2-1:`failure_stage` 是⑥三档的**唯一**判定点,此前零覆盖(把它改成恒
|
||||
//! `RolledBack`,presenter 254 条全绿)。这里直接喂 core 的机器位,断言的是**判定**,
|
||||
//! 不是渲染(渲染在 app-iced 的 `packui/model/tests/stage.rs`)。
|
||||
//!
|
||||
//! 抓得住:`None if report.rolled_back` 被写反(预检失败 → 「已回滚」,正是⑥要消灭的谎);
|
||||
//! `is_complete()` 被丢掉(回滚没做完也说成零残留);`status != Failed` 也发阶段。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_core::addon::import::{ImportError, ImportReport, ImportStatus, RollbackOutcome};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 一份「失败」的空报告(还没决定回滚结果)。
|
||||
fn failed() -> ImportReport {
|
||||
let mut report = ImportReport::empty(PathBuf::from("C:/tmp/.minecraft"));
|
||||
report.fail(ImportError::integrity("sha1 mismatch"));
|
||||
report
|
||||
}
|
||||
|
||||
/// 预检就失败(**从未写过目标目录**)⇒ `NotStarted`。
|
||||
///
|
||||
/// core 的 `abandoned()` 就是这一形:`rollback == None` + `rolled_back == true`。
|
||||
#[test]
|
||||
fn preflight_failure_is_not_started() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = true;
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::NotStarted),
|
||||
"没写过目标目录就是「根本没开始」,不许说成「已回滚」"
|
||||
);
|
||||
assert_eq!(failure_stage(&report).unwrap().slug(), "not_started");
|
||||
// 渲染层同一份判据(`failed_report_with` 走的是同一条)。
|
||||
assert_eq!(
|
||||
failed_report_with(
|
||||
PathBuf::from("C:/tmp"),
|
||||
std::path::Path::new("Better MC.mrpack"),
|
||||
ImportErrorView {
|
||||
code: "pack_traversal".to_string(),
|
||||
label: "Path traversal".to_string(),
|
||||
message: "../etc/passwd".to_string(),
|
||||
hint: None,
|
||||
},
|
||||
)
|
||||
.failure_stage,
|
||||
Some(ImportFailureStageView::NotStarted),
|
||||
"宿主侧失败(目标实例不可用)也是「没开始写」,不是「已回滚」"
|
||||
);
|
||||
}
|
||||
|
||||
/// 跑过回滚且完整 ⇒ `RolledBack`。
|
||||
#[test]
|
||||
fn a_complete_rollback_is_rolled_back() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = true;
|
||||
report.rollback = Some(RollbackOutcome {
|
||||
attempted: 3,
|
||||
failed: Vec::new(),
|
||||
});
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RolledBack)
|
||||
);
|
||||
assert_eq!(failure_stage(&report).unwrap().slug(), "rolled_back");
|
||||
}
|
||||
|
||||
/// 跑过回滚但有失败项 ⇒ `RollbackIncomplete`(目标目录可能与导入前不同)。
|
||||
#[test]
|
||||
fn a_partial_rollback_is_incomplete() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = false;
|
||||
report.rollback = Some(RollbackOutcome {
|
||||
attempted: 3,
|
||||
failed: vec!["mods/a.jar: 占用中".to_string()],
|
||||
});
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RollbackIncomplete)
|
||||
);
|
||||
assert_eq!(
|
||||
failure_stage(&report).unwrap().slug(),
|
||||
"rollback_incomplete"
|
||||
);
|
||||
}
|
||||
|
||||
/// 既没回滚也没宣称干净(core 的 `failed()` 单独一支)⇒ 只能如实说「可能不一致」。
|
||||
#[test]
|
||||
fn a_failure_without_any_claim_is_incomplete_not_not_started() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = false;
|
||||
report.rollback = None;
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RollbackIncomplete),
|
||||
"没跑回滚也不宣称干净:不许说「没开始写」"
|
||||
);
|
||||
}
|
||||
|
||||
/// 不是致命失败 ⇒ 没有「失败阶段」可言(成功/部分成功都不该发这一段)。
|
||||
#[test]
|
||||
fn a_non_fatal_report_has_no_stage() {
|
||||
let mut report = ImportReport::empty(PathBuf::from("C:/tmp"));
|
||||
report.rolled_back = false;
|
||||
assert_eq!(failure_stage(&report), None, "Success 不该有失败阶段");
|
||||
report.status = ImportStatus::Partial;
|
||||
assert_eq!(failure_stage(&report), None, "Partial 不该有失败阶段");
|
||||
}
|
||||
Reference in new issue
Block a user