release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run

本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
This commit is contained in:
AskaEth committed 2026-11-11 21:41:38 +08:00
commit a5d1dec495
1580 files changed
+320938

No files matched your search

+294
View File
@@ -0,0 +1,294 @@
//! `ull java list` / `ull java install` / `ull launch` 的 Java 选择(D13)CLI 端。
//!
//! 不联网:`install` 只测参数校验与稳定错误码;`launch --dry-run` 断言 `java` 块的结构
//! 与「实例覆盖优先」——真正的下载/解压由 `core::game::java` 的测试与本机验收覆盖。
#[path = "support/mod.rs"]
mod support;
use support::{parse_json, run_cli, write_version, TempDir};
/// 一个可组装的版本 id。
const VERSION: &str = "1.20.1";
#[test]
fn java_list_reports_the_managed_root() {
let dir = TempDir::new("java-list");
let (code, stdout, stderr) = run_cli(&["--json", "--data-dir", &dir.arg(), "java", "list"]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
let json = &envelope["data"];
assert!(
json["managed_root"]
.as_str()
.is_some_and(|root| root.ends_with("java-managed")),
"managed_root 应是 data/java-managed:{json}"
);
// D23:多根扫描 + 落点模式要说清(`--data-dir` 是 custom 数据模式 → portable 落点)。
assert!(
json["managed_roots"]
.as_array()
.is_some_and(|roots| !roots.is_empty()),
"{json}"
);
assert_eq!(json["install"]["mode"], "portable", "{json}");
assert!(
json["install"]["root"]
.as_str()
.is_some_and(|root| root.ends_with("java-managed")),
"落点根应是数据根里的 java-managed:{json}"
);
assert!(json["candidates"].is_array(), "{json}");
}
#[test]
fn java_vendors_lists_the_four_vendors_with_the_current_order() {
let dir = TempDir::new("java-vendors");
let (code, stdout, stderr) = run_cli(&["--json", "--data-dir", &dir.arg(), "java", "vendors"]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
let json = &envelope["data"];
let order: Vec<&str> = json["order"]
.as_array()
.expect("order 数组")
.iter()
.filter_map(|item| item.as_str())
.collect();
assert_eq!(order.len(), 4, "{json}");
assert!(order.contains(&"temurin"), "{json}");
assert!(order.contains(&"oracle"), "{json}");
assert!(order.contains(&"microsoft"), "{json}");
assert!(order.contains(&"zulu"), "{json}");
// 本机时区/语言决定 domestic/global,两种默认序都合法,但来源必须明示。
let source = json["priority_source"].as_str().unwrap_or("?");
assert_eq!(
source, "region_default",
"没设 java.vendorPriority 时:{json}"
);
let vendors = json["vendors"].as_array().expect("vendors 数组");
assert_eq!(vendors.len(), 4);
let microsoft = vendors
.iter()
.find(|row| row["vendor"] == "microsoft")
.expect("应有 microsoft 行");
assert_eq!(
microsoft["supported"].as_array().map(Vec::len),
Some(4),
"MS 只发 25/21/17/11:{microsoft}"
);
assert!(
microsoft["sources"]
.as_array()
.is_some_and(|sources| !sources.is_empty()),
"MS 的源(aka.ms → VS CDN)要列出来:{microsoft}"
);
}
#[test]
fn java_install_rejects_an_unknown_vendor_without_network() {
let dir = TempDir::new("java-vendor-bad");
let (code, stdout, _stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"java",
"install",
"21",
"--vendor",
"graalvm",
]);
assert_eq!(code, 1);
let envelope = parse_json(&stdout);
assert_eq!(envelope["error"]["code"], "usage", "{envelope}");
}
#[test]
fn java_install_rejects_an_absurd_major_without_network() {
let dir = TempDir::new("java-install");
let (code, stdout, _stderr) =
run_cli(&["--json", "--data-dir", &dir.arg(), "java", "install", "4"]);
assert_eq!(code, 1);
let envelope = parse_json(&stdout);
assert_eq!(envelope["error"]["code"], "usage", "{envelope}");
}
#[test]
fn launch_dry_run_reports_the_java_requirement_of_the_version() {
let dir = TempDir::new("java-launch");
write_version(dir.path(), VERSION);
let (code, stdout, stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"launch",
VERSION,
"--dry-run",
]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
let json = &envelope["data"];
let java = &json["java"];
// 1.20.1 的清单没写 javaVersion → 静态表兜底 17。
assert_eq!(java["required_major"], 17, "{json}");
assert_eq!(java["requirement_source"], "table", "{json}");
assert_eq!(json["version_id"], VERSION);
// `source` 只可能是探测到的来源之一(本机未必装了 Java)。
let source = java["source"].as_str().unwrap_or("none");
assert!(
["instance", "managed", "JAVA_HOME", "PATH", "none"].contains(&source),
"意外的来源:{source}"
);
// M220:`binary` 是 D4 校验过的那个,`launch_binary` 是装配落地的那个——两者都得给出来。
assert!(
java["launch_binary"].as_str().is_some(),
"`--json` 的 java 块要写明实际装配的 java 路径:{json}"
);
}
#[test]
fn instance_java_override_wins_over_the_system_java() {
let dir = TempDir::new("java-override");
write_version(dir.path(), VERSION);
// 故意给一个不存在的路径:只要能进 `source=instance` 就说明覆盖优先级生效
// (探测失败只影响 major,不该把用户指定的路径换成系统 Java)。
let fake = dir.path().join("some-jdk").join("bin").join("java");
let (code, stdout, stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"launch",
VERSION,
"--dry-run",
"--java",
&fake.display().to_string(),
]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
let json = &envelope["data"];
assert_eq!(json["java"]["source"], "instance", "{json}");
assert_eq!(
json["java"]["binary"].as_str(),
Some(fake.display().to_string().as_str())
);
}
#[test]
fn legacy_version_requires_java_8_and_flags_a_newer_runtime() {
let dir = TempDir::new("java-legacy");
write_version(dir.path(), "1.12.2");
let (code, stdout, stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"launch",
"1.12.2",
"--dry-run",
]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
let json = &envelope["data"];
assert_eq!(json["java"]["required_major"], 8, "{json}");
assert_eq!(json["java"]["required"], "Java 8", "{json}");
// 本机若有比 8 新的 Java,必须给出 java_too_new 告警(不能只靠「最低版本」)。
if let Some(code) = json["java"]["mismatch"]["code"].as_str() {
assert!(code == "java_too_new" || code == "java_too_old", "{json}");
assert!(
json["warnings"]
.as_array()
.is_some_and(|warnings| !warnings.is_empty()),
"不符时必须进 warnings:{json}"
);
}
}
/// 写一份「清单自己声明要 Java `required`」的版本 JSON(其余字段与 `support::write_version` 同)。
///
/// 本机任何一个 Java 都不满足 `required = 99` ⇒ D4 必然要拦(`java_too_old`),
/// 与「这台机器装了哪几个 JDK」无关、也不需要联网:用例的观察点是**拦截发生在哪一步**。
fn write_version_requiring_java(root: &std::path::Path, required: u32) {
let dir = root.join(".minecraft").join("versions").join(VERSION);
std::fs::create_dir_all(&dir).expect("创建版本目录");
let version = serde_json::json!({
"id": VERSION,
"type": "release",
"mainClass": "net.minecraft.client.main.Main",
"assets": "5",
"assetIndex": { "id": "5" },
"javaVersion": { "majorVersion": required },
"arguments": {
"game": ["--username", "${auth_player_name}", "--version", "${version_name}"],
"jvm": ["-Djava.library.path=${natives_directory}", "-cp", "${classpath}"]
},
"libraries": [],
"downloads": { "client": { "size": 4, "url": "http://example.invalid/client.jar" } }
});
std::fs::write(
dir.join(format!("{VERSION}.json")),
serde_json::to_vec_pretty(&version).expect("序列化版本 JSON"),
)
.expect("写版本 JSON");
std::fs::write(dir.join(format!("{VERSION}.jar")), b"junk").expect("写 client jar");
}
/// M220 nit3:D4 的「Java 不符」拦截必须排在身份解析**之前**。
///
/// 身份解析有副作用(微软轨会联网刷新令牌、刷新成功还会回写凭据),一个注定要被拦下的
/// 启动不该先跑那一遍。观察点:数据根里放两个离线账户 ⇒ 解析身份必然报 `account_ambiguous`,
/// 而这次启动注定被 D4 拦下 ⇒ 报出来的必须是 `java_mismatch`。
/// 老实现(拦在身份之后)在这里会先撞上 `account_ambiguous`,用例必红。
///
/// 对照组:`--dry-run` 本来就只提示不拦(D4 只管真启动),所以同一条版本在 dry-run 下
/// 会走到身份解析那一步——这一对断言把「拦截在哪一步」钉死。
#[test]
fn the_java_mismatch_block_runs_before_identity_resolution() {
let dir = TempDir::new("java-block-order");
write_version_requiring_java(dir.path(), 99);
for name in ["alpha", "beta"] {
let (code, _, stderr) =
run_cli(&["accounts", "add", "offline", name, "--data-dir", &dir.arg()]);
assert_eq!(code, 0, "建离线账户失败:{stderr}");
}
// 前置:这条版本的要求确实没人满足(否则「注定被拦」这个前提不成立,直接判红)。
let (code, stdout, stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"launch",
VERSION,
"--dry-run",
"--account",
"alpha",
]);
assert_eq!(code, 0, "stderr: {stderr}");
let envelope = parse_json(&stdout);
assert_eq!(
envelope["data"]["java"]["mismatch"]["code"], "java_too_old",
"前置:本机探测到的 Java 必须都不满足 Java 99:{envelope}"
);
// 真启动(不指定账户):两个账户 ⇒ 解析身份会报 account_ambiguous,但 D4 拦得更早。
let (code, stdout, _stderr) = run_cli(&["--json", "--data-dir", &dir.arg(), "launch", VERSION]);
assert_eq!(code, 1);
let envelope = parse_json(&stdout);
assert_eq!(
envelope["error"]["code"], "java_mismatch",
"D4 拦截必须排在身份解析之前(否则注定被拦的启动也先刷了一遍令牌):{envelope}"
);
// 对照:dry-run 不拦(D4 只管真启动)⇒ 它会走到身份解析,报的是账户歧义。
let (code, stdout, _stderr) = run_cli(&[
"--json",
"--data-dir",
&dir.arg(),
"launch",
VERSION,
"--dry-run",
]);
assert_eq!(code, 1);
let envelope = parse_json(&stdout);
assert_eq!(
envelope["error"]["code"], "account_ambiguous",
"dry-run 不该被 D4 拦住(它只提示),应当走到身份解析:{envelope}"
);
}