fix(m168): 复审 r1 五项 —— 修红的 integration target、独立失败域、整体时限

复审 r1(p1-1/fix-then-merge)五项全改:

1. **p1-1 CI 会红的 4 条 integration test**(`crates/core/tests/setting/`,我上一轮只跑了 `--lib`,
   scope 漏掉了覆盖 schema 版本号的目录式 target):
   - `java_install_keys` / `isolation_keys` / `appearance_keys`:版本断言 1.6.0 → 1.7.0(M168 的
     `ui.font_cdn`),并写明「每加一批键推一次 minor 是纪律」。
   - `update_keys::a_read_only_launcher_config_never_retires_the_legacy_file`:夹具从**写死
     1.7.0** 改成 `launcher_version().minor + 1` 经 `SchemaRef::url()` 生成。写死数字的版本一推
     这条守卫就变成「同版本」⇒ 文件可写 ⇒ 迁移真的执行 ⇒ 旧文件被退役,而断言还绿着(用户数据
     守卫静默失效)。相对推导不会过期。
2. **p2-1 备用镜像不备用**:`cdn` 与 `fastly` 同属 Fastly(都 CNAME 到 jsdelivr.map.fastly.net),
   换掉。现在首选 `gcore.jsdelivr.net`(Cloudflare,独立失败域),二选 `cdn.jsdelivr.net`
   (Fastly)。注释里放了实测表(本机 + 复审机两份数据、字节数、sha256、IPv6 慢路径),
   并写清排序依据(Fastly 侧 IPv6 实测 67 KB/s ⇒ 丢掉 4 分钟;Cloudflare 侧两次都完整下完)。
3. **p2-2 整体时限**:`core::download::font` 新增 `ATTEMPT_DEADLINE = 90 s` /
   `TOTAL_DEADLINE = 180 s`,落在本模块自己的候选循环里(不动引擎全体语义):每次尝试
   `min(单次上限, 剩余总预算)`,超时归 `Network` 档。新增守卫
   `a_trickling_connection_is_cut_by_the_deadline`(回环服务端每 100 ms 挤 1 字节、预算 300 ms)。
   顺带修掉一个真 bug:`tokio::time::timeout` 在运行时外构造会 panic(5 条既有用例当场抓到),
   现在整段包进 `block_on(async { … })`。UI 文案改成「网络不通或太慢(超时)」,不校验失败混。
4. **p2-3 说 `Source::PendingPin` 的两处**:`fonts/cjk.rs` 的模块文档已改;`ui/lang.rs::log_cjk`
   那句「D28 的下载通道还是 PendingPin」(在下载已发起之后打印,等于当面否定本功能)改成
   「已按 D28 发起下载,见 [font] cjk-download … 与设置页那一行」。全仓再扫,只剩注释里的历史说明。
5. **p2-4 假守卫**:判据抽成纯函数 `install::plan(strategy, current) -> Step`(`Skip` / `Apply` /
   `Start`),`after_probe` 只执行。旧的空转用例换成打真判据的打表用例;变异证明:删掉
   `Skip` 那一臂 ⇒ `a_started_or_finished_install_is_not_restarted` 红(已实跑)。

次要项也一并处理:`from_cache` 改成**本次是否真的命中缓存**(`CdnFetcher::was_cached()`,
`has_cached()` 降级为诊断字段 `cache_present=`);`FontSpec` 的 URL 口径收紧为「非回环必须
https」(`is_acceptable_url` + 守卫);`ui.font_cdn` 写进状态行 hint;文档里的 17.0 MB → 16.9 MB。
`download/font.rs` 拆出 `font/spec.rs` 守 400 行。

验证(这台机器上单个 800 MB 测试 exe 链接要 10 分钟,链接期间文件被 Defender 扫描,故耗时很长):
- `cargo test -p unlucky-core --all-targets` → **1188 passed / 0 failed**(16 个 target,含
  `setting` 那个 115 条、上一轮红 4 条的 integration target)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed
- `cargo test -p unlucky-app-iced --lib` → 685 passed / 0 failed
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → 干净
- `cargo fmt --all -- --check` → 干净;全仓受检文件行数最大 388(<400)
This commit is contained in:
AskaEth committed 2026-11-08 14:38:23 +08:00
1 parent 449ded2cb7
commit 875b792206
17 files changed
+512 -170

No files matched your search

+8 -3
View File
@@ -42,7 +42,12 @@ macro_rules! font_install_entries {
"Download failed ({reason}). The interface keeps working with system fonts; switch the language again to retry.",
"获取失败({reason})。界面照常可用,用的是系统字体;再切一次语言可以重试。"
),
FontCjkReasonNetwork => ("network unreachable", "网络不通"),
// 超时也归这一档(M168 复审 P2-2):用户要分清「太慢/没网」与「内容不对」,
// 但不需要知道我们内部把它叫 Timeout 还是 Network。
FontCjkReasonNetwork => (
"network unreachable or too slow (timed out)",
"网络不通或太慢(超时)"
),
FontCjkReasonIntegrity => (
"the mirror sent content that failed verification",
"镜像内容校验不通过"
@@ -57,8 +62,8 @@ macro_rules! font_install_entries {
"无法探测系统字体,因此没有下载"
),
FontCjkStatusHint => (
"Downloaded once into the data directory and cached; the next start does not download it again.",
"只下这一次,缓存在数据目录里,下次启动不重下。"
"Downloaded once into the data directory and cached; the next start does not download it again. A mirror can be set with `ui.font_cdn`.",
"只下这一次,缓存在数据目录里,下次启动不重下。镜像可用配置键 ui.font_cdn 指定。"
),
} }
};
+2 -2
View File
@@ -215,10 +215,10 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
("FontCjkStatusSystem", "Ready (system font: {family})", "已就绪(系统字体:{family})"),
("FontCjkStatusDownloading", "Fetching a Chinese font… ({size})", "正在获取中文字体…({size})"),
("FontCjkStatusFailed", "Download failed ({reason}). The interface keeps working with system fonts; switch the language again to retry.", "获取失败({reason})。界面照常可用,用的是系统字体;再切一次语言可以重试。"),
("FontCjkReasonNetwork", "network unreachable", "网络不通"),
("FontCjkReasonNetwork", "network unreachable or too slow (timed out)", "网络不通或太慢(超时)"),
("FontCjkReasonIntegrity", "the mirror sent content that failed verification", "镜像内容校验不通过"),
("FontCjkReasonDisk", "cannot write to the data directory", "写不进数据目录"),
("FontCjkStatusUnknown", "Not determined yet; nothing was downloaded", "尚未判定;没有下载任何字体"),
("FontCjkStatusUnprobeable", "System fonts cannot be probed, so nothing was downloaded", "无法探测系统字体,因此没有下载"),
("FontCjkStatusHint", "Downloaded once into the data directory and cached; the next start does not download it again.", "只下这一次,缓存在数据目录里,下次启动不重下。"),
("FontCjkStatusHint", "Downloaded once into the data directory and cached; the next start does not download it again. A mirror can be set with `ui.font_cdn`.", "只下这一次,缓存在数据目录里,下次启动不重下。镜像可用配置键 ui.font_cdn 指定。"),
];
+26 -6
View File
@@ -72,8 +72,12 @@ pub enum Source {
/// (不是 `@main`)、字节数与 `sha256` 一起钉,任何一项对不上就拒绝落盘(见
/// `unlucky_core::download::font`)。
///
/// `urls` 是**候选**:首选挂了才试下一个(两个都是同一份字节的镜像,哈希只认一份——
/// 镜像若给了别的内容,校验那关会当场拒掉,而不是把别的东西注册成字体)。
/// `urls` 是**候选**:首选挂了才试下一个。两个候选**必须是不同的失败域**(Cloudflare 侧 +
/// Fastly 侧,见 [`NOTO_SANS_SC`] 的实测表);同一家 CDN 的两个域名不算冗余。
///
/// 每次尝试与整条取回都有整体时限(`unlucky_core::download::font` 的 `ATTEMPT_DEADLINE` /
/// `TOTAL_DEADLINE`):慢而不死的连接(IPv6 爬行实测 67 KB/s)会在时限处被打断、转下一个
/// 候选,而不是让用户在设置页干等四分钟。超时归「网络」档,界面文案是「网络不通或太慢」。
Pinned {
/// URL 候选(按顺序尝试)。
urls: &'static [&'static str],
@@ -105,7 +109,7 @@ pub struct CjkFont {
/// |---|---|
/// | 上游 | `google/fonts` 仓库的 `ofl/notosanssc/NotoSansSC[wght].ttf` |
/// | 版本 | commit `2894aab31764f10f29c421bdfd2340d3b382d384`(该文件的最后一次改动,2022-12-09) |
/// | 字节数 | 17 772 300(≈17.0 MiB) |
/// | 字节数 | 17 772 300(16.95 MiB) |
/// | sha256 | `a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da` |
///
/// 为什么钉 commit 而不是 `@main`:`@main` 会随上游改动漂移,哈希一漂就成了「每次中文环境都下
@@ -119,11 +123,27 @@ pub const NOTO_SANS_SC: CjkFont = CjkFont {
license: "OFL-1.1",
weights: &[100, 200, 300, 400, 500, 600, 700, 800, 900],
source: Source::Pinned {
// 两个候选**必须是不同的失败域**(M168 复审 P2-1):`cdn.jsdelivr.net` 与
// `fastly.jsdelivr.net` 都 CNAME 到 `jsdelivr.map.fastly.net`(同一批 Fastly 边缘),
// 「一个坏了换另一个」等于没有冗余——上一版就是这样,被复审当场打掉。
//
// 实测(两份独立的国内网络取证,同一份 17 772 300 B 内容,sha256 都是 a3041811…):
//
// | 域名 | 解析 | 本机(本 worktree 开发机) | 复审机(无代理) |
// |---|---|---|---|
// | gcore.jsdelivr.net | Cloudflare 104.16.x | 13.3 s 完整 | 16.6 s 完整 |
// | testingcf.jsdelivr.net | Cloudflare 104.16.x | 11.8 s 完整 | 14.8 s 完整 |
// | cdn.jsdelivr.net | Fastly 151.101.x | 11.9 s 完整(另两次 5.7 s / 14.9 s)| 1.1 s,但**走 IPv6 时 3 次都掉到 ≈67 KB/s**(60 s 只到 4.0 MB)|
// | fastly.jsdelivr.net | 同上(Fastly)| — | 20 s 只到 2.5 MB(比它「要备的那条」还慢)|
//
// 排序依据(不是「听说更稳」):**首选放 Cloudflare 那一侧**——Fastly 侧的 IPv6 边缘
// 在复审环境里会掉进 60~77 KB/s 的慢路径(那条路上 17.7 MB 要 ≈4 分钟),而 Cloudflare
// 侧两份取证都完整下完、且是独立的失败域。二选留 Fastly 侧:本机它反而最快(5.7~11.9 s),
// 保留它是为了「Cloudflare 一侧整体不可用时还有另一家 CDN」。
// 先后只影响速度,不影响正确性:sha256 只认一份,镜像给了别的内容会被当场拒。
urls: &[
// 首选:jsDelivr(国内多数线路可达;`@<commit>` 保证内容不变)
"https://gcore.jsdelivr.net/gh/google/fonts@2894aab31764f10f29c421bdfd2340d3b382d384/ofl/notosanssc/NotoSansSC%5Bwght%5D.ttf",
"https://cdn.jsdelivr.net/gh/google/fonts@2894aab31764f10f29c421bdfd2340d3b382d384/ofl/notosanssc/NotoSansSC%5Bwght%5D.ttf",
// 备用:同一份内容的另一个 jsDelivr 边缘(首选被墙/被限速时才用)
"https://fastly.jsdelivr.net/gh/google/fonts@2894aab31764f10f29c421bdfd2340d3b382d384/ofl/notosanssc/NotoSansSC%5Bwght%5D.ttf",
],
sha256: "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
bytes: 17_772_300,