fix(m161-r1): 崩溃/静默失败不再冒充「启动进程失败」;补三处假绿的守卫
复审 P1-1(用户可见假文案):`LaunchEnd` 在 `failure == None`(跑起来之后崩的)时
临时造了 `Stage::Spawn + format!("exit {:?}")`,于是磁贴写「启动失败 · 启动游戏进程」、
详情页漏出 `exit Some(1)`、静默失败丢掉「exit 0 却没起来」,而同一时刻 Toast 说「崩溃」。
- 新增 `LaunchFailureCause{Stage|Crashed|SilentFailure}`(dto/launch_failure.rs),
`LaunchFailureCause::of(outcome, failure, exit_code)` 是唯一组装点;`InstanceRuntime::Failed{cause}`。
`LaunchFailureStage` 保持「只装装配阶段」(文档那句「崩溃是结局不是阶段」现在成立)。
- 界面:`InstancesFailed`(框架)+ `InstancesCrashed` + `InstancesSilentFailed`;
删掉 `InstancesFailedStage`。退出码按数字出词,不再有 Debug 文本。
- 用例:presenter 断言**枚举变体**(三种形状逐条)、app 断言**具体句子**
(「不许出现 Some(」「进程起来了不许说启动游戏进程」「静默失败要点出没起来」)。
mock 夹具与 launch_state_tests 的期望值同步;launch-error 三张图重拍。
复审 P2-1(`convert::failure_stage` 零覆盖):新增 `convert/stage_tests.rs` 5 条(三档 +
「既没回滚也没宣称干净」+ 非致命无阶段 + `failed_report_with` ⇒ NotStarted)。
变异实测:改成恒 `RolledBack` ⇒ 3 条红。
复审 P2-2(矮窗口用例的算式三个数全错、`body_height` 零测试):用例改成直接调
`frame::body_height`,chrome 从 `Ui::metrics` + `WidthClass::gutter` 现取,并把门槛与真实
chrome 绑死(chrome+MIN ⇒ Split、-1px ⇒ ScrollWhole);新增 `frame` 自己的 `body_height` 用例。
变异实测:`2.0 * gutter` → `gutter` ⇒ 2 条红。
复审 P2-3(源指纹闸门 / `Refused` 零覆盖):新增两条 core 用例——闸门(原地留着 prefs.json
+ 预先写好的同源收据 ⇒ Nothing 且收据未被重写,反证:源变了允许重放)、`Refused`
(`$schema` 1.7.0 ⇒ ReadOnly ⇒ 不动旧文件、不写收据、理由含「不可写」)。
变异实测:删闸门 ⇒ 1 条红;`can_save` 守卫改 `if false` ⇒ 1 条红。
顺手收:
- CLI `pack import` 那句「已整包回滚」也按三档出词(`RollbackStage` + `rollback_line`),
加 2 条单测。CLI 不依赖 presenter,判定暂与 presenter 各一份 → 已 file finding 建议下沉 core。
- 设置页 `status_text()` 抽出 applying 档,用例断言「正在安装…」且非错误色。
金表:新 key 加在 `golden.rs`(81 行),未动 `golden/tail.rs`(211 行),避免与 M162 的拆分冲突。
This commit is contained in:
1 parent
6804b0ad1d
commit
79b796fd58
21 files changed
+894
-168
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use super::launch_failure::{LaunchFailureStage, LaunchFailureView};
|
||||
pub use super::launch_failure::{LaunchFailureCause, LaunchFailureStage, LaunchFailureView};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -212,13 +212,12 @@ pub enum InstanceRuntime {
|
||||
/// 上次启动失败(磁贴显示「重试」)
|
||||
///
|
||||
/// M161 之前这里只有一个 `reason: String`,而且里面已经写着「启动失败:…」——
|
||||
/// 界面再包一层 `InstancesFailed`(`Launch failed · {reason}`)就成了「英文前缀 + 中文句子」,
|
||||
/// 中文界面则是「启动失败 · 启动失败:…」。现在拆成「阶段(可本地化)+ 原文(不翻译)」。
|
||||
/// 界面再包一层就成了「英文前缀 + 中文句子」,中文界面则是「启动失败 · 启动失败:…」。
|
||||
/// 现在给的是结构化原因(见 [`LaunchFailureCause`]):**没起来**与**起来又崩**是两种形状,
|
||||
/// 界面各说各的话(复审 P1-1:早先崩溃被说成「启动游戏进程失败」)。
|
||||
Failed {
|
||||
/// 失败阶段
|
||||
stage: LaunchFailureStage,
|
||||
/// 原始错误文本(不翻译)
|
||||
detail: String,
|
||||
/// 这一场为什么是失败态
|
||||
cause: LaunchFailureCause,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -72,3 +72,81 @@ impl LaunchFailureView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 磁贴/详情上「这一场为什么是失败态」的**完整原因**。
|
||||
///
|
||||
/// 两种**互斥**的形状,混成一种就会说假话(M161 复审 P1-1 抓的就是这个):
|
||||
///
|
||||
/// | 形状 | 意思 | 界面该说什么 |
|
||||
/// |---|---|---|
|
||||
/// | [`Self::Stage`] | 进程**没起来**(装配/启动阶段失败) | 「启动失败 · <哪一步>:<原文>」 |
|
||||
/// | [`Self::Crashed`] | 进程起来过,**崩了** | 「启动失败 · 游戏崩溃(退出码 N)」 |
|
||||
/// | [`Self::SilentFailure`] | 进程起来过,但 exit 0 却没真正跑起来 | 「启动失败 · 退出码 0,但游戏没起来」 |
|
||||
///
|
||||
/// 为什么把结局也放在这里:磁贴的状态行与 Toast 必须说同一件事。早先「起来了又崩」被
|
||||
/// 硬塞进 [`LaunchFailureStage::Spawn`](因为 `LaunchFinished.failure` 在这次是 `None`),
|
||||
/// 于是磁贴写「启动游戏进程失败」而 Toast 写「游戏崩溃」——同一时刻两句话打架,而且
|
||||
/// `detail` 还漏出了 `exit Some(1)` 这种 Rust Debug 文本。
|
||||
///
|
||||
/// `detail` 只在 [`Self::Stage`] 里有:那是 core / io 给的原文(路径、版本 id、os error),
|
||||
/// **故意不翻译**。结局类没有什么「原文」可给,退出码是数字,措辞由界面按语言出。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum LaunchFailureCause {
|
||||
/// 装配/启动阶段失败:进程**没起来**
|
||||
Stage {
|
||||
/// 失败在上面的哪一步
|
||||
stage: LaunchFailureStage,
|
||||
/// 那一步的原始错误文本(不翻译)
|
||||
detail: String,
|
||||
},
|
||||
/// 进程起来过,结局是崩溃
|
||||
Crashed {
|
||||
/// 退出码(`None` = 平台没给)
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
/// 进程起来过,但疑似静默失败(exit 0 却没真正跑起来)
|
||||
SilentFailure {
|
||||
/// 退出码(通常是 0)
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LaunchFailureCause {
|
||||
/// 稳定 slug(日志 / 测试用;**不要改名**)。
|
||||
#[must_use]
|
||||
pub const fn slug(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Stage { .. } => "stage",
|
||||
Self::Crashed { .. } => "crashed",
|
||||
Self::SilentFailure { .. } => "silent_failure",
|
||||
}
|
||||
}
|
||||
|
||||
/// 进程到底有没有起来过。
|
||||
#[must_use]
|
||||
pub const fn started(&self) -> bool {
|
||||
!matches!(self, Self::Stage { .. })
|
||||
}
|
||||
|
||||
/// 由一次收尾组装原因。
|
||||
///
|
||||
/// `failure` 有值 = 装配/启动阶段失败(进程没起来);`None` = 跑起来了,此时**只能**
|
||||
/// 看 `outcome`(`Ok` 不该走到这里,兜底按崩溃处理)。
|
||||
#[must_use]
|
||||
pub fn of(
|
||||
outcome: crate::dto::LogOutcome,
|
||||
failure: Option<&LaunchFailureView>,
|
||||
exit_code: Option<i32>,
|
||||
) -> Self {
|
||||
match failure {
|
||||
Some(failure) => Self::Stage {
|
||||
stage: failure.stage,
|
||||
detail: failure.detail.clone(),
|
||||
},
|
||||
None if outcome == crate::dto::LogOutcome::SilentFail => {
|
||||
Self::SilentFailure { exit_code }
|
||||
}
|
||||
None => Self::Crashed { exit_code },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ pub use content::{
|
||||
pub use events::{AppCommand, AppEvent, Backend, CoreEvent, LaunchFinished, LiveLogBatch};
|
||||
pub use install::{derive_speed, human_bytes, InstallProgressView, InstallStage};
|
||||
pub use instances::{
|
||||
CreateRequest, InstanceId, InstanceRuntime, InstanceSummary, LaunchFailureStage,
|
||||
LaunchFailureView, LaunchStage, LoaderInfo, LoaderKind, Progress,
|
||||
CreateRequest, InstanceId, InstanceRuntime, InstanceSummary, LaunchFailureCause,
|
||||
LaunchFailureStage, LaunchFailureView, LaunchStage, LoaderInfo, LoaderKind, Progress,
|
||||
};
|
||||
pub use java::{JavaChainOutcome, JavaChainStep, JavaPageView, JavaRuntimeView, JavaVendorView};
|
||||
pub use launch_args::{LaunchArgsPatch, LaunchArgsView};
|
||||
|
||||
@@ -375,3 +375,7 @@ pub(super) fn slug(text: &str) -> String {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert/stage_tests.rs"]
|
||||
mod stage_tests;
|
||||
@@ -0,0 +1,108 @@
|
||||
//! M161 复审 P2-1:`failure_stage` 是⑥三档的**唯一**判定点,此前零覆盖(把它改成恒
|
||||
//! `RolledBack`,presenter 254 条全绿)。这里直接喂 core 的机器位,断言的是**判定**,
|
||||
//! 不是渲染(渲染在 app-iced 的 `packui/model/tests/stage.rs`)。
|
||||
//!
|
||||
//! 抓得住:`None if report.rolled_back` 被写反(预检失败 → 「已回滚」,正是⑥要消灭的谎);
|
||||
//! `is_complete()` 被丢掉(回滚没做完也说成零残留);`status != Failed` 也发阶段。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_core::addon::import::{ImportError, ImportReport, ImportStatus, RollbackOutcome};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 一份「失败」的空报告(还没决定回滚结果)。
|
||||
fn failed() -> ImportReport {
|
||||
let mut report = ImportReport::empty(PathBuf::from("C:/tmp/.minecraft"));
|
||||
report.fail(ImportError::integrity("sha1 mismatch"));
|
||||
report
|
||||
}
|
||||
|
||||
/// 预检就失败(**从未写过目标目录**)⇒ `NotStarted`。
|
||||
///
|
||||
/// core 的 `abandoned()` 就是这一形:`rollback == None` + `rolled_back == true`。
|
||||
#[test]
|
||||
fn preflight_failure_is_not_started() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = true;
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::NotStarted),
|
||||
"没写过目标目录就是「根本没开始」,不许说成「已回滚」"
|
||||
);
|
||||
assert_eq!(failure_stage(&report).unwrap().slug(), "not_started");
|
||||
// 渲染层同一份判据(`failed_report_with` 走的是同一条)。
|
||||
assert_eq!(
|
||||
failed_report_with(
|
||||
PathBuf::from("C:/tmp"),
|
||||
std::path::Path::new("Better MC.mrpack"),
|
||||
ImportErrorView {
|
||||
code: "pack_traversal".to_string(),
|
||||
label: "Path traversal".to_string(),
|
||||
message: "../etc/passwd".to_string(),
|
||||
hint: None,
|
||||
},
|
||||
)
|
||||
.failure_stage,
|
||||
Some(ImportFailureStageView::NotStarted),
|
||||
"宿主侧失败(目标实例不可用)也是「没开始写」,不是「已回滚」"
|
||||
);
|
||||
}
|
||||
|
||||
/// 跑过回滚且完整 ⇒ `RolledBack`。
|
||||
#[test]
|
||||
fn a_complete_rollback_is_rolled_back() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = true;
|
||||
report.rollback = Some(RollbackOutcome {
|
||||
attempted: 3,
|
||||
failed: Vec::new(),
|
||||
});
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RolledBack)
|
||||
);
|
||||
assert_eq!(failure_stage(&report).unwrap().slug(), "rolled_back");
|
||||
}
|
||||
|
||||
/// 跑过回滚但有失败项 ⇒ `RollbackIncomplete`(目标目录可能与导入前不同)。
|
||||
#[test]
|
||||
fn a_partial_rollback_is_incomplete() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = false;
|
||||
report.rollback = Some(RollbackOutcome {
|
||||
attempted: 3,
|
||||
failed: vec!["mods/a.jar: 占用中".to_string()],
|
||||
});
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RollbackIncomplete)
|
||||
);
|
||||
assert_eq!(
|
||||
failure_stage(&report).unwrap().slug(),
|
||||
"rollback_incomplete"
|
||||
);
|
||||
}
|
||||
|
||||
/// 既没回滚也没宣称干净(core 的 `failed()` 单独一支)⇒ 只能如实说「可能不一致」。
|
||||
#[test]
|
||||
fn a_failure_without_any_claim_is_incomplete_not_not_started() {
|
||||
let mut report = failed();
|
||||
report.rolled_back = false;
|
||||
report.rollback = None;
|
||||
assert_eq!(
|
||||
failure_stage(&report),
|
||||
Some(ImportFailureStageView::RollbackIncomplete),
|
||||
"没跑回滚也不宣称干净:不许说「没开始写」"
|
||||
);
|
||||
}
|
||||
|
||||
/// 不是致命失败 ⇒ 没有「失败阶段」可言(成功/部分成功都不该发这一段)。
|
||||
#[test]
|
||||
fn a_non_fatal_report_has_no_stage() {
|
||||
let mut report = ImportReport::empty(PathBuf::from("C:/tmp"));
|
||||
report.rolled_back = false;
|
||||
assert_eq!(failure_stage(&report), None, "Success 不该有失败阶段");
|
||||
report.status = ImportStatus::Partial;
|
||||
assert_eq!(failure_stage(&report), None, "Partial 不该有失败阶段");
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
//!
|
||||
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.tx` / `self.launches` 等。
|
||||
|
||||
use crate::dto::{
|
||||
AppEvent, CoreEvent, InstanceRuntime, LaunchFailureStage, LaunchFailureView, LogOutcome,
|
||||
PackEvent,
|
||||
};
|
||||
use crate::dto::{AppEvent, CoreEvent, InstanceRuntime, LaunchFailureCause, LogOutcome, PackEvent};
|
||||
use crate::toast::ToastKind;
|
||||
|
||||
use super::{addon, logs, CoreHost, HostTask, LiveLaunch};
|
||||
@@ -163,20 +160,19 @@ impl CoreHost {
|
||||
self.failed.remove(&finished.id);
|
||||
InstanceRuntime::Idle
|
||||
} else {
|
||||
// 记住失败(阶段 + 原文):随后的 refresh_instances 会重建快照,
|
||||
// 记住失败:随后的 refresh_instances 会重建快照,
|
||||
// 不记的话磁贴的「失败/重试」外观会被 Idle 盖掉。
|
||||
let failure = finished.failure.clone().unwrap_or_else(|| {
|
||||
// 跑起来再崩:没有「装配阶段」可言,用结局本身当阶段外的说明。
|
||||
LaunchFailureView::new(
|
||||
LaunchFailureStage::Spawn,
|
||||
format!("exit {:?}", finished.exit_code),
|
||||
)
|
||||
});
|
||||
self.failed.insert(finished.id.clone(), failure.clone());
|
||||
InstanceRuntime::Failed {
|
||||
stage: failure.stage,
|
||||
detail: failure.detail,
|
||||
}
|
||||
//
|
||||
// 原因**按形状**组装(复审 P1-1):`failure` 有值 = 进程没起来(阶段失败);
|
||||
// 没值 = 跑起来了,那就必须看 `outcome`(崩溃 / 疑似静默失败),
|
||||
// 不许再拿「启动游戏进程失败」去顶——那句话在进程明明起来时说不过去。
|
||||
let cause = LaunchFailureCause::of(
|
||||
finished.outcome,
|
||||
finished.failure.as_ref(),
|
||||
finished.exit_code,
|
||||
);
|
||||
self.failed.insert(finished.id.clone(), cause.clone());
|
||||
InstanceRuntime::Failed { cause }
|
||||
};
|
||||
self.send(CoreEvent::InstanceRuntimeChanged {
|
||||
id: finished.id.clone(),
|
||||
|
||||
@@ -213,13 +213,15 @@ fn refresh_keeps_pending_as_busy_and_failed_as_failed() {
|
||||
.into_iter()
|
||||
.find(|instance| instance.id == id)
|
||||
.expect("实例存在");
|
||||
// 跑起来又崩:原因是**结局**,不是「进程起不来」(复审 P1-1)。
|
||||
// 抓得住:把崩溃塞回 `LaunchFailureStage::Spawn`(磁贴会说「启动游戏进程失败」,假话),
|
||||
// 或者把退出码漏成 `Some(1)` 这种 Debug 文本(这里只认数字)。
|
||||
assert_eq!(
|
||||
failed.runtime,
|
||||
InstanceRuntime::Failed {
|
||||
stage: crate::dto::LaunchFailureStage::Spawn,
|
||||
detail: "exit Some(1)".to_string()
|
||||
cause: crate::dto::LaunchFailureCause::Crashed { exit_code: Some(1) }
|
||||
},
|
||||
"失败原因要经得住一次刷新(否则磁贴的「重试」外观会被 Idle 盖掉)"
|
||||
"崩溃要如实报成「起来了又崩」,且经得住一次刷新"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,13 +260,101 @@ fn launch_end_before_launch_started_still_releases_the_instance() {
|
||||
assert_eq!(
|
||||
failed.runtime,
|
||||
InstanceRuntime::Failed {
|
||||
stage: crate::dto::LaunchFailureStage::Identity,
|
||||
detail: "找不到账户 `steve`".to_string()
|
||||
cause: crate::dto::LaunchFailureCause::Stage {
|
||||
stage: crate::dto::LaunchFailureStage::Identity,
|
||||
detail: "找不到账户 `steve`".to_string()
|
||||
}
|
||||
},
|
||||
"早失败也要在磁贴上留下「失败/重试」,不能被 Idle 盖掉"
|
||||
"早失败(进程没起来)要留在阶段那一支,不能被 Idle 盖掉"
|
||||
);
|
||||
}
|
||||
|
||||
/// **三种失败形状各归各的**(复审 P1-1 的核心判据)。
|
||||
///
|
||||
/// | 收尾事件 | 期望原因 |
|
||||
/// |---|---|
|
||||
/// | `failure: Some(..)` | `Stage{..}`(进程**没起来**)|
|
||||
/// | `failure: None` + `Crashed` | `Crashed{exit_code}`(起来又崩)|
|
||||
/// | `failure: None` + `SilentFail` | `SilentFailure{exit_code}` |
|
||||
///
|
||||
/// 抓得住:把「起来了又崩」塞进 `LaunchFailureStage::Spawn`(磁贴会说「启动游戏进程失败」,
|
||||
/// 而进程明明起来了);或者把静默失败也塌成崩溃(丢掉「退出码 0 却没跑起来」这条信息)。
|
||||
/// 断言的是**枚举变体**,不是字符串里有没有某个词。
|
||||
#[test]
|
||||
fn a_launch_end_without_a_stage_reports_the_outcome_not_a_start_failure() {
|
||||
use crate::dto::{LaunchFailureCause, LaunchFailureStage, LaunchFailureView};
|
||||
|
||||
/// 一行用例:`(标签, 结局, 阶段失败, 退出码, 期望原因)`。
|
||||
type Case = (
|
||||
&'static str,
|
||||
LogOutcome,
|
||||
Option<LaunchFailureView>,
|
||||
Option<i32>,
|
||||
LaunchFailureCause,
|
||||
);
|
||||
|
||||
let cases: Vec<Case> = vec![
|
||||
(
|
||||
"spawn-failed",
|
||||
LogOutcome::Crashed,
|
||||
Some(LaunchFailureView::new(
|
||||
LaunchFailureStage::Spawn,
|
||||
"os error 5",
|
||||
)),
|
||||
None,
|
||||
LaunchFailureCause::Stage {
|
||||
stage: LaunchFailureStage::Spawn,
|
||||
detail: "os error 5".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"crashed-after-start",
|
||||
LogOutcome::Crashed,
|
||||
None,
|
||||
Some(1),
|
||||
LaunchFailureCause::Crashed { exit_code: Some(1) },
|
||||
),
|
||||
(
|
||||
"silent-after-start",
|
||||
LogOutcome::SilentFail,
|
||||
None,
|
||||
Some(0),
|
||||
LaunchFailureCause::SilentFailure { exit_code: Some(0) },
|
||||
),
|
||||
];
|
||||
|
||||
for (label, outcome, failure, exit_code, expected) in cases {
|
||||
let (mut host, id, _root) = host_with_instance(label);
|
||||
host.mark_pending(&id);
|
||||
host.handle(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id: id.clone(),
|
||||
session: format!("20261031T120000-{label}"),
|
||||
outcome,
|
||||
exit_code,
|
||||
uptime_ms: 1_000,
|
||||
failure,
|
||||
})));
|
||||
let failed = instances_from_events({
|
||||
host.refresh_instances();
|
||||
host.drain_events()
|
||||
})
|
||||
.into_iter()
|
||||
.find(|instance| instance.id == id)
|
||||
.expect("实例存在");
|
||||
match (&failed.runtime, &expected) {
|
||||
(InstanceRuntime::Failed { cause }, want) => {
|
||||
assert_eq!(cause, want, "{label}:失败原因说错了");
|
||||
assert_eq!(
|
||||
cause.slug(),
|
||||
want.slug(),
|
||||
"{label}:slug 与变体不一致(日志/事件流按 slug 判)"
|
||||
);
|
||||
}
|
||||
(other, want) => panic!("{label}:期望 Failed{{cause:{want:?}}},实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从事件队列里取最后一次 `InstancesChanged`。
|
||||
fn instances_from_events(events: Vec<AppEvent>) -> Vec<crate::dto::InstanceSummary> {
|
||||
events
|
||||
|
||||
@@ -41,7 +41,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::dto::{
|
||||
AccountView, AppCommand, AppEvent, Backend, CoreEvent, DiagnosisReport, InstallProgressView,
|
||||
InstanceRuntime, JavaChainStep, LaunchFailureView, NoticeView, SkinImage,
|
||||
InstanceRuntime, JavaChainStep, LaunchFailureCause, NoticeView, SkinImage,
|
||||
};
|
||||
use crate::mock::MockBackend;
|
||||
use crate::toast::{Toast, ToastKind};
|
||||
@@ -93,7 +93,7 @@ pub struct CoreHost {
|
||||
pending: BTreeSet<String>,
|
||||
/// 上次启动失败的实例与原因(刷新快照时保住磁贴的「失败/重试」外观)
|
||||
/// 上次启动失败(阶段 + 原文;磁贴的「失败/重试」外观靠它重建)
|
||||
failed: BTreeMap<String, LaunchFailureView>,
|
||||
failed: BTreeMap<String, LaunchFailureCause>,
|
||||
/// 实时日志尾随器
|
||||
tail: logs::Tail,
|
||||
/// AI 对话的临时后端(agent GUI host 落地前的占位)
|
||||
@@ -229,10 +229,9 @@ impl CoreHost {
|
||||
stage: crate::dto::LaunchStage::Launch,
|
||||
progress: crate::dto::Progress { done: 0, total: 1 },
|
||||
};
|
||||
} else if let Some(failure) = self.failed.get(&instance.id) {
|
||||
} else if let Some(cause) = self.failed.get(&instance.id) {
|
||||
instance.runtime = InstanceRuntime::Failed {
|
||||
stage: failure.stage,
|
||||
detail: failure.detail.clone(),
|
||||
cause: cause.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ pub fn mock_instances() -> Vec<InstanceSummary> {
|
||||
version: "21.1.72".to_string(),
|
||||
},
|
||||
last_played: Some("2026-10-27T12:40:00Z".to_string()),
|
||||
// 静默失败 = **起来过**但 exit 0 没真正跑起来(不是「进程起不来」,见复审 P1-1)。
|
||||
runtime: InstanceRuntime::Failed {
|
||||
stage: crate::dto::LaunchFailureStage::Spawn,
|
||||
detail: "exit 0 但游戏没起来(静默失败)".to_string(),
|
||||
cause: crate::dto::LaunchFailureCause::SilentFailure { exit_code: Some(0) },
|
||||
},
|
||||
icon: None,
|
||||
isolation: "isolated".to_string(),
|
||||
|
||||
Reference in new issue
Block a user