use anyhow::Result;
use assert_cmd::Command;
use std::path::Path;
use tempfile::TempDir;
use tokio::fs;
mod common;
use common::TestProject;
async fn create_test_manifest(gitignore: bool, source_dir: &Path) -> String {
let source_path = source_dir.display().to_string().replace('\\', "/");
format!(
r#"
[sources]
[target]
agents = ".claude/agents"
snippets = ".agpm/snippets"
commands = ".claude/commands"
gitignore = {}
[agents.test-agent]
path = "{}/agents/test.md"
[snippets.test-snippet]
path = "{}/snippets/test.md"
[commands.test-command]
path = "{}/commands/test.md"
"#,
gitignore, source_path, source_path, source_path
)
}
async fn create_test_manifest_default(source_dir: &Path) -> String {
let source_path = source_dir.display().to_string().replace('\\', "/");
format!(
r#"
[sources]
[target]
agents = ".claude/agents"
snippets = ".agpm/snippets"
commands = ".claude/commands"
[agents.test-agent]
path = "{}/agents/test.md"
"#,
source_path
)
}
async fn create_test_lockfile() -> String {
r#"
version = 1
[[agents]]
name = "test-agent"
path = "source/agents/test.md"
checksum = ""
installed_at = ".claude/agents/test-agent.md"
[[snippets]]
name = "test-snippet"
path = "source/snippets/test.md"
checksum = ""
installed_at = ".agpm/snippets/test-snippet.md"
artifact_type = "agpm"
[[commands]]
name = "test-command"
path = "source/commands/test.md"
checksum = ""
installed_at = ".claude/commands/test-command.md"
"#
.to_string()
}
async fn create_test_source_files(source_dir: &Path) -> Result<()> {
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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest_default(&source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.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_disabled() {
agpm_cli::test_utils::init_test_logging(None);
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(false, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.join(".gitignore");
assert!(!gitignore_path.exists(), "Gitignore should not be created when disabled");
}
#[tokio::test]
async fn test_gitignore_preserves_user_entries() {
agpm_cli::test_utils::init_test_logging(None);
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
fs::create_dir_all(project_dir.join(".claude")).await.unwrap();
let gitignore_path = project_dir.join(".gitignore");
let user_content = r#"# User's custom comment
*.backup
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();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let updated_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("# User's custom comment"));
assert!(updated_content.contains("*.backup"));
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/test-snippet.md"));
}
#[tokio::test]
async fn test_gitignore_preserves_content_after_agpm_section() {
agpm_cli::test_utils::init_test_logging(None);
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
fs::create_dir_all(project_dir.join(".claude")).await.unwrap();
let gitignore_path = project_dir.join(".gitignore");
let user_content = r#"# Project gitignore
*.backup
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();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let updated_content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("# Project gitignore"));
assert!(updated_content.contains("*.backup"));
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/test-snippet.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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("update")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let manifest_content = r#"
[sources]
test-source = "https://github.com/test/repo.git"
[target]
gitignore = true
[scripts.external-script]
source = "test-source"
path = "scripts/test.sh"
version = "v1.0.0"
"#;
fs::write(&manifest_path, manifest_content).await.unwrap();
let lockfile_content = r#"
version = 1
[[sources]]
name = "test-source"
url = "https://github.com/test/repo.git"
commit = "abc123"
[[scripts]]
name = "external-script"
source = "test-source"
url = "https://github.com/test/repo.git"
path = "scripts/test.sh"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:test"
installed_at = "scripts/external.sh"
[[agents]]
name = "internal-agent"
source = "test-source"
url = "https://github.com/test/repo.git"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:test"
installed_at = ".claude/agents/internal.md"
"#;
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, lockfile_content).await.unwrap();
fs::create_dir_all(project_dir.join(".claude/agents")).await.unwrap();
fs::create_dir_all(project_dir.join("scripts")).await.unwrap();
fs::write(project_dir.join("scripts/external.sh"), "#!/bin/bash\n").await.unwrap();
fs::write(project_dir.join(".claude/agents/internal.md"), "# Internal\n").await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.join(".gitignore");
if gitignore_path.exists() {
let content = fs::read_to_string(&gitignore_path).await.unwrap();
assert!(content.contains("../scripts/external.sh"), "External paths should use ../ prefix");
assert!(
content.contains(".claude/agents/internal.md"),
"Internal paths should use / prefix"
);
}
}
#[tokio::test]
async fn test_gitignore_empty_lockfile() {
agpm_cli::test_utils::init_test_logging(None);
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, "version = 1\n").await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.join(".gitignore");
let first_content = if gitignore_path.exists() {
fs::read_to_string(&gitignore_path).await.unwrap()
} else {
String::new()
};
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
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 temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
let gitignore_path = project_dir.join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore should be created");
fs::write(&manifest_path, create_test_manifest(false, &source_dir).await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
assert!(gitignore_path.exists(), "Gitignore should still exist when disabled");
fs::write(&manifest_path, 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();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
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(&source_dir).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/test-agent.md").exists());
assert!(project_dir.join(".agpm/snippets/test-snippet.md").exists());
assert!(project_dir.join(".claude/commands/test-command.md").exists());
git.add_all().unwrap();
let status = git.status_porcelain().unwrap();
assert!(
!status.contains("agents/test-agent.md"),
"Agent file should be ignored by git\nGit status:\n{}",
status
);
assert!(
!status.contains("snippets/test-snippet.md"),
"Snippet file should be ignored by git\nGit status:\n{}",
status
);
assert!(
!status.contains("commands/test-command.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/test-agent.md").unwrap(),
"Agent file should be ignored by git check-ignore"
);
assert!(
git.check_ignore(".agpm/snippets/test-snippet.md").unwrap(),
"Snippet file should be ignored by git check-ignore"
);
assert!(
git.check_ignore(".claude/commands/test-command.md").unwrap(),
"Command file should be ignored by git check-ignore"
);
}
#[tokio::test]
async fn test_gitignore_disabled_files_not_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(&source_dir).await.unwrap();
let git = project.init_git_repo().unwrap();
project.write_manifest(&create_test_manifest(false, &source_dir).await).await.unwrap();
project.run_agpm(&["install", "--quiet"]).unwrap().assert_success();
assert!(project_dir.join(".claude/agents/test-agent.md").exists());
assert!(project_dir.join(".agpm/snippets/test-snippet.md").exists());
assert!(project_dir.join(".claude/commands/test-command.md").exists());
git.add_all().unwrap();
let status = git.status_porcelain().unwrap();
assert!(
status.contains("agents/test-agent.md"),
"Agent file should NOT be ignored when gitignore is disabled
Git status:
{}",
status
);
assert!(
status.contains("snippets/test-snippet.md"),
"Snippet file should NOT be ignored when gitignore is disabled
Git status:
{}",
status
);
assert!(
status.contains("commands/test-command.md"),
"Command file should NOT be ignored when gitignore is disabled
Git status:
{}",
status
);
assert!(
!project_dir.join(".gitignore").exists(),
"Gitignore file should not exist when disabled"
);
}
#[tokio::test]
async fn test_gitignore_malformed_existing() {
agpm_cli::test_utils::init_test_logging(None);
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let source_dir = temp.path().join("source");
create_test_source_files(&source_dir).await.unwrap();
fs::create_dir_all(project_dir.join(".claude")).await.unwrap();
let gitignore_path = project_dir.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();
let manifest_path = project_dir.join("agpm.toml");
fs::write(&manifest_path, create_test_manifest(true, &source_dir).await).await.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
fs::write(&lockfile_path, create_test_lockfile().await).await.unwrap();
Command::cargo_bin("agpm")
.unwrap()
.arg("install")
.arg("--quiet")
.current_dir(project_dir)
.assert();
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"));
}