#![cfg(test)]
use std::path::Path;
use code_repo_wiki::config::schema::{LlmProviderType, LlmSection, WikiConfig};
use code_repo_wiki::config::schema::{WikiSection};
fn build_git_repo(repo: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(repo.join("src").join("net"))?;
std::fs::create_dir_all(repo.join("src").join("http"))?;
std::fs::write(
repo.join("src").join("net").join("tcp.rs"),
"pub fn tcp_process(x: u32) -> u32 { udp_process(x) + 1 }\n",
)?;
std::fs::write(
repo.join("src").join("net").join("udp.rs"),
"pub fn udp_process(x: u32) -> u32 { x + 2 }\n",
)?;
std::fs::write(
repo.join("src").join("http").join("server.rs"),
"pub fn http_serve(x: u32) -> u32 { client_render(x) + tcp_process(x) }\n",
)?;
std::fs::write(
repo.join("src").join("http").join("client.rs"),
"pub fn client_render(x: u32) -> u32 { x + 3 }\n",
)?;
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)?)?;
let git = git2::Repository::init(repo)?;
let mut cfg = git.config()?;
cfg.set_str("user.name", "test")?;
cfg.set_str("user.email", "test@test.com")?;
Ok(())
}
fn git_commit_all(repo: &Path, message: &str) -> String {
let repo = git2::Repository::open(repo).expect("打开 git 仓库失败");
let mut index = repo.index().unwrap();
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None).unwrap();
index.write().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let sig = git2::Signature::now("test", "test@test.com").unwrap();
let commit_id = match repo.head().ok() {
Some(head) => {
let parent = head.peel_to_commit().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent]).unwrap()
}
None => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[]).unwrap(),
};
commit_id.to_string()
}
fn read_api(repo: &Path) -> String {
std::fs::read_to_string(repo.join(".code-repo-wiki").join("wiki").join("zh").join("api.md"))
.unwrap_or_default()
}
fn doc_titles(result: &code_repo_wiki::AnalysisResult) -> Vec<String> {
let mut titles: Vec<String> = result.documents.iter().map(|d| d.title.clone()).collect();
titles.sort();
titles
}
#[test]
fn test_incremental_git_diff_scenarios() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_git_e2e_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("构造临时仓库失败");
build_git_repo(&repo).expect("构造 fixture 失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
let tcp_mod = repo.join("src").join("net").join("tcp.rs");
git_commit_all(&repo, "init");
let base = code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full).expect("全量生成失败");
let base_api = read_api(&repo);
assert!(base_api.contains("tcp_process"), "基线 api.md 应含 tcp_process 签名");
let module_names: Vec<&str> = base.graph.modules.iter().map(|m| m.name.as_str()).collect();
assert!(
module_names.iter().any(|n| n.contains("net")) && module_names.iter().any(|n| n.contains("http")),
"net/http 应聚为两个独立社区,实际: {module_names:?}"
);
std::fs::write(&tcp_mod, "pub fn tcp_process(x: u32) -> u32 {\n udp_process(x) + 42\n}\n").unwrap();
git_commit_all(&repo, "change body");
let inc_a = code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Incremental { watch_paths: vec![], change_kind: None })
.expect("增量生成失败");
let titles_a = doc_titles(&inc_a);
assert!(
titles_a.iter().any(|t| t.contains("net")),
"实现级变化应重生成 net 社区文档,实际: {titles_a:?}"
);
assert!(
!titles_a.iter().any(|t| t.contains("http")),
"实现级变化不应重生成依赖方 http 社区文档,实际: {titles_a:?}"
);
assert_eq!(
read_api(&repo),
base_api,
"函数体修改不影响 api.md(签名未变)"
);
std::fs::write(&tcp_mod, "pub fn tcp_process(x: u32, y: u32) -> u32 { udp_process(x) + y }\n").unwrap();
git_commit_all(&repo, "change signature");
let inc_b = code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Incremental { watch_paths: vec![], change_kind: None })
.expect("增量生成失败");
let new_api = read_api(&repo);
assert!(
new_api.contains("tcp_process(x: u32, y: u32)") && !new_api.contains("tcp_process(x: u32)"),
"api.md 应反映新签名,实际: {new_api}"
);
let titles_b = doc_titles(&inc_b);
assert!(
titles_b.iter().any(|t| t.contains("http")),
"签名变更应传播到调用方 http 社区并重生成其文档(T2 闭环),实际: {titles_b:?}"
);
let inc_c = 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_c.documents.is_empty(),
"无变更时 documents 应为空,实际: {:?}",
doc_titles(&inc_c)
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_incremental_git_whitespace_only_change_keeps_pages() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_git_wsonly_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("构造临时仓库失败");
build_git_repo(&repo).expect("构造 fixture 失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
let tcp_mod = repo.join("src").join("net").join("tcp.rs");
git_commit_all(&repo, "init");
code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full)
.expect("全量生成失败");
let base_api = read_api(&repo);
std::fs::write(&tcp_mod, "pub fn tcp_process(x: u32) -> u32 { udp_process(x) + 42 } \n").unwrap();
git_commit_all(&repo, "whitespace only");
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("纯空白增量失败");
let titles = doc_titles(&inc);
assert!(
titles.iter().any(|t| t.contains("src::http")) && titles.iter().any(|t| t.contains("src::net")),
"纯空白变化应走快照回填(全集含 http),而非重生成(仅含 net),实际: {titles:?}"
);
assert_eq!(
read_api(&repo),
base_api,
"纯空白变化不影响 api.md(签名与行号均未变)"
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_incremental_git_delete_isolated_file_keeps_others() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_git_delisol_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("构造临时仓库失败");
build_git_repo(&repo).expect("构造 fixture 失败");
let isolated = repo.join("standalone.rs");
std::fs::write(&isolated, "pub fn isolated_helper() -> u32 { 7 }\n").unwrap();
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
git_commit_all(&repo, "init with standalone");
code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full)
.expect("全量生成失败");
let wiki_dir = repo.join(".code-repo-wiki").join("wiki").join("zh");
let pages_before: Vec<String> = std::fs::read_dir(&wiki_dir)
.map(|es| {
es.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "md"))
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
assert!(
pages_before.iter().any(|p| p.contains("net") || p.contains("http")),
"基线应含 net/http 社区页: {pages_before:?}"
);
std::fs::remove_file(&isolated).expect("删除 standalone.rs 失败");
git_commit_all(&repo, "delete standalone");
code_repo_wiki::run_pipeline(
Some(&config_path),
None,
false,
&root,
&code_repo_wiki::GenerationMode::Incremental { watch_paths: vec![], change_kind: None },
)
.expect("删除增量失败");
let pages_after: Vec<String> = std::fs::read_dir(&wiki_dir)
.map(|es| {
es.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "md"))
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
assert!(
pages_after.iter().any(|p| p.contains("net") || p.contains("http")),
"删除孤立文件后 net/http 社区产物不得被清空,实际: {pages_after:?}"
);
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn test_force_incremental_regenerates_all() {
let repo = std::env::temp_dir().join(format!("code_repo_wiki_git_force_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&repo);
std::fs::create_dir_all(&repo).expect("构造临时仓库失败");
build_git_repo(&repo).expect("构造 fixture 失败");
let root = code_repo_wiki::project::ProjectRoot::new(repo.clone());
let config_path = repo.join("config.toml");
let tcp_mod = repo.join("src").join("net").join("tcp.rs");
git_commit_all(&repo, "init");
code_repo_wiki::run_pipeline(Some(&config_path), None, false, &root, &code_repo_wiki::GenerationMode::Full)
.expect("全量生成失败");
std::fs::write(&tcp_mod, "pub fn tcp_process(x: u32) -> u32 { udp_process(x) + 42 }\n").unwrap();
git_commit_all(&repo, "change body");
let inc = code_repo_wiki::run_pipeline(
Some(&config_path),
None,
true,
&root,
&code_repo_wiki::GenerationMode::Incremental { watch_paths: vec![], change_kind: None },
)
.expect("force 增量失败");
let titles = doc_titles(&inc);
assert!(
titles.iter().any(|t| t.contains("net")) && titles.iter().any(|t| t.contains("http")),
"force 增量应全量重生成(含未变更的 http 社区),实际: {titles:?}"
);
let _ = std::fs::remove_dir_all(&repo);
}