feat(presenter): 导入已有版本的 DTO 与三条后台线程(M223)
core 的能力(M221)接进 presenter 的 IPC 面: - dto/adopt.rs:AdoptCommand / AdoptEvent / 六种视图 / AdoptState + AdoptStage。 全部 Serialize+Deserialize,**不 import 任何 core 类型**(契约 §5.4); - host/adopt.rs:spawn_scan(adopt::scan)/ spawn_adopt(plan 或 adopt),结果经 HostTask::Adopt 回流;core 的候选/计划/结果/失败逐字段搬进 DTO,problems[] 一条不少; - CoreHost 加 adopt_epoch(每条命令 +1、关面板也 +1),连点与迟到的回程不会点亮面板; - 登记成功后补一次 refresh_instances(新实例这一拍就出现在列表里)。 运行目录只从 plan()/adopt() 的返回值取(缺省档可能带 -2 后缀,只有 core 知道用了哪个); target_notes() 的原文随 AdoptPlanView::notes 上屏,界面不另写一份后果话术。
This commit is contained in:
1 parent
9ab909d01c
commit
26e4fbb91d
6 files changed
+1012
No files matched your search
@@ -0,0 +1,136 @@
|
||||
//! `dto::adopt` 的 IPC 契约用例(M223 从 `dto/adopt.rs` **纯搬移**,断言一条未改)。
|
||||
//!
|
||||
//! 为什么搬:`dto/adopt.rs` 落笔 512 行,越过 400 行硬线(AGENTS §3.1);口径与
|
||||
//! [`crate::dto::events::tests`] 完全一致——**纯搬移、一行不改**。`adopt.rs` 是非 `mod.rs`
|
||||
//! 文件,`mod tests;` 的落点就是本文件。
|
||||
|
||||
use super::*;
|
||||
|
||||
fn candidate(version_id: &str, problems: Vec<AdoptProblemView>) -> AdoptCandidateView {
|
||||
AdoptCandidateView {
|
||||
version_id: version_id.to_string(),
|
||||
jar_present: true,
|
||||
inherits: Vec::new(),
|
||||
registered: Vec::new(),
|
||||
problems,
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_view() -> AdoptScanView {
|
||||
AdoptScanView {
|
||||
versions_dir: PathBuf::from("/game/versions"),
|
||||
candidates: vec![
|
||||
candidate("1.20.1", Vec::new()),
|
||||
candidate(
|
||||
"1.19.4",
|
||||
vec![AdoptProblemView {
|
||||
code: "manifest_missing".to_string(),
|
||||
message: "版本 1.19.4 没有清单文件".to_string(),
|
||||
}],
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn events_round_trip_through_json() {
|
||||
let event = AdoptEvent::Failed(Box::new(AdoptFailureView {
|
||||
code: "adopt_not_adoptable".to_string(),
|
||||
message: "版本 1.19.4 不能登记".to_string(),
|
||||
problems: vec![AdoptProblemView {
|
||||
code: "manifest_missing".to_string(),
|
||||
message: "清单缺了".to_string(),
|
||||
}],
|
||||
}));
|
||||
let text = serde_json::to_string(&event).expect("序列化");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<AdoptEvent>(&text).expect("反序列化"),
|
||||
event
|
||||
);
|
||||
|
||||
let command = AdoptCommand::Preview(Box::new(AdoptRequestView {
|
||||
version_id: "1.20.1".to_string(),
|
||||
name: Some("生存".to_string()),
|
||||
target: AdoptTargetView::Shared,
|
||||
}));
|
||||
let text = serde_json::to_string(&command).expect("序列化");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<AdoptCommand>(&text).expect("反序列化"),
|
||||
command
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_summarises_adoptable_and_empty() {
|
||||
let view = scan_view();
|
||||
assert_eq!(view.total(), 2);
|
||||
assert_eq!(view.adoptable(), 1);
|
||||
assert!(!view.is_empty());
|
||||
assert!(AdoptScanView {
|
||||
versions_dir: PathBuf::from("/game/versions"),
|
||||
candidates: Vec::new(),
|
||||
}
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_slugs_round_trip_and_mark_foreign_dirs() {
|
||||
for target in AdoptTargetView::ALL {
|
||||
assert_eq!(AdoptTargetView::from_slug(target.slug()), Some(target));
|
||||
}
|
||||
assert_eq!(AdoptTargetView::default(), AdoptTargetView::InstanceDir);
|
||||
assert!(!AdoptTargetView::InstanceDir.touches_foreign_dir());
|
||||
assert!(AdoptTargetView::Shared.touches_foreign_dir());
|
||||
assert!(AdoptTargetView::VersionDir.touches_foreign_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_follows_the_four_stages() {
|
||||
let mut state = AdoptState::default();
|
||||
assert_eq!(state.stage(), AdoptStage::Pick);
|
||||
|
||||
state.apply(AdoptEvent::Scan(Box::new(scan_view())));
|
||||
assert_eq!(state.stage(), AdoptStage::Pick);
|
||||
assert_eq!(state.scan.as_ref().map(AdoptScanView::adoptable), Some(1));
|
||||
|
||||
let plan = AdoptPlanView {
|
||||
version_id: "1.20.1".to_string(),
|
||||
name: "1.20.1".to_string(),
|
||||
target: AdoptTargetView::InstanceDir,
|
||||
isolation: AdoptIsolationView::Custom,
|
||||
run_dir: PathBuf::from("/ull/data/instances/1.20.1"),
|
||||
notes: vec!["运行目录 = /ull/data/instances/1.20.1".to_string()],
|
||||
};
|
||||
state.apply(AdoptEvent::Planned(Box::new(plan.clone())));
|
||||
assert_eq!(state.stage(), AdoptStage::Preview);
|
||||
assert_eq!(
|
||||
state.plan.as_ref().map(|plan| &plan.run_dir),
|
||||
Some(&plan.run_dir)
|
||||
);
|
||||
|
||||
// 失败会把预检顶掉:留着预检,确认按钮就会在「不能登记」的结论上继续亮着
|
||||
state.apply(AdoptEvent::Failed(Box::new(AdoptFailureView {
|
||||
code: "adopt_duplicate_name".to_string(),
|
||||
message: "已有同名实例".to_string(),
|
||||
problems: Vec::new(),
|
||||
})));
|
||||
assert_eq!(state.stage(), AdoptStage::Failed);
|
||||
assert!(state.plan.is_none());
|
||||
|
||||
state.apply(AdoptEvent::Done(Box::new(AdoptDoneView {
|
||||
instance_id: "id-1".to_string(),
|
||||
name: "1.20.1".to_string(),
|
||||
version_id: "1.20.1".to_string(),
|
||||
isolation: AdoptIsolationView::Custom,
|
||||
run_dir: PathBuf::from("/ull/data/instances/1.20.1"),
|
||||
dir_created: true,
|
||||
notes: Vec::new(),
|
||||
})));
|
||||
assert_eq!(state.stage(), AdoptStage::Done);
|
||||
assert!(state.failure.is_none());
|
||||
|
||||
// 关面板 = 全清(再开要重新扫,否则画的是上一轮的候选)
|
||||
state.apply(AdoptEvent::Cleared);
|
||||
assert_eq!(state.stage(), AdoptStage::Pick);
|
||||
assert!(state.scan.is_none() && state.done.is_none());
|
||||
}
|
||||
Reference in new issue
Block a user