优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)

This commit is contained in:
2026-06-18 22:57:19 +08:00
parent 0ca5d9805f
commit a2871a66e0
87 changed files with 5720 additions and 3012 deletions

View File

@@ -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(())
}
}

View File

@@ -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 = "![build status](https://example.com/build.svg)\n![my license](https://example.com/lic.svg)\n![coverage](https://example.com/cov.svg)\n![architecture](./arch.png)\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 = "![first](./dup.png)\n![second](./dup.png)\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 = "![build](https://img.shields.io/x) ![shot](./shot.png) ![alt2](./two.png)\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("![unclosed alt").is_empty());
assert!(collect_images("![alt](no-close-paren").is_empty());
}
}