fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债
① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
- 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
SettingSchema 加 with_all 供域键表批量登记。
- 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
- presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
- CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。
② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
(记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。
③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
(忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。
④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。
⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。
⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。
顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
This commit is contained in:
1 parent
b2d9f3927f
commit
03741ecd2d
69 files changed
+2653
-707
No files matched your search
@@ -168,6 +168,9 @@ pub struct App {
|
||||
/// 已经提示过的取消次数(M105):`AppState::install_cancelled` 是「发生过几次取消」,
|
||||
/// 这里是「其中几次已经弹过提示」。两者不等 ⇒ 弹一次 i18n 提示。
|
||||
pub(super) install_cancelled_seen: u64,
|
||||
/// 已经提示过的启动收尾次数(M161):`AppState::launch_ends` 是「收尾过几场」,
|
||||
/// 这里是「其中几场已经弹过 Toast」。两者不等 ⇒ 弹一次(与 `install_cancelled_seen` 同构)。
|
||||
pub(super) launch_ends_seen: u64,
|
||||
/// 内容库页的页面内选择(M109):内容类型 / 选中项目 / 目标实例 / 关键词草稿。
|
||||
///
|
||||
/// 为什么在 `App` 而不是 presenter:这四样是「用户在这一屏上点了什么」,重建快照不该冲掉;
|
||||
|
||||
@@ -89,6 +89,7 @@ impl App {
|
||||
let mut view = pages::Model::new(
|
||||
pages::Screen::Instances,
|
||||
width,
|
||||
height,
|
||||
Some(pages::Screen::Instances.nav()),
|
||||
);
|
||||
view.lang = lang.lang();
|
||||
@@ -126,6 +127,7 @@ impl App {
|
||||
download_draft: pages::InstallDraft::default(),
|
||||
launch_form: pages::LaunchForm::default(),
|
||||
install_cancelled_seen: 0,
|
||||
launch_ends_seen: 0,
|
||||
content: pages::content::PageState::default(),
|
||||
account_form: pages::account::Form::new(),
|
||||
account: pages::account::Page::build(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
use unlucky_presenter::toast::ToastKind;
|
||||
|
||||
use crate::app::view;
|
||||
use crate::pages;
|
||||
|
||||
use unlucky_presenter::router::Route;
|
||||
@@ -36,6 +37,23 @@ impl App {
|
||||
self.state.toasts.push(ToastKind::Info, text);
|
||||
}
|
||||
|
||||
// M161:启动收尾的提示语也在这里弹(presenter 只发结构:结局 + 退出码 + 失败阶段)。
|
||||
// 此前 presenter 直接推一条自己拼好的中文 Toast,英文界面会弹中文。
|
||||
if self.state.launch_ends != self.launch_ends_seen {
|
||||
self.launch_ends_seen = self.state.launch_ends;
|
||||
if let Some(finished) = self.state.last_launch_end.as_deref() {
|
||||
let text = view::instances::launch_toast(finished, self.lang());
|
||||
let kind = if finished.outcome == unlucky_presenter::dto::LogOutcome::Ok
|
||||
&& finished.failure.is_none()
|
||||
{
|
||||
ToastKind::Success
|
||||
} else {
|
||||
ToastKind::Error
|
||||
};
|
||||
self.state.toasts.push(kind, text);
|
||||
}
|
||||
}
|
||||
|
||||
// 分类是**页面自己持有的过滤条件**(真 host 丢弃 `ListVersions` 的分类参数):
|
||||
// 重建视图模型时带回去,否则每次事件都会把 chip 打回「全部」(复审 P2-1)。
|
||||
let category = self.view.download.category;
|
||||
@@ -48,6 +66,7 @@ impl App {
|
||||
self.view = super::super::view::model(
|
||||
&self.state,
|
||||
width,
|
||||
self.height,
|
||||
self.options.force_state,
|
||||
category,
|
||||
&draft,
|
||||
|
||||
@@ -20,8 +20,18 @@ impl App {
|
||||
/// 一条更新动作。
|
||||
pub(super) fn on_update_action(&mut self, action: UpdateAction) -> Task<Message> {
|
||||
match action {
|
||||
UpdateAction::CheckNow => self.dispatch(AppCommand::CheckUpdate),
|
||||
UpdateAction::Apply => self.dispatch(AppCommand::ApplyUpdate),
|
||||
UpdateAction::CheckNow => {
|
||||
if self.update_busy() {
|
||||
return self.refuse_busy("立即检查");
|
||||
}
|
||||
self.dispatch(AppCommand::CheckUpdate)
|
||||
}
|
||||
UpdateAction::Apply => {
|
||||
if self.update_busy() {
|
||||
return self.refuse_busy("下载并安装");
|
||||
}
|
||||
self.dispatch(AppCommand::ApplyUpdate)
|
||||
}
|
||||
UpdateAction::Dismiss => self.dispatch(AppCommand::DismissUpdate),
|
||||
UpdateAction::SetAutoCheck(on) => self.dispatch(AppCommand::SetUpdateAutoCheck(on)),
|
||||
UpdateAction::RestartNow => return self.restart_now(),
|
||||
@@ -29,6 +39,25 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// 有没有一条检查/下载正在跑。
|
||||
///
|
||||
/// 与 `pages::Update::busy()` **同一个判据**(`applying` 或 `status == Checking`):
|
||||
/// presenter 的 `begin_check` / `begin_apply` 在 inflight 时静默 `return None`,
|
||||
/// 所以「忙碌」这件事必须在按下之前就挡住——否则用户点了没反应,还以为按钮坏了(M161)。
|
||||
///
|
||||
/// 这一层是**最后一道**:按钮已经画成禁用(`pages/settings/update.rs` 的 `can_check` /
|
||||
/// `can_apply`),但快捷键/未来新增的入口仍会走到这里。
|
||||
fn update_busy(&self) -> bool {
|
||||
let view = &self.state.update;
|
||||
view.applying || view.outcome.is_checking()
|
||||
}
|
||||
|
||||
/// 忙碌时把这条动作丢掉(不派发、不乐观改状态),并在日志里留一行取证。
|
||||
fn refuse_busy(&self, what: &str) -> Task<Message> {
|
||||
eprintln!("[update] 已有一条检查/下载在跑,忽略重复的「{what}」");
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// 立即重启:按同样的命令行拉起自己,然后退出。
|
||||
///
|
||||
/// 截图通道(`--screenshot`)**不重启**:那条路每跑一次就该落一张图然后退出,重启会覆盖同一
|
||||
@@ -142,6 +171,47 @@ mod tests {
|
||||
assert_eq!(last(&seen), AppCommand::SetUpdateAutoCheck(false));
|
||||
}
|
||||
|
||||
/// **忙碌时按下去不发命令**(M161:此前点了没反应,因为 presenter 在 inflight 时静默返回)。
|
||||
///
|
||||
/// 三条都要钉住:①`Checking` 时「立即检查」不派发;②`applying` 时「下载并安装」不派发;
|
||||
/// ③忙碌结束后**恢回可点**(失败/超时不该把按钮永久钉死)。
|
||||
#[test]
|
||||
fn pressing_while_busy_dispatches_nothing() {
|
||||
let (mut app, seen) = app();
|
||||
|
||||
app.state.update.outcome = unlucky_presenter::dto::UpdateOutcome::Checking;
|
||||
let _ = app.on_update_action(UpdateAction::CheckNow);
|
||||
assert!(
|
||||
seen.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.is_empty(),
|
||||
"正在检查时再按「立即检查」不许派发第二条命令"
|
||||
);
|
||||
|
||||
// 检查回来(失败也算回来):不忙了,能再点。
|
||||
app.state.update.outcome = unlucky_presenter::dto::UpdateOutcome::Failed(
|
||||
unlucky_presenter::dto::UpdateFailureView::new("network", "连不上", true),
|
||||
);
|
||||
let _ = app.on_update_action(UpdateAction::CheckNow);
|
||||
assert_eq!(last(&seen), AppCommand::CheckUpdate, "失败之后要能重试");
|
||||
|
||||
// 下载/校验/替换期间:`Apply` 与 `CheckNow` 都不许派发。
|
||||
let before = seen.lock().unwrap_or_else(|p| p.into_inner()).len();
|
||||
app.state.update.applying = true;
|
||||
let _ = app.on_update_action(UpdateAction::Apply);
|
||||
let _ = app.on_update_action(UpdateAction::CheckNow);
|
||||
assert_eq!(
|
||||
seen.lock().unwrap_or_else(|p| p.into_inner()).len(),
|
||||
before,
|
||||
"applying 期间不许派发任何新的检查/安装"
|
||||
);
|
||||
|
||||
// 应用结束:恢复可点。
|
||||
app.state.update.applying = false;
|
||||
let _ = app.on_update_action(UpdateAction::Apply);
|
||||
assert_eq!(last(&seen), AppCommand::ApplyUpdate, "失败/完成后恢回可点");
|
||||
}
|
||||
|
||||
/// 截图通道不重启:取证进程不许把自己重启成死循环。
|
||||
#[test]
|
||||
fn the_screenshot_channel_never_restarts() {
|
||||
|
||||
@@ -21,12 +21,12 @@ use super::state::{log_first_frame, App};
|
||||
use super::APP_TITLE;
|
||||
|
||||
mod account_page;
|
||||
mod instances;
|
||||
pub(super) mod instances;
|
||||
mod logs;
|
||||
mod rail;
|
||||
mod update;
|
||||
|
||||
use instances::{detail_of, runtime_status, supporting_line};
|
||||
use instances::{detail_of, runtime_status_row, supporting_line};
|
||||
use logs::logs_model;
|
||||
use update::update_of;
|
||||
|
||||
@@ -140,6 +140,7 @@ impl App {
|
||||
pub(super) fn model(
|
||||
state: &AppState,
|
||||
width: f32,
|
||||
height: f32,
|
||||
forced: Option<crate::cli::ForcedState>,
|
||||
category: pages::Category,
|
||||
draft: &pages::InstallDraft,
|
||||
@@ -158,7 +159,7 @@ pub(super) fn model(
|
||||
id: cell.instance.id.clone(),
|
||||
name: cell.instance.name.clone(),
|
||||
supporting: supporting_line(cell.instance, catalog),
|
||||
status: runtime_status(&cell.instance.runtime, catalog),
|
||||
status: runtime_status_row(&cell.instance.runtime, catalog),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -174,6 +175,7 @@ pub(super) fn model(
|
||||
pages::Model {
|
||||
screen,
|
||||
window_width: width,
|
||||
window_height: height,
|
||||
active: route.nav(),
|
||||
instances: pages::Instances { rows, detail },
|
||||
download: pages::Download::from_snapshot(state, category, draft, lang.catalog()),
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
//! 2. **运行状态只翻标签本体**:`Running · pid 1234` → `运行中 · pid 1234`,`pid` 与数字、
|
||||
//! `·` 分隔符一个字都不动([`Key::InstancesRunning`] 的占位符只有 `{pid}`)。
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{InstanceRuntime, InstanceSummary, LaunchStage, LoaderKind};
|
||||
use unlucky_i18n::{Catalog, Key, Lang};
|
||||
use unlucky_presenter::dto::{
|
||||
InstanceRuntime, InstanceSummary, LaunchFailureStage, LaunchFinished, LaunchStage, LoaderKind,
|
||||
LogOutcome,
|
||||
};
|
||||
|
||||
use crate::pages::{Detail, LaunchFacts, Meta};
|
||||
|
||||
@@ -59,7 +62,7 @@ fn isolation(raw: &str, catalog: Catalog) -> String {
|
||||
}
|
||||
|
||||
/// 运行状态(列表行的 overline 与详情页的状态行共用一份文案)。
|
||||
pub(super) fn runtime_status(runtime: &InstanceRuntime, catalog: Catalog) -> Option<String> {
|
||||
pub(crate) fn runtime_status(runtime: &InstanceRuntime, catalog: Catalog) -> Option<String> {
|
||||
match runtime {
|
||||
InstanceRuntime::Idle => None,
|
||||
// 「阶段 · 百分比」:百分比是数字,不过词表;只有阶段名换语言
|
||||
@@ -71,13 +74,78 @@ pub(super) fn runtime_status(runtime: &InstanceRuntime, catalog: Catalog) -> Opt
|
||||
InstanceRuntime::Running { pid } => {
|
||||
Some(catalog.render(Key::InstancesRunning, &[("pid", &pid.to_string())]))
|
||||
}
|
||||
// `reason` 是 core 给的诊断原文(**数据**,不翻译);只有前半句是界面文案
|
||||
InstanceRuntime::Failed { reason } => {
|
||||
Some(catalog.render(Key::InstancesFailed, &[("reason", reason)]))
|
||||
}
|
||||
// `detail` 是 core/io 给的原文(**数据**,不翻译);「哪一步」与前半句是界面文案。
|
||||
// M161 之前这里只有 `reason`,而 reason 里已经是「启动失败:…」,于是英文界面会出现
|
||||
// 「Launch failed · 启动失败:找不到 Java」——半句英文半句中文。
|
||||
//
|
||||
// 这一份是**详情页**用的全文(详情列会自动换行);列表行那份是 [`runtime_status_row`]
|
||||
// (单行、不许折行,见 D-M79-5)。
|
||||
InstanceRuntime::Failed { stage, detail } => Some(catalog.render(
|
||||
Key::LaunchStageFailed,
|
||||
&[
|
||||
("stage", failure_stage_label(*stage, catalog)),
|
||||
("detail", detail.as_str()),
|
||||
],
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 列表行用的运行状态:**单行**(`ListItem` 的 overline 不折行,D-M79-5)。
|
||||
///
|
||||
/// 与 [`runtime_status`] 只差一处:失败时不带 `detail`——那一段是原始错误文本(可能是一长串
|
||||
/// 路径),放进 60/76 高的单行 overline 会被裁成半截字。全文在**详情页**([`runtime_status`])。
|
||||
pub(crate) fn runtime_status_row(runtime: &InstanceRuntime, catalog: Catalog) -> Option<String> {
|
||||
match runtime {
|
||||
InstanceRuntime::Failed { stage, .. } => Some(catalog.render(
|
||||
Key::InstancesFailedStage,
|
||||
&[("stage", failure_stage_label(*stage, catalog))],
|
||||
)),
|
||||
other => runtime_status(other, catalog),
|
||||
}
|
||||
}
|
||||
|
||||
/// 失败阶段文案(presenter 给 slug,界面在这里选词)。
|
||||
///
|
||||
/// 与 [`stage_label`] 同一套做法:**不**复用 presenter 侧的任何中文常量。
|
||||
pub(crate) fn failure_stage_label(stage: LaunchFailureStage, catalog: Catalog) -> &'static str {
|
||||
catalog.text(match stage {
|
||||
LaunchFailureStage::VersionManifest => Key::LaunchStageVersionManifest,
|
||||
LaunchFailureStage::Inheritance => Key::LaunchStageInheritance,
|
||||
LaunchFailureStage::InstanceConfig => Key::LaunchStageInstanceConfig,
|
||||
LaunchFailureStage::Isolation => Key::LaunchStageIsolation,
|
||||
LaunchFailureStage::Identity => Key::LaunchStageIdentity,
|
||||
LaunchFailureStage::Spawn => Key::LaunchStageSpawn,
|
||||
})
|
||||
}
|
||||
|
||||
/// 一次启动收尾的 Toast 文案(**唯一的产地**:presenter 只发结构,不拼句子)。
|
||||
///
|
||||
/// 三种结局各有各的说法:正常结束报时长,崩溃/静默失败报「结局 + 阶段 + 原文」,
|
||||
/// 组装阶段就失败(`failure` 有值)报「哪一步 + 原文」。
|
||||
#[must_use]
|
||||
pub(crate) fn launch_toast(finished: &LaunchFinished, lang: Lang) -> String {
|
||||
let catalog = lang.catalog();
|
||||
if let Some(failure) = &finished.failure {
|
||||
return catalog.render(
|
||||
Key::LaunchStageFailed,
|
||||
&[
|
||||
("stage", failure_stage_label(failure.stage, catalog)),
|
||||
("detail", failure.detail.as_str()),
|
||||
],
|
||||
);
|
||||
}
|
||||
let outcome = catalog.text(match finished.outcome {
|
||||
LogOutcome::Ok => Key::LaunchOutcomeNormal,
|
||||
LogOutcome::Crashed => Key::LaunchOutcomeCrash,
|
||||
LogOutcome::SilentFail => Key::LaunchOutcomeSilent,
|
||||
});
|
||||
let seconds = format!("{:.1}", finished.uptime_ms as f64 / 1000.0);
|
||||
catalog.render(
|
||||
Key::LaunchToastEnded,
|
||||
&[("outcome", outcome), ("seconds", seconds.as_str())],
|
||||
)
|
||||
}
|
||||
|
||||
/// 阶段文案:**不**复用 `LaunchStage::label()`(那是核心层的中文诊断串,英文界面会漏中文)。
|
||||
fn stage_label(stage: LaunchStage, catalog: Catalog) -> &'static str {
|
||||
catalog.text(match stage {
|
||||
|
||||
@@ -74,7 +74,9 @@ fn crashed_only() -> AppState {
|
||||
id: "dev".to_string(),
|
||||
session: CRASH.to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
exit_code: Some(1),
|
||||
uptime_ms: 3_000,
|
||||
failure: None,
|
||||
}));
|
||||
|
||||
state
|
||||
@@ -166,7 +168,9 @@ fn an_imported_crash_file_still_shows_its_findings() {
|
||||
id: "dev".to_string(),
|
||||
session: CRASH.to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
exit_code: Some(1),
|
||||
uptime_ms: 3_000,
|
||||
failure: None,
|
||||
}));
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(
|
||||
"file:crash-2026-11-02_19.40.15-client.txt",
|
||||
|
||||
@@ -39,6 +39,7 @@ fn at_lang(state: &AppState, forced: Option<crate::cli::ForcedState>, lang: Lang
|
||||
model(
|
||||
state,
|
||||
1180.0,
|
||||
760.0,
|
||||
forced,
|
||||
pages::Category::All,
|
||||
&draft,
|
||||
@@ -125,11 +126,18 @@ fn a_busy_instance_offers_stop_instead_of_play() {
|
||||
#[test]
|
||||
fn failed_instances_can_be_retried() {
|
||||
let failed = instance(InstanceRuntime::Failed {
|
||||
reason: "exit 1".to_string(),
|
||||
stage: unlucky_presenter::dto::LaunchFailureStage::Spawn,
|
||||
detail: "exit 1".to_string(),
|
||||
});
|
||||
// 列表行是**单行**:只放「哪一步」(原文太长会被 `Wrapping::None` 裁成半截字)。
|
||||
assert_eq!(
|
||||
runtime_status(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||||
Some("Launch failed · exit 1")
|
||||
super::instances::runtime_status_row(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||||
Some("Launch failed · Starting the game process")
|
||||
);
|
||||
// 详情页给全文(阶段 + 原文)。
|
||||
assert_eq!(
|
||||
super::instances::runtime_status(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||||
Some("Launch failed · Starting the game process: exit 1")
|
||||
);
|
||||
assert!(
|
||||
detail_of(&failed, Lang::En.catalog()).can_launch,
|
||||
@@ -319,3 +327,67 @@ fn the_agent_route_has_no_page() {
|
||||
assert!(model.instances.detail.is_none());
|
||||
assert_eq!(model.active, None, "AI 页不高亮任何导航项");
|
||||
}
|
||||
|
||||
/// **启动收尾的提示语跟着界面语言走**(M161 收的债)。
|
||||
///
|
||||
/// 此前 `LaunchFinished.summary` 是 presenter 拼的中文,英文界面上弹的就是中文;
|
||||
/// 现在 presenter 只给结构(结局 + 退出码 + 失败阶段),文案在这里按 key 现求。
|
||||
///
|
||||
/// 抓得住:①谁把中文串再写回 presenter(英文那一行会露中文);②失败时丢掉 `detail`
|
||||
/// (原文是取证信息,不许翻也不许删);③「跑起来再崩」被说成「启动失败在某一步」。
|
||||
#[test]
|
||||
fn the_launch_toast_speaks_the_interface_language() {
|
||||
use unlucky_presenter::dto::{
|
||||
LaunchFailureStage, LaunchFailureView, LaunchFinished, LogOutcome,
|
||||
};
|
||||
|
||||
let finished = |outcome, failure| LaunchFinished {
|
||||
id: "survival".to_string(),
|
||||
session: "20261031T120000-abcd12".to_string(),
|
||||
outcome,
|
||||
exit_code: Some(1),
|
||||
uptime_ms: 12_500,
|
||||
failure,
|
||||
};
|
||||
|
||||
// 正常结束:只报结局 + 时长(`正常结束 · 12.5 秒` / `Finished normally · 12.5s`)。
|
||||
let ok = finished(LogOutcome::Ok, None);
|
||||
let en = super::instances::launch_toast(&ok, Lang::En);
|
||||
let zh = super::instances::launch_toast(&ok, Lang::ZhCn);
|
||||
assert_eq!(en, "Finished normally · 12.5s");
|
||||
assert_eq!(zh, "正常结束 · 12.5 秒");
|
||||
assert_ne!(en, zh, "切语言时提示语要变");
|
||||
|
||||
// 装配阶段失败:说清**哪一步**,并把原文原样带出来。
|
||||
let failed = finished(
|
||||
LogOutcome::Crashed,
|
||||
Some(LaunchFailureView::new(
|
||||
LaunchFailureStage::InstanceConfig,
|
||||
"permission denied (os error 5)",
|
||||
)),
|
||||
);
|
||||
let en = super::instances::launch_toast(&failed, Lang::En);
|
||||
let zh = super::instances::launch_toast(&failed, Lang::ZhCn);
|
||||
assert!(
|
||||
en.starts_with("Launch failed · Reading the instance settings"),
|
||||
"{en}"
|
||||
);
|
||||
assert!(zh.starts_with("启动失败 · 读实例设置"), "{zh}");
|
||||
for text in [&en, &zh] {
|
||||
assert!(
|
||||
text.contains("permission denied (os error 5)"),
|
||||
"原文(取证信息)不许丢:{text}"
|
||||
);
|
||||
}
|
||||
// 英文界面里不许出现中文(反之亦然)——这条是本次债的核心。
|
||||
assert!(!en.chars().any(is_cjk), "英文提示里混了中文:{en}");
|
||||
assert!(
|
||||
!zh.contains("Launch failed"),
|
||||
"中文提示里混了英文前缀:{zh}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 有没有中文字符(上面那条「不许混语言」的判据)。
|
||||
fn is_cjk(ch: char) -> bool {
|
||||
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||
}
|
||||
Reference in new issue
Block a user