fix(app-iced): M95 r4 —— 归属判据回归 / 置信度档 / 运行中页头
代码复审 r3 的四条 p2,修三条、登记一条(P2-4 页脚余量 13px,后置)。 **P2-1(回归修复)**:r3 把归属判据写成「有选中就只认选中」,把**导入崩溃文件**与 `open_session` 这两条路径的诊断一并关掉了——`559ba20` 是**无条件**画的,所以这是 r3 引入的**代码级回归**, 不是既有缺口(今天 iced 侧没有 dispatch 站点,那些还在遗留的 Slint shell 里,故复审未判 p1)。 判据改成三分支(`app/view/logs.rs`): | live_session | selected_session | 报告算数的条件 | |---|---|---| | Some(s) | 任意 | `report.session == s`(只有它在线时日志行是它在追加,别人的报告挂上来就是串场) | | None | Some(s) | `report.session == s` **或** 报告不属于任何已知会话(导入用 `file:<文件名>`,永不在索引里) | | None | None | 全部算数(此时日志行只可能来自 `open_session`/导入,与报告同一次调用发出) | 新增两条测试(都走真实 reducer):`an_imported_crash_file_still_shows_its_findings`、 `a_live_run_says_it_is_running_instead_of_nothing_runs`。 残留(今天不可达,已登记):`open_session(A)` 在 `selected_session` 仍停在上场 B 时会被挡掉—— 真要修得让 presenter 在 `LogLinesChanged` 记下「日志行属于哪一场」。 **P2-2 置信度档(视觉变更)**:`Severity`(借 core 的 `critical/warning/notice` 词汇)→ `Confidence`(`High/Medium/Low`)+ 新 key `LogsConfidenceHigh/Medium/Low`(中英齐,标题明确写 「高/中/低置信度」)。阈值仍是 80/50,但它现在是**档位定义**而不是「严重度代理」。 摘要里的 `findings=rule:high(85)` 同步换档位 slug。DTO 补齐 `severity` 之后应换回真严重度(finding)。 **P2-3 运行中的页头**:session 索引只在**结束**时追加,运行中的那一场不在 `log_sessions` 里 ⇒ 拿不到实例名与结局 ⇒ r3 之前页头在日志滚动时一直说「当前没有运行中的实例」(demo 伪造了身份, 截图照不出来)。`Logs` 新增 `live: bool`(`live_session.is_some()`),`with_status` 三级回退: 有实例+结局 → `实例 · 时间 · 结局`;运行中但没结局 → 新 key `LogsSubtitleLive`(「Running now — logs stream as they arrive」);都没有 → `LogsSubtitleIdle`。 新增 `a_live_run_says_it_is_running_instead_of_nothing_runs`(真实 reducer:只有 `LogAppended(running=true)`,断言副标题不是 Idle 文案且含 `Running now`)。 新增 demo 状态 `ULL_LOGS_FIXTURE=running` 用于出图。 **变异实测**(每条:回退 → 红 → 放回 → 绿):见报告。 验证:`fmt --check` 通过;`clippy -p unlucky-app-iced -p unlucky-i18n --all-targets -D warnings` 干净; `cargo test` **280 + 6 + 23 passed / 0 failed**;行数最大 `pages/mod.rs` 367、`app/view.rs` 324、 `app/view/logs.rs` 337。
This commit is contained in:
1 parent
5ed2bf3a7d
commit
0eb069fa37
9 files changed
+414
-267
No files matched your search
@@ -19,16 +19,28 @@
|
||||
//! | 概念 | 取值 |
|
||||
//! |---|---|
|
||||
//! | **身份**(这一场是谁) | `live_session`(正在跑)→ `selected_session`(打开/刚结束的),**不从 `diagnosis` 反推** |
|
||||
//! | 诊断结论 | 只有 `diagnosis.session == 身份` 才画;身份为空时才信任报告(那时日志行只可能来自 `open_session`,它和报告是同一次调用发的) |
|
||||
//! | 诊断结论 | 见下表的三个分支(r4 修正了 r3 的一条**回归**:r3 只认身份,把「导入崩溃文件」与「打开会话」两条路径的诊断一并关掉了) |
|
||||
//! | 实例/时间/结局(给页头描述) | 身份优先;身份为空才退到报告的 session |
|
||||
//!
|
||||
//! 归属判据(`report.session` 与身份的关系):
|
||||
//!
|
||||
//! | `live_session` | `selected_session` | 报告算数的条件 |
|
||||
//! |---|---|---|
|
||||
//! | `Some(live)` | 任意 | `report.session == live` —— 只有它在线时日志行是它自己在追加,别人的报告挂上来就是串场 |
|
||||
//! | `None` | `Some(sel)` | `report.session == sel` **或** 报告不属于任何已知会话(`ImportCrashLog` 用 `file:<文件名>` 当会话 id,永远不在索引里) |
|
||||
//! | `None` | `None` | 全部算数 —— 此时日志行只可能来自 `open_session` 或导入,两者都与报告同一次调用发出 |
|
||||
//!
|
||||
//! 已知残留(今天 iced 侧不可达,登记在报告里):`open_session(A)` 在 `selected_session` 仍停在上
|
||||
//! 一场 B 时会被第三行判据挡掉(A 是已知会话且 ≠ B)——真正要修得让 presenter 在
|
||||
//! `LogLinesChanged` 时把「日志行属于哪一场」记下来(`AppState` 今天没有这个字段)。
|
||||
//!
|
||||
//! 「没有命中规则」的**占位报告**也不画:host 的 `open_session` 在规则引擎无结论时会发一份
|
||||
//! `rule: ""` / `confidence: 0` / `title: "该会话没有命中已知规则"` 的假报告(`CoreEvent::DiagnosisReady`
|
||||
//! 是非 Option,host 无法表达「没有结论」)——它是标记不是结论,画出来等于给用户看一条「严重度提示」。
|
||||
//!
|
||||
//! 已知缺口(已报 finding,不在本 mission 修):`clear_logs` 之后 `selected_session` 不清,
|
||||
//! 身份会继续指向那一场;导入崩溃文件(`session: "file:…"`)没有对应的日志行载入,所以那份
|
||||
//! 报告在本页永远不会显示(r2 会把它挂到不相干的日志行上——两害相权取不显示)。
|
||||
//! 身份会继续指向那一场;导入崩溃文件只发 `DiagnosisReady`(不带日志行),所以页面上会出现
|
||||
//! 「结论 + 上一场的日志正文」——行由 `open_session` 负责,那是 presenter 的缺口。
|
||||
|
||||
use unlucky_presenter::dto::{Diagnosis, LogLevel, LogOutcome};
|
||||
use unlucky_presenter::state::AppState;
|
||||
@@ -38,16 +50,34 @@ use crate::pages;
|
||||
/// 日志页视图模型:presenter 快照 → 要画的东西。
|
||||
///
|
||||
/// 两处 DTO 侧的**信息丢失**(已报 finding):`dto::Diagnosis` 没有 core 的 `severity`
|
||||
/// (严重度只能按置信度折算,阈值是债)与 `cause.key`(`title`/`hint` 只能当数据画)。
|
||||
/// (所以页面按置信度档分组,不冒称严重度)与 `cause.key`(`title`/`hint` 只能当数据画)。
|
||||
pub(super) fn logs_model(state: &AppState) -> pages::Logs {
|
||||
let identity = state
|
||||
.live_session
|
||||
.as_deref()
|
||||
.or(state.selected_session.as_deref());
|
||||
let live = state.live_session.as_deref();
|
||||
let selected = state.selected_session.as_deref();
|
||||
let identity = live.or(selected);
|
||||
|
||||
let report = state.diagnosis.as_ref().filter(|report| match identity {
|
||||
Some(id) => report.session == id,
|
||||
None => true,
|
||||
// 归属判据(r4 修正 r3 的**回归**:r3 的「有选中就只认选中」把导入/打开这两条路径的诊断
|
||||
// 一起关掉了)。三条命中任一即认:
|
||||
//
|
||||
// 1. 报告属于**正在跑**的那一场 —— 这一条必须单独成立:只有它在线时日志行是它自己在追加,
|
||||
// 别的会话的报告挂上来就是串场(r3 修的正是这个);
|
||||
// 2. 报告属于**当前选中**的那一场(没有 live 会话时);
|
||||
// 3. 报告的会话**不在会话索引里** —— `ImportCrashLog` 用 `file:<文件名>` 当会话 id,
|
||||
// `open_session` 也不一定把 id 写进 `selected_session`;这两条路径与报告是同一次调用发出的,
|
||||
// 把它们一并关掉就是把「导入崩溃日志」这条功能画成空(r3 的实测回归)。
|
||||
let known = |id: &str| state.log_sessions.iter().any(|session| session.id == id);
|
||||
let report = state.diagnosis.as_ref().filter(|report| {
|
||||
let id = report.session.as_str();
|
||||
match (live, selected) {
|
||||
// 有会话在跑:日志行是它自己在追加,别的会话的报告挂上来就是串场(r3 修的 P1)。
|
||||
(Some(live), _) => id == live,
|
||||
// 没有在跑、但有选中项:报告要么是它的,要么是「会话索引里没有的那一份」
|
||||
// (`ImportCrashLog` 用 `file:<文件名>`;`open_session` 不写 `selected_session`,
|
||||
// 所以只能靠「不在索引里」认它)。
|
||||
(None, Some(selected)) => id == selected || !known(id),
|
||||
// 什么身份都没有:日志行只可能来自 `open_session` 或导入,两者与报告同一次发出。
|
||||
(None, None) => true,
|
||||
}
|
||||
});
|
||||
|
||||
// 身份为空时,描述的来源只能退到报告(见模块文档的表格)。
|
||||
@@ -59,6 +89,9 @@ pub(super) fn logs_model(state: &AppState) -> pages::Logs {
|
||||
instance: session.map(|session| session.instance.clone()),
|
||||
when: session.map(|session| session.when.clone()),
|
||||
outcome: session.map(|session| log_outcome(session.outcome)),
|
||||
// 运行中的会话不在 `log_sessions` 里(索引只在结束时追加),所以页头要靠这个标记
|
||||
// 走「正在运行」那条文案,而不是「当前没有运行中的实例」。
|
||||
live: live.is_some(),
|
||||
lines: state
|
||||
.log_lines
|
||||
.iter()
|
||||
@@ -95,11 +128,11 @@ fn log_outcome(outcome: LogOutcome) -> pages::Outcome {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一条诊断结论 → 页面结论(严重度折算见 [`logs_model`] 的文档)。
|
||||
/// 一条诊断结论 → 页面结论(置信度档见 [`pages::Confidence`])。
|
||||
fn diagnosis_of(diagnosis: &Diagnosis) -> pages::Finding {
|
||||
pages::Finding {
|
||||
rule: diagnosis.rule.clone(),
|
||||
severity: pages::Severity::of_confidence(diagnosis.confidence),
|
||||
confidence_band: pages::Confidence::of(diagnosis.confidence),
|
||||
title: diagnosis.title.clone(),
|
||||
hint: diagnosis.hint.clone(),
|
||||
confidence: diagnosis.confidence,
|
||||
@@ -108,177 +141,4 @@ fn diagnosis_of(diagnosis: &Diagnosis) -> pages::Finding {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::dto::{
|
||||
CoreEvent, DiagnosisReport, LaunchFinished, LiveLogBatch, LogLevel, LogLine, LogOutcome,
|
||||
LogSession,
|
||||
};
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
use crate::pages;
|
||||
|
||||
use super::logs_model;
|
||||
|
||||
/// 崩溃的那一场。
|
||||
const CRASH: &str = "s-1029-1840";
|
||||
/// 崩溃之后又启动的、正常跑的一场。
|
||||
const OK_RUN: &str = "s-1029-1922";
|
||||
|
||||
fn session(id: &str, when: &str, outcome: LogOutcome) -> LogSession {
|
||||
LogSession {
|
||||
id: id.to_string(),
|
||||
instance: "开发测试".to_string(),
|
||||
when: when.to_string(),
|
||||
outcome,
|
||||
duration_secs: 62,
|
||||
}
|
||||
}
|
||||
|
||||
fn line(text: &str, level: LogLevel) -> LogLine {
|
||||
LogLine {
|
||||
text: text.to_string(),
|
||||
level,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(session: &str, rule: &str) -> DiagnosisReport {
|
||||
DiagnosisReport {
|
||||
session: session.to_string(),
|
||||
diagnosis: pages_dto(rule),
|
||||
}
|
||||
}
|
||||
|
||||
fn pages_dto(rule: &str) -> unlucky_presenter::dto::Diagnosis {
|
||||
unlucky_presenter::dto::Diagnosis {
|
||||
rule: rule.to_string(),
|
||||
title: "显卡驱动过旧(OpenGL 版本不足)".to_string(),
|
||||
confidence: 92,
|
||||
hint: "升级显卡驱动".to_string(),
|
||||
fix: Some("change_graphics.driver".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// **真实数据路径**(复审要求:不许造注入源绕过去):
|
||||
/// 崩溃一场跑完 → 再启动一场正常跑。`state.diagnosis` 在这里**故意不清**,
|
||||
/// 因为 presenter 里根本没有清零点——这就是线上发生串场的条件。
|
||||
fn crashed_only() -> AppState {
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![
|
||||
session(CRASH, "10-29 18:40", LogOutcome::Crashed),
|
||||
session(OK_RUN, "10-29 19:22", LogOutcome::Ok),
|
||||
]));
|
||||
|
||||
// 第一场跑起来(live_session = CRASH)→ 日志 → 规则结论 → 收尾(selected_session = CRASH)
|
||||
state.apply_core(CoreEvent::LogAppended(LiveLogBatch {
|
||||
session: CRASH.to_string(),
|
||||
lines: vec![line("[18:40] boom: GLFW error", LogLevel::Error)],
|
||||
running: true,
|
||||
}));
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old")));
|
||||
state.apply_core(CoreEvent::LaunchFinished(LaunchFinished {
|
||||
id: "dev".to_string(),
|
||||
session: CRASH.to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
}));
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// 崩溃之后又启动一场正常跑:`live_session` 前进,`diagnosis` 仍是上一场的。
|
||||
fn after_a_new_run() -> AppState {
|
||||
let mut state = crashed_only();
|
||||
state.apply_core(CoreEvent::LogAppended(LiveLogBatch {
|
||||
session: OK_RUN.to_string(),
|
||||
lines: vec![line("[19:22] launching 1.20.4", LogLevel::Info)],
|
||||
running: true,
|
||||
}));
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_run_does_not_inherit_the_previous_crash() {
|
||||
let state = after_a_new_run();
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
|
||||
// 抓得住:身份被上一场的 `diagnosis.session` 顶掉(r2 的 bug)——那时 outcome 会是
|
||||
// Crashed、findings 挂着 gpu.driver_old、摘要里 `lines=` 与结论不同源。
|
||||
// 抓不住:presenter 自己 `clear_logs` 后 `selected_session` 不清的缺口(已报 finding),
|
||||
// 以及多场并行会话(本仓一次只跑一场)。
|
||||
assert_eq!(
|
||||
logs.outcome,
|
||||
Some(pages::Outcome::Ok),
|
||||
"身份应该是正在跑的这一场"
|
||||
);
|
||||
|
||||
let status = logs.status.clone().expect("有会话身份就该有副标题");
|
||||
assert!(status.contains("Finished normally"), "{status}");
|
||||
assert!(
|
||||
!status.contains("The game crashed"),
|
||||
"页头把新会话标成了上一场的崩溃:{status}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
logs.findings.is_empty(),
|
||||
"上一场的结论不能挂到这一场:{:?}",
|
||||
logs.findings
|
||||
);
|
||||
|
||||
// 摘要必须与身份同源:outcome 是 ok、不含旧规则、行数就是当前这些行。
|
||||
let summary = logs.summary();
|
||||
assert!(summary.contains("outcome=ok"), "{summary}");
|
||||
assert!(!summary.contains("gpu.driver_old"), "{summary}");
|
||||
assert!(
|
||||
summary.contains(&format!("lines={}", logs.lines.len())),
|
||||
"{summary}"
|
||||
);
|
||||
assert!(!summary.contains("findings="), "{summary}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_crash_itself_is_still_reported() {
|
||||
// 上一条测试的**对照**:崩溃当场必须画结论,否则那条断言「findings 为空」是空的。
|
||||
// 抓得住:把判别写成「永远不画诊断」(用空断言骗过 P2)。
|
||||
// 抓不住:结论排序/去重(那是 core 的规则引擎的事)。
|
||||
let state = crashed_only();
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
|
||||
assert_eq!(logs.outcome, Some(pages::Outcome::Crashed));
|
||||
assert_eq!(logs.findings.len(), 1, "{:?}", logs.findings);
|
||||
assert_eq!(logs.findings[0].rule, "gpu.driver_old");
|
||||
let status = logs.status.clone().unwrap_or_default();
|
||||
assert!(status.contains("The game crashed"), "{status}");
|
||||
assert!(logs.summary().contains("gpu.driver_old:critical(92)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hosts_no_rule_matched_placeholder_is_not_a_finding() {
|
||||
// host 的 `open_session` 在规则引擎无结论时发一份 `rule: ""` 的假报告(非 Option 逼的)。
|
||||
// 抓得住:把「没命中」画成一条严重度结论(用户会去修一个不存在的毛病)。
|
||||
// 抓不住:host 该不该改发 `None`(那是 presenter 的 finding)。
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![session(
|
||||
CRASH,
|
||||
"10-29 18:40",
|
||||
LogOutcome::Crashed,
|
||||
)]));
|
||||
state.apply_core(CoreEvent::LogLinesChanged(vec![line(
|
||||
"[18:40] nothing to see",
|
||||
LogLevel::Info,
|
||||
)]));
|
||||
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "")));
|
||||
assert!(
|
||||
logs_model(&state).findings.is_empty(),
|
||||
"空 rule 是『没命中』的占位,不是结论"
|
||||
);
|
||||
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old")));
|
||||
assert_eq!(
|
||||
logs_model(&state).findings.len(),
|
||||
1,
|
||||
"换成真规则就得画出来(证明上面的过滤不是把诊断一起关了)"
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
@@ -0,0 +1,242 @@
|
||||
//! 日志页展示转换的守门测试(从 `app/view/logs.rs` 拆出:那边已经 382 行,贴着 400 硬上限)。
|
||||
//!
|
||||
//! 全部走**真实 reducer 路径**(`AppState::apply_core`),不造注入源——r3 的教训是
|
||||
//! 「测试全走注入源而真实路径零覆盖」,所以这里的夹具就是线上那串事件本身。
|
||||
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::dto::{
|
||||
CoreEvent, DiagnosisReport, LaunchFinished, LiveLogBatch, LogLevel, LogLine, LogOutcome,
|
||||
LogSession,
|
||||
};
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
use crate::pages;
|
||||
|
||||
use super::logs_model;
|
||||
|
||||
/// 崩溃的那一场。
|
||||
const CRASH: &str = "s-1029-1840";
|
||||
/// 崩溃之后又启动的、正常跑的一场。
|
||||
const OK_RUN: &str = "s-1029-1922";
|
||||
|
||||
fn session(id: &str, when: &str, outcome: LogOutcome) -> LogSession {
|
||||
LogSession {
|
||||
id: id.to_string(),
|
||||
instance: "开发测试".to_string(),
|
||||
when: when.to_string(),
|
||||
outcome,
|
||||
duration_secs: 62,
|
||||
}
|
||||
}
|
||||
|
||||
fn line(text: &str, level: LogLevel) -> LogLine {
|
||||
LogLine {
|
||||
text: text.to_string(),
|
||||
level,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(session: &str, rule: &str) -> DiagnosisReport {
|
||||
DiagnosisReport {
|
||||
session: session.to_string(),
|
||||
diagnosis: pages_dto(rule),
|
||||
}
|
||||
}
|
||||
|
||||
fn pages_dto(rule: &str) -> unlucky_presenter::dto::Diagnosis {
|
||||
unlucky_presenter::dto::Diagnosis {
|
||||
rule: rule.to_string(),
|
||||
title: "显卡驱动过旧(OpenGL 版本不足)".to_string(),
|
||||
confidence: 92,
|
||||
hint: "升级显卡驱动".to_string(),
|
||||
fix: Some("change_graphics.driver".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// **真实数据路径**(复审要求:不许造注入源绕过去):
|
||||
/// 崩溃一场跑完 → 再启动一场正常跑。`state.diagnosis` 在这里**故意不清**,
|
||||
/// 因为 presenter 里根本没有清零点——这就是线上发生串场的条件。
|
||||
fn crashed_only() -> AppState {
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![
|
||||
session(CRASH, "10-29 18:40", LogOutcome::Crashed),
|
||||
session(OK_RUN, "10-29 19:22", LogOutcome::Ok),
|
||||
]));
|
||||
|
||||
// 第一场跑起来(live_session = CRASH)→ 日志 → 规则结论 → 收尾(selected_session = CRASH)
|
||||
state.apply_core(CoreEvent::LogAppended(LiveLogBatch {
|
||||
session: CRASH.to_string(),
|
||||
lines: vec![line("[18:40] boom: GLFW error", LogLevel::Error)],
|
||||
running: true,
|
||||
}));
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old")));
|
||||
state.apply_core(CoreEvent::LaunchFinished(LaunchFinished {
|
||||
id: "dev".to_string(),
|
||||
session: CRASH.to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
}));
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// 崩溃之后又启动一场正常跑:`live_session` 前进,`diagnosis` 仍是上一场的。
|
||||
fn after_a_new_run() -> AppState {
|
||||
let mut state = crashed_only();
|
||||
state.apply_core(CoreEvent::LogAppended(LiveLogBatch {
|
||||
session: OK_RUN.to_string(),
|
||||
lines: vec![line("[19:22] launching 1.20.4", LogLevel::Info)],
|
||||
running: true,
|
||||
}));
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_run_does_not_inherit_the_previous_crash() {
|
||||
let state = after_a_new_run();
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
|
||||
// 抓得住:身份被上一场的 `diagnosis.session` 顶掉(r2 的 bug)——那时 outcome 会是
|
||||
// Crashed、findings 挂着 gpu.driver_old、摘要里 `lines=` 与结论不同源。
|
||||
// 抓不住:presenter 自己 `clear_logs` 后 `selected_session` 不清的缺口(已报 finding),
|
||||
// 以及多场并行会话(本仓一次只跑一场)。
|
||||
assert_eq!(
|
||||
logs.outcome,
|
||||
Some(pages::Outcome::Ok),
|
||||
"身份应该是正在跑的这一场"
|
||||
);
|
||||
|
||||
let status = logs.status.clone().expect("有会话身份就该有副标题");
|
||||
assert!(status.contains("Finished normally"), "{status}");
|
||||
assert!(
|
||||
!status.contains("The game crashed"),
|
||||
"页头把新会话标成了上一场的崩溃:{status}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
logs.findings.is_empty(),
|
||||
"上一场的结论不能挂到这一场:{:?}",
|
||||
logs.findings
|
||||
);
|
||||
|
||||
// 摘要必须与身份同源:outcome 是 ok、不含旧规则、行数就是当前这些行。
|
||||
let summary = logs.summary();
|
||||
assert!(summary.contains("outcome=ok"), "{summary}");
|
||||
assert!(!summary.contains("gpu.driver_old"), "{summary}");
|
||||
assert!(
|
||||
summary.contains(&format!("lines={}", logs.lines.len())),
|
||||
"{summary}"
|
||||
);
|
||||
assert!(!summary.contains("findings="), "{summary}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_crash_itself_is_still_reported() {
|
||||
// 上一条测试的**对照**:崩溃当场必须画结论,否则那条断言「findings 为空」是空的。
|
||||
// 抓得住:把判别写成「永远不画诊断」(用空断言骗过 P2)。
|
||||
// 抓不住:结论排序/去重(那是 core 的规则引擎的事)。
|
||||
let state = crashed_only();
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
|
||||
assert_eq!(logs.outcome, Some(pages::Outcome::Crashed));
|
||||
assert_eq!(logs.findings.len(), 1, "{:?}", logs.findings);
|
||||
assert_eq!(logs.findings[0].rule, "gpu.driver_old");
|
||||
let status = logs.status.clone().unwrap_or_default();
|
||||
assert!(status.contains("The game crashed"), "{status}");
|
||||
assert!(logs.summary().contains("gpu.driver_old:high(92)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_imported_crash_file_still_shows_its_findings() {
|
||||
// r3 的**回归**:r3 把归属判据写成「有选中就只认选中」,于是导入崩溃日志
|
||||
// (`ImportCrashLog` → `DiagnosisReady` 带 `file:<文件名>` 的会话 id,且**不发**
|
||||
// `LogLinesChanged`)被整条丢掉。今天 iced 侧还没有 dispatch 站点(那些还在遗留的
|
||||
// Slint shell 里),所以复审没判 p1;这条测试是为了**将来接线的人**不再踩。
|
||||
// 抓得住:只有「属于当前选中/live 会话」才画的窄判据(导入的 `file:` 会话不等于任何
|
||||
// 索引里的会话,会被误杀)。
|
||||
// 抓不住:导入后日志正文该显示什么(`ImportCrashLog` 不载入日志行,那是 presenter 的事)。
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![session(
|
||||
CRASH,
|
||||
"10-29 18:40",
|
||||
LogOutcome::Crashed,
|
||||
)]));
|
||||
// 上一场刚结束 ⇒ selected_session = CRASH(过期选中项),随后用户导入一份崩溃文件
|
||||
state.apply_core(CoreEvent::LaunchFinished(LaunchFinished {
|
||||
id: "dev".to_string(),
|
||||
session: CRASH.to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
}));
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(
|
||||
"file:crash-2026-11-02_19.40.15-client.txt",
|
||||
"integrity.unsatisfied_link",
|
||||
)));
|
||||
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
assert_eq!(
|
||||
logs.findings.len(),
|
||||
1,
|
||||
"导入的崩溃日志必须画出它的结论:{:?}",
|
||||
logs.findings
|
||||
);
|
||||
assert_eq!(logs.findings[0].rule, "integrity.unsatisfied_link");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_live_run_says_it_is_running_instead_of_nothing_runs() {
|
||||
// 视觉复审 r3 的 P2-3:session 索引只在**结束**时追加(`host::logs::sessions`),
|
||||
// 所以运行中的那一场永远不在 `log_sessions` 里 ⇒ 拿不到实例名与结局 ⇒ r3 的页头在
|
||||
// 每次实时运行都说「当前没有运行中的实例」。demo 伪造了身份,截图上根本照不出来。
|
||||
// 抓得住:页头退回 Idle 文案(这次走真实 reducer:只有 `LogAppended(running=true)`)。
|
||||
// 抓不住:文案长度在某尺寸下会不会撞右侧动作区(那要看截图)。
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![session(
|
||||
CRASH,
|
||||
"10-29 18:40",
|
||||
LogOutcome::Crashed,
|
||||
)]));
|
||||
state.apply_core(CoreEvent::LogAppended(LiveLogBatch {
|
||||
session: OK_RUN.to_string(),
|
||||
lines: vec![line("[19:22] launching", LogLevel::Info)],
|
||||
running: true,
|
||||
}));
|
||||
|
||||
let logs = logs_model(&state).with_status(Lang::En);
|
||||
assert!(logs.live, "live_session 非空就该标记为运行中");
|
||||
let status = logs.status.clone().expect("运行中必须有副标题");
|
||||
let idle = unlucky_i18n::catalog::Catalog::of(Lang::En)
|
||||
.text(unlucky_i18n::catalog::Key::LogsSubtitleIdle);
|
||||
assert_ne!(status, idle, "运行中的页头不能说「没有运行」");
|
||||
assert!(status.contains("Running now"), "{status}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hosts_no_rule_matched_placeholder_is_not_a_finding() {
|
||||
// host 的 `open_session` 在规则引擎无结论时发一份 `rule: ""` 的假报告(非 Option 逼的)。
|
||||
// 抓得住:把「没命中」画成一条严重度结论(用户会去修一个不存在的毛病)。
|
||||
// 抓不住:host 该不该改发 `None`(那是 presenter 的 finding)。
|
||||
let mut state = AppState::new();
|
||||
state.apply_core(CoreEvent::LogSessionsChanged(vec![session(
|
||||
CRASH,
|
||||
"10-29 18:40",
|
||||
LogOutcome::Crashed,
|
||||
)]));
|
||||
state.apply_core(CoreEvent::LogLinesChanged(vec![line(
|
||||
"[18:40] nothing to see",
|
||||
LogLevel::Info,
|
||||
)]));
|
||||
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "")));
|
||||
assert!(
|
||||
logs_model(&state).findings.is_empty(),
|
||||
"空 rule 是『没命中』的占位,不是结论"
|
||||
);
|
||||
|
||||
state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old")));
|
||||
assert_eq!(
|
||||
logs_model(&state).findings.len(),
|
||||
1,
|
||||
"换成真规则就得画出来(证明上面的过滤不是把诊断一起关了)"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user