use std::path::Path;
use super::*;
fn run_git(dir: &Path, args: &[&str]) {
let status = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.status()
.unwrap_or_else(|e| panic!("failed to spawn git {args:?}: {e}"));
assert!(status.success(), "git {args:?} failed in {}", dir.display());
}
fn init_git_repo(dir: &Path) {
run_git(dir, &["init", "-q"]);
run_git(dir, &["config", "user.email", "test@example.com"]);
run_git(dir, &["config", "user.name", "Test"]);
}
fn commit_all(dir: &Path, message: &str) {
run_git(dir, &["add", "-A"]);
run_git(dir, &["commit", "-q", "-m", message]);
}
fn write(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
}
fn write_agent_manifest(project: &Path, filenames: &[&str]) {
let dir = project.join(".claude").join("agents");
std::fs::create_dir_all(&dir).unwrap();
let managed: serde_json::Map<String, serde_json::Value> = filenames
.iter()
.map(|f| {
(
f.to_string(),
serde_json::json!({
"source_chain": [f],
"checksum": "deadbeef",
"deployed_at": "2026-01-01T00:00:00Z",
"origin": "bundled",
}),
)
})
.collect();
let manifest = serde_json::json!({ "version": 1, "managed": managed });
std::fs::write(
dir.join(crate::core::agent_manifest::MANIFEST_FILE),
serde_json::to_string_pretty(&manifest).unwrap(),
)
.unwrap();
}
fn write_skill_manifest(project: &Path, keys: &[&str]) {
let dir = project.join(".claude").join("skills");
std::fs::create_dir_all(&dir).unwrap();
let managed: serde_json::Map<String, serde_json::Value> = keys
.iter()
.map(|k| {
(
k.to_string(),
serde_json::json!({ "checksum": "deadbeef", "deployed_at": "2026-01-01T00:00:00Z" }),
)
})
.collect();
let manifest = serde_json::json!({ "version": 1, "managed": managed });
std::fs::write(
dir.join(crate::core::skill_manifest::SKILL_MANIFEST_FILE),
serde_json::to_string_pretty(&manifest).unwrap(),
)
.unwrap();
}
#[test]
fn check_ok_when_no_project_dir() {
let check = check_scaffold_tracking(None);
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn check_ok_when_not_a_git_repo() {
let tmp = crate::test_support::hermetic_temp_dir();
write_skill_manifest(tmp.path(), &["documentation-style"]);
write(
tmp.path(),
".claude/skills/documentation-style/SKILL.md",
"content",
);
let check = check_scaffold_tracking(Some(tmp.path()));
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn check_ok_on_clean_repo() {
let tmp = crate::test_support::hermetic_temp_dir();
init_git_repo(tmp.path());
write(tmp.path(), "README.md", "hello");
commit_all(tmp.path(), "init");
let check = check_scaffold_tracking(Some(tmp.path()));
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
}
#[test]
fn check_warns_on_true_collision_only() {
let tmp = crate::test_support::hermetic_temp_dir();
let project = tmp.path();
init_git_repo(project);
write_skill_manifest(
project,
&[
"documentation-style",
"documentation-style/references/one.md",
],
);
write(
project,
".claude/skills/documentation-style/SKILL.md",
"entry point",
);
write(
project,
".claude/skills/documentation-style/references/one.md",
"reference",
);
write(
project,
".claude/skills/my-project-custom-skill/SKILL.md",
"hand placed, intentionally committed",
);
commit_all(project, "commit scaffolding + custom skill");
write_agent_manifest(project, &["engineer.md"]);
write(project, ".claude/agents/engineer.md", "agent body");
let check = check_scaffold_tracking(Some(project));
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
assert!(
check
.message
.contains(".claude/skills/documentation-style/SKILL.md"),
"missing true-collision entry point in: {}",
check.message
);
assert!(
check
.message
.contains(".claude/skills/documentation-style/references/one.md"),
"missing true-collision reference file in: {}",
check.message
);
assert!(
!check.message.contains("my-project-custom-skill"),
"must not flag a tracked-but-unmanaged project-custom skill: {}",
check.message
);
assert!(
!check.message.contains("engineer.md"),
"must not flag a managed-but-untracked agent file: {}",
check.message
);
}
#[test]
fn regenerated_paths_includes_managed_agent() {
let tmp = crate::test_support::hermetic_temp_dir();
write_agent_manifest(tmp.path(), &["engineer.md"]);
let paths = regenerated_scaffold_paths(tmp.path()).unwrap();
assert!(paths.contains(".claude/agents/engineer.md"));
}
#[test]
fn regenerated_paths_includes_skill_entry_and_references() {
let tmp = crate::test_support::hermetic_temp_dir();
write_skill_manifest(
tmp.path(),
&["tm-doctor", "tm-doctor/references/checklist.md"],
);
let paths = regenerated_scaffold_paths(tmp.path()).unwrap();
assert!(paths.contains(".claude/skills/tm-doctor/SKILL.md"));
assert!(paths.contains(".claude/skills/tm-doctor/references/checklist.md"));
}
#[test]
fn regenerated_paths_includes_all_output_styles() {
let tmp = crate::test_support::hermetic_temp_dir();
let paths = regenerated_scaffold_paths(tmp.path()).unwrap();
for style in crate::core::bundle::OUTPUT_STYLES {
assert!(
paths.contains(&format!(".claude/output-styles/{}", style.file_name)),
"missing bundled style {} in {paths:?}",
style.file_name
);
}
}
#[test]
fn tracked_scaffold_paths_returns_none_outside_git_repo() {
let tmp = crate::test_support::hermetic_temp_dir();
assert!(tracked_scaffold_paths(tmp.path()).is_none());
}
#[test]
fn tracked_scaffold_paths_lists_committed_files() {
let tmp = crate::test_support::hermetic_temp_dir();
init_git_repo(tmp.path());
write(tmp.path(), ".claude/skills/foo/SKILL.md", "x");
write(tmp.path(), "README.md", "unrelated");
commit_all(tmp.path(), "init");
let tracked = tracked_scaffold_paths(tmp.path()).expect("must resolve inside a git repo");
assert!(tracked.contains(".claude/skills/foo/SKILL.md"));
assert!(
!tracked.contains("README.md"),
"pathspec must scope to the three harness subtrees only"
);
}
#[test]
fn remediation_message_lists_exact_paths() {
let mut collisions = std::collections::BTreeSet::new();
collisions.insert(".claude/skills/documentation-style/SKILL.md".to_string());
let message = remediation_message(&collisions);
assert!(message.contains("git rm -r --cached"));
assert!(message.contains(".claude/skills/documentation-style/SKILL.md"));
}