重构: 前端DRY收口+后端测试类型对齐

- 新增 useToast composable: 消除 AiChat/Settings/Projects 4处 toast 重复
  统一默认3000ms(Projects原4000ms为操作类提示保留参数覆盖)
- 新增 utils/json.ts parseJsonArray: 消除 parseStack/parseTags/ModuleNode
  3处JSON字符串数组解析重复
- 新增 utils/html.ts escapeHtml: 消除 useMarkdown/FilePreview 2处重复
- ProjectDetail score-bar 内联三元改用 scoreTier(消除最后一处阈值硬编码)
- ConversationSidebar 删除 formatTime 透传包装(直接用 formatRelative)
- 清理死代码: parseTs/stringifyError/ErrorSink/_Unused 改私有或删除
  wrapNakedDiff 改私有(无外部 import)
- ModuleNode shortPath 改名 truncatedPath(与 useToolCard.shortPath 语义不同)
This commit is contained in:
2026-07-03 00:18:21 +08:00
parent 249b3b9ea8
commit 3d8b755229
22 changed files with 163 additions and 124 deletions

View File

@@ -485,6 +485,7 @@ pub(crate) async fn process_tool_calls(
#[cfg(test)]
mod tests {
use super::*;
use super::path_auth::extract_file_tool_paths;
/// grep 走单路径授权申请路径NeedsAuth非 search_files 盲拒Denied
///

View File

@@ -189,6 +189,7 @@ mod tests {
status: ProjectStatus::InProgress,
description: "AI-native dev tool".into(),
path: Some(SanitizedPath::new("E:/wk-lab/devflow")),
extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(seg.starts_with("--- 以下是用户选择的上下文参考"), "头部应隔离标注, got: {}", seg);
@@ -206,6 +207,7 @@ mod tests {
status: TaskStatus::Todo,
description: "do X".into(),
project_name: Some("devflow".into()),
extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(seg.contains("【任务】Implement X"));
@@ -234,6 +236,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
extra: vec![],
},
Augmentation::Project {
id: "p1".into(),
@@ -241,6 +244,7 @@ mod tests {
status: ProjectStatus::Planning,
description: String::new(),
path: None,
extra: vec![],
},
];
let seg = build_augmentation_segment(&augs, "zh");
@@ -258,6 +262,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "en");
assert!(seg.starts_with("--- The following is the context"), "en 头部: {}", seg);
@@ -271,6 +276,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "fr");
assert!(seg.starts_with("--- 以下是用户选择的上下文参考"), "未知 lang 应按 zh 兜底");
@@ -284,6 +290,7 @@ mod tests {
status: ProjectStatus::Planning,
description: String::new(),
path: None,
extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(!seg.contains("目录:"), "无 path 不应渲染目录行: {}", seg);

View File

@@ -637,7 +637,7 @@ pub async fn ai_conversation_export(
#[cfg(test)]
mod tests {
use super::*;
use df_ai::provider::{ChatMessage, ContentPart, MessageRole, MessageStatus, ToolCall, ToolCallFunction, ToolType};
use df_ai::provider::{ChatMessage, ContentPart, MessageRole, MessageStatus, ToolCall, ToolCallFunction};
fn base_msg() -> ChatMessage {
ChatMessage {
@@ -821,7 +821,7 @@ mod tests {
serde_json::to_string(&msg.tool_calls).unwrap()
);
assert_eq!(back.model.as_deref(), Some("m"));
assert_eq!(back.status.as_deref(), Some("compressed"));
assert_eq!(back.status.as_ref(), Some(&MessageStatus::Compressed));
assert_eq!(back.reasoning_content.as_deref(), Some("r"));
assert_eq!(back.timestamp, Some(123));
}
@@ -875,7 +875,7 @@ mod tests {
},
}]),
status: if i % 6 == 0 {
Some("compressed".into())
Some(MessageStatus::Compressed)
} else {
None
},
@@ -980,7 +980,7 @@ mod tests {
id: Some("m_summary".into()),
role: MessageRole::Assistant,
content: "压缩摘要".into(),
status: Some("compressed".into()),
status: Some(MessageStatus::Compressed),
..base_msg()
},
ChatMessage {

View File

@@ -4081,7 +4081,7 @@ mod tests {
id: "proj-svc-1".to_string(),
name: "proj-svc-1".to_string(),
description: String::new(),
status: "planning".to_string(),
status: ProjectStatus::Planning,
idea_id: None,
path: None,
stack: None,