use std::path::PathBuf;
mod common;
use common::{run_bin_with_envs, unique_dir};
const START: &str = "<!-- CODE-REPO-WIKI:START -->";
const END: &str = "<!-- CODE-REPO-WIKI:END -->";
const LEGACY_START: &str = "<!-- REPO-WIKI:START -->";
fn setup(tag: &str) -> (PathBuf, PathBuf, Vec<(&'static str, String)>) {
let dir = unique_dir(tag);
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let home = unique_dir(&format!("{tag}_home"));
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs = vec![
("HOME", home.to_string_lossy().into_owned()),
("USERPROFILE", home.to_string_lossy().into_owned()),
];
(dir, home, envs)
}
#[test]
fn test_install_wiki_creates_agents_md() {
let (work_dir, _home, envs) = setup("creates_agents_md");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(
out.status.success(),
"install 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let path = work_dir.join("AGENTS.md");
assert!(path.exists(), "AGENTS.md 应被创建: {}", path.display());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains(START), "应含 START 标记,实际: {content}");
assert!(content.contains(END), "应含 END 标记,实际: {content}");
assert!(content.contains("code-repo-wiki update"), "应含 code-repo-wiki update 指引,实际: {content}");
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_idempotent_preserves_user_content() {
let (work_dir, _home, envs) = setup("idempotent");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let path = work_dir.join("AGENTS.md");
std::fs::write(&path, "# 用户项目说明\n").unwrap();
let out1 = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out1.status.success(), "首次 install 应成功");
let content1 = std::fs::read_to_string(&path).unwrap();
let out2 = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out2.status.success(), "重复 install 应成功");
let content2 = std::fs::read_to_string(&path).unwrap();
assert_eq!(content1, content2, "重复 install 应幂等(内容不变)");
assert!(content2.starts_with("# 用户项目说明\n"), "用户内容应保留在块之前,实际: {content2}");
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_uninstall_wiki_removes_block() {
let (work_dir, _home, envs) = setup("uninstall");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let path = work_dir.join("AGENTS.md");
std::fs::write(
&path,
"用户头部\n\n<!-- REPO-WIKI:START -->\n旧名块内容\n<!-- REPO-WIKI:END -->\n\n<!-- CODE-REPO-WIKI:START -->\n块内容\n<!-- CODE-REPO-WIKI:END -->\n用户尾部\n",
)
.unwrap();
let out = run_bin_with_envs(&work_dir, &["uninstall", "--force"], &envs_ref);
assert!(
out.status.success(),
"uninstall 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = std::fs::read_to_string(&path).unwrap();
assert!(
!content.contains(START) && !content.contains(END) && !content.contains(LEGACY_START),
"新旧两代标记都应被移除,实际: {content}"
);
assert!(
!content.contains("块内容") && !content.contains("旧名块内容"),
"两代标记块内容都应被移除,实际: {content}"
);
assert!(content.contains("用户头部") && content.contains("用户尾部"), "用户内容应保留,实际: {content}");
let out2 = run_bin_with_envs(&work_dir, &["uninstall", "--force"], &envs_ref);
assert!(out2.status.success(), "未安装时卸载应退出码 0");
let stdout = String::from_utf8_lossy(&out2.stdout);
assert!(stdout.contains("未安装"), "应提示未安装,实际: {stdout}");
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_migrates_legacy_block() {
let (work_dir, _home, envs) = setup("migrate_legacy");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let path = work_dir.join("AGENTS.md");
std::fs::write(
&path,
"用户头部\n\n<!-- REPO-WIKI:START -->\n旧名块内容\n<!-- REPO-WIKI:END -->\n用户尾部\n",
)
.unwrap();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains(START) && content.contains(END), "应注入新标记块,实际: {content}");
assert!(!content.contains(LEGACY_START), "旧标记块应被迁移替换,实际: {content}");
assert!(!content.contains("旧名块内容"), "旧块内容应被替换,实际: {content}");
assert!(content.contains("用户头部") && content.contains("用户尾部"), "用户内容应保留: {content}");
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_also_claude_writes_both() {
let (work_dir, home, envs) = setup("also_claude");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install", "--claude"], &envs_ref);
assert!(out.status.success(), "install --claude 应成功");
let agents = std::fs::read_to_string(work_dir.join("AGENTS.md")).unwrap();
let claude = std::fs::read_to_string(work_dir.join("CLAUDE.md")).unwrap();
assert!(agents.contains(START) && agents.contains(END), "AGENTS.md 应含标记对");
assert!(claude.contains(START) && claude.contains(END), "CLAUDE.md 应含标记对");
assert_eq!(agents, claude, "CLAUDE.md 应原样写入与 AGENTS.md 相同的注入块");
let claude_json = home.join(".claude.json");
assert!(claude_json.exists(), "--claude 应注册用户级 ~/.claude.json,实际路径: {}", claude_json.display());
let parsed: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&claude_json).unwrap()).unwrap();
assert!(parsed["mcpServers"]["code-repo-wiki"].is_object(), "~/.claude.json 应含 code-repo-wiki MCP 条目");
assert!(!work_dir.join(".mcp.json").exists(), "v39 起不再写项目根 .mcp.json");
let out2 = run_bin_with_envs(&work_dir, &["uninstall", "--force"], &envs_ref);
assert!(out2.status.success(), "uninstall 应成功");
let agents2 = std::fs::read_to_string(work_dir.join("AGENTS.md")).unwrap();
let claude2 = std::fs::read_to_string(work_dir.join("CLAUDE.md")).unwrap();
assert!(!agents2.contains(START), "AGENTS.md 标记应被移除");
assert!(!claude2.contains(START), "CLAUDE.md 标记应被移除");
let parsed2: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&claude_json).unwrap()).unwrap();
assert!(parsed2["mcpServers"].get("code-repo-wiki").is_none(), "uninstall 应移除 ~/.claude.json MCP 条目");
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_half_marker_errors_and_preserves_file() {
let (work_dir, _home, envs) = setup("half_marker");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let path = work_dir.join("AGENTS.md");
let broken = "# 标题\n<!-- REPO-WIKI:START -->\n";
std::fs::write(&path, broken).unwrap();
for cmd in ["install", "uninstall"] {
let args: Vec<&str> = if cmd == "install" {
vec!["install"]
} else {
vec!["uninstall", "--force"]
};
let out = run_bin_with_envs(&work_dir, &args, &envs_ref);
assert!(!out.status.success(), "{cmd} 遇半标记应非 0 退出码");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(combined.contains("不完整"), "{cmd} 应报半标记错误,实际: {combined}");
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
broken,
"{cmd} 失败时不应修改文件"
);
}
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_template_uses_config_paths() {
let (work_dir, _home, envs) = setup("template_config");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
std::fs::write(work_dir.join("config.toml"), "[wiki]\nlanguage = \"en\"\n").unwrap();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(
out.status.success(),
"install 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = std::fs::read_to_string(work_dir.join("AGENTS.md")).unwrap();
assert!(
content.contains("`.code-repo-wiki/wiki/en/overview.md`"),
"应渲染 .code-repo-wiki/wiki/en/overview.md, 实际: {content}"
);
assert!(
!content.contains(".code-repo-wiki/wiki/zh"),
"不应残留默认 zh 路径, 实际: {content}"
);
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_template_defaults_without_config() {
let (work_dir, home, envs) = setup("template_default");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains("未找到有效配置"),
"用户级配置存在(install 自动创建)时不应提示未找到配置,实际: {combined}"
);
let user_cfg = home.join(".code-repo-wiki").join("config.toml");
assert!(
user_cfg.exists(),
"install 应创建用户级默认配置: {}",
user_cfg.display()
);
let content = std::fs::read_to_string(work_dir.join("AGENTS.md")).unwrap();
assert!(
content.contains("`.code-repo-wiki/wiki/zh/overview.md`"),
"默认应渲染 .code-repo-wiki/zh 路径,实际: {content}"
);
let _ = std::fs::remove_dir_all(&work_dir);
}
#[test]
fn test_install_wiki_renders_with_user_config() {
let (work_dir, home, envs) = setup("user_cfg_render");
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let user_dir = home.join(".code-repo-wiki");
std::fs::create_dir_all(&user_dir).unwrap();
std::fs::write(user_dir.join("config.toml"), "[wiki]\nlanguage = \"en\"\n").unwrap();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains("未找到有效配置"),
"用户级配置存在时不应提示未找到配置,实际: {combined}"
);
let content = std::fs::read_to_string(work_dir.join("AGENTS.md")).unwrap();
assert!(
content.contains("`.code-repo-wiki/wiki/en/overview.md`"),
"注入块应渲染用户级 language=en 路径,实际: {content}"
);
let _ = std::fs::remove_dir_all(&work_dir);
}