Merge branch 'feat/m306-legacytest-net-the-legacy-pass-thro'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s

This commit is contained in:
AskaEth committed 2026-11-13 23:07:45 +08:00
commit db24379e35
3 files changed
+142 -6

No files matched your search

@@ -102,6 +102,19 @@ fn profile(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
/// ///
/// 反查不到根 ⇒ 放行(M273 老档案,理由见模块文档):不猜、也不在这里改成「拒绝」, /// 反查不到根 ⇒ 放行(M273 老档案,理由见模块文档):不猜、也不在这里改成「拒绝」,
/// 否则所有老用户的 `ext-` 磁贴会集体点不动。 /// 否则所有老用户的 `ext-` 磁贴会集体点不动。
///
/// **别把末尾那三行当死代码删掉**(M306 两次复审的账,别只记一半):
///
/// - 删掉它,`cargo test` **仍然全绿**(M304 r2 实测:整棵 presenter 426 passed / 0 failed);
/// - 但**不是无声的**:`unused_imports` 会点名 `legacy_external_instance_id`,
/// `clippy -- -D warnings` 当场红(M306 r1 复审实测 EXIT=101)。`#![allow(dead_code)]` 只盖
/// 「函数变死码」,盖不住「use 行变空」——那是 `unused` 组里**另一条** lint;
/// - **这道保险可以被绕过**:把 import 一起删掉、或在那行加 `#[allow(unused_imports)]`,编译器
/// 就真的不吭声了。所以还有两道网,删之前先跑:
/// `host::game::tests::a_m273_legacy_ext_id_is_allowed`(判**行为**)与
/// `the_legacy_pass_through_is_still_in_the_source`(直接扫本文件的**源码**)。
///
/// 拒绝那一侧由 `an_ext_id_from_no_visible_root_is_refused` 守着。
fn discovered(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> { fn discovered(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
for root in roots::all(env) { for root in roots::all(env) {
if external_instance_id(root.key(), &instance.version_id) != instance.id { if external_instance_id(root.key(), &instance.version_id) != instance.id {
+125 -3
View File
@@ -207,9 +207,11 @@ fn isolation_write_then_read_round_trips() {
/// | `legacy_external_instance_id("1.20.4")` | `ext-52d6191436663b4e`(= M273 的「只哈希版本目录名」)| /// | `legacy_external_instance_id("1.20.4")` | `ext-52d6191436663b4e`(= M273 的「只哈希版本目录名」)|
/// | `external_instance_id("c:/mc/.minecraft", "1.20.4")` | `ext-1e6c24c50b4a95c2`(= root + `0` + version)| /// | `external_instance_id("c:/mc/.minecraft", "1.20.4")` | `ext-1e6c24c50b4a95c2`(= root + `0` + version)|
/// ///
/// 抓得住:把分隔符那一次乘法漏掉 / 把 `0` 字节也过一遍 `xor`(两种都改哈希)——那时**所有**存量 /// 抓得住:漏掉段间那次乘法、或改动迭代顺序——那时**所有**存量 `ext-` id 都会换一个值
/// `ext-` id 都会换一个值(`profiles.json` 里旧的 `ext-` 档案一律反查不到)。M304 把哈希抽成 /// (`profiles.json` 里旧的 `ext-` 档案一律反查不到)。段间那个分隔符**本身**不单独进哈希:它
/// [`super::discovered`] 里的一个多段函数,这条就是那次重构的回归网。 /// 等价于多乘一次素数(`hash ^= 0` 是恒等),痕迹只在这一次乘法上——所以「分隔符」与「那一次
/// 乘法」是同一件事,不是两处可分别改动的输入。M304 把哈希抽成 [`super::discovered`] 里的一个
/// 多段函数,这条就是那次重构的回归网。
#[test] #[test]
fn instance_id_hashes_are_the_released_ones() { fn instance_id_hashes_are_the_released_ones() {
assert_eq!( assert_eq!(
@@ -223,3 +225,123 @@ fn instance_id_hashes_are_the_released_ones() {
"M274 的 id 公式变了(存量 ext- 档案一律反查不到)" "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(()); }` 这三行
/// ——本条立即红(M306 实测:`Err("实例 `1.20.4` 所属的游戏目录已不在…")`)。
///
/// 但**别把编译器当第二道网**(M306 r1 复审实测,本文件早先那句「编译器一声不吭」是错的):
/// `#![allow(dead_code)]` 只盖「函数变死码」,盖不住「use 行变空」——`unused_imports` 属于
/// `unused` 组里另一条 lint,删掉那三行时它会点名 `legacy_external_instance_id`,CI 的
/// `-D warnings` 当场红。可**这道保险也能绕过**:连 import 一起删、或在那行加
/// `#[allow(unused_imports)]`,编译器就真的不吭声了(那时 `cargo test` 依旧全绿)。
/// 所以源码侧由下面的 `the_legacy_pass_through_is_still_in_the_source` 兜底。
/// 抓不住:反查落空后「该拒绝」那一侧(下一条用例管)。
#[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})"
);
}
/// `live.rs` 的**代码**:每行剥掉 `//` 之后的部分(`//` / `///` / `//!` 都从此处走)。
///
/// 必须剥:`live.rs` 的文档里写着那条 `if` 的原文(就是给下一个「清死代码」的人看的),注释里
/// 出现不算数——否则下面的守卫会**自证**地永远绿(M306 r1 复审要的正是这道网,别让它空转)。
fn live_code() -> String {
include_str!("refresh/live.rs")
.lines()
.map(|line| line.split("//").next().unwrap_or_default())
.collect::<Vec<_>>()
.join("\n")
}
/// **源码级守卫**:`live.rs` 里那条「M273 老档案放行」必须**原样还在**,且没人把编译器的提醒关掉。
///
/// 为什么行为用例不够(M306 r1 复审证明):删掉那三行时编译器**会**给信号(`unused_imports`
/// 点名 `legacy_external_instance_id`,`-D warnings` 判红)——**但这道保险可绕过**:把 import
/// 一起删掉、或加 `#[allow(unused_imports)]`,编译期**一条 warning 都不打**(M306 r1 实测:
/// `grep -c '^warning'` = 0),`cargo test` 也依旧全绿。
/// 那时只有扫源码能说话:`a_m273_legacy_ext_id_is_allowed` 判**行为**,本条判**判据还在不在**。
///
/// 抓得住:那三行被删(注释里残留的原文不算)/import 里的名字被删/有人加 `unused_imports` 豁免。
/// 抓不住:把判据换个写法搬去别处——那时本条会红,请顺手把下面的字面量改成新形状(别把守卫删了)。
#[test]
fn the_legacy_pass_through_is_still_in_the_source() {
let code = live_code();
let branch = "if legacy_external_instance_id(&instance.version_id) == instance.id {";
let import =
"use super::super::discovered::{external_instance_id, legacy_external_instance_id};";
assert!(
code.contains(branch) && code.contains(import),
"live.rs::discovered 里那条「M273 老档案放行」不见了(分支在 = {},import 在 = {}):\
删掉它会让所有存量 M273 用户的 `ext-` 磁贴点不动,还配一句错的「所属的游戏目录已不在」",
code.contains(branch),
code.contains(import)
);
assert!(
!code.contains("unused_imports"),
"live.rs 里出现了 `unused_imports` 豁免:它会把「删掉放行分支后 use 行变空」这条唯一的\
编译期提醒关掉——那是本条守卫存在的理由,不许豁免"
);
}
+4 -3
View File
@@ -25,7 +25,6 @@ use crate::dto::{
LogSession, LogSession,
NoticeView, NoticeView,
PackState, PackState,
Progress,
SettingGroupView, SettingGroupView,
UpdateView, UpdateView,
VersionSummary, VersionSummary,
@@ -355,8 +354,10 @@ impl AppState {
.collect() .collect()
} }
/// 覆盖某实例的启动进度(mock / 真实后端共用同一入口)。 /// 覆盖某实例的启动进度(**只给单测用**:生产路径没有调用方,它就地改 `instances` 而不动
pub fn set_progress(&mut self, id: &str, progress: Progress) { /// `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 Some(instance) = self.instances.iter_mut().find(|item| item.id == id) {
if let InstanceRuntime::Busy { if let InstanceRuntime::Busy {
progress: current, .. progress: current, ..