use assert_cmd::Command;
use predicates::prelude::*;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::fs;
async fn setup_test_project() -> Result<TempDir> {
let temp_dir = TempDir::new()?;
let project_dir = temp_dir.path();
let resources_dir = temp_dir.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 manifest_content = format!(
r#"[sources]
local = "{}"
[target]
agents = ".claude/agents"
snippets = ".agpm/snippets"
commands = ".claude/commands"
mcp-servers = ".claude/agpm/mcp-servers"
scripts = ".claude/agpm/scripts"
hooks = ".claude/agpm/hooks"
gitignore = true
"#,
resources_dir.display().to_string().replace('\\', "/")
);
fs::write(project_dir.join("agpm.toml"), 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(temp_dir)
}
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 {
if let Some(name) = resource.get("name").and_then(|v| v.as_str())
&& name == resource_name
{
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 temp_dir = setup_test_project().await.unwrap();
let project_dir = temp_dir.path();
let lockfile_path = project_dir.join("agpm.lock");
let mut cmd = Command::cargo_bin("agpm").unwrap();
cmd.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("command")
.arg("local:commands/test-command.md")
.arg("--name")
.arg("test-command");
cmd.assert().success().stdout(predicate::str::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 mut cmd2 = Command::cargo_bin("agpm").unwrap();
cmd2.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("agent")
.arg("local:agents/test-agent.md")
.arg("--name")
.arg("test-agent");
cmd2.assert().success().stdout(predicate::str::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 temp_dir = setup_test_project().await.unwrap();
let project_dir = temp_dir.path();
let lockfile_path = project_dir.join("agpm.lock");
let mut cmd1 = Command::cargo_bin("agpm").unwrap();
cmd1.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("command")
.arg("local:commands/test-command.md")
.arg("--name")
.arg("test-command")
.assert()
.success();
let command_deps_step1 = get_lockfile_dependencies(&lockfile_path, "test-command").await;
assert!(!command_deps_step1.is_empty(), "Command should have dependencies");
let mut cmd2 = Command::cargo_bin("agpm").unwrap();
cmd2.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("agent")
.arg("local:agents/test-agent.md")
.arg("--name")
.arg("test-agent")
.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");
let mut cmd3 = Command::cargo_bin("agpm").unwrap();
cmd3.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("snippet")
.arg("local:snippets/helper-snippet.md")
.arg("--name")
.arg("helper-snippet")
.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"
);
}
#[allow(unused_imports)]
use anyhow::Result;
#[tokio::test]
async fn test_incremental_add_with_shared_dependency() {
let temp_dir = setup_test_project().await.unwrap();
let project_dir = temp_dir.path();
let resources_dir = temp_dir.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_dir.join("agpm.lock");
Command::cargo_bin("agpm")
.unwrap()
.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("command")
.arg("local:commands/test-command.md")
.arg("--name")
.arg("test-command")
.assert()
.success();
Command::cargo_bin("agpm")
.unwrap()
.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("command")
.arg("local:commands/second-command.md")
.arg("--name")
.arg("second-command")
.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");
Command::cargo_bin("agpm")
.unwrap()
.current_dir(project_dir)
.arg("add")
.arg("dep")
.arg("snippet")
.arg("local:snippets/test-snippet.md")
.arg("--name")
.arg("test-snippet")
.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"
);
}