use anyhow::Result;
use crate::common::{ManifestBuilder, TestProject};
#[tokio::test]
async fn test_version_conflict_uses_correct_metadata() -> 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("commands", "old-command", "# Old Command\n\nThis is the old command.")
.await?;
repo.add_resource(
"snippets",
"shared",
r#"---
dependencies:
commands:
- path: ../commands/old-command.md
version: v1.0.0
---
# Shared Snippet v1.0.0
This is version 1.0.0 of the shared snippet.
"#,
)
.await?;
repo.commit_all("Release v1.0.0")?;
repo.tag_version("v1.0.0")?;
tokio::fs::remove_file(repo.path.join("commands/old-command.md")).await?;
repo.add_resource("commands", "new-command", "# New Command\n\nThis is the new command.")
.await?;
repo.add_resource(
"snippets",
"shared",
r#"---
dependencies:
commands:
- path: ../commands/new-command.md
version: v2.0.0
---
# Shared Snippet v2.0.0
This is version 2.0.0 of the shared snippet.
"#,
)
.await?;
repo.commit_all("Release v2.0.0")?;
repo.tag_version("v2.0.0")?;
repo.add_resource(
"agents",
"first",
r#"---
dependencies:
snippets:
- path: ../snippets/shared.md
version: v2.0.0
---
# First Agent
Requires shared@v2.0.0
"#,
)
.await?;
repo.add_resource(
"agents",
"second",
r#"---
dependencies:
snippets:
- path: ../snippets/shared.md
version: v2.0.0
---
# Second Agent
Also requires shared@v2.0.0
"#,
)
.await?;
repo.commit_all("Add agents")?;
repo.tag_version("v2.0.1")?;
let source_url = repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_agent("first", |d| d.source("community").path("agents/first.md").version("v2.0.1"))
.add_agent("second", |d| d.source("community").path("agents/second.md").version("v2.0.1"))
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed, stderr: {}", output.stderr);
let new_command_path = project.project_path().join(".claude/commands/agpm/new-command.md");
let old_command_path = project.project_path().join(".claude/commands/agpm/old-command.md");
assert!(
tokio::fs::metadata(&new_command_path).await.is_ok(),
"New command should exist at {:?} (from v2.0.0 metadata)",
new_command_path
);
assert!(
tokio::fs::metadata(&old_command_path).await.is_err(),
"Old command should NOT exist at {:?} (v1.0.0 metadata should not be used)",
old_command_path
);
Ok(())
}
#[tokio::test]
async fn test_transitive_version_conflict_metadata_from_winner() -> 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("commands", "old-dep", "# Old Dep\n\nOld command.").await?;
repo.add_resource(
"snippets",
"shared",
r#"---
dependencies:
commands:
- path: ../commands/old-dep.md
---
# Shared v1.0.0
Version 1 with old-dep.
"#,
)
.await?;
repo.commit_all("Add v1.0.0 resources")?;
repo.tag_version("v1.0.0")?;
repo.add_resource("commands", "new-dep", "# New Dep\n\nNew command.").await?;
repo.add_resource(
"snippets",
"shared",
r#"---
dependencies:
commands:
- path: ../commands/new-dep.md
---
# Shared v2.0.0
Version 2 with new-dep.
"#,
)
.await?;
repo.commit_all("Update to v2.0.0")?;
repo.tag_version("v2.0.0")?;
repo.add_resource(
"agents",
"parent-a",
r#"---
dependencies:
snippets:
- path: ../snippets/shared.md
version: ">=v1.0.0"
---
# Parent A
Depends on shared@>=v1.0.0 (accepts any version >= 1.0.0).
"#,
)
.await?;
repo.add_resource(
"agents",
"parent-b",
r#"---
dependencies:
snippets:
- path: ../snippets/shared.md
version: ">=v1.5.0"
---
# Parent B
Depends on shared@>=v1.5.0 (intersection with parent-a is >=v1.5.0).
"#,
)
.await?;
repo.commit_all("Add parent agents")?;
repo.tag_version("v3.0.0")?;
let source_url = repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_agent("parent-a", |d| {
d.source("community").path("agents/parent-a.md").version("v3.0.0")
})
.add_agent("parent-b", |d| {
d.source("community").path("agents/parent-b.md").version("v3.0.0")
})
.build();
project.write_manifest(&manifest).await?;
project.run_agpm(&["install"])?.assert_success();
let new_dep_path = project.project_path().join(".claude/commands/agpm/new-dep.md");
let old_dep_path = project.project_path().join(".claude/commands/agpm/old-dep.md");
assert!(
tokio::fs::metadata(&new_dep_path).await.is_ok(),
"new-dep should be installed (exists at v3.0.0)"
);
assert!(
tokio::fs::metadata(&old_dep_path).await.is_err(),
"old-dep should NOT be installed (doesn't exist at v3.0.0)"
);
let lockfile_content = project.read_lockfile().await?;
assert!(
lockfile_content.contains(r#"name = "snippets/shared""#)
&& lockfile_content.contains("v3.0.0"),
"Lockfile should show shared at v3.0.0 (highest version satisfying both constraints)"
);
let shared_path = project.project_path().join(".claude/snippets/agpm/shared.md");
let shared_content = tokio::fs::read_to_string(&shared_path).await?;
assert!(
shared_content.contains("Version 2 with new-dep"),
"Shared snippet should have v2.0.0 content (unchanged in v3.0.0)"
);
Ok(())
}
#[tokio::test]
async fn test_type_resolution_fallback_ambiguity() -> 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", "helper", "# Helper Snippet\n\nHelper snippet.").await?;
repo.add_resource("agents", "helper", "# Helper Agent\n\nHelper agent.").await?;
repo.add_resource(
"agents",
"main",
r#"---
dependencies:
snippets:
- path: ../snippets/helper.md
version: v1.0.0
agents:
- path: ./helper.md
version: v1.0.0
---
# Main Agent
This agent depends on both helper snippet and helper agent (same name, different types).
"#,
)
.await?;
repo.commit_all("Add resources")?;
repo.tag_version("v1.0.0")?;
let source_url = repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("community", &source_url)
.add_standard_agent("main", "community", "agents/main.md")
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed: {}", output.stderr);
let lockfile_content = project.read_lockfile().await?;
let has_snippet_helper = lockfile_content.contains(r#"name = "snippets/helper""#)
&& lockfile_content.contains(r#"path = "snippets/helper.md""#);
assert!(has_snippet_helper, "Lockfile should have helper snippet:\n{}", lockfile_content);
let has_agent_helper = lockfile_content.contains(r#"name = "agents/helper""#)
&& lockfile_content.contains(r#"path = "agents/helper.md""#);
assert!(has_agent_helper, "Lockfile should have helper agent:\n{}", lockfile_content);
let snippet_path = project.project_path().join(".claude/snippets/agpm/helper.md");
let agent_path = project.project_path().join(".claude/agents/agpm/helper.md");
assert!(
tokio::fs::metadata(&snippet_path).await.is_ok(),
"Snippet helper should be installed at {:?}",
snippet_path
);
assert!(
tokio::fs::metadata(&agent_path).await.is_ok(),
"Agent helper should be installed at {:?}",
agent_path
);
Ok(())
}
#[test]
fn test_generate_dependency_name_collisions() {
use std::path::Path;
fn generate_dependency_name_current(path: &str) -> String {
let path = Path::new(path);
let without_ext = path.with_extension("");
let path_str = without_ext.to_string_lossy();
let components: Vec<&str> = path_str.split('/').collect();
if components.len() > 1 {
components[1..].join("/")
} else {
components[0].to_string()
}
}
let name1 = generate_dependency_name_current("snippets/commands/commit.md");
let name2 = generate_dependency_name_current("snippets/logit/commit.md");
let name3 = generate_dependency_name_current("snippets/utils/commit.md");
println!("Corrected name generation:");
println!(" snippets/commands/commit.md -> {}", name1);
println!(" snippets/logit/commit.md -> {}", name2);
println!(" snippets/utils/commit.md -> {}", name3);
let name4 = generate_dependency_name_current("snippets/commands/commit.md");
assert_eq!(name1, name4, "Same path should generate same name");
assert_eq!(name1, "commands/commit");
assert_eq!(name2, "logit/commit");
assert_eq!(name3, "utils/commit");
}