Files
Unlucky-Launcher/crates/core/src/diagnose/session_tests.rs
T
AskaEth edd6402fd5 fix(core): 规则结论改在脱敏前算,会话路上 fabric.loader_0_12 能命中(M241)
会话 JSONL 是写前脱敏的,而脱敏的「≥64 无点号不透明串」是有损的:
fabric.loader_0_12 唯一的证据行(Sponge mixin 类路径 66 字符)被抹成
<redacted:opaque>,规则在会话路上永不命中。

修法不动脱敏策略、也不往盘上多写一份原文:
1. SessionWriter 在内存里留一个 2000 行的原始尾窗(不落盘);
2. 收尾时把这些**原始**事实交给新的 diagnose::session::conclude 跑规则,
   产出契约形状的 diagnosis payload —— 它落盘前仍过 Redactor::scrub_json;
3. 日志页优先读这条落盘结论,老会话才回落到「在脱敏文本上重跑规则」。

为什么不是「原文与脱敏文本并存」:会话 JSONL 里存原文会踩
core/tests/launch/pipeline_cases.rs 早已钉死的「事件流不得含凭据明文」。

顺带把 EventKind 纯搬移到 launch/session/event_kind.rs 守 400 行上限。
2026-11-11 09:19:42 +08:00

91 lines
3.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `diagnose::session` 的单测(拆文件守 400 行上限)。
use super::*;
use crate::launch::redact::Redactor;
/// `fabric.loader_0_12`(critical / priority 80)在 `rules.d/fabric.yaml` 里的唯一证据行。
const EVIDENCE: &str = "java.lang.NoClassDefFoundError: \
org/spongepowered/asm/mixin/transformer/FabricMixinTransformerProxy";
fn facts<'a>(stdout: &'a [String], stderr: &'a [String]) -> RawFacts<'a> {
RawFacts {
exit_code: Some(1),
duration_ms: 4_200,
error_lines: 2,
stdout,
stderr,
}
}
fn record_id(record: &Value) -> Option<&str> {
record
.get("candidates")
.and_then(Value::as_array)
.and_then(|items| items.first())
.and_then(|item| item.get("id"))
.and_then(Value::as_str)
}
/// **判据 ①**:原始证据行能命中 `fabric.loader_0_12`;同一行被脱敏后命不中。
///
/// 后半句不是「补充说明」,它是这个模块存在的理由——会话 JSONL 里只有脱敏文本。
#[test]
fn raw_evidence_hits_the_fabric_rule_while_scrubbed_evidence_misses_it() {
let raw = [EVIDENCE.to_string()];
let empty: [String; 0] = [];
let record = conclude(&facts(&empty, &raw)).expect("原始证据应当有结论");
assert_eq!(record_id(&record), Some("fabric.loader_0_12"), "{record}");
assert_eq!(record["source"], serde_json::json!("rules"));
assert_eq!(record["needs_more_info"], serde_json::json!(false));
let scrubbed = [Redactor::new().scrub(EVIDENCE).into_owned()];
assert!(
scrubbed[0].contains("<redacted:opaque>"),
"夹具前提:这条证据行确实会被抹:{}",
scrubbed[0]
);
let after = conclude(&facts(&empty, &scrubbed));
assert_ne!(
after.as_ref().and_then(record_id),
Some("fabric.loader_0_12"),
"脱敏后的文本不该再命中——这就是 M239 报的那个 bug"
);
}
/// 没有文本就没有结论(不写空事件)。
#[test]
fn no_text_means_no_conclusion() {
let empty: [String; 0] = [];
assert!(conclude(&facts(&empty, &empty)).is_none());
}
/// 认不出来的日志也不写事件(让读回 JSONL 的兜底路径去跑,避免两条路给出不同结论)。
#[test]
fn unrecognized_output_means_no_conclusion() {
let stdout = ["[12:00:00] [main/INFO]: Setting user".to_string()];
let empty: [String; 0] = [];
assert!(conclude(&facts(&stdout, &empty)).is_none());
}
/// 结论里带的是**已渲染**的文案与物化动作,展示侧不必再自己拼。
#[test]
fn the_record_carries_rendered_text_and_actions() {
let raw = ["Fabric loader failed".to_string(), EVIDENCE.to_string()];
let empty: [String; 0] = [];
let record = conclude(&facts(&empty, &raw)).expect("应有结论");
let candidate = &record["candidates"][0];
assert!(!candidate["message"].as_str().unwrap_or_default().is_empty());
assert_eq!(
candidate["fix_actions"][0]["kind"],
serde_json::json!("reinstall")
);
assert!(
candidate["evidence"][0]
.as_str()
.unwrap_or_default()
.contains("NoClassDefFoundError"),
"{candidate}"
);
}