#[cfg(test)]
mod installer_tests {
use crate::cache::Cache;
use crate::installer::{
InstallContext, ResourceFilter, install_resource, install_resource_with_progress,
install_resources, install_updated_resources, update_gitignore,
};
use crate::lockfile::{LockFile, LockedResource};
use crate::manifest::Manifest;
use crate::utils::ensure_dir;
use indicatif::ProgressBar;
use std::sync::Arc;
use tempfile::TempDir;
fn create_test_locked_resource(name: &str, is_local: bool) -> LockedResource {
if is_local {
LockedResource {
name: name.to_string(),
source: None,
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: String::new(),
context_checksum: None,
installed_at: String::new(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
}
} else {
LockedResource {
name: name.to_string(),
source: Some("test_source".to_string()),
url: Some("https://github.com/test/repo.git".to_string()),
path: "resources/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:test".to_string(),
context_checksum: None,
installed_at: String::new(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
}
}
}
#[tokio::test]
async fn test_install_resource_local() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("test.md");
std::fs::write(&local_file, "# Test Resource\nThis is a test").unwrap();
let mut entry = create_test_locked_resource("local-test", true);
entry.path = local_file.to_string_lossy().to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, "agents", &context).await;
assert!(result.is_ok(), "Failed to install local resource: {:?}", result);
let (installed, _checksum, _context_checksum, _applied_patches) = result.unwrap();
assert!(installed, "Should have installed new resource");
let expected_path = project_dir.join("agents").join("local-test.md");
assert!(expected_path.exists(), "Installed file not found");
let content = std::fs::read_to_string(expected_path).unwrap();
assert_eq!(content, "# Test Resource\nThis is a test");
}
#[tokio::test]
async fn test_install_resource_with_custom_path() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("test.md");
std::fs::write(&local_file, "# Custom Path Test").unwrap();
let mut entry = create_test_locked_resource("custom-test", true);
entry.path = local_file.to_string_lossy().to_string();
entry.installed_at = "custom/location/resource.md".to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, "agents", &context).await;
assert!(result.is_ok());
let (installed, _checksum, _context_checksum, _applied_patches) = result.unwrap();
assert!(installed, "Should have installed new resource");
let expected_path = project_dir.join("custom/location/resource.md");
assert!(expected_path.exists(), "File not installed at custom path");
}
#[tokio::test]
async fn test_install_resource_local_missing_file() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let mut entry = create_test_locked_resource("missing-test", true);
entry.path = "/non/existent/file.md".to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, "agents", &context).await;
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Local file") && error_msg.contains("not found"));
}
#[tokio::test]
async fn test_install_resource_invalid_markdown_frontmatter() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("invalid.md");
std::fs::write(&local_file, "---\ninvalid: yaml: [\n---\nContent").unwrap();
let mut entry = create_test_locked_resource("invalid-test", true);
entry.path = local_file.to_string_lossy().to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, "agents", &context).await;
if let Err(e) = &result {
eprintln!("ERROR: {:#}", e);
}
assert!(result.is_ok());
let (installed, _checksum, _context_checksum, _applied_patches) = result.unwrap();
assert!(installed);
let dest_path = project_dir.join("agents/invalid-test.md");
assert!(dest_path.exists());
let installed_content = std::fs::read_to_string(&dest_path).unwrap();
assert!(installed_content.contains("---"));
assert!(installed_content.contains("invalid: yaml:"));
assert!(installed_content.contains("Content"));
}
#[tokio::test]
async fn test_install_resource_with_progress() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let pb = ProgressBar::new(1);
let local_file = temp_dir.path().join("test.md");
std::fs::write(&local_file, "# Progress Test").unwrap();
let mut entry = create_test_locked_resource("progress-test", true);
entry.path = local_file.to_string_lossy().to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource_with_progress(&entry, "agents", &context, &pb).await;
assert!(result.is_ok());
let expected_path = project_dir.join("agents").join("progress-test.md");
assert!(expected_path.exists());
}
#[tokio::test]
async fn test_install_resources_empty() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let lockfile = LockFile::new();
let manifest = Manifest::new();
let results = install_resources(
ResourceFilter::All,
&Arc::new(lockfile),
&manifest,
project_dir,
cache,
false,
None,
None,
false, None, )
.await
.unwrap();
assert_eq!(results.installed_count, 0, "Should install 0 resources from empty lockfile");
}
#[tokio::test]
async fn test_install_resources_multiple() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let file1 = temp_dir.path().join("agent.md");
let file2 = temp_dir.path().join("snippet.md");
let file3 = temp_dir.path().join("command.md");
std::fs::write(&file1, "# Agent").unwrap();
std::fs::write(&file2, "# Snippet").unwrap();
std::fs::write(&file3, "# Command").unwrap();
let mut lockfile = LockFile::new();
let mut agent = create_test_locked_resource("test-agent", true);
agent.path = file1.to_string_lossy().to_string();
agent.installed_at = ".claude/agents/test-agent.md".to_string();
lockfile.agents.push(agent);
let mut snippet = create_test_locked_resource("test-snippet", true);
snippet.path = file2.to_string_lossy().to_string();
snippet.resource_type = crate::core::ResourceType::Snippet;
snippet.tool = Some("agpm".to_string()); snippet.installed_at = ".agpm/snippets/test-snippet.md".to_string();
lockfile.snippets.push(snippet);
let mut command = create_test_locked_resource("test-command", true);
command.path = file3.to_string_lossy().to_string();
command.resource_type = crate::core::ResourceType::Command;
command.installed_at = ".claude/commands/test-command.md".to_string();
lockfile.commands.push(command);
let manifest = Manifest::new();
let results = install_resources(
ResourceFilter::All,
&Arc::new(lockfile),
&manifest,
project_dir,
cache,
false,
None,
None,
false, None, )
.await
.unwrap();
assert_eq!(results.installed_count, 3, "Should install 3 resources");
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());
}
#[tokio::test]
async fn test_install_updated_resources() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let file1 = temp_dir.path().join("agent.md");
let file2 = temp_dir.path().join("snippet.md");
std::fs::write(&file1, "# Updated Agent").unwrap();
std::fs::write(&file2, "# Updated Snippet").unwrap();
let mut lockfile = LockFile::new();
let mut agent = create_test_locked_resource("test-agent", true);
agent.path = file1.to_string_lossy().to_string();
lockfile.agents.push(agent);
let mut snippet = create_test_locked_resource("test-snippet", true);
snippet.path = file2.to_string_lossy().to_string();
lockfile.snippets.push(snippet);
let manifest = Manifest::new();
let lockfile = Arc::new(lockfile);
let updates = vec![(
"test-agent".to_string(),
None, "v1.0.0".to_string(),
"v1.1.0".to_string(),
)];
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
Some(&manifest),
Some(&lockfile),
None,
None,
None,
None,
None, );
let count = install_updated_resources(
&updates, &lockfile, &manifest, &context, None, false, )
.await
.unwrap();
assert_eq!(count, 1, "Should install 1 updated resource");
assert!(project_dir.join(".claude/agents/test-agent.md").exists());
assert!(!project_dir.join(".claude/snippets/test-snippet.md").exists()); }
#[tokio::test]
async fn test_install_updated_resources_quiet_mode() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let file = temp_dir.path().join("command.md");
std::fs::write(&file, "# Command").unwrap();
let mut lockfile = LockFile::new();
let mut command = create_test_locked_resource("test-command", true);
command.path = file.to_string_lossy().to_string();
command.resource_type = crate::core::ResourceType::Command;
lockfile.commands.push(command);
let manifest = Manifest::new();
let lockfile = Arc::new(lockfile);
let updates = vec![(
"test-command".to_string(),
None, "v1.0.0".to_string(),
"v2.0.0".to_string(),
)];
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
Some(&manifest),
Some(&lockfile),
None,
None,
None,
None,
None, );
let count = install_updated_resources(
&updates, &lockfile, &manifest, &context, None, true, )
.await
.unwrap();
assert_eq!(count, 1);
assert!(project_dir.join(".claude/commands/test-command.md").exists());
}
#[tokio::test]
async fn test_install_resource_for_parallel() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("parallel.md");
std::fs::write(&local_file, "# Parallel Test").unwrap();
let mut entry = create_test_locked_resource("parallel-test", true);
entry.path = local_file.to_string_lossy().to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, ".claude", &context).await;
assert!(result.is_ok());
let expected_path = project_dir.join(&entry.installed_at);
assert!(expected_path.exists());
}
#[tokio::test]
async fn test_install_resource_creates_nested_directories() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("nested.md");
std::fs::write(&local_file, "# Nested Test").unwrap();
let mut entry = create_test_locked_resource("nested-test", true);
entry.path = local_file.to_string_lossy().to_string();
entry.installed_at = "very/deeply/nested/path/resource.md".to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None, );
let result = install_resource(&entry, "agents", &context).await;
assert!(result.is_ok());
let (installed, _checksum, _context_checksum, _applied_patches) = result.unwrap();
assert!(installed, "Should have installed new resource");
let expected_path = project_dir.join("very/deeply/nested/path/resource.md");
assert!(expected_path.exists());
}
#[tokio::test]
async fn test_update_gitignore_creates_new_file() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let mut lockfile = LockFile::new();
let mut agent = create_test_locked_resource("test-agent", true);
agent.installed_at = ".claude/agents/test-agent.md".to_string();
lockfile.agents.push(agent);
let mut snippet = create_test_locked_resource("test-snippet", true);
snippet.installed_at = ".agpm/snippets/test-snippet.md".to_string();
lockfile.snippets.push(snippet);
let result = update_gitignore(&lockfile, project_dir, true);
assert!(result.is_ok());
let gitignore_path = project_dir.join(".gitignore");
assert!(gitignore_path.exists(), "Gitignore file should be created");
let content = std::fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains("AGPM managed entries"));
assert!(content.contains(".claude/agents/test-agent.md"));
assert!(content.contains(".agpm/snippets/test-snippet.md"));
}
#[tokio::test]
async fn test_update_gitignore_disabled() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let lockfile = LockFile::new();
let result = update_gitignore(&lockfile, project_dir, false);
assert!(result.is_ok());
let gitignore_path = project_dir.join(".gitignore");
assert!(!gitignore_path.exists(), "Gitignore should not be created when disabled");
}
#[tokio::test]
async fn test_update_gitignore_preserves_user_entries() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let claude_dir = project_dir.join(".claude");
ensure_dir(&claude_dir).unwrap();
let gitignore_path = project_dir.join(".gitignore");
let existing_content = "# User comment\n\
user-file.txt\n\
# AGPM managed entries - do not edit below this line\n\
.claude/agents/old-entry.md\n\
# End of AGPM managed entries\n";
std::fs::write(&gitignore_path, existing_content).unwrap();
let mut lockfile = LockFile::new();
let mut agent = create_test_locked_resource("new-agent", true);
agent.installed_at = ".claude/agents/new-agent.md".to_string();
lockfile.agents.push(agent);
let result = update_gitignore(&lockfile, project_dir, true);
assert!(result.is_ok());
let updated_content = std::fs::read_to_string(&gitignore_path).unwrap();
assert!(updated_content.contains("user-file.txt"));
assert!(updated_content.contains("# User comment"));
assert!(updated_content.contains(".claude/agents/new-agent.md"));
assert!(!updated_content.contains(".claude/agents/old-entry.md"));
}
#[tokio::test]
async fn test_update_gitignore_handles_external_paths() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let mut lockfile = LockFile::new();
let mut script = create_test_locked_resource("test-script", true);
script.installed_at = "scripts/test.sh".to_string();
lockfile.scripts.push(script);
let mut agent = create_test_locked_resource("test-agent", true);
agent.installed_at = ".claude/agents/test.md".to_string();
lockfile.agents.push(agent);
let result = update_gitignore(&lockfile, project_dir, true);
assert!(result.is_ok());
let gitignore_path = project_dir.join(".gitignore");
let content = std::fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains("scripts/test.sh"));
assert!(content.contains(".claude/agents/test.md"));
}
#[tokio::test]
async fn test_update_gitignore_migrates_ccpm_entries() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
tokio::fs::create_dir_all(project_dir.join(".claude/agents")).await.unwrap();
let gitignore_path = project_dir.join(".gitignore");
let legacy_content = r#"# User's custom entries
temp/
# CCPM managed entries - do not edit below this line
.claude/agents/old-ccpm-agent.md
.claude/commands/old-ccpm-command.md
# End of CCPM managed entries
# More user entries
local-config.json
"#;
tokio::fs::write(&gitignore_path, legacy_content).await.unwrap();
let mut lockfile = LockFile::new();
let mut agent = create_test_locked_resource("new-agent", true);
agent.installed_at = ".claude/agents/new-agent.md".to_string();
lockfile.agents.push(agent);
let result = update_gitignore(&lockfile, project_dir, true);
assert!(result.is_ok());
let updated_content = tokio::fs::read_to_string(&gitignore_path).await.unwrap();
assert!(updated_content.contains("temp/"));
assert!(updated_content.contains("local-config.json"));
assert!(updated_content.contains("# AGPM managed entries - do not edit below this line"));
assert!(updated_content.contains("# End of AGPM managed entries"));
assert!(!updated_content.contains("# CCPM managed entries"));
assert!(!updated_content.contains("# End of CCPM managed entries"));
assert!(!updated_content.contains("old-ccpm-agent.md"));
assert!(!updated_content.contains("old-ccpm-command.md"));
assert!(updated_content.contains(".claude/agents/new-agent.md"));
}
#[tokio::test]
async fn test_install_updated_resources_not_found() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let lockfile = Arc::new(LockFile::new());
let manifest = Manifest::new();
let updates = vec![(
"non-existent".to_string(),
None, "v1.0.0".to_string(),
"v2.0.0".to_string(),
)];
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
Some(&manifest),
Some(&lockfile),
None,
None,
None,
None,
None, );
let count =
install_updated_resources(&updates, &lockfile, &manifest, &context, None, false)
.await
.unwrap();
assert_eq!(count, 0, "Should install 0 resources when not found");
}
#[tokio::test]
async fn test_local_dependency_change_detection() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let local_file = temp_dir.path().join("test.md");
std::fs::write(&local_file, "# Test Resource\nOriginal content").unwrap();
let mut entry = create_test_locked_resource("local-change-test", true);
entry.path = local_file.to_string_lossy().to_string();
entry.installed_at = "agents/local-change-test.md".to_string();
let context = InstallContext::new(
project_dir,
&cache,
false,
false,
None,
None,
None,
None,
None,
None,
None,
);
let result = install_resource(&entry, "agents", &context).await;
assert!(result.is_ok(), "Failed initial install: {:?}", result);
let (installed, checksum1, _, _) = result.unwrap();
assert!(installed, "Should have installed new resource");
let installed_path = project_dir.join("agents/local-change-test.md");
assert!(installed_path.exists(), "Installed file not found");
let content1 = std::fs::read_to_string(&installed_path).unwrap();
assert_eq!(content1, "# Test Resource\nOriginal content");
std::fs::write(&local_file, "# Test Resource\nModified content").unwrap();
let mut old_entry = entry.clone();
old_entry.checksum = checksum1.clone();
let mut old_lockfile = LockFile::default();
old_lockfile.agents.push(old_entry);
let context_with_old = InstallContext::new(
project_dir,
&cache,
false,
false,
None, None, Some(&old_lockfile), None,
None,
None,
None,
);
let result = install_resource(&entry, "agents", &context_with_old).await;
assert!(result.is_ok(), "Failed second install: {:?}", result);
let (reinstalled, checksum2, _, _) = result.unwrap();
assert!(reinstalled, "Should have detected local file change and reinstalled");
assert_ne!(checksum1, checksum2, "Checksum should change when content changes");
let content2 = std::fs::read_to_string(&installed_path).unwrap();
assert_eq!(content2, "# Test Resource\nModified content");
}
#[tokio::test]
async fn test_git_dependency_early_exit_still_works() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let cache = Cache::with_dir(temp_dir.path().join("cache")).unwrap();
let mut entry = create_test_locked_resource("git-test", false);
entry.resolved_commit = Some("a".repeat(40)); entry.checksum = "sha256:test123".to_string();
entry.installed_at = "agents/git-test.md".to_string();
let installed_path = project_dir.join("agents/git-test.md");
ensure_dir(installed_path.parent().unwrap()).unwrap();
std::fs::write(&installed_path, "# Git Resource\nContent").unwrap();
let mut old_lockfile = LockFile::default();
old_lockfile.agents.push(entry.clone());
let _context = InstallContext::new(
project_dir,
&cache,
false,
false,
None, None, Some(&old_lockfile), None,
None,
None,
None,
);
}
}