feat(dlflow): M105 安装选项区(四家加载器 + 官方版本列表 + 实例名)与取消安装真停
用户实跑反馈两条,本轮各自定位到根因并修掉:
1. 「下载页没有前置加载器选项」= 功能未做:`Message::InstallVersion` 直接派
`AppCommand::InstallVersion`(= 只装原版),core 早在 M32/M35/M45 就有四家加载器的
安装管线。现在选中版本之后面板先给「安装选项」区(原版 / Forge / NeoForge / Fabric /
Quilt + 该加载器的官方版本列表 + 实例名),点「安装」才派
`AppCommand::CreateInstance`。
- 加载器版本列表复用 core 的 `LoaderSource::loader_versions`,经新增的
`AppCommand::ListLoaderVersions` / `CoreEvent::LoaderVersions` 回流;
缓存键是 `(加载器, 原版版本)`,切版本不会串列表;取不到给具名原因并**禁掉确认**。
- 不兼容组合用静态规则在**选中那一刻**说清(NeoForge 需 1.20.1+、Fabric/Quilt 需
1.14+、远古版四家都不支持),不让用户「选完才失败」,也不为不可能的组合作无谓联网。
- 选项草稿(选中 + 加载器 + 加载器版本 + 实例名)住在 `App`,`refresh_view` 带进页面
模型(与分类筛选同一做法;页面自己存不住跨事件的东西)。
2. 「点取消没反应」= 真 bug,两跳各断一半:
- 第一跳(UI 侧):取消确实生效了,但**还在跑的后台线程继续发进度**,`InstallProgress(None)`
刚把面板收回,下一条「下载中」立刻又点亮它 —— 用户看到的就是「点了没反应」。
修法:`CoreHost` 加安装代次 `install_epoch`(开工与取消都 +1),`HostTask::Install` /
`InstallDone` 带代次回来,代次不符一律丢。
- 第二跳(core 侧):`execute` 没有任何取消入参,取消只停在段边界,传输根本没停。
新增 `execute_with_cancel(…, cancel: Option<&AtomicBool>)`(既有 `execute` 原样包装,
CLI 一字未改):取消后不再起新文件,在飞的那个与「取消 future」`select` 赛跑,取消赢就
drop 掉下载 future(响应流关闭、`.ullpart` 由 `PartFile::Drop` 清掉),并且不写版本清单,
返回新的 `InstallErrorKind::Cancelled`。
- 另外:取消后**绝不登记实例**(文件段跑完但用户已取消的那一路也拦住);取消文案把
「哪些留着、哪些一定没发生」说清(D21)。
实测(真网络装置 `host/cancel_tests.rs`,`#[ignore]` 守着,跑法见文件头):
取消前 files_done=2 / `.ullpart` 613 字节;取消后 20 s 进度事件 **0 条**、`.ullpart` **0 字节**
(-613)、版本清单未落盘、实例目录不存在。取消前是 +201466 字节(同一个装置量的)。
文案全部进 i18n(中英,9 条新 key + 金表同步)。
This commit is contained in:
1 parent
d690d09b84
commit
30b1ff6f48
39 files changed
+2550
-518
No files matched your search
@@ -0,0 +1,254 @@
|
||||
//! 安装选项的决策单测(M105):兼容性真值表 + 确认键门 + 默认加载器版本。
|
||||
//!
|
||||
//! 全部是**纯函数**(`OptionsView::of` 吃 `AppState` 与 `VersionRow`,不碰 iced),
|
||||
//! 所以不开图形环境也能钉住「什么组合能装、什么组合按下不去」。
|
||||
//!
|
||||
//! **为什么用 `AppState` 而不是 mock 后端**:加载器版本列表是快照字段
|
||||
//! (`state.loader_versions`),灌一条 `CoreEvent::LoaderVersions` 就能造出三种状态
|
||||
//! (没查过 / 查到了 / 取不到),比 mock 更直接,也不依赖 `MockBackend` 的脚本。
|
||||
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::dto::{
|
||||
AncientKind, AppEvent, CoreEvent, LoaderKind, VersionKind, VersionSummary,
|
||||
};
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
use super::model::{release_rank, Incompat, InstallDraft, Loader, LoaderVersionsView, OptionsView};
|
||||
use crate::pages::download::model::VersionRow;
|
||||
|
||||
fn row(id: &str, kind: VersionKind) -> VersionRow {
|
||||
VersionRow::of(
|
||||
&VersionSummary {
|
||||
id: id.to_string(),
|
||||
kind,
|
||||
released: "2026-09-20".to_string(),
|
||||
installed: false,
|
||||
},
|
||||
Lang::En.catalog(),
|
||||
)
|
||||
}
|
||||
|
||||
fn state_with(kind: LoaderKind, mc: &str, items: Result<Vec<&str>, &str>) -> AppState {
|
||||
let mut state = AppState::new();
|
||||
state.apply(AppEvent::Core(CoreEvent::LoaderVersions {
|
||||
kind,
|
||||
mc_version: mc.to_string(),
|
||||
result: items
|
||||
.map(|list| {
|
||||
list.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.map_err(str::to_string),
|
||||
}));
|
||||
state
|
||||
}
|
||||
|
||||
fn draft(loader: Loader) -> InstallDraft {
|
||||
InstallDraft {
|
||||
selected: Some("1.20.1".to_string()),
|
||||
loader,
|
||||
loader_version: String::new(),
|
||||
instance_name: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本号解析:只有纯数字三段(或缺末段)才算正式版。
|
||||
#[test]
|
||||
fn release_rank_only_accepts_plain_release_ids() {
|
||||
assert_eq!(release_rank("1.20.1"), Some((1, 20, 1)));
|
||||
assert_eq!(release_rank("1.20"), Some((1, 20, 0)), "缺末段按 0 算");
|
||||
assert_eq!(release_rank("24w14a"), None, "快照不是正式版号");
|
||||
assert_eq!(release_rank("rd-132211"), None);
|
||||
assert_eq!(
|
||||
release_rank("1.20.1-pre3"),
|
||||
None,
|
||||
"预发布不许当正式版放过去"
|
||||
);
|
||||
assert_eq!(release_rank("1.20.1.2"), None, "多一段不认");
|
||||
}
|
||||
|
||||
/// 兼容性真值表(任务书点名的「老版本 × NeoForge」在这条里)。
|
||||
#[test]
|
||||
fn loader_support_matrix_is_explicit() {
|
||||
// 原版永远可以
|
||||
for id in ["1.21.9", "1.12.2", "rd-132211"] {
|
||||
let kind = if id.starts_with("rd") {
|
||||
VersionKind::Ancient(AncientKind::Classic)
|
||||
} else {
|
||||
VersionKind::Release
|
||||
};
|
||||
assert_eq!(Loader::Vanilla.supports(id, kind), Ok(()));
|
||||
}
|
||||
// NeoForge 要 1.20.1+
|
||||
assert_eq!(
|
||||
Loader::NeoForge.supports("1.20.1", VersionKind::Release),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
Loader::NeoForge.supports("1.12.2", VersionKind::Release),
|
||||
Err(Incompat::NeedsAtLeast("1.20.1")),
|
||||
"老版本 × NeoForge 必须在选中那一刻就说清,而不是点安装才失败"
|
||||
);
|
||||
// Fabric/Quilt 要 1.14+
|
||||
assert_eq!(
|
||||
Loader::Fabric.supports("1.20.1", VersionKind::Release),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
Loader::Fabric.supports("1.12.2", VersionKind::Release),
|
||||
Err(Incompat::NeedsAtLeast("1.14"))
|
||||
);
|
||||
// Forge 任何正式版都行,但预览版/远古版没有 maven 条目
|
||||
assert_eq!(
|
||||
Loader::Forge.supports("1.12.2", VersionKind::Release),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
Loader::Forge.supports("24w14a", VersionKind::Snapshot),
|
||||
Err(Incompat::Unsupported)
|
||||
);
|
||||
// 远古版:四家全都不支持
|
||||
let ancient = ("a1.2.6", VersionKind::Ancient(AncientKind::OldAlpha));
|
||||
for loader in [
|
||||
Loader::Fabric,
|
||||
Loader::Quilt,
|
||||
Loader::Forge,
|
||||
Loader::NeoForge,
|
||||
] {
|
||||
assert_eq!(
|
||||
loader.supports(ancient.0, ancient.1),
|
||||
Err(Incompat::Unsupported)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 不兼容的组合:给具名提示,并且**确认键按下不去**。
|
||||
#[test]
|
||||
fn incompatible_combination_disables_confirm_with_a_named_reason() {
|
||||
let state = AppState::new();
|
||||
let options = OptionsView::of(
|
||||
&row("1.12.2", VersionKind::Release),
|
||||
&state,
|
||||
&draft(Loader::NeoForge),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(!options.can_confirm, "不兼容就不许按下确认");
|
||||
let problem = options.problem.expect("要给出具名原因");
|
||||
assert!(problem.contains("NeoForge"), "{problem}");
|
||||
assert!(problem.contains("1.20.1"), "{problem}");
|
||||
}
|
||||
|
||||
/// 默认加载器版本 = 列表里最新的一条(core 的默认口径);用户选过就用用户的。
|
||||
#[test]
|
||||
fn loader_version_defaults_to_the_newest_and_honours_the_choice() {
|
||||
let state = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec!["0.16.5", "0.16.4"]));
|
||||
let options = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&state,
|
||||
&draft(Loader::Fabric),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(options.can_confirm);
|
||||
assert_eq!(options.loader_version, "0.16.5", "没选过就用最新");
|
||||
assert_eq!(options.loader_version_arg(), Some("0.16.5"));
|
||||
|
||||
let mut chosen = draft(Loader::Fabric);
|
||||
chosen.loader_version = "0.16.4".to_string();
|
||||
let options = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&state,
|
||||
&chosen,
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert_eq!(options.loader_version, "0.16.4", "用户选的就是用户选的");
|
||||
}
|
||||
|
||||
/// 三条「列表还没到位」的状态都不许按下确认,且各自说清是哪种。
|
||||
#[test]
|
||||
fn versions_pending_or_failed_blocks_confirm() {
|
||||
let bare = AppState::new();
|
||||
let loading = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&bare,
|
||||
&draft(Loader::Fabric),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert_eq!(loading.versions, LoaderVersionsView::Loading);
|
||||
assert!(!loading.can_confirm, "还没查过就不许装");
|
||||
|
||||
let failed = state_with(LoaderKind::Fabric, "1.20.1", Err("network down"));
|
||||
let options = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&failed,
|
||||
&draft(Loader::Fabric),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(!options.can_confirm);
|
||||
assert!(
|
||||
options
|
||||
.problem
|
||||
.as_deref()
|
||||
.is_some_and(|p| p.contains("network down")),
|
||||
"取不到要给具名原因(不是空下拉)"
|
||||
);
|
||||
|
||||
let empty = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec![]));
|
||||
let options = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&empty,
|
||||
&draft(Loader::Fabric),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(!options.can_confirm, "官方没有版本就不许装");
|
||||
assert!(options.problem.is_some());
|
||||
}
|
||||
|
||||
/// 缓存键带原版版本:A 版本的列表不会挂到 B 版本下面。
|
||||
#[test]
|
||||
fn cached_versions_are_keyed_by_minecraft_version() {
|
||||
let state = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec!["0.16.5"]));
|
||||
let other = OptionsView::of(
|
||||
&row("1.21.9", VersionKind::Release),
|
||||
&state,
|
||||
&draft(Loader::Fabric),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert_eq!(
|
||||
other.versions,
|
||||
LoaderVersionsView::Loading,
|
||||
"换原版版本就得重新查,不许复用上一版的结果"
|
||||
);
|
||||
}
|
||||
|
||||
/// 原版:不查列表、不需要加载器版本,确认键直接可用。
|
||||
#[test]
|
||||
fn vanilla_needs_no_loader_version_at_all() {
|
||||
let state = AppState::new();
|
||||
let options = OptionsView::of(
|
||||
&row("1.20.1", VersionKind::Release),
|
||||
&state,
|
||||
&draft(Loader::Vanilla),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(options.can_confirm);
|
||||
assert_eq!(options.versions, LoaderVersionsView::Ready(Vec::new()));
|
||||
assert_eq!(options.loader_version, "");
|
||||
assert_eq!(options.loader_version_arg(), None);
|
||||
}
|
||||
|
||||
/// 已装的版本:确认键给「已安装」禁用态(列表行上也有勾,按钮再点一次不会更清楚)。
|
||||
#[test]
|
||||
fn installed_versions_cannot_be_confirmed_again() {
|
||||
let state = AppState::new();
|
||||
let mut installed = row("1.20.1", VersionKind::Release);
|
||||
installed.installed = true;
|
||||
let options = OptionsView::of(
|
||||
&installed,
|
||||
&state,
|
||||
&draft(Loader::Vanilla),
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(options.installed);
|
||||
assert!(!options.can_confirm);
|
||||
}
|
||||
Reference in new issue
Block a user