#![cfg(test)]
use std::path::Path;
use code_repo_wiki::config::schema::{LlmProviderType, LlmSection, WikiConfig};
use code_repo_wiki::config::schema::{WikiSection};
fn build_fixture_repo(repo: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(repo.join("src"))?;
std::fs::create_dir_all(repo.join("src").join("a"))?;
std::fs::create_dir_all(repo.join("src").join("b"))?;
std::fs::write(
repo.join("src").join("a").join("mod.rs"),
r#"
//! 模块 A
pub struct Alpha;
impl Alpha {
pub fn run(&self) -> u32 { 42 }
}
"#,
)?;
std::fs::write(
repo.join("src").join("b").join("mod.rs"),
r#"
//! 模块 B
pub fn beta() -> &'static str { "beta" }
"#,
)?;
let config = WikiConfig {
output_dir: Some((repo.join(".code-repo-wiki").to_string_lossy().into_owned()).into()),
wiki: WikiSection {
language: "zh".into(),
guide: Default::default(),
},
llm: LlmSection {
provider: LlmProviderType::Mock,
..Default::default()
},
..Default::default()
};
std::fs::write(
repo.join("config.toml"),
toml::to_string_pretty(&config)?,
)?;
Ok(())
}
fn list_wiki_pages(repo: &Path) -> Vec<String> {
let dir = repo.join(".code-repo-wiki").join("wiki").join("zh");
let mut names: Vec<String> = std::fs::read_dir(&dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false))
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
names.sort();
names
}
#[test]
fn test_e2e_full_pipeline() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_e2e_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("创建临时仓库失败");
build_fixture_repo(&repo).expect("构造测试仓库失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
let result = code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full).expect("全量生成失败");
assert!(result.stats.files_scanned >= 2, "应扫描到至少 2 个文件");
assert!(result.stats.total_entities >= 2, "应解析出至少 2 个实体");
assert!(!result.documents.is_empty(), "应生成文档");
let wiki_dir = repo.join(".code-repo-wiki").join("wiki").join("zh");
let cards_dir = repo.join(".code-repo-wiki").join("cards").join("zh");
let assets_dir = repo.join(".code-repo-wiki").join("assets").join("diagrams");
for p in [&wiki_dir, &cards_dir, &assets_dir] {
assert!(p.exists(), "产物目录应存在: {}", p.display());
}
assert!(
wiki_dir.join("api.md").exists(),
"api.md 应落盘(主语言 zh)"
);
assert!(
wiki_dir.join("overview.md").exists(),
"overview.md 应落盘(独立生成)"
);
assert!(
wiki_dir.join("architecture.md").exists(),
"architecture.md 应落盘(架构概览)"
);
let pages_after_generate = list_wiki_pages(&repo);
assert!(
pages_after_generate.len() >= 2,
"应至少生成 2 个模块页, 实际: {:?}",
pages_after_generate
);
std::fs::write(
repo.join("src").join("a").join("mod.rs"),
r#"
//! 模块 A(已修改)
pub struct Alpha;
impl Alpha {
pub fn run(&self) -> u32 { 100 }
pub fn extra(&self) -> u32 { 1 }
}
"#,
)
.expect("修改 a/mod.rs 失败");
let inc = code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Incremental { watch_paths: vec![], change_kind: None })
.expect("增量更新失败");
assert!(!inc.documents.is_empty(), "增量更新应重新生成文档");
let pages_after_update = list_wiki_pages(&repo);
assert_eq!(
pages_after_update, pages_after_generate,
"增量更新不应增删页面(只重写内容)"
);
std::fs::remove_file(repo.join("src").join("a").join("mod.rs")).expect("删除 a/mod.rs 失败");
let deleted_path = repo.join("src").join("a").join("mod.rs");
let del = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Incremental {
watch_paths: vec![deleted_path],
change_kind: Some(code_repo_wiki::incremental::watch::ChangeKind::Deleted),
},
)
.expect("删除增量更新失败");
let src_page = repo.join(".code-repo-wiki").join("wiki").join("zh").join("src.md");
let src_content = std::fs::read_to_string(&src_page).unwrap_or_default();
assert!(
!src_content.contains("Alpha"),
"删除后模块页不应包含模块 A 实体(Alpha), 实际: {:?}",
src_content
);
assert!(
del.documents.iter().all(|d| d.module_path.first() != Some(&"a".to_string())),
"删除路径不应再生成模块 A 的文档"
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_e2e_delete_only_module_keeps_other_modules() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_e2e_delonly_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("创建临时仓库失败");
build_fixture_repo(&repo).expect("构造测试仓库失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full)
.expect("全量生成失败");
let before = list_wiki_pages(&repo);
let b_page = repo.join(".code-repo-wiki").join("wiki").join("zh").join("src_b.md");
assert!(b_page.exists(), "模块 b 页面应存在: {:?}", before);
let a_file = repo.join("src").join("a").join("mod.rs");
std::fs::remove_file(&a_file).expect("删除 a/mod.rs 失败");
code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Incremental {
watch_paths: vec![a_file],
change_kind: Some(code_repo_wiki::incremental::watch::ChangeKind::Deleted),
},
)
.expect("删除增量更新失败");
assert!(
b_page.exists(),
"模块 b 页面不应被删除(仅删除模块 a 的唯一文件)"
);
let after = list_wiki_pages(&repo);
assert!(
after.iter().any(|n| n == "src_b.md"),
"删除后模块 b 页面应仍在: {:?}",
after
);
let _ = std::fs::remove_dir_all(&repo);
}