#![cfg(test)]
use std::path::Path;
use code_repo_wiki::config::schema::{
LlmProviderType, LlmSection, WikiConfig, WikiGuideSection, WikiSection,
};
static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
fn build_fixture_repo(repo: &Path, guide: WikiGuideSection) -> anyhow::Result<()> {
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,
},
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
}
fn setup(guide: WikiGuideSection) -> (std::path::PathBuf, code_repo_wiki::project::ProjectRoot, std::path::PathBuf) {
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let repo = std::env::temp_dir().join(format!("code_repo_wiki_guide_{}_{}", std::process::id(), seq));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("创建临时仓库失败");
build_fixture_repo(&repo, guide).expect("构造测试仓库失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
(repo, root, config_path)
}
#[test]
fn test_guide_pages_filters_unmatched_modules() {
let guide = WikiGuideSection {
pages: vec!["src/a".into()],
priority: vec![],
notes: vec![],
};
let (repo, root, config_path) = setup(guide);
let result = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.expect("全量生成失败");
let pages = list_wiki_pages(&repo);
assert!(
pages.iter().any(|p| p == "src_a.md"),
"白名单内模块 src/a 应生成独立页, 实际: {:?}",
pages
);
assert!(
!pages.iter().any(|p| p == "src_b.md"),
"白名单外模块 src/b 不应生成独立页, 实际: {:?}",
pages
);
assert!(
pages.iter().any(|p| p == "overview.md"),
"overview.md 应保留(全量汇总), 实际: {:?}",
pages
);
assert!(
result.documents.iter().all(|d| d.module_path != vec!["src".to_string(), "b".to_string()]),
"生成结果不应包含 src/b 模块文档"
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_guide_pages_empty_match_errors() {
let guide = WikiGuideSection {
pages: vec!["nonexistent/module".into()],
priority: vec![],
notes: vec![],
};
let (repo, root, config_path) = setup(guide);
let err = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.err()
.expect("空匹配应显式报错(结果应为 Err)");
assert!(
err.to_string().contains("未匹配任何模块"),
"报错应说明未匹配模块, 实际: {}",
err
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_guide_priority_orders_pages() {
let guide = WikiGuideSection {
pages: vec!["src/a".into(), "src/b".into()],
priority: vec!["src/b".into(), "src/a".into()],
notes: vec![],
};
let (repo, root, config_path) = setup(guide);
let result = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.expect("全量生成失败");
let module_pages: Vec<Vec<String>> = result
.documents
.iter()
.filter(|d| d.kind == code_repo_wiki::model::DocumentKind::WikiPage)
.map(|d| d.module_path.clone())
.collect();
let pos_b = module_pages
.iter()
.position(|p| p == &vec!["src".to_string(), "b".to_string()])
.expect("src/b 模块页应存在");
let pos_a = module_pages
.iter()
.position(|p| p == &vec!["src".to_string(), "a".to_string()])
.expect("src/a 模块页应存在");
assert!(
pos_b < pos_a,
"priority 指定 src/b 在前, 实际顺序: {:?}",
module_pages
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_guide_pages_keeps_existing_unmatched_pages() {
let (repo, root, config_path) = setup(WikiGuideSection::default());
code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.expect("首轮全量生成失败");
assert!(
list_wiki_pages(&repo).iter().any(|p| p == "src_b.md"),
"首轮应生成 src_b.md"
);
let config2 = WikiConfig {
output_dir: Some((repo.join(".code-repo-wiki").to_string_lossy().into_owned()).into()),
wiki: WikiSection {
language: "zh".into(),
guide: WikiGuideSection {
pages: vec!["src/a".into()],
priority: vec![],
notes: vec![],
},
},
llm: LlmSection {
provider: LlmProviderType::Mock,
..Default::default()
},
..Default::default()
};
std::fs::write(repo.join("config2.toml"), toml::to_string_pretty(&config2).expect("序列化 config2 失败"))
.expect("写 config2 失败");
code_repo_wiki::run_pipeline(
Some(&repo.join("config2.toml")),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.expect("二轮全量生成失败");
let pages = list_wiki_pages(&repo);
assert!(
pages.iter().any(|p| p == "src_b.md"),
"白名单外既有页面 src_b.md 应保留(清理语义=模块消失才删), 实际: {:?}",
pages
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_guide_incremental_skips_unmatched_without_error() {
let guide = WikiGuideSection {
pages: vec!["src/a".into()],
priority: vec![],
notes: vec![],
};
let (repo, root, config_path) = setup(guide);
code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Full,
)
.expect("全量生成失败");
std::fs::write(
repo.join("src").join("b").join("mod.rs"),
r#"
//! 模块 B(已修改)
pub fn beta() -> &'static str { "beta2" }
"#,
)
.expect("修改 b/mod.rs 失败");
let inc = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Incremental {
watch_paths: vec![repo.join("src").join("b").join("mod.rs")],
change_kind: None,
},
)
.expect("增量更新不应因白名单外变更报错");
assert!(
inc.documents.iter().all(|d| d.module_path != vec!["src".to_string(), "b".to_string()]),
"增量不应生成白名单外模块 src/b 的文档"
);
let _ = std::fs::remove_dir_all(&repo);
}