From 2c5fa53cc98a7d167026bdf13e227e5fb417f90a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 13 Nov 2026 22:39:44 +0800 Subject: [PATCH] =?UTF-8?q?test(presenter):=20M306=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E7=BB=99=E3=80=8C=E8=80=81=E6=A1=A3=E6=A1=88=E6=94=BE=E8=A1=8C?= =?UTF-8?q?=E3=80=8D=E6=8C=82=E7=BD=91=20+=20=E6=94=B6=E4=B8=89=E5=A4=84?= =?UTF-8?q?=E9=A1=BA=E5=B8=A6=EF=BC=88=E9=94=99=E6=96=87=E6=A1=A3=20/=20se?= =?UTF-8?q?t=5Fprogress=20=E5=88=86=E5=8F=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - host/game/tests.rs:+2 条判据。①`legacy_external_instance_id(version)` 造出的 M273 老磁贴 ⇒ `Scan::revalidate` 必须 `Ok(())`;②同一个版本、id 换成 `external_instance_id(别的根, 版本)` ⇒ 必须 `Err` 且具名。变异实测:只删 `live.rs::discovered` 里那三行 ⇒ ① 红 (`Err("实例 1.20.4 所属的游戏目录已不在…")`),② 不假红;还原后两条全绿 - host/game/tests.rs:210 的错话改正:段间分隔符**等价于一次乘法**(`hash ^= 0` 是恒等), 不是「0 字节也过一遍 xor 也会改哈希」 - state.rs:`set_progress` 降成 `#[cfg(test)]`(它就地改 `instances` 而不动 `published` ⇒ 两份 会分叉;生产推进度走 `CoreEvent::InstanceRuntimeChanged` → `apply`),只被它用的 `Progress` 导入改成路径写法,文件 398 行(门禁 400) - live.rs:`discovered` 的文档点名守着那三行的两条用例(crate 头 `#![allow(dead_code)]`, 删了编译器一声不吭) --- .../presenter/src/host/game/refresh/live.rs | 5 ++ crates/presenter/src/host/game/tests.rs | 80 ++++++++++++++++++- crates/presenter/src/state.rs | 7 +- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/crates/presenter/src/host/game/refresh/live.rs b/crates/presenter/src/host/game/refresh/live.rs index 9086e90..33c8371 100644 --- a/crates/presenter/src/host/game/refresh/live.rs +++ b/crates/presenter/src/host/game/refresh/live.rs @@ -102,6 +102,11 @@ fn profile(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> { /// /// 反查不到根 ⇒ 放行(M273 老档案,理由见模块文档):不猜、也不在这里改成「拒绝」, /// 否则所有老用户的 `ext-` 磁贴会集体点不动。 +/// +/// **末尾那条放行分支由 `host::game::tests::a_m273_legacy_ext_id_is_allowed` 守着**:本 crate 头是 +/// `#![allow(dead_code)]`,删掉它编译器一个信号都不给(M304 r2 复审实测:删掉后整棵 presenter +/// 426 passed / 0 failed),所以「清死代码」之前请先跑那条用例。拒绝那一侧由同文件的 +/// `an_ext_id_from_no_visible_root_is_refused` 守着。 fn discovered(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> { for root in roots::all(env) { if external_instance_id(root.key(), &instance.version_id) != instance.id { diff --git a/crates/presenter/src/host/game/tests.rs b/crates/presenter/src/host/game/tests.rs index ed0436d..dd82ede 100644 --- a/crates/presenter/src/host/game/tests.rs +++ b/crates/presenter/src/host/game/tests.rs @@ -207,9 +207,11 @@ fn isolation_write_then_read_round_trips() { /// | `legacy_external_instance_id("1.20.4")` | `ext-52d6191436663b4e`(= M273 的「只哈希版本目录名」)| /// | `external_instance_id("c:/mc/.minecraft", "1.20.4")` | `ext-1e6c24c50b4a95c2`(= root + `0` + version)| /// -/// 抓得住:把分隔符那一次乘法漏掉 / 把 `0` 字节也过一遍 `xor`(两种都改哈希)——那时**所有**存量 -/// `ext-` id 都会换一个值(`profiles.json` 里旧的 `ext-` 档案一律反查不到)。M304 把哈希抽成 -/// [`super::discovered`] 里的一个多段函数,这条就是那次重构的回归网。 +/// 抓得住:漏掉段间那次乘法、或改动迭代顺序——那时**所有**存量 `ext-` id 都会换一个值 +/// (`profiles.json` 里旧的 `ext-` 档案一律反查不到)。段间那个分隔符**本身**不单独进哈希:它 +/// 等价于多乘一次素数(`hash ^= 0` 是恒等),痕迹只在这一次乘法上——所以「分隔符」与「那一次 +/// 乘法」是同一件事,不是两处可分别改动的输入。M304 把哈希抽成 [`super::discovered`] 里的一个 +/// 多段函数,这条就是那次重构的回归网。 #[test] fn instance_id_hashes_are_the_released_ones() { assert_eq!( @@ -223,3 +225,75 @@ fn instance_id_hashes_are_the_released_ones() { "M274 的 id 公式变了(存量 ext- 档案一律反查不到)" ); } + +/// 一条 `ext-` 磁贴,版本本体**不在盘上**(这个临时根里没有 `versions/`)。 +/// +/// 走生产的构造函数([`super::discovered::external_summary`],id = M274 的 `fnv(根 ‖ 0 ‖ 版本)`), +/// 调用方只把 id 换成要判的那一种写法——这样「磁贴的形状」与界面上的永远同源。`roots[0]` 恒为 +/// 配置根([`super::roots::all`] 的不变式:配置根永远第一位)。 +fn ext_tile(env: &HostEnv, version_id: &str) -> InstanceSummary { + let roots = super::roots::all(env); + super::discovered::external_summary(env, &roots[0], version_id, &BTreeMap::new()) +} + +/// 建一个只有 `config/` 的临时根(用例自己的,`pid` + 标签让并行的两条不撞)。 +fn bare_root(label: &str) -> (HostEnv, std::path::PathBuf) { + let dir = std::env::temp_dir().join(format!("ull-host-game-{label}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let env = HostEnv::at_root(&dir); + std::fs::create_dir_all(env.layout.config()).expect("config 目录"); + (env, dir) +} + +/// **M273 老档案必须放行**(M306:给 `refresh/live.rs::discovered` 最后那三行挂网)。 +/// +/// 判据:`ext-` id 只哈希版本目录名(M274 之前的公式)⇒ `roots::all` 的每个根都反查不到它。 +/// 这种 id 的语义本来就是「按配置根解析」,真没版本时启动链给具名阶段失败(进程一个都没起来) +/// ——所以这里必须 `Ok(())`。拒绝它 = 所有存量 M273 用户的 `ext-` 磁贴集体点不动,还配一句错的 +/// 原因(「所属的游戏目录已不在」)。 +/// +/// 抓得住:**只**删掉 `live.rs::discovered` 里 +/// `if legacy_external_instance_id(&instance.version_id) == instance.id { return Ok(()); }` 这三行 +/// ——本条立即红。这是它唯一的网:crate 头是 `#![allow(dead_code)]`,编译器一声不吭(M304 r2 +/// 实测过删掉后整棵 presenter 426 passed / 0 failed)。 +/// 抓不住:反查落空后「该拒绝」那一侧(下一条用例管)。 +#[test] +fn a_m273_legacy_ext_id_is_allowed() { + let (env, dir) = bare_root("legacy-ok"); + let mut tile = ext_tile(&env, "1.20.4"); + tile.id = super::discovered::legacy_external_instance_id("1.20.4"); + + // 先清临时根再断言:断言失败也不在 /tmp 留空壳。 + let verdict = Scan::revalidate(&env, &tile); + std::fs::remove_dir_all(&dir).ok(); + assert!( + verdict.is_ok(), + "M273 老档案(id 只哈希版本目录名)必须放行,否则存量 `ext-` 磁贴全部点不动:{verdict:?}" + ); +} + +/// **换个根的 `ext-` id、反查落空,必须拒绝并具名**(同一条分叉的另一侧)。 +/// +/// 与上一条**同一个版本**,只把 id 换成 `external_instance_id(别的根, 版本)`:盘上根本没有那个根 +/// ⇒ 一个根都对不上,而它也不是老档案 ⇒ 必须 `Err`。放行会让 `game_for` 退回配置根,配置根里 +/// 恰好有同名版本就把**另一份**拉起来(gameDir / 存档都在另一边)。 +/// +/// 抓得住:把那条拒绝改写成 `Ok(())`(复审现场的另一半)。它与上一条各钉分叉的一侧,所以 +/// 只删老档案那三行时本条**不会**假红——红色只会出现在上一条。 +#[test] +fn an_ext_id_from_no_visible_root_is_refused() { + let (env, dir) = bare_root("legacy-refused"); + let mut tile = ext_tile(&env, "1.20.4"); + // 盘上不存在的根:`roots::all` 只给配置根 ⇒ 这个 id 一个根都对不上,且它不是老档案的公式。 + tile.id = super::discovered::external_instance_id("d:/m306-no-such-root/.minecraft", "1.20.4"); + + let verdict = Scan::revalidate(&env, &tile); + std::fs::remove_dir_all(&dir).ok(); + let reason = verdict.expect_err( + "反查落空又不是老档案 ⇒ 必须拒绝(放行会让 game_for 退回配置根,把同名版本拉起来)", + ); + assert!( + reason.contains("游戏目录已不在") && reason.contains("1.20.4"), + "具名原因要说清是哪条实例、为什么(现在是:{reason})" + ); +} diff --git a/crates/presenter/src/state.rs b/crates/presenter/src/state.rs index 7421dae..70e443c 100644 --- a/crates/presenter/src/state.rs +++ b/crates/presenter/src/state.rs @@ -25,7 +25,6 @@ use crate::dto::{ LogSession, NoticeView, PackState, - Progress, SettingGroupView, UpdateView, VersionSummary, @@ -355,8 +354,10 @@ impl AppState { .collect() } - /// 覆盖某实例的启动进度(mock / 真实后端共用同一入口)。 - pub fn set_progress(&mut self, id: &str, progress: Progress) { + /// 覆盖某实例的启动进度(**只给单测用**:生产路径没有调用方,它就地改 `instances` 而不动 + /// `published` ⇒ 两份会分叉;生产推进度走 `CoreEvent::InstanceRuntimeChanged` → `apply`)。 + #[cfg(test)] + pub fn set_progress(&mut self, id: &str, progress: crate::dto::Progress) { if let Some(instance) = self.instances.iter_mut().find(|item| item.id == id) { if let InstanceRuntime::Busy { progress: current, ..