三条 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)。
64 lines
3.1 KiB
Rust
64 lines
3.1 KiB
Rust
//! 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))
|
||
}
|