优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)
This commit is contained in:
@@ -72,13 +72,4 @@ impl ProjectManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新项目状态
|
||||
///
|
||||
/// TODO: 状态转换校验
|
||||
pub fn transition_status(project: &mut Project, new_status: ProjectStatus) -> anyhow::Result<()> {
|
||||
// TODO: 校验状态转换是否合法
|
||||
project.status = new_status;
|
||||
project.updated_at = chrono::Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,14 @@ pub fn is_monorepo(root: &Path) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// package.json workspaces 字段(npm/yarn)
|
||||
// package.json workspaces 字段(npm/yarn)。数组(`["packages/*"]`)或对象
|
||||
// (`{"packages":[...]}`,Yarn)均为真正的工作区;JSON 显式 null 表示「无」,
|
||||
// 不应误判为 monorepo(`.is_some()` 对 key 存在但值为 null 仍返回 true → 误报)。
|
||||
let pkg_path = root.join("package.json");
|
||||
if pkg_path.is_file() {
|
||||
if let Ok(content) = std::fs::read_to_string(&pkg_path) {
|
||||
if let Ok(pkg) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||
if pkg.get("workspaces").is_some() {
|
||||
if pkg.get("workspaces").is_some_and(|v| !v.is_null()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -540,9 +542,13 @@ fn is_pure_badge_line(line: &str) -> bool {
|
||||
let mut rest = line;
|
||||
loop {
|
||||
// 找下一个 ![ 或 <img 或 <a
|
||||
// 注:HTML 标签为纯 ASCII,用 to_ascii_lowercase 做「不区分大小写」匹配即可;
|
||||
// 若用 to_lowercase(),非 ASCII 字符(如 İ→i̇、ß→ss)在小写化时改变字节长度,
|
||||
// 返回的索引会偏离原 rest 的字节边界,后续 &rest[..pos]/&rest[pos..] 切到错位/panic。
|
||||
// to_ascii_lowercase 仅改 ASCII 字母、保持非 ASCII 字节不变 → 字节长度不变 → 索引对齐。
|
||||
let img_md = rest.find("![");
|
||||
let img_html = rest.to_lowercase().find("<img");
|
||||
let anchor_html = rest.to_lowercase().find("<a ");
|
||||
let img_html = rest.to_ascii_lowercase().find("<img");
|
||||
let anchor_html = rest.to_ascii_lowercase().find("<a ");
|
||||
let earliest = [img_md, img_html, anchor_html]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -896,4 +902,114 @@ mod tests {
|
||||
assert!(desc.contains("已截断"));
|
||||
fs::remove_dir_all(&d).ok();
|
||||
}
|
||||
|
||||
// ── normalize_path 跨平台归一化 ──
|
||||
// 锁定:① 反斜杠→正斜杠 ② 小写 ③ 尾部分隔符裁剪 ④ 相同逻辑路径相等(防重复录入绕过)。
|
||||
// 注:canonicalize 成功路径在不同 OS 返回不同形态(Windows 带前缀),故只断言跨平台不变性,
|
||||
// 不断言精确串,避免与平台耦合。
|
||||
#[test]
|
||||
fn normalize_path_uses_forward_slash_and_lowercase() {
|
||||
// 不存在的路径走降级分支(trim + replace + lowercase)
|
||||
let n = normalize_path(r"C:\Foo\Bar\");
|
||||
assert!(
|
||||
!n.contains('\\'),
|
||||
"反斜杠未归一: {n}"
|
||||
);
|
||||
assert_eq!(n, n.to_lowercase(), "未小写: {n}");
|
||||
assert!(
|
||||
!n.ends_with('/') && !n.ends_with('\\'),
|
||||
"尾部分隔符未裁剪: {n}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_equivalent_inputs_equal() {
|
||||
// 两种写法应归一为同一串(去重场景)
|
||||
let a = normalize_path(r"C:\Foo\Bar");
|
||||
let b = normalize_path("C:/Foo/Bar/");
|
||||
assert_eq!(a, b, "等价路径归一不等: {a} vs {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_relative_is_lowercase_and_forward_slash() {
|
||||
let n = normalize_path(r"..\Some\Path");
|
||||
assert!(!n.contains('\\'), "反斜杠未归一: {n}");
|
||||
assert_eq!(n, n.to_lowercase(), "未小写: {n}");
|
||||
}
|
||||
|
||||
// ── is_monorepo workspaces:null 防回归(wd2fnjh3s) ──
|
||||
// .is_some_and(!is_null) 修复点:key 存在但值为 JSON null 时不得判为 monorepo。
|
||||
#[test]
|
||||
fn is_monorepo_workspaces_null_not_misclassified() {
|
||||
let d = scratch("ws-null");
|
||||
fs::write(
|
||||
d.join("package.json"),
|
||||
r#"{"name":"x","workspaces":null}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!is_monorepo(&d),
|
||||
"workspaces: null 不应判为 monorepo(回归 wd2fnjh3s)"
|
||||
);
|
||||
fs::remove_dir_all(&d).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_monorepo_workspaces_object_treated_as_real() {
|
||||
// Yarn 形式 {"packages":[...]} —— 非 null,应判为 monorepo
|
||||
let d = scratch("ws-obj");
|
||||
fs::write(
|
||||
d.join("package.json"),
|
||||
r#"{"name":"y","workspaces":{"packages":["packages/*"]}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(is_monorepo(&d), "Yarn workspaces 对象形式应判为 monorepo");
|
||||
fs::remove_dir_all(&d).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_monorepo_non_dir_returns_false() {
|
||||
assert!(!is_monorepo(Path::new("definitely-not-exist-xyz-456")));
|
||||
}
|
||||
|
||||
// ── collect_images 徽章过滤逻辑补强 ──
|
||||
#[test]
|
||||
fn collect_images_filters_badge_by_keyword() {
|
||||
// 非徽章域名,但 alt/src 含关键词(build/license/coverage)应过滤
|
||||
let content = "\n\n\n\n";
|
||||
let imgs = collect_images(content);
|
||||
assert!(imgs.iter().all(|i| !i.alt.contains("build")), "build 关键词徽章未过滤");
|
||||
assert!(imgs.iter().all(|i| !i.alt.contains("license")), "license 关键词徽章未过滤");
|
||||
assert!(imgs.iter().all(|i| !i.alt.contains("coverage")), "coverage 关键词徽章未过滤");
|
||||
assert_eq!(imgs.len(), 1, "应仅保留架构图: {imgs:?}");
|
||||
assert_eq!(imgs[0].src, "./arch.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_images_dedups_same_src() {
|
||||
// 同 src 不同 alt 去重,只留首个
|
||||
let content = "\n\n";
|
||||
let imgs = collect_images(content);
|
||||
assert_eq!(imgs.len(), 1, "同 src 应去重: {imgs:?}");
|
||||
assert_eq!(imgs[0].alt, "first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_images_handles_multiple_in_one_line() {
|
||||
// 同行多图 + 徽章混排:徽章过滤、内容图保留
|
||||
let content = "  \n";
|
||||
let imgs = collect_images(content);
|
||||
let srcs: Vec<_> = imgs.iter().map(|i| i.src.as_str()).collect();
|
||||
assert!(srcs.contains(&"./shot.png"), "同行内容图丢失: {srcs:?}");
|
||||
assert!(srcs.contains(&"./two.png"), "同行内容图丢失: {srcs:?}");
|
||||
assert!(!srcs.contains(&"https://img.shields.io/x"), "同行徽章未过滤: {srcs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_images_empty_and_malformed() {
|
||||
// 无图片 / 不闭合语法不 panic,返回空
|
||||
assert!(collect_images("plain text no images").is_empty());
|
||||
assert!(collect_images(".is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user