use anyhow::Result;
use crate::common::{ManifestBuilder, TestProject};
#[tokio::test]
async fn test_local_file_dependency_skips_transitive_with_warning() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let local_agent_path = project.project_path().join("local-agent.md");
let local_agent_content = r#"---
dependencies:
snippets:
- path: ../snippets/helper.md
version: v1.0.0
---
# Local Agent
This is a local agent with transitive dependencies.
"#;
tokio::fs::write(&local_agent_path, local_agent_content).await?;
let manifest = ManifestBuilder::new().add_local_agent("local-agent", "local-agent.md").build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(
!output.success,
"Install should fail when transitive dependency path resolution fails"
);
assert!(
output.stderr.contains("Failed to resolve transitive dependency")
|| output.stderr.contains("Failed to fetch resource")
|| output.stderr.contains("file access")
|| output.stderr.contains("File system error: resolving path"),
"Error should indicate transitive dependency failure, got: {}",
output.stderr
);
Ok(())
}
#[tokio::test]
async fn test_mixed_local_remote_transitive_tree() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let repo = project.create_source_repo("community").await?;
repo.add_resource("snippets", "remote-helper", "# Remote Helper\n\nFrom Git source.").await?;
repo.add_resource(
"agents",
"remote-parent",
r#"---
dependencies:
snippets:
- path: ../snippets/remote-helper.md
version: v1.0.0
---
# Remote Parent Agent
Depends on remote-helper from same Git source.
"#,
)
.await?;
repo.commit_all("Add remote resources")?;
repo.tag_version("v1.0.0")?;
let local_snippet_path = project.project_path().join("local-snippet.md");
let local_snippet_content = "# Local Snippet\n\nLocal file without transitive dependencies.";
tokio::fs::write(&local_snippet_path, local_snippet_content).await?;
let source_url = repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("remote-parent", "community", "agents/remote-parent.md")
.add_local_snippet("local-snippet", "local-snippet.md")
.build();
project.write_manifest(&manifest).await?;
project.run_agpm(&["install"])?.assert_success();
let installed_local = project.project_path().join(".agpm/snippets/local-snippet.md");
assert!(
tokio::fs::metadata(&installed_local).await.is_ok(),
"Local snippet should be installed"
);
let installed_remote_parent =
project.project_path().join(".claude/agents/agpm/remote-parent.md");
assert!(
tokio::fs::metadata(&installed_remote_parent).await.is_ok(),
"Remote parent agent should be installed"
);
let installed_remote_helper =
project.project_path().join(".claude/snippets/agpm/remote-helper.md");
assert!(
tokio::fs::metadata(&installed_remote_helper).await.is_ok(),
"Remote helper (transitive) should be installed"
);
let lockfile_content = project.read_lockfile().await?;
assert!(
lockfile_content.contains(r#"name = "local-snippet""#),
"Lockfile should contain local-snippet"
);
assert!(
lockfile_content.contains(r#"name = "agents/remote-parent""#),
"Lockfile should contain remote-parent with canonical name"
);
assert!(
lockfile_content.contains(r#"name = "snippets/remote-helper""#),
"Lockfile should contain remote-helper (transitive)"
);
assert!(
lockfile_content.contains(r#"source = "community""#),
"Lockfile should show community source for remote resources"
);
Ok(())
}
#[tokio::test]
async fn test_local_with_current_dir_transitive() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join("agents");
tokio::fs::create_dir_all(&agents_dir).await?;
let helper_path = agents_dir.join("helper.md");
tokio::fs::write(&helper_path, "# Helper Agent\n\nA helper agent without dependencies.")
.await?;
let local_agent_path = agents_dir.join("local-agent.md");
tokio::fs::write(
&local_agent_path,
r#"---
dependencies:
agents:
- path: ./helper.md
---
# Local Agent
This is a local agent with a transitive dependency on ./helper.md.
"#,
)
.await?;
let manifest =
ManifestBuilder::new().add_local_agent("local-agent", "agents/local-agent.md").build();
project.write_manifest(&manifest).await?;
project.run_agpm(&["install"])?.assert_success();
let installed_local = project.project_path().join(".claude/agents/agpm/local-agent.md");
let installed_helper = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(
tokio::fs::metadata(&installed_local).await.is_ok(),
"Local agent should be installed at {:?}",
installed_local
);
assert!(
tokio::fs::metadata(&installed_helper).await.is_ok(),
"Helper agent (transitive) should be installed at {:?}",
installed_helper
);
let lockfile_content = project.read_lockfile().await?;
assert!(
lockfile_content.contains(r#"name = "agents/local-agent""#),
"Lockfile should contain local-agent with canonical name. Lockfile:\n{}",
lockfile_content
);
assert!(
lockfile_content.contains(r#"name = "agents/helper""#),
"Lockfile should contain helper (transitive). Lockfile:\n{}",
lockfile_content
);
assert!(lockfile_content.contains(r#"path = "agents/helper.md""#));
Ok(())
}
#[tokio::test]
async fn test_local_with_parent_dir_transitive() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join("agents");
let subfolder = agents_dir.join("subfolder");
tokio::fs::create_dir_all(&subfolder).await?;
let helper_path = agents_dir.join("helper.md");
tokio::fs::write(&helper_path, "# Helper Agent\n\nA helper agent without dependencies.")
.await?;
let local_agent_path = subfolder.join("local-agent.md");
tokio::fs::write(
&local_agent_path,
r#"---
dependencies:
agents:
- path: ../helper.md
---
# Local Agent
This agent depends on ../helper.md (parent directory).
"#,
)
.await?;
let manifest = ManifestBuilder::new()
.add_agent("local-agent", |d| d.path("agents/subfolder/local-agent.md").flatten(false))
.build();
project.write_manifest(&manifest).await?;
project.run_agpm(&["install"])?.assert_success();
let installed_local =
project.project_path().join(".claude/agents/agpm/subfolder/local-agent.md");
let installed_helper = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(tokio::fs::metadata(&installed_local).await.is_ok(), "Local agent should be installed");
assert!(
tokio::fs::metadata(&installed_helper).await.is_ok(),
"Helper agent (transitive from parent dir) should be installed"
);
let lockfile_content = project.read_lockfile().await?;
assert!(lockfile_content.contains(r#"name = "agents/subfolder/local-agent""#));
assert!(lockfile_content.contains(r#"name = "agents/helper""#));
assert!(lockfile_content.contains(r#"path = "agents/helper.md""#));
Ok(())
}
#[tokio::test]
async fn test_local_with_cross_directory_transitive() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join("agents");
let snippets_dir = project.project_path().join("snippets");
tokio::fs::create_dir_all(&agents_dir).await?;
tokio::fs::create_dir_all(&snippets_dir).await?;
let utils_path = snippets_dir.join("utils.md");
tokio::fs::write(&utils_path, "# Utils Snippet\n\nUtility functions.").await?;
let local_agent_path = agents_dir.join("local-agent.md");
tokio::fs::write(
&local_agent_path,
r#"---
dependencies:
snippets:
- path: ../snippets/utils.md
---
# Local Agent
This agent depends on a snippet in a different directory.
"#,
)
.await?;
let manifest =
ManifestBuilder::new().add_local_agent("local-agent", "agents/local-agent.md").build();
project.write_manifest(&manifest).await?;
project.run_agpm(&["install"])?.assert_success();
let installed_agent = project.project_path().join(".claude/agents/agpm/local-agent.md");
assert!(tokio::fs::metadata(&installed_agent).await.is_ok(), "Local agent should be installed");
let installed_snippet = project.project_path().join(".claude/snippets/agpm/utils.md");
assert!(
tokio::fs::metadata(&installed_snippet).await.is_ok(),
"Utils snippet (transitive) should be installed to .claude/snippets (inheriting parent's tool)"
);
let lockfile_content = project.read_lockfile().await?;
assert!(lockfile_content.contains(r#"name = "agents/local-agent""#));
assert!(lockfile_content.contains(r#"name = "snippets/utils""#));
assert!(lockfile_content.contains(r#"path = "snippets/utils.md""#));
Ok(())
}
#[tokio::test]
async fn test_local_transitive_missing_file() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join("agents");
tokio::fs::create_dir_all(&agents_dir).await?;
let local_agent_path = agents_dir.join("local-agent.md");
tokio::fs::write(
&local_agent_path,
r#"---
dependencies:
agents:
- path: ./missing.md
---
# Local Agent
This agent has a transitive dependency that doesn't exist.
"#,
)
.await?;
let manifest =
ManifestBuilder::new().add_local_agent("local-agent", "agents/local-agent.md").build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(!output.success, "Install should fail when transitive dependency is missing");
assert!(
output.stderr.contains("Failed to resolve transitive dependency")
|| output.stderr.contains("Failed to fetch resource")
|| output.stderr.contains("file access")
|| output.stderr.contains("File system error: resolving path"),
"Error should indicate transitive dependency failure, got: {}",
output.stderr
);
Ok(())
}
#[tokio::test]
async fn test_local_transitive_invalid_path_format() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let agents_dir = project.project_path().join("agents");
tokio::fs::create_dir_all(&agents_dir).await?;
let helper_path = agents_dir.join("helper.md");
tokio::fs::write(&helper_path, "# Helper\n").await?;
let local_agent_path = agents_dir.join("local-agent.md");
tokio::fs::write(
&local_agent_path,
r#"---
dependencies:
agents:
- path: helper.md
---
# Local Agent
This agent has a bare filename transitive dependency that gets auto-normalized.
"#,
)
.await?;
let manifest =
ManifestBuilder::new().add_local_agent("local-agent", "agents/local-agent.md").build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(
output.success,
"Install should succeed with auto-normalized bare filename: {}",
output.stderr
);
let installed_agent = project.project_path().join(".claude/agents/agpm/local-agent.md");
let installed_helper = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(installed_agent.exists(), "Local agent should be installed");
assert!(
installed_helper.exists(),
"Helper agent should be installed via transitive dependency"
);
Ok(())
}
#[tokio::test]
async fn test_local_transitive_outside_manifest_directory() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let project_parent = project.project_path().parent().unwrap();
let shared_dir = project_parent.join("shared");
tokio::fs::create_dir_all(&shared_dir).await?;
let shared_snippet = shared_dir.join("utils.md");
tokio::fs::write(
&shared_snippet,
r#"# Shared Utils
Common utilities shared across projects.
"#,
)
.await?;
let agents_dir = project.project_path().join("agents");
tokio::fs::create_dir_all(&agents_dir).await?;
let relative_to_shared = "../../shared/utils.md";
let agent_path = agents_dir.join("my-agent.md");
tokio::fs::write(
&agent_path,
format!(
r#"---
dependencies:
snippets:
- path: {}
tool: agpm
---
# My Agent
Uses a shared snippet outside the project directory.
"#,
relative_to_shared
),
)
.await?;
let manifest = ManifestBuilder::new().add_local_agent("my-agent", "agents/my-agent.md").build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed with cross-directory transitive dependency");
let installed_agent = project.project_path().join(".claude/agents/agpm/my-agent.md");
assert!(tokio::fs::metadata(&installed_agent).await.is_ok(), "Agent should be installed");
let expected_snippet_path = project.project_path().join(".agpm/snippets/shared/utils.md");
assert!(
tokio::fs::metadata(&expected_snippet_path).await.is_ok(),
"Shared snippet should be installed at .agpm/snippets/shared/utils.md"
);
let installed_content = tokio::fs::read(&expected_snippet_path).await?;
let expected_content = b"# Shared Utils\nCommon utilities shared across projects.\n";
assert_eq!(
installed_content, expected_content,
"Installed snippet should have correct content"
);
let lockfile_content = project.read_lockfile().await?;
assert!(
lockfile_content.contains(r#"name = "agents/my-agent""#),
"Lockfile should contain agent with canonical name"
);
assert!(
lockfile_content.contains(r#"path = "../shared/utils.md""#),
"Lockfile should contain manifest-relative path (with ../) for cross-directory dependency.\nLockfile:\n{}",
lockfile_content
);
assert!(
lockfile_content.contains(r#"tool = "agpm""#),
"Lockfile should specify agpm tool for snippet"
);
Ok(())
}