use anyhow::Result;
use std::path::Path;
use tokio::fs;
use crate::common::{ManifestBuilder, TestProject};
async fn create_test_manifest(gitignore: bool, _source_dir: &Path) -> String {
ManifestBuilder::new()
.with_target_config(|t| {
t.agents(".claude/agents")
.snippets(".agpm/snippets")
.commands(".claude/commands")
.gitignore(gitignore)
})
.add_agent("test-agent", |d| d.path("../sources/source/agents/test.md").flatten(false))
.add_snippet("test-snippet", |d| {
d.path("../sources/source/snippets/test.md").flatten(false)
})
.add_command("test-command", |d| {
d.path("../sources/source/commands/test.md").flatten(false)
})
.build()
}
async fn create_test_manifest_default(_source_dir: &Path) -> String {
ManifestBuilder::new()
.with_target_config(|t| {
t.agents(".claude/agents").snippets(".agpm/snippets").commands(".claude/commands")
})
.add_agent("test-agent", |d| d.path("../sources/source/agents/test.md").flatten(false))
.build()
}
async fn create_test_source_files(project: &TestProject) -> Result<()> {
let source_dir = project.sources_path().join("source");
fs::create_dir_all(source_dir.join("agents")).await?;
fs::create_dir_all(source_dir.join("snippets")).await?;
fs::create_dir_all(source_dir.join("commands")).await?;
fs::write(source_dir.join("agents/test.md"), "# Test Agent\n").await?;
fs::write(source_dir.join("snippets/test.md"), "# Test Snippet\n").await?;
fs::write(source_dir.join("commands/test.md"), "# Test Command\n").await?;
Ok(())
}
#[tokio::test]
async fn test_gitignore_enabled_by_default() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
project.write_manifest(&create_test_manifest_default(&source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created by default");
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("AGPM managed entries"));
assert!(content.contains("# End of AGPM managed entries"));
}
#[tokio::test]
async fn test_gitignore_explicitly_enabled() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created");
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("AGPM managed entries"));
assert!(content.contains("AGPM managed entries - do not edit below this line"));
assert!(content.contains("# End of AGPM managed entries"));
}
#[tokio::test]
async fn test_gitignore_preserves_user_entries() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
fs::create_dir_all(project.project_path().join(".claude")).await.unwrap();
let gitignore_path = project.project_path().join(".gitignore");
let user_content = r#"# User's custom comment
user-file.txt
temp/
# AGPM managed entries - do not edit below this line
.claude/agents/old-agent.md
# End of AGPM managed entries
"#;
fs::write(&gitignore_path, user_content).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let updated_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("# User's custom comment"));
assert!(updated_content.contains("user-file.txt"));
assert!(updated_content.contains("temp/"));
assert!(updated_content.contains("AGPM managed entries"));
assert!(updated_content.contains("# End of AGPM managed entries"));
assert!(updated_content.contains(".agpm/snippets/sources/source/snippets/test.md"));
}
#[tokio::test]
async fn test_gitignore_preserves_content_after_agpm_section() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
fs::create_dir_all(project.project_path().join(".claude")).await.unwrap();
let gitignore_path = project.project_path().join(".gitignore");
let user_content = r#"# Project gitignore
temp/
# AGPM managed entries - do not edit below this line
.claude/agents/old-agent.md
# End of AGPM managed entries
# Additional entries after AGPM section
local-config.json
debug/
# End comment
"#;
fs::write(&gitignore_path, user_content).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let updated_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("# Project gitignore"));
assert!(updated_content.contains("temp/"));
assert!(updated_content.contains("AGPM managed entries"));
assert!(updated_content.contains("# End of AGPM managed entries"));
assert!(updated_content.contains(".agpm/snippets/sources/source/snippets/test.md"));
assert!(updated_content.contains("# Additional entries after AGPM section"));
assert!(updated_content.contains("local-config.json"));
assert!(updated_content.contains("debug/"));
assert!(updated_content.contains("# End comment"));
assert!(!updated_content.contains(".claude/agents/old-agent.md"));
}
#[tokio::test]
async fn test_gitignore_update_command() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
project.run_agpm(&["update", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
if gitignore_path.exists() {
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("AGPM managed entries"));
}
}
#[tokio::test]
async fn test_gitignore_handles_external_paths() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let repo = project.create_source_repo("test-source").await.unwrap();
repo.add_resource("agents", "test-agent", "# Test Agent\n").await.unwrap();
fs::create_dir_all(repo.path.join("scripts")).await.unwrap();
fs::write(repo.path.join("scripts/test.sh"), "#!/bin/bash\necho 'test'\n").await.unwrap();
repo.git.add_all().unwrap();
repo.git.commit("Initial commit").unwrap();
repo.git.tag("v1.0.0").unwrap();
let url = repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = ManifestBuilder::new()
.add_source("test-source", &url)
.with_gitignore(true)
.add_script("external-script", |d| {
d.source("test-source").path("scripts/test.sh").version("v1.0.0")
})
.add_agent("internal-agent", |d| {
d.source("test-source").path("agents/test-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest_content).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created");
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("AGPM managed entries"), "Should have AGPM section");
assert!(content.contains("# End of AGPM managed entries"), "Should have end marker");
assert!(
content.contains(".claude/scripts/test.sh")
|| content.contains(".claude/scripts/external-script.sh"),
"Script path should be in gitignore. Content:\n{}",
content
);
assert!(
content.contains(".claude/agents/test-agent.md")
|| content.contains(".claude/agents/internal-agent.md"),
"Agent path should be in gitignore. Content:\n{}",
content
);
}
#[tokio::test]
async fn test_gitignore_empty_lockfile() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let manifest_content = ManifestBuilder::new()
.with_target_config(|t| {
t.agents(".claude/agents")
.snippets(".agpm/snippets")
.commands(".claude/commands")
.gitignore(true)
})
.build();
project.write_manifest(&manifest_content).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created even with empty lockfile");
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("AGPM managed entries"));
assert!(content.contains("# End of AGPM managed entries"));
}
#[tokio::test]
async fn test_gitignore_idempotent() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
let first_content = if gitignore_path.exists() {
fs::read_to_string(&gitignore_path).await.unwrap()
} else {
String::new()
};
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let second_content = if gitignore_path.exists() {
fs::read_to_string(&gitignore_path).await.unwrap()
} else {
String::new()
};
assert_eq!(first_content, second_content, "Gitignore should be idempotent");
}
#[tokio::test]
async fn test_gitignore_switch_enabled_disabled() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let gitignore_path = project.project_path().join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created");
project.write_manifest(&create_test_manifest(false, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
assert!(gitignore_path.exists(), "Gitignore should still exist when disabled");
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
let content = fs::read_to_string(&gitignore_path).await.unwrap();
let modified_content =
content.replace("# AGPM managed entries", "user-custom.txt\n\n# AGPM managed entries");
fs::write(&gitignore_path, modified_content).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let final_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(
final_content.contains("user-custom.txt"),
"User entries should be preserved when re-enabling"
);
}
#[tokio::test]
async fn test_gitignore_actually_ignored_by_git() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let project_dir = project.project_path().to_path_buf();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
let git = project.init_git_repo().unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
assert!(project_dir.join(".claude/agents/sources/source/agents/test.md").exists());
assert!(project_dir.join(".agpm/snippets/sources/source/snippets/test.md").exists());
assert!(project_dir.join(".claude/commands/sources/source/commands/test.md").exists());
git.add_all().unwrap();
let status = git.status_porcelain().unwrap();
assert!(
!status.contains("sources/source/agents/test.md"),
"Agent file should be ignored by git\nGit status:\n{}",
status
);
assert!(
!status.contains("sources/source/snippets/test.md"),
"Snippet file should be ignored by git\nGit status:\n{}",
status
);
assert!(
!status.contains("sources/source/commands/test.md"),
"Command file should be ignored by git\nGit status:\n{}",
status
);
assert!(
status.contains(".gitignore"),
"Gitignore file should be tracked by git\nGit status:\n{}",
status
);
assert!(
status.contains("agpm.toml"),
"Manifest should be tracked by git\nGit status:\n{}",
status
);
assert!(
status.contains("agpm.lock"),
"Lockfile should be tracked by git\nGit status:\n{}",
status
);
assert!(
git.check_ignore(".claude/agents/sources/source/agents/test.md").unwrap(),
"Agent file should be ignored by git check-ignore"
);
assert!(
git.check_ignore(".agpm/snippets/sources/source/snippets/test.md").unwrap(),
"Snippet file should be ignored by git check-ignore"
);
assert!(
git.check_ignore(".claude/commands/sources/source/commands/test.md").unwrap(),
"Command file should be ignored by git check-ignore"
);
}
#[tokio::test]
async fn test_gitignore_malformed_existing() {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await.unwrap();
let source_dir = project.sources_path().join("source");
create_test_source_files(&project).await.unwrap();
fs::create_dir_all(project.project_path().join(".claude")).await.unwrap();
let gitignore_path = project.project_path().join(".gitignore");
let malformed_content = r#"# Some content
user-file.txt
# AGPM managed entries - do not edit below this line
/old/entry.md
# Missing end marker!
"#;
fs::write(&gitignore_path, malformed_content).await.unwrap();
project.write_manifest(&create_test_manifest(true, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
let updated_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("# End of AGPM managed entries"));
assert!(updated_content.contains("user-file.txt"));
assert!(updated_content.contains("AGPM managed entries"));
}