use agpm_cli::utils::normalize_path_for_storage;
use anyhow::Result;
use std::path::PathBuf;
use tokio::fs;
use crate::common::{ManifestBuilder, TestProject};
async fn setup_test_project() -> Result<TestProject> {
let project = TestProject::new().await?;
let resources_dir = project.sources_path().join("local-resources");
fs::create_dir_all(&resources_dir).await?;
fs::create_dir_all(resources_dir.join("commands")).await?;
fs::create_dir_all(resources_dir.join("agents")).await?;
fs::create_dir_all(resources_dir.join("snippets")).await?;
let resources_url = normalize_path_for_storage(&resources_dir);
let manifest_content = ManifestBuilder::new()
.add_source("local", &resources_url)
.with_target_config(|t| {
t.agents(".claude/agents")
.snippets(".agpm/snippets")
.commands(".claude/commands")
.mcp_servers(".mcp-servers")
.scripts(".claude/scripts")
.hooks(".hooks")
.gitignore(true)
})
.build();
project.write_manifest(&manifest_content).await?;
let command_content = r#"---
title: Test Command
description: A test command with dependencies
dependencies:
agents:
- path: ../agents/test-agent.md
snippets:
- path: ../snippets/test-snippet.md
---
# Test Command
This command depends on an agent and a snippet.
"#;
fs::write(resources_dir.join("commands/test-command.md"), command_content).await?;
let agent_content = r#"---
title: Test Agent
description: A test agent with dependencies
dependencies:
snippets:
- path: ../snippets/helper-snippet.md
---
# Test Agent
This agent depends on a helper snippet.
"#;
fs::write(resources_dir.join("agents/test-agent.md"), agent_content).await?;
fs::write(resources_dir.join("snippets/test-snippet.md"), "# Test Snippet\n\nA test snippet.")
.await?;
fs::write(
resources_dir.join("snippets/helper-snippet.md"),
"# Helper Snippet\n\nA helper snippet.",
)
.await?;
Ok(project)
}
async fn get_lockfile_dependencies(lockfile_path: &PathBuf, resource_name: &str) -> Vec<String> {
let lockfile_content = fs::read_to_string(lockfile_path).await.unwrap();
let lockfile: toml::Value = toml::from_str(&lockfile_content).unwrap();
for resource_type in &["agents", "snippets", "commands", "scripts", "hooks", "mcp_servers"] {
if let Some(resources) = lockfile.get(resource_type).and_then(|v| v.as_array()) {
for resource in resources {
let name_matches = resource
.get("name")
.and_then(|v| v.as_str())
.map(|n| n == resource_name)
.unwrap_or(false);
let alias_matches = resource
.get("manifest_alias")
.and_then(|v| v.as_str())
.map(|a| a == resource_name)
.unwrap_or(false);
if name_matches || alias_matches {
if let Some(deps) = resource.get("dependencies").and_then(|v| v.as_array()) {
return deps.iter().filter_map(|v| v.as_str().map(String::from)).collect();
}
return Vec::new();
}
}
}
}
Vec::new()
}
#[tokio::test]
async fn test_incremental_add_preserves_transitive_dependencies() {
let project = setup_test_project().await.unwrap();
let lockfile_path = project.project_path().join("agpm.lock");
let output = project
.run_agpm(&[
"add",
"dep",
"command",
"local:commands/test-command.md",
"--name",
"test-command",
])
.unwrap();
output.assert_success().assert_stdout_contains("Added command 'test-command'");
assert!(lockfile_path.exists(), "Lockfile should be created");
let command_deps = get_lockfile_dependencies(&lockfile_path, "test-command").await;
assert!(
!command_deps.is_empty(),
"Command should have transitive dependencies after first add. Found: {:?}",
command_deps
);
assert!(
command_deps.iter().any(|d| d.contains("test-agent")),
"Command should depend on test-agent. Found: {:?}",
command_deps
);
let output2 = project
.run_agpm(&["add", "dep", "agent", "local:agents/test-agent.md", "--name", "test-agent"])
.unwrap();
output2.assert_success().assert_stdout_contains("Added agent 'test-agent'");
let command_deps_after = get_lockfile_dependencies(&lockfile_path, "test-command").await;
assert!(
!command_deps_after.is_empty(),
"Command dependencies should be preserved after adding agent as base dep! \
This was the bug we fixed. Found: {:?}",
command_deps_after
);
assert!(
command_deps_after.iter().any(|d| d.contains("test-agent")),
"Command -> Agent dependency should be maintained! Found: {:?}",
command_deps_after
);
let agent_deps = get_lockfile_dependencies(&lockfile_path, "test-agent").await;
assert!(
!agent_deps.is_empty(),
"Agent should have its transitive dependencies. Found: {:?}",
agent_deps
);
assert!(
agent_deps.iter().any(|d| d.contains("helper-snippet")),
"Agent should depend on helper-snippet. Found: {:?}",
agent_deps
);
}
#[tokio::test]
async fn test_incremental_add_chain_of_three_dependencies() {
let project = setup_test_project().await.unwrap();
let lockfile_path = project.project_path().join("agpm.lock");
project
.run_agpm(&[
"add",
"dep",
"command",
"local:commands/test-command.md",
"--name",
"test-command",
])
.unwrap()
.assert_success();
let command_deps_step1 = get_lockfile_dependencies(&lockfile_path, "test-command").await;
assert!(!command_deps_step1.is_empty(), "Command should have dependencies");
project
.run_agpm(&["add", "dep", "agent", "local:agents/test-agent.md", "--name", "test-agent"])
.unwrap()
.assert_success();
let command_deps_step2 = get_lockfile_dependencies(&lockfile_path, "test-command").await;
let agent_deps_step2 = get_lockfile_dependencies(&lockfile_path, "test-agent").await;
assert!(!command_deps_step2.is_empty(), "Command deps should persist after step 2");
assert!(!agent_deps_step2.is_empty(), "Agent should have dependencies");
project
.run_agpm(&[
"add",
"dep",
"snippet",
"local:snippets/helper-snippet.md",
"--name",
"helper-snippet",
])
.unwrap()
.assert_success();
let command_deps_final = get_lockfile_dependencies(&lockfile_path, "test-command").await;
let agent_deps_final = get_lockfile_dependencies(&lockfile_path, "test-agent").await;
assert!(
!command_deps_final.is_empty(),
"Command should still have dependencies after all adds"
);
assert!(
!agent_deps_final.is_empty(),
"Agent should still have dependencies after helper-snippet becomes base dep"
);
assert!(
command_deps_final.iter().any(|d| d.contains("test-agent")),
"Command -> Agent dependency should be maintained"
);
assert!(
agent_deps_final.iter().any(|d| d.contains("helper-snippet")),
"Agent -> Snippet dependency should be maintained"
);
}
#[tokio::test]
async fn test_incremental_add_with_shared_dependency() {
let project = setup_test_project().await.unwrap();
let resources_dir = project.sources_path().join("local-resources");
let command2_content = r#"---
title: Second Command
dependencies:
snippets:
- path: ../snippets/test-snippet.md
---
# Second Command
This command also uses test-snippet.
"#;
fs::write(resources_dir.join("commands/second-command.md"), command2_content).await.unwrap();
let lockfile_path = project.project_path().join("agpm.lock");
project
.run_agpm(&[
"add",
"dep",
"command",
"local:commands/test-command.md",
"--name",
"test-command",
])
.unwrap()
.assert_success();
project
.run_agpm(&[
"add",
"dep",
"command",
"local:commands/second-command.md",
"--name",
"second-command",
])
.unwrap()
.assert_success();
let cmd1_deps = get_lockfile_dependencies(&lockfile_path, "test-command").await;
let cmd2_deps = get_lockfile_dependencies(&lockfile_path, "second-command").await;
assert!(!cmd1_deps.is_empty(), "First command should have dependencies");
assert!(!cmd2_deps.is_empty(), "Second command should have dependencies");
project
.run_agpm(&[
"add",
"dep",
"snippet",
"local:snippets/test-snippet.md",
"--name",
"test-snippet",
])
.unwrap()
.assert_success();
let cmd1_deps_after = get_lockfile_dependencies(&lockfile_path, "test-command").await;
let cmd2_deps_after = get_lockfile_dependencies(&lockfile_path, "second-command").await;
assert!(
cmd1_deps_after.iter().any(|d| d.contains("test-snippet")),
"First command should still depend on test-snippet"
);
assert!(
cmd2_deps_after.iter().any(|d| d.contains("test-snippet")),
"Second command should still depend on test-snippet"
);
}