use std::path::Path;
use code_repo_wiki::config::schema::WikiConfig;
use code_repo_wiki::ingest::parser::ParserRegistry;
use code_repo_wiki::ingest::scanner::Scanner;
use code_repo_wiki::search::text::TextEngine;
fn fixture_src() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("sample-repo")
.join("src")
}
fn scan_fixture() -> Vec<code_repo_wiki::ingest::parser::FileInsight> {
let root = fixture_src();
let scanner = Scanner::new(&root);
let files = scanner.scan().expect("扫描 fixture 目录失败");
let registry = ParserRegistry::new();
let mut insights = Vec::new();
for file in &files {
if let Some(processor) = registry.get_for_file(file)
&& let Ok(source) = std::fs::read_to_string(file)
&& let Ok(insight) = processor.parse(&source, file)
{
insights.push(insight);
}
}
insights
}
#[test]
fn test_scan_and_parse_fixture() {
let insights = scan_fixture();
assert_eq!(insights.len(), 3, "应扫描到 3 个 Rust 文件");
for insight in &insights {
assert!(
!insight.entities.is_empty(),
"文件 {} 应包含至少一个实体",
insight.path.display()
);
}
let all_names: Vec<&str> = insights
.iter()
.flat_map(|i| i.entities.iter().map(|e| e.name.as_str()))
.collect();
assert!(all_names.contains(&"authenticate"), "应包含 authenticate 函数");
assert!(all_names.contains(&"User"), "应包含 User 结构体");
assert!(all_names.contains(&"SessionStore"), "应包含 SessionStore 结构体");
assert!(all_names.contains(&"save_session"), "应包含 save_session 函数");
}
#[test]
fn test_build_graph_fixture() {
let insights = scan_fixture();
let graph = code_repo_wiki::analysis::build_graph(&insights).expect("构建图失败");
assert!(
graph.graph.node_count() >= 10,
"图应至少包含 10 个节点(3 文件 + 7+ 实体),实际: {}",
graph.graph.node_count()
);
assert!(
graph.graph.edge_count() >= 3,
"图应至少包含 3 条边,实际: {}",
graph.graph.edge_count()
);
}
#[test]
fn test_search_index_build_and_query() {
let insights = scan_fixture();
let graph = code_repo_wiki::analysis::build_graph(&insights).expect("构建图失败");
let tmp_dir = std::env::temp_dir().join(format!("code_repo_wiki_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp_dir);
std::fs::create_dir_all(&tmp_dir).expect("创建临时目录失败");
let index_path = tmp_dir.join("text_index.bin");
let items: Vec<(code_repo_wiki::model::CodeNode, String)> = graph
.graph
.node_indices()
.filter_map(|idx| {
let node = graph.graph.node_weight(idx)?;
if matches!(
node.kind,
code_repo_wiki::model::NodeKind::Project
| code_repo_wiki::model::NodeKind::Module
| code_repo_wiki::model::NodeKind::File
) {
return None;
}
let source = node
.signature
.clone()
.unwrap_or_else(|| node.name.clone());
Some((node.clone(), source))
})
.collect();
let (mut engine, _) = TextEngine::open(&index_path).expect("打开索引失败");
engine.index_batch(&items).expect("批量索引失败");
assert!(engine.doc_count() > 0, "索引应包含文档");
let results = engine.search("authenticate", 5).expect("搜索失败");
assert!(
!results.is_empty(),
"搜索 'authenticate' 应返回结果"
);
assert!(
results[0].0.name.contains("authenticate"),
"首个结果应为 authenticate"
);
let results2 = engine.search("SessionStore", 5).expect("搜索失败");
assert!(!results2.is_empty(), "搜索 'SessionStore' 应返回结果");
let _ = std::fs::remove_dir_all(&tmp_dir);
}
#[test]
fn test_incremental_remove_by_file() {
let tmp_dir = std::env::temp_dir().join(format!("code_repo_wiki_incr_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp_dir);
std::fs::create_dir_all(&tmp_dir).expect("创建临时目录失败");
let index_path = tmp_dir.join("text_index.bin");
let (mut engine, _) = TextEngine::open(&index_path).expect("打开索引失败");
let node_a = code_repo_wiki::model::CodeNode {
id: code_repo_wiki::model::NodeId::new(0),
kind: code_repo_wiki::model::NodeKind::Function,
name: "alpha_handler".into(),
file_path: Some("src/alpha.rs".into()),
line_range: Some((1, 5)),
doc_comment: None,
signature: Some("fn alpha_handler()".into()), visibility: None,
module_path: vec![],
};
let node_b = code_repo_wiki::model::CodeNode {
id: code_repo_wiki::model::NodeId::new(1),
kind: code_repo_wiki::model::NodeKind::Function,
name: "beta_processor".into(),
file_path: Some("src/beta.rs".into()),
line_range: Some((1, 3)),
doc_comment: None,
signature: Some("fn beta_processor()".into()), visibility: None,
module_path: vec![],
};
engine
.index_batch(&[(node_a, "fn alpha_handler()".into()), (node_b, "fn beta_processor()".into())])
.expect("索引失败");
assert_eq!(engine.doc_count(), 2);
let removed = engine.remove_by_file("src/alpha.rs").expect("删除失败");
assert_eq!(removed, 1, "应删除 1 条");
assert_eq!(engine.doc_count(), 1, "剩余 1 条");
let results = engine.search("alpha_handler", 5).expect("搜索失败");
assert!(results.is_empty(), "删除后搜索 alpha_handler 应无结果");
let results = engine.search("beta_processor", 5).expect("搜索失败");
assert!(!results.is_empty(), "beta_processor 应仍存在");
let _ = std::fs::remove_dir_all(&tmp_dir);
}
#[test]
fn test_config_roundtrip() {
let config = WikiConfig::default();
let toml_str = toml::to_string_pretty(&config).expect("序列化失败");
let parsed: WikiConfig = toml::from_str(&toml_str).expect("反序列化失败");
assert_eq!(parsed.llm.model, "deepseek-v4-flash");
assert_eq!(parsed.llm.api_key_env, "OPENCODEGO2_API_KEY");
assert_eq!(parsed.wiki.language, "zh");
assert_eq!(parsed.output_dir(), std::path::Path::new(".code-repo-wiki"));
assert!(!parsed.embed.model.is_empty());
}