test(m126 r2): 加一支枚举不更新对外契约现在必然红(集合断言 + help 守卫)

r2 复审指出「加一家加载器会被逼着表态」只做到一半:编译期只拦「漏 match」,三处消费方
用最省事的 `=> Vanilla` 糊过去既编得过、测试也全绿——新加载器会静默显示成原版,正是本
mission 要消灭的形态(r1 里「Rift 会红」是负样本名字撞上的巧合,不是守卫)。

- presenter 的 `loader_kind_ids_are_the_released_contract` 增加**第 ② 段**:把 core 的
  `LoaderKind::ALL.map(as_str)` 与独立手写的七个 id 做**集合相等**比较。加一支而不更新
  契约 → 红;改词表 → 红。(r1 删掉 `assert!(ALL.len()==7)` 后缺的正是这一环:数组宽度
  只保证「表里那一行写成 7 个」,不保证枚举没多一支。)
- 新增 `cli::commands::instances::help_vocabulary_tests::help_lists_every_stable_id`:
  按 `LoaderKind::ALL` 遍历 `ull help instances` 的词表行,漏写一个新 id 必红(此前
  `help.rs:67` 那段词表没有任何测试,而旧的 `help_documents_the_loader_field_contract`
  只查关键词,加一家新加载器时它照样绿)。
- `addon::loader::tests::vocabulary` 补 `installable_list_matches_the_from_core_mapping`:
  把唯一还手写的 `LoaderKind::all()` 与 `from_core` 认可的集合对齐(不重抄字面量)。
- 模块文档第 ③ 步改成真话:契约测试会拦「改词表」与「加新种类」;但 `=> Vanilla` 这种
  糊法**编得过**,拦住它的是那条集合断言,不是编译器(改名同理,只有契约测试会红)。
- 一并修掉 presenter 那条测试 doc 里指向已被删除的 `MARKERS` /
  `markers_cover_every_kind_that_has_one` 的引用,改指向现在真实存在的守卫。
- loader.rs 从 398 压到 395(≤400,留 5 行余量;拆测试到子模块需要 scope 放宽到
  `crates/core/src/game/loader/**`,超出本任务文件清单,已按规矩记账)。

验收实测(临时加 `Abyss` + 三处 `=> Vanilla` 糊过去,已还原):presenter 契约测试红
(left 8 项 / right 7 项),CLI help 测试红(漏了 `abyss`);core 往返测试与安装侧清单
守卫、CLI 的 `json_rows_*` 仍绿(它们本就管别的事)。
This commit is contained in:
AskaEth committed 2026-11-05 18:08:59 +08:00
1 parent a322cfcbfe
commit 14ac0dc248
4 files changed
+101 -35

No files matched your search

+29 -9
View File
@@ -281,14 +281,19 @@ mod tests {
assert_eq!(LoaderKind::parse("unknown"), None);
}
/// DTO 的 `as_str` / `parse` 是 core 词表的**委派**(M126),而词表本身是**对外契约**:
/// 七个 id 在这里独立抄一遍(0.2.0 已发布形状,改名等于违约)。
/// 对外契约:七个 id 在测试里**独立抄一遍**(0.2.0 已发布形状),并与 core 的 `ALL` 做
/// **集合比较**——这是「加了一支枚举却没人更新对外词表」的唯一守卫。
///
/// 抓得住:core 的 `as_str` 或委派接线改错(`OptiFine` 映到 `LiteLoader`、映射漏了哪支);
/// 词表改字/改大小写(`neoforge` → `neo_forge`);`parse` 收进了不该有的别名(`neo-forge`)。
/// 抓不住:core 的 `ALL` / `MARKERS` 之间对不对得上(那是 core 的
/// `markers_cover_every_kind_that_has_one` 的职责),以及真二进制输出面上的 JSON
/// (那是 CLI 的 `json_rows_expose_loader_kind_and_version`)。
/// 为什么不是编译器管:编译期只拦得到「漏 match」,而新种类在三处消费方用最省事的
/// `=> Vanilla` 糊过去**编得过**,那时新加载器会静默显示成原版(`game::loader` 模块文档
/// 第 ③ 步)。真正拦住它的是下面第 ② 段的集合断言。
///
/// 抓得住:改词表(`neoforge` → `neo_forge`);**新增一家加载器而没主动更新这份契约**
/// (core 的 `ALL` 多一支 ⇒ 集合不等);DTO 委派接错支(`OptiFine` 映到 `LiteLoader`);
/// `parse` 收进不该有的别名。
/// 抓不住:core 的解析规则(那是 `game::loader` 的
/// `every_stable_id_parses_back_to_its_kind` 等测试的职责),以及真二进制输出面的 JSON
/// (CLI 的 `json_rows_expose_loader_kind_and_version`)。
#[test]
fn loader_kind_ids_are_the_released_contract() {
const CONTRACT: [(LoaderKind, &str); 7] = [
@@ -300,12 +305,28 @@ mod tests {
(LoaderKind::OptiFine, "optifine"),
(LoaderKind::LiteLoader, "liteloader"),
];
// ① DTO 这一层:每支的 id、反查、大小写。
for (kind, id) in CONTRACT {
assert_eq!(kind.as_str(), id, "{kind:?} 的稳定 id 变了");
assert_eq!(LoaderKind::parse(id), Some(kind), "`{id}` 认不回来");
let upper = id.to_ascii_uppercase();
assert_eq!(LoaderKind::parse(&upper), Some(kind), "大写 `{upper}`");
}
// ② core 的词表 ↔ 这份契约:**集合相等**才过(多一支、少一支、改一个字都红)。
let mut actual: Vec<&str> = CoreLoaderKind::ALL
.iter()
.map(|kind| kind.as_str())
.collect();
actual.sort_unstable();
let mut expected: Vec<&str> = CONTRACT.iter().map(|(_, id)| *id).collect();
expected.sort_unstable();
assert_eq!(
actual, expected,
"core 的 `LoaderKind::ALL` 必须逐字等于对外契约:加一家加载器时先改这里 + help"
);
// GUI 输入别名(不是对外 id):不装加载器有三种写法;别名与拼法都不许混进词表。
for alias in ["none", "", " "] {
assert_eq!(
@@ -314,8 +335,7 @@ mod tests {
"`{alias}`"
);
}
assert_eq!(LoaderKind::parse("neo-forge"), None);
assert_eq!(LoaderKind::parse("rift"), None);
assert_eq!(LoaderKind::parse("neo-forge"), None, "别名只归安装侧");
}
#[test]