Files
Unlucky-Launcher/crates/presenter/src/host/launch/revalidate_tests.rs
T
AskaEth 2fb61d8006 fix(launch): M304 r1 —— 根不见了必须拒绝 + 运行时态落进快照 + 真 host 端到端同拍判据
三条 r1 复审 P2,全部按代码修(不是只改注释)。

**P2-1(唯一实质问题)**:`ext-` 实例「所属根消失 / 改名」被静默放行 ⇒ 可能起错副本。
`live.rs::discovered` 反查落空时原来一律 `Ok(())`;而 `roots::all` 对非配置根是
`if !extra.is_dir() { continue; }`,于是官方根被删 / 改名 / 盘没挂上时反查必然落空 ——
放行之后 `build_request` → `game_for` 会退回配置根,配置根里恰好有同名 `version_id`
就把**另一份**拉起来(gameDir / 存档都在另一边),相对改前(同步扫盘 ⇒ 扫不出来 ⇒ 拒绝)
是回归。改法:把 M273 的「只哈希版本目录名」搬进 `discovered.rs`
(`legacy_external_instance_id`,与 `external_instance_id` 共用同一个多段 FNV),反查落空时
**老档案放行、其余一律拒绝**并给具名原因(+ `stale` 触发重扫)。
另加 `game::tests::instance_id_hashes_are_the_released_ones`:两个哈希公式的字面量钉死
——这次把哈希抽成公共函数,公式一漂就会误杀所有存量 `ext-` 档案。

**P2-2**:`send()` 只认 `InstancesChanged`,而 app 侧 `state.rs:274` 把
`InstanceRuntimeChanged` **就地**写进自己的列表 ⇒ `runtime` 这一位必定分叉,而
`accept::running_pid` 的兜底读的正是它。改:`send()` 同时把运行时态落到 `published`
对应条目,并加用例 `runtime_events_land_in_the_published_snapshot`。

**P2-3**:补真 `CoreHost` + 真 app 的端到端判据(`app/update/instances/scan_phase.rs`):
临时数据根 + 真 host,`advance` 到首份快照落地后点启动,**`advance` 之前**详情页必须已
`can_stop`。变异(把 Busy 挪进启动线程、只有回程才发)实测必红。

判据(新增 3 条 + 1 条钉公式):`revalidate_tests`(根不见了 ⇒ 拒绝)、
`target_tests`(运行时态落快照)、`scan_phase`(真 host 同一拍)、`game::tests`(哈希公式)。
用例夹具按 400 行硬线拆到 `launch/fixtures.rs`。

门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets` 全绿
(426 + 1076)· `clippy --workspace --all-targets -- -D warnings` 0 ·
`fmt --all --check` 0 · 行数全 ≤400(最大 366;未碰的 `app/update/instances.rs` 399)。
2026-11-13 20:46:00 +08:00

64 lines
3.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! M304 r1 复审的两条判据:**所属根不见了必须拒绝**、**运行时态事件要落进已发布快照**。
//!
//! 前半条是真风险(复审 P2-1):`revalidate` 对 `ext-` 实例是「逐个根重算 id 定位」,而
//! `roots::all` 对非配置根是 `if !extra.is_dir() { continue; }`。根被删 / 改名 / 盘没挂上 ⇒ 一个都
//! 对不上 ⇒ 若照旧放行,`build_request` → `game_for` 退回**配置根**,配置根里恰好有同名版本就把
//! **另一份**拉起来(gameDir / 存档都在另一边)。改前的同步扫盘扫不出来 ⇒ 拒绝,所以放行是回归。
//!
//! 夹具见 [`super::fixtures`],其余判据见 [`super::target_tests`]。
use crate::dto::{AppCommand, AppEvent, Backend, CoreEvent, InstanceRuntime};
use super::super::CoreHost;
use super::fixtures::{error_toast, host_with_official, land_snapshot, profile};
/// 从事件里挑出一条「运行时变成 Busy」的 id(受理成功的证据)。
fn busy_id(events: &[AppEvent]) -> Option<String> {
events.iter().find_map(|event| match event {
AppEvent::Core(CoreEvent::InstanceRuntimeChanged {
id,
runtime: InstanceRuntime::Busy { .. },
}) => Some(id.clone()),
_ => None,
})
}
/// **P2-1**:实例属于另一个根,快照落地之后那个根不见了 ⇒ 点它必须**拒绝并具名**。
///
/// 抓得住:`revalidate` 在反查落空时直接 `Ok(())`(= 复审的现场,这条立即红)。
/// 抓不住:M273 老档案那条**放行**分支——它由 presenter 的 `game::tests_multiroot3`(老 id 的
/// 反查回落)与 `host::game::refresh` 的 `legacy_external_instance_id` 共用同一份哈希守。
#[test]
fn an_instance_whose_root_disappeared_is_refused_with_a_named_reason() {
let demo = profile("demo", "1.20.1");
let (mut host, _root, official) =
host_with_official("root-gone", std::slice::from_ref(&demo), "1.20.9");
land_snapshot(&mut host);
let tile = tile_of_version(&host, "1.20.9");
// 快照之后根没了(盘掉线 / 被移走 / 改名)
std::fs::remove_dir_all(&official).expect("删掉官方根");
host.dispatch(AppCommand::LaunchInstance(tile));
let events = host.drain_events();
assert!(
busy_id(&events).is_none(),
"所属根不见了不许受理(放行会让 game_for 退回配置根,把另一份拉起来):{events:?}"
);
assert!(host.pending.is_empty(), "拒绝受理 ⇒ 不进 pending");
let message = error_toast(&events);
assert!(
message.contains("游戏目录已不在") && message.contains("1.20.9"),
"具名原因要说出是哪个实例、为什么(现在是:{message})"
);
}
/// 快照里那个版本的磁贴 id(`ext-` 前缀 = 自动发现的实例)。
fn tile_of_version(host: &CoreHost, version: &str) -> String {
host.published
.iter()
.find(|instance| instance.version_id == version)
.map(|instance| instance.id.clone())
.unwrap_or_else(|| panic!("快照里没有版本 `{version}` 的磁贴:{:?}", host.published))
}