fix: E2E mock 统一 + CSV 引号字段解析

E2E:
- createIpcMock 支持 overrides 参数,保持 default: throw 行为
- search-clean.spec.ts 删除 30 行内联 mock,改用 createIpcMock(overrides)

Rust:
- 新增 parse_csv_line: 支持引号包裹字段 + 双引号转义 (RFC 4180 子集)
- import_csv 改用 parse_csv_line 替代 split(',')
- 与 TS 端 parseCsvLine 逻辑对称

测试: Rust 48 → 52 (+4), Frontend 100

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 23:51:28 +08:00
parent 461ef231e4
commit bce2dc8641
3 changed files with 77 additions and 28 deletions
+66 -5
View File
@@ -64,6 +64,39 @@ fn import_json(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
Ok((sanitize_paths(data.system), sanitize_paths(data.user)))
}
/// 解析 CSV 行,支持引号包裹的字段(RFC 4180 子集)
/// 与 TS 端 src/core/import-export.ts parseCsvLine 逻辑一致
fn parse_csv_line(line: &str) -> Vec<String> {
let mut fields = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if in_quotes {
if ch == '"' {
if chars.peek() == Some(&'"') {
current.push('"');
chars.next(); // 跳过转义引号
} else {
in_quotes = false;
}
} else {
current.push(ch);
}
} else if ch == '"' {
in_quotes = true;
} else if ch == ',' {
fields.push(current);
current = String::new();
} else {
current.push(ch);
}
}
fields.push(current);
fields
}
fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
let mut sys = Vec::new();
let mut usr = Vec::new();
@@ -81,17 +114,17 @@ fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
trimmed = stripped;
}
// 跳过 header 行 "type,path"
let fields: Vec<&str> = trimmed.split(',').collect();
if fields.len() >= 2 {
let c0 = fields[0].trim().to_lowercase();
let c1 = fields[1].trim().to_lowercase();
let header_fields = parse_csv_line(trimmed);
if header_fields.len() >= 2 {
let c0 = header_fields[0].trim().to_lowercase();
let c1 = header_fields[1].trim().to_lowercase();
if c0 == "type" && c1 == "path" {
continue;
}
}
}
let fields: Vec<&str> = trimmed.split(',').collect();
let fields = parse_csv_line(trimmed);
if fields.len() >= 2 {
match fields[0].trim().to_lowercase().as_str() {
"system" | "sys" => sys.push(fields[1].trim().to_string()),
@@ -303,4 +336,32 @@ mod tests {
let result = sanitize_paths(vec![" ".into(), "C:\\ok".into()]);
assert_eq!(result, vec!["C:\\ok"]);
}
#[test]
fn parse_csv_line_basic() {
assert_eq!(parse_csv_line("a,b,c"), vec!["a", "b", "c"]);
}
#[test]
fn parse_csv_line_quoted_comma() {
assert_eq!(
parse_csv_line(r#"system,"C:\Program Files, Inc\bin""#),
vec!["system", r#"C:\Program Files, Inc\bin"#]
);
}
#[test]
fn parse_csv_line_escaped_quotes() {
assert_eq!(
parse_csv_line(r#"system,"He said ""hello""""#),
vec!["system", r#"He said "hello""#]
);
}
#[test]
fn import_csv_quoted_comma_path() {
let csv = "type,path\nsystem,\"C:\\Program Files, Inc\\bin\"\n";
let (sys, _) = import_csv(csv).unwrap();
assert_eq!(sys, vec!["C:\\Program Files, Inc\\bin"]);
}
}