use crate::common::{ManifestBuilder, TestProject};
use anyhow::Result;
use tokio::fs;
#[tokio::test]
async fn test_migrate_ccpm_toml_to_agpm_toml() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "helper", "# Helper Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let ccpm_manifest = format!(
r#"[sources]
community = "{}"
[agents]
helper = {{ source = "community", path = "agents/helper.md", version = "v1.0.0" }}
"#,
source_url
);
let ccpm_toml_path = project.project_path().join("ccpm.toml");
let agpm_toml_path = project.project_path().join("agpm.toml");
fs::write(&ccpm_toml_path, &ccpm_manifest).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(!ccpm_toml_path.exists(), "ccpm.toml should be renamed");
assert!(agpm_toml_path.exists(), "agpm.toml should exist");
let agpm_content = fs::read_to_string(&agpm_toml_path).await?;
assert!(agpm_content.contains("community"), "Content should be preserved");
assert!(agpm_content.contains("agents/helper.md"), "Dependencies should be preserved");
let install_output = project.run_agpm(&["install"])?;
install_output.assert_success();
let agent_path = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(agent_path.exists(), "Agent should be installed after migration");
Ok(())
}
#[tokio::test]
async fn test_migrate_ccpm_lock_to_agpm_lock() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "test", "# Test Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let ccpm_manifest = format!(
r#"[sources]
community = "{}"
[agents]
test = {{ source = "community", path = "agents/test.md", version = "v1.0.0" }}
"#,
source_url
);
let ccpm_toml_path = project.project_path().join("ccpm.toml");
let ccpm_lock_path = project.project_path().join("ccpm.lock");
let agpm_toml_path = project.project_path().join("agpm.toml");
let agpm_lock_path = project.project_path().join("agpm.lock");
fs::write(&ccpm_toml_path, &ccpm_manifest).await?;
let ccpm_lock = "version = 1\n";
fs::write(&ccpm_lock_path, ccpm_lock).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(!ccpm_toml_path.exists(), "ccpm.toml should be renamed");
assert!(!ccpm_lock_path.exists(), "ccpm.lock should be renamed");
assert!(agpm_toml_path.exists(), "agpm.toml should exist");
assert!(agpm_lock_path.exists(), "agpm.lock should exist");
Ok(())
}
#[tokio::test]
async fn test_migrate_ccpm_with_existing_agpm_conflicts() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let ccpm_toml_path = project.project_path().join("ccpm.toml");
let agpm_toml_path = project.project_path().join("agpm.toml");
fs::write(&ccpm_toml_path, "[sources]\n").await?;
fs::write(&agpm_toml_path, "[sources]\n").await?;
let output = project.run_agpm(&["migrate"])?;
assert!(!output.success, "Should fail when both ccpm.toml and agpm.toml exist");
let error_output = format!("{}\n{}", output.stdout, output.stderr);
assert!(
error_output.to_lowercase().contains("conflict"),
"Error should mention conflict. Output: {}",
error_output
);
assert!(ccpm_toml_path.exists(), "ccpm.toml should remain");
assert!(agpm_toml_path.exists(), "agpm.toml should remain");
Ok(())
}
#[tokio::test]
async fn test_migrate_ccpm_preserves_dependencies() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "agent1", "# Agent 1").await?;
source_repo.add_resource("agents", "agent2", "# Agent 2").await?;
source_repo.add_resource("snippets", "snippet1", "# Snippet 1").await?;
source_repo.add_resource("commands", "command1", "# Command 1").await?;
source_repo.commit_all("Add resources")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let ccpm_manifest = format!(
r#"[sources]
community = "{}"
[agents]
agent1 = {{ source = "community", path = "agents/agent1.md", version = "v1.0.0" }}
agent2 = {{ source = "community", path = "agents/agent2.md", version = "v1.0.0" }}
[snippets]
snippet1 = {{ source = "community", path = "snippets/snippet1.md", version = "v1.0.0" }}
[commands]
command1 = {{ source = "community", path = "commands/command1.md", version = "v1.0.0" }}
"#,
source_url
);
fs::write(project.project_path().join("ccpm.toml"), &ccpm_manifest).await?;
let migrate_output = project.run_agpm(&["migrate", "--skip-install"])?;
migrate_output.assert_success();
let install_output = project.run_agpm(&["install"])?;
install_output.assert_success();
let agents_dir = project.project_path().join(".claude/agents/agpm");
let snippets_dir = project.project_path().join(".agpm/snippets");
let commands_dir = project.project_path().join(".claude/commands/agpm");
assert!(agents_dir.join("agent1.md").exists(), "agent1 should be installed");
assert!(agents_dir.join("agent2.md").exists(), "agent2 should be installed");
assert!(snippets_dir.join("snippet1.md").exists(), "snippet1 should be installed");
assert!(commands_dir.join("command1.md").exists(), "command1 should be installed");
Ok(())
}
#[tokio::test]
async fn test_migrate_removes_managed_gitignore_section() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let gitignore_content = r#"# User entries before
node_modules/
*.log
# AGPM managed entries - do not edit
.claude/agents/*.md
.claude/snippets/*.md
# End of AGPM managed entries
# User entries after
.env
"#;
fs::write(project.project_path().join(".gitignore"), gitignore_content).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test Agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "test"
source = "test"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/test.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_gitignore = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(
!new_gitignore.contains("AGPM managed entries - do not edit"),
"Old managed section marker should be removed"
);
assert!(!new_gitignore.contains("End of AGPM managed entries"), "End marker should be removed");
assert!(
new_gitignore.contains("# AGPM managed paths"),
"New header should be present. Gitignore:\n{}",
new_gitignore
);
assert!(new_gitignore.contains(".claude/*/agpm/"), "Claude agpm paths should be added");
assert!(new_gitignore.contains(".opencode/*/agpm/"), "OpenCode agpm paths should be added");
assert!(new_gitignore.contains(".agpm/"), "Cache dir should be added");
assert!(new_gitignore.contains("agpm.private.toml"), "Private toml should be added");
assert!(new_gitignore.contains("agpm.private.lock"), "Private lock should be added");
assert!(new_gitignore.contains("node_modules/"), "User entries before should be preserved");
assert!(new_gitignore.contains(".env"), "User entries after should be preserved");
assert!(
agents_dir.join("agpm/test.md").exists(),
"Resource should be moved to agpm/ subdirectory"
);
assert!(!agents_dir.join("test.md").exists(), "Original resource should be removed");
Ok(())
}
#[tokio::test]
async fn test_migrate_ccpm_managed_gitignore_section() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let gitignore_content = r#"# User entries
build/
# CCPM managed entries - do not edit
.claude/agents/*.md
# End of CCPM managed entries
"#;
fs::write(project.project_path().join(".gitignore"), gitignore_content).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("legacy.md"), "# Legacy Agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "legacy"
source = "test"
path = "agents/legacy.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/legacy.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_gitignore = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(
!new_gitignore.contains("CCPM managed entries"),
"CCPM managed section should be removed"
);
assert!(new_gitignore.contains("# AGPM managed paths"), "New header should be present");
assert!(new_gitignore.contains(".claude/*/agpm/"), "New paths should be added");
assert!(new_gitignore.contains("build/"), "User entries should be preserved");
Ok(())
}
#[tokio::test]
async fn test_migrate_mixed_content_gitignore() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let gitignore_content = r#"# === User Section 1 ===
node_modules/
dist/
*.log
# AGPM managed entries - do not edit
.claude/agents/*.md
.claude/commands/*.md
.claude/snippets/*.md
# End of AGPM managed entries
# === User Section 2 ===
.env
.env.local
secrets/
# More user content
coverage/
"#;
fs::write(project.project_path().join(".gitignore"), gitignore_content).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "test"
source = "test"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/test.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_gitignore = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(new_gitignore.contains("User Section 1"), "Section 1 should be preserved");
assert!(new_gitignore.contains("User Section 2"), "Section 2 should be preserved");
assert!(new_gitignore.contains("node_modules/"), "node_modules should be preserved");
assert!(new_gitignore.contains(".env"), ".env should be preserved");
assert!(new_gitignore.contains("secrets/"), "secrets/ should be preserved");
assert!(new_gitignore.contains("coverage/"), "coverage/ should be preserved");
assert!(
!new_gitignore.contains("AGPM managed entries - do not edit"),
"Old managed section should be removed"
);
assert!(new_gitignore.contains("# AGPM managed paths"), "New header should be present");
assert!(new_gitignore.contains(".claude/*/agpm/"), "New paths should be added");
Ok(())
}
#[tokio::test]
async fn test_migrate_gitignore_preserves_formatting() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let gitignore_content = r#"# Comment line
*.log
# Another comment
node_modules/
# AGPM managed entries
.claude/agents/*.md
# End of AGPM managed entries
# Final section
.env
"#;
fs::write(project.project_path().join(".gitignore"), gitignore_content).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_gitignore = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(new_gitignore.contains("# Comment line"), "Comments should be preserved");
assert!(new_gitignore.contains("# Another comment"), "Comments should be preserved");
assert!(new_gitignore.contains("# Final section"), "Comments should be preserved");
Ok(())
}
#[tokio::test]
async fn test_migrate_moves_agents_to_agpm_subdirectory() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("agent1.md"), "# Agent 1").await?;
fs::write(agents_dir.join("agent2.md"), "# Agent 2").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "agent1"
source = "test"
path = "agents/agent1.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/agent1.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
[[agents]]
name = "agent2"
source = "test"
path = "agents/agent2.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/agent2.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let agpm_dir = agents_dir.join("agpm");
assert!(agpm_dir.join("agent1.md").exists(), "agent1 should be in agpm/");
assert!(agpm_dir.join("agent2.md").exists(), "agent2 should be in agpm/");
assert!(!agents_dir.join("agent1.md").exists(), "agent1 should not be at old path");
assert!(!agents_dir.join("agent2.md").exists(), "agent2 should not be at old path");
Ok(())
}
#[tokio::test]
async fn test_migrate_moves_all_resource_types() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let claude_dir = project.project_path().join(".claude");
fs::create_dir_all(claude_dir.join("agents")).await?;
fs::create_dir_all(claude_dir.join("commands")).await?;
fs::create_dir_all(claude_dir.join("snippets")).await?;
fs::create_dir_all(claude_dir.join("scripts")).await?;
fs::write(claude_dir.join("agents/agent.md"), "# Agent").await?;
fs::write(claude_dir.join("commands/cmd.md"), "# Command").await?;
fs::write(claude_dir.join("snippets/snip.md"), "# Snippet").await?;
fs::write(claude_dir.join("scripts/script.md"), "# Script").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "agent"
source = "test"
path = "agents/agent.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/agent.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
[[commands]]
name = "cmd"
source = "test"
path = "commands/cmd.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/commands/cmd.md"
dependencies = []
resource_type = "Command"
tool = "claude-code"
[[snippets]]
name = "snip"
source = "test"
path = "snippets/snip.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/snippets/snip.md"
dependencies = []
resource_type = "Snippet"
tool = "agpm"
[[scripts]]
name = "script"
source = "test"
path = "scripts/script.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/scripts/script.md"
dependencies = []
resource_type = "Script"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(claude_dir.join("agents/agpm/agent.md").exists(), "Agent should be moved");
assert!(claude_dir.join("commands/agpm/cmd.md").exists(), "Command should be moved");
assert!(claude_dir.join("snippets/agpm/snip.md").exists(), "Snippet should be moved");
assert!(claude_dir.join("scripts/agpm/script.md").exists(), "Script should be moved");
assert!(!claude_dir.join("agents/agent.md").exists(), "Agent should not be at old path");
assert!(!claude_dir.join("commands/cmd.md").exists(), "Command should not be at old path");
assert!(!claude_dir.join("snippets/snip.md").exists(), "Snippet should not be at old path");
assert!(!claude_dir.join("scripts/script.md").exists(), "Script should not be at old path");
Ok(())
}
#[tokio::test]
async fn test_migrate_updates_lockfile_paths() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let old_lockfile = r#"version = 1
[[agents]]
name = "test"
source = "community"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:placeholder"
context_checksum = "sha256:placeholder"
installed_at = ".claude/agents/test.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), old_lockfile).await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test").await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_lockfile = project.read_lockfile().await?;
assert!(
new_lockfile.contains(".claude/agents/agpm/test.md"),
"Lockfile should have updated path. Actual:\n{}",
new_lockfile
);
assert!(
!new_lockfile.contains("installed_at = \".claude/agents/test.md\""),
"Old path format should be replaced"
);
Ok(())
}
#[tokio::test]
async fn test_migrate_opencode_paths() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let opencode_dir = project.project_path().join(".opencode");
fs::create_dir_all(opencode_dir.join("agent")).await?;
fs::write(opencode_dir.join("agent/test.md"), "# OpenCode Agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "test"
source = "test"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".opencode/agent/test.md"
dependencies = []
resource_type = "Agent"
tool = "opencode"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(
opencode_dir.join("agent/agpm/test.md").exists(),
"OpenCode agent should be moved to agpm/"
);
assert!(
!opencode_dir.join("agent/test.md").exists(),
"OpenCode agent should not be at old path"
);
Ok(())
}
#[tokio::test]
async fn test_migrate_multi_tool_project() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let claude_dir = project.project_path().join(".claude/agents");
let opencode_dir = project.project_path().join(".opencode/agent");
fs::create_dir_all(&claude_dir).await?;
fs::create_dir_all(&opencode_dir).await?;
fs::write(claude_dir.join("claude-agent.md"), "# Claude Agent").await?;
fs::write(opencode_dir.join("opencode-agent.md"), "# OpenCode Agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "claude-agent"
source = "test"
path = "agents/claude-agent.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/claude-agent.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
[[agents]]
name = "opencode-agent"
source = "test"
path = "agents/opencode-agent.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".opencode/agent/opencode-agent.md"
dependencies = []
resource_type = "Agent"
tool = "opencode"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(
claude_dir.join("agpm/claude-agent.md").exists(),
"Claude agent should be moved to agpm/"
);
assert!(
opencode_dir.join("agpm/opencode-agent.md").exists(),
"OpenCode agent should be moved to agpm/"
);
assert!(!claude_dir.join("claude-agent.md").exists(), "Claude agent should not be at old path");
assert!(
!opencode_dir.join("opencode-agent.md").exists(),
"OpenCode agent should not be at old path"
);
Ok(())
}
#[tokio::test]
async fn test_migrate_multi_tool_lockfile_paths() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let old_lockfile = r#"version = 1
[[agents]]
name = "claude-agent"
source = "community"
path = "agents/claude.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:placeholder"
context_checksum = "sha256:placeholder"
installed_at = ".claude/agents/claude.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
[[agents]]
name = "opencode-agent"
source = "community"
path = "agents/opencode.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:placeholder"
context_checksum = "sha256:placeholder"
installed_at = ".opencode/agent/opencode.md"
dependencies = []
resource_type = "Agent"
tool = "opencode"
"#;
fs::write(project.project_path().join("agpm.lock"), old_lockfile).await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let claude_dir = project.project_path().join(".claude/agents");
let opencode_dir = project.project_path().join(".opencode/agent");
fs::create_dir_all(&claude_dir).await?;
fs::create_dir_all(&opencode_dir).await?;
fs::write(claude_dir.join("claude.md"), "# Claude").await?;
fs::write(opencode_dir.join("opencode.md"), "# OpenCode").await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let new_lockfile = project.read_lockfile().await?;
assert!(
new_lockfile.contains(".claude/agents/agpm/"),
"Claude path should be updated. Lockfile:\n{}",
new_lockfile
);
assert!(
new_lockfile.contains(".opencode/agent/agpm/"),
"OpenCode path should be updated. Lockfile:\n{}",
new_lockfile
);
Ok(())
}
#[tokio::test]
async fn test_migrate_no_orphan_files_after_migration() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "test", "# Test Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
community = "{}"
[agents]
test = {{ source = "community", path = "agents/test.md", version = "v1.0.0" }}
"#,
source_url
);
fs::write(project.project_path().join("ccpm.toml"), &manifest).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test").await?;
let lockfile = r#"version = 1
[[agents]]
name = "test"
source = "community"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/test.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("ccpm.lock"), lockfile).await?;
let migrate_output = project.run_agpm(&["migrate"])?;
migrate_output.assert_success();
assert!(!agents_dir.join("test.md").exists(), "No files should remain at old path");
assert!(agents_dir.join("agpm/test.md").exists(), "Files should be at new path");
let lockfile = project.load_lockfile()?;
for agent in &lockfile.agents {
let installed_path = project.project_path().join(&agent.installed_at);
assert!(
installed_path.exists(),
"Lockfile path {} should exist on disk",
agent.installed_at
);
}
Ok(())
}
#[tokio::test]
async fn test_migrate_lockfile_filesystem_consistency() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "agent1", "# Agent 1").await?;
source_repo.add_resource("agents", "agent2", "# Agent 2").await?;
source_repo.add_resource("snippets", "snippet1", "# Snippet 1").await?;
source_repo.commit_all("Add resources")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("agent1", "community", "agents/agent1.md")
.add_standard_agent("agent2", "community", "agents/agent2.md")
.add_snippet("snippet1", |d| {
d.source("community").path("snippets/snippet1.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
let lockfile = project.load_lockfile()?;
for agent in &lockfile.agents {
let path = project.project_path().join(&agent.installed_at);
assert!(path.exists(), "Agent {} should exist at {}", agent.name, agent.installed_at);
}
for snippet in &lockfile.snippets {
let path = project.project_path().join(&snippet.installed_at);
assert!(path.exists(), "Snippet {} should exist at {}", snippet.name, snippet.installed_at);
}
let agents_agpm_dir = project.project_path().join(".claude/agents/agpm");
if agents_agpm_dir.exists() {
let agent_files: Vec<_> = std::fs::read_dir(&agents_agpm_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
.collect();
assert_eq!(
agent_files.len(),
lockfile.agents.len(),
"Agent file count should match lockfile"
);
}
Ok(())
}
#[tokio::test]
async fn test_migrate_stale_directory_cleanup() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join(".claude/agents");
let commands_dir = project.project_path().join(".claude/commands");
fs::create_dir_all(&agents_dir).await?;
fs::create_dir_all(&commands_dir).await?;
fs::write(agents_dir.join("agent.md"), "# Agent").await?;
fs::write(commands_dir.join("cmd.md"), "# Command").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "agent"
source = "test"
path = "agents/agent.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/agent.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
[[commands]]
name = "cmd"
source = "test"
path = "commands/cmd.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/commands/cmd.md"
dependencies = []
resource_type = "Command"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(agents_dir.exists(), "agents directory should still exist");
assert!(commands_dir.exists(), "commands directory should still exist");
assert!(agents_dir.join("agpm/agent.md").exists(), "Agent should be in agpm/");
assert!(commands_dir.join("agpm/cmd.md").exists(), "Command should be in agpm/");
Ok(())
}
#[tokio::test]
async fn test_migrate_cleanup_with_dependency_removal() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "keep", "# Keep Agent").await?;
source_repo.add_resource("agents", "remove", "# Remove Agent").await?;
source_repo.commit_all("Add agents")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest1 = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("keep", "community", "agents/keep.md")
.add_standard_agent("remove", "community", "agents/remove.md")
.build();
project.write_manifest(&manifest1).await?;
let output1 = project.run_agpm(&["install"])?;
output1.assert_success();
let agents_dir = project.project_path().join(".claude/agents/agpm");
assert!(agents_dir.join("keep.md").exists(), "keep agent should be installed");
assert!(agents_dir.join("remove.md").exists(), "remove agent should be installed");
let manifest2 = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("keep", "community", "agents/keep.md")
.build();
project.write_manifest(&manifest2).await?;
let output2 = project.run_agpm(&["install"])?;
output2.assert_success();
assert!(agents_dir.join("keep.md").exists(), "keep agent should still exist");
assert!(!agents_dir.join("remove.md").exists(), "remove agent should be cleaned up");
let lockfile = project.load_lockfile()?;
assert_eq!(lockfile.agents.len(), 1, "Lockfile should have 1 agent");
Ok(())
}
#[tokio::test]
async fn test_migrate_preserves_user_files() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("agpm-managed.md"), "# AGPM Managed").await?;
fs::write(agents_dir.join("user-notes.txt"), "User notes").await?;
fs::write(agents_dir.join("README"), "User readme").await?;
fs::write(agents_dir.join("user-agent.md"), "# User created agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "agpm-managed"
source = "test"
path = "agents/agpm-managed.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/agpm-managed.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(agents_dir.join("agpm/agpm-managed.md").exists(), "AGPM file should be moved");
assert!(!agents_dir.join("agpm-managed.md").exists(), "AGPM file should not be at old path");
assert!(agents_dir.join("user-notes.txt").exists(), "User .txt file should remain");
assert!(agents_dir.join("README").exists(), "User README should remain");
assert!(agents_dir.join("user-agent.md").exists(), "User .md file should remain (not tracked)");
Ok(())
}
#[tokio::test]
async fn test_migrate_resource_count_validation() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "a1", "# Agent 1").await?;
source_repo.add_resource("agents", "a2", "# Agent 2").await?;
source_repo.add_resource("agents", "a3", "# Agent 3").await?;
source_repo.commit_all("Add agents")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("a1", "community", "agents/a1.md")
.add_standard_agent("a2", "community", "agents/a2.md")
.add_standard_agent("a3", "community", "agents/a3.md")
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
let lockfile = project.load_lockfile()?;
assert_eq!(lockfile.agents.len(), 3, "Lockfile should have 3 agents");
let agents_dir = project.project_path().join(".claude/agents/agpm");
let actual_count = std::fs::read_dir(&agents_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
.count();
assert_eq!(actual_count, 3, "Should have 3 actual agent files");
Ok(())
}
#[tokio::test]
async fn test_migrate_cli_dry_run_output() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
fs::write(project.project_path().join("ccpm.toml"), "[sources]\n").await?;
let output = project.run_agpm(&["migrate", "--dry-run"])?;
output.assert_success();
assert!(
project.project_path().join("ccpm.toml").exists(),
"ccpm.toml should not be renamed in dry-run"
);
assert!(
!project.project_path().join("agpm.toml").exists(),
"agpm.toml should not be created in dry-run"
);
let combined_output = format!("{}\n{}", output.stdout, output.stderr);
assert!(
combined_output.contains("ccpm.toml") || combined_output.contains("CCPM"),
"Output should mention CCPM files. Output: {}",
combined_output
);
Ok(())
}
#[tokio::test]
async fn test_migrate_cli_format_only_flag() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
fs::write(project.project_path().join("ccpm.toml"), "[sources]\n").await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("test.md"), "# Test").await?;
let lockfile = r#"version = 1
[[agents]]
name = "test"
source = "test"
path = "agents/test.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/test.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--format-only", "--skip-install"])?;
output.assert_success();
assert!(
project.project_path().join("ccpm.toml").exists(),
"ccpm.toml should NOT be renamed with --format-only"
);
assert!(agents_dir.join("agpm/test.md").exists(), "Resources should still be moved");
Ok(())
}
#[tokio::test]
async fn test_migrate_cli_skip_install_flag() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "test", "# Test").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
community = "{}"
[agents]
test = {{ source = "community", path = "agents/test.md", version = "v1.0.0" }}
"#,
source_url
);
fs::write(project.project_path().join("ccpm.toml"), &manifest).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(project.project_path().join("agpm.toml").exists(), "agpm.toml should exist");
let agents_dir = project.project_path().join(".claude/agents/agpm");
assert!(
!agents_dir.join("test.md").exists(),
"Agent should not be installed with --skip-install"
);
Ok(())
}
#[tokio::test]
async fn test_migrate_full_workflow_e2e() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "workflow-agent", "# Workflow Agent").await?;
source_repo.add_resource("snippets", "workflow-snippet", "# Workflow Snippet").await?;
source_repo.commit_all("Add resources")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
community = "{}"
[agents]
workflow = {{ source = "community", path = "agents/workflow-agent.md", version = "v1.0.0" }}
[snippets]
workflow = {{ source = "community", path = "snippets/workflow-snippet.md", version = "v1.0.0" }}
"#,
source_url
);
fs::write(project.project_path().join("ccpm.toml"), &manifest).await?;
let gitignore = r#"node_modules/
# AGPM managed entries
.claude/agents/*.md
# End of AGPM managed entries
"#;
fs::write(project.project_path().join(".gitignore"), gitignore).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("old-agent.md"), "# Old Agent").await?;
let migrate_output = project.run_agpm(&["migrate"])?;
migrate_output.assert_success();
assert!(project.project_path().join("agpm.toml").exists(), "agpm.toml should exist");
assert!(!project.project_path().join("ccpm.toml").exists(), "ccpm.toml should be gone");
let new_gitignore = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(
!new_gitignore.contains("AGPM managed entries\n"),
"Old managed section header should be removed"
);
assert!(
new_gitignore.contains("# AGPM managed paths"),
"New paths section should be added. Gitignore:\n{}",
new_gitignore
);
assert!(
new_gitignore.contains(".claude/*/agpm/"),
"New paths should include agpm subdirectory"
);
assert!(
project.project_path().join(".claude/agents/agpm/workflow-agent.md").exists(),
"Installed agent should be at new path"
);
let validate_output = project.run_agpm(&["validate"])?;
assert!(
validate_output.success || validate_output.stderr.contains("warning"),
"Validate should pass or only warn"
);
Ok(())
}
#[tokio::test]
async fn test_migrate_empty_project() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
let combined_output = format!("{}\n{}", output.stdout, output.stderr);
assert!(
combined_output.contains("No migration") || combined_output.contains("up to date"),
"Should indicate no migration needed. Output: {}",
combined_output
);
Ok(())
}
#[tokio::test]
async fn test_migrate_already_migrated() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("community").await?;
source_repo.add_resource("agents", "test", "# Test").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("test", "community", "agents/test.md")
.build();
project.write_manifest(&manifest).await?;
let install_output = project.run_agpm(&["install"])?;
install_output.assert_success();
let migrate_output1 = project.run_agpm(&["migrate", "--skip-install"])?;
migrate_output1.assert_success();
let migrate_output2 = project.run_agpm(&["migrate", "--skip-install"])?;
migrate_output2.assert_success();
let agent_path = project.project_path().join(".claude/agents/agpm/test.md");
assert!(agent_path.exists(), "Agent should still be at correct path");
Ok(())
}
#[tokio::test]
async fn test_migrate_nested_resource_paths() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(agents_dir.join("subdir")).await?;
fs::write(agents_dir.join("direct-agent.md"), "# Direct Agent").await?;
fs::write(agents_dir.join("subdir/nested-agent.md"), "# Nested Agent").await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let lockfile = r#"version = 1
[[agents]]
name = "direct-agent"
source = "test"
path = "agents/direct-agent.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/direct-agent.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["migrate", "--skip-install"])?;
output.assert_success();
assert!(
agents_dir.join("agpm/direct-agent.md").exists(),
"Direct agent should be moved to agpm/"
);
assert!(!agents_dir.join("direct-agent.md").exists(), "Direct agent should not be at old path");
assert!(
agents_dir.join("subdir/nested-agent.md").exists(),
"Nested agent in subdirectory should remain (not auto-migrated)"
);
Ok(())
}
#[tokio::test]
async fn test_install_detects_legacy_format_non_interactive() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test").await?;
source_repo.add_resource("agents", "helper", "# Helper Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test", &source_url)
.add_agent("helper", |d| d.source("test").path("agents/helper.md").version("v1.0.0"))
.build();
fs::write(project.project_path().join("agpm.toml"), &manifest).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("helper.md"), "# Helper Agent").await?;
let lockfile = r#"version = 1
[[agents]]
name = "helper"
source = "test"
path = "agents/helper.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/helper.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let output = project.run_agpm(&["install"])?;
let stderr = &output.stderr;
assert!(
stderr.contains("Legacy AGPM format detected") || stderr.contains("agpm migrate"),
"Should detect legacy format and suggest migration. stderr: {}",
stderr
);
Ok(())
}
#[tokio::test]
async fn test_install_detects_legacy_gitignore_section_non_interactive() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
fs::write(project.project_path().join("agpm.toml"), "[sources]\n").await?;
let gitignore = r#"# User entries
node_modules/
# AGPM managed entries - do not edit below this line
.claude/agents/test.md
# End of AGPM managed entries
"#;
fs::write(project.project_path().join(".gitignore"), gitignore).await?;
fs::write(project.project_path().join("agpm.lock"), "version = 1\n").await?;
let output = project.run_agpm(&["install"])?;
let stderr = &output.stderr;
assert!(
stderr.contains("Legacy AGPM format detected") || stderr.contains("agpm migrate"),
"Should detect legacy format (gitignore section) and suggest migration. stderr: {}",
stderr
);
let gitignore_content = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(
gitignore_content.contains("# AGPM managed entries"),
"gitignore should NOT be modified (non-interactive mode)"
);
Ok(())
}
#[tokio::test]
async fn test_install_after_migration_works() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test").await?;
source_repo.add_resource("agents", "helper", "# Helper Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test", &source_url)
.add_agent("helper", |d| d.source("test").path("agents/helper.md").version("v1.0.0"))
.build();
fs::write(project.project_path().join("agpm.toml"), &manifest).await?;
let agents_dir = project.project_path().join(".claude/agents");
fs::create_dir_all(&agents_dir).await?;
fs::write(agents_dir.join("helper.md"), "# Helper Agent").await?;
let lockfile = r#"version = 1
[[agents]]
name = "helper"
source = "test"
path = "agents/helper.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/helper.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let migrate_output = project.run_agpm(&["migrate", "--skip-install"])?;
migrate_output.assert_success();
assert!(
agents_dir.join("agpm/helper.md").exists(),
"Agent should be at new path after migration"
);
let install_output = project.run_agpm(&["install"])?;
install_output.assert_success();
let new_agent_path = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(new_agent_path.exists(), "Agent should remain at new path");
Ok(())
}
#[tokio::test]
async fn test_install_with_yes_flag_performs_full_migration() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test").await?;
source_repo.add_resource("agents", "helper", "# Helper Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
test = "{source_url}"
[tools.claude-code]
path = ".claude"
resources = {{ agents = {{ path = "agents", flatten = true }} }}
[agents]
helper = {{ source = "test", path = "agents/helper.md", version = "v1.0.0" }}
"#
);
fs::write(project.project_path().join("agpm.toml"), &manifest).await?;
let old_agent_path = project.project_path().join(".claude/agents/helper.md");
fs::create_dir_all(old_agent_path.parent().unwrap()).await?;
fs::write(&old_agent_path, "# Helper Agent").await?;
let lockfile = r#"version = 1
[[agents]]
name = "helper"
source = "test"
path = "agents/helper.md"
version = "v1.0.0"
resolved_commit = "abc123"
checksum = "sha256:abc"
context_checksum = "sha256:def"
installed_at = ".claude/agents/helper.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
"#;
fs::write(project.project_path().join("agpm.lock"), lockfile).await?;
let gitignore = r#"# User entries
node_modules/
# AGPM managed entries - do not edit below this line
.claude/agents/helper.md
# End of AGPM managed entries
"#;
fs::write(project.project_path().join(".gitignore"), gitignore).await?;
let output = project.run_agpm(&["install", "-y"])?;
output.assert_success();
let manifest_content = fs::read_to_string(project.project_path().join("agpm.toml")).await?;
assert!(
manifest_content.contains("# [tools.claude-code]"),
"Manifest should have commented-out tools section. Content:\n{}",
manifest_content
);
assert!(
manifest_content.contains("agents/agpm"),
"Manifest should reference agents/agpm in comments. Content:\n{}",
manifest_content
);
assert!(
!manifest_content.contains("\n[tools.claude-code]"),
"Manifest should NOT have active [tools.claude-code] section. Content:\n{}",
manifest_content
);
assert!(!old_agent_path.exists(), "Old artifact at {:?} should be cleaned up", old_agent_path);
let new_agent_path = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(new_agent_path.exists(), "New artifact should exist at {:?}", new_agent_path);
let lockfile_content = fs::read_to_string(project.project_path().join("agpm.lock")).await?;
assert!(
lockfile_content.contains(".claude/agents/agpm/helper.md"),
"Lockfile should have new installed_at path. Content:\n{}",
lockfile_content
);
let gitignore_content = fs::read_to_string(project.project_path().join(".gitignore")).await?;
assert!(
!gitignore_content.contains("# AGPM managed entries"),
".gitignore should NOT have old marker. Content:\n{}",
gitignore_content
);
assert!(
gitignore_content.contains("# AGPM managed paths"),
".gitignore should have new marker. Content:\n{}",
gitignore_content
);
assert!(
gitignore_content.contains(".claude/*/agpm/"),
".gitignore should have new wildcard pattern. Content:\n{}",
gitignore_content
);
assert!(
gitignore_content.contains("node_modules/"),
".gitignore should preserve user content. Content:\n{}",
gitignore_content
);
Ok(())
}