use anyhow::Result;
use tokio::fs;
mod common;
mod fixtures;
mod test_config;
use common::{TestProject, TestSourceRepo};
async fn setup_git_repo_with_versions(repo: &TestSourceRepo) -> Result<String> {
let repo_path = &repo.path;
let git = &repo.git;
fs::create_dir_all(repo_path.join("agents")).await?;
fs::create_dir_all(repo_path.join("snippets")).await?;
fs::write(repo_path.join("agents/example.md"), "# Example Agent v1.0.0\nInitial version")
.await?;
fs::write(repo_path.join("snippets/utils.md"), "# Utils Snippet v1.0.0\nInitial version")
.await?;
git.add_all()?;
git.commit("Initial commit v1.0.0")?;
git.tag("v1.0.0")?;
let v1_commit = git.get_commit_hash()?;
fs::write(repo_path.join("agents/example.md"), "# Example Agent v1.1.0\nMinor update").await?;
git.add_all()?;
git.commit("Version 1.1.0")?;
git.tag("v1.1.0")?;
fs::write(repo_path.join("agents/example.md"), "# Example Agent v1.2.0\nAnother minor update")
.await?;
git.add_all()?;
git.commit("Version 1.2.0")?;
git.tag("v1.2.0")?;
fs::write(repo_path.join("agents/example.md"), "# Example Agent v2.0.0\nMajor version").await?;
git.add_all()?;
git.commit("Version 2.0.0 - Breaking changes")?;
git.tag("v2.0.0")?;
git.ensure_branch("main")?;
git.create_branch("develop")?;
fs::write(
repo_path.join("agents/example.md"),
"# Example Agent - Development\nUnstable development version",
)
.await?;
fs::write(
repo_path.join("agents/experimental.md"),
"# Experimental Agent\nOnly in develop branch",
)
.await?;
git.add_all()?;
git.commit("Development changes")?;
git.create_branch("feature/new-agent")?;
fs::write(repo_path.join("agents/feature.md"), "# Feature Agent\nNew feature in progress")
.await?;
git.add_all()?;
git.commit("Add feature agent")?;
git.ensure_branch("main")?;
Ok(v1_commit)
}
#[tokio::test]
async fn test_install_with_exact_version_tag() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = "v1.0.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.0.0"));
assert!(!installed.contains("v1.1.0"));
assert!(!installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_install_with_caret_version_range() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = "^1.0.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.2.0"));
assert!(!installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_install_with_tilde_version_range() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = "~1.1.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.1.0"));
assert!(!installed.contains("v1.2.0"));
}
#[tokio::test]
async fn test_install_with_branch_reference() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
dev-example = {{ source = "versioned", path = "agents/example.md", branch = "develop" }}
experimental = {{ source = "versioned", path = "agents/experimental.md", branch = "develop" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let example_content =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(example_content.contains("Development"));
assert!(example_content.contains("Unstable"));
assert!(project.project_path().join(".claude/agents/experimental.md").exists());
}
#[tokio::test]
async fn test_install_with_feature_branch() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
feature = {{ source = "versioned", path = "agents/feature.md", branch = "feature/new-agent" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let feature_content =
fs::read_to_string(project.project_path().join(".claude/agents/feature.md")).await.unwrap();
assert!(feature_content.contains("Feature Agent"));
assert!(feature_content.contains("New feature in progress"));
}
#[tokio::test]
async fn test_install_with_commit_hash() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
let v1_commit = setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
pinned = {{ source = "versioned", path = "agents/example.md", rev = "{}" }}
"#,
source_repo.path.display().to_string().replace('\\', "/"),
v1_commit
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.0.0"));
assert!(installed.contains("Initial version"));
}
#[tokio::test]
async fn test_install_with_wildcard_version() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
any = {{ source = "versioned", path = "agents/example.md", version = "*" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_install_with_mixed_versioning_methods() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
let v1_commit = setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
stable = {{ source = "versioned", path = "agents/example.md", version = "v1.1.0" }}
compatible = {{ source = "versioned", path = "agents/example.md", version = "^1.0.0" }}
develop = {{ source = "versioned", path = "agents/example.md", branch = "develop" }}
pinned = {{ source = "versioned", path = "agents/example.md", rev = "{}" }}
"#,
source_repo.path.display().to_string().replace('\\', "/"),
v1_commit
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
assert!(!output.success, "Expected install to fail due to version conflicts");
assert!(
output.stderr.contains("Version conflicts"),
"Expected version conflict, got: {}",
output.stderr
);
assert!(output.stderr.contains("example.md"));
}
#[tokio::test]
async fn test_version_constraint_with_greater_than() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = ">=1.1.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_version_constraint_with_range() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = ">=1.1.0, <2.0.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.2.0"));
assert!(!installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_update_branch_reference() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
dev = {{ source = "versioned", path = "agents/example.md", branch = "develop" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
source_repo.git.checkout("develop").unwrap();
fs::write(
source_repo.path.join("agents/example.md"),
"# Example Agent - Updated Development\nNewer unstable version",
)
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Update develop branch").unwrap();
let output = project.run_agpm(&["update"]).unwrap();
output.assert_success();
let file_path = project.project_path().join(".claude/agents/example.md");
let updated = fs::read_to_string(&file_path).await.unwrap_or_else(|e| {
panic!("Failed to read file {file_path:?}: {e}");
});
println!("File content after update: {updated:?}");
assert!(updated.contains("Updated Development"), "File content: {updated}");
assert!(updated.contains("Newer unstable"));
}
#[tokio::test]
async fn test_lockfile_records_correct_version_info() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let feature_commit = {
source_repo.git.checkout("feature/new-agent").unwrap();
let commit = source_repo.git.get_commit_hash().unwrap();
source_repo.git.ensure_branch("main").unwrap();
commit
};
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
tagged = {{ source = "versioned", path = "agents/example.md", version = "v1.1.0" }}
branched = {{ source = "versioned", path = "agents/experimental.md", branch = "develop" }}
committed = {{ source = "versioned", path = "agents/feature.md", rev = "{}" }}
"#,
source_repo.path.display().to_string().replace('\\', "/"),
feature_commit
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile = project.read_lockfile().await.unwrap();
assert!(lockfile.contains("tagged"));
assert!(lockfile.contains("branched"));
assert!(lockfile.contains("committed"));
}
#[tokio::test]
async fn test_error_on_invalid_version_constraint() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = "v99.0.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
assert!(!output.success, "Expected command to fail but it succeeded");
assert!(
output.stderr.contains("Git operation failed")
|| output.stderr.contains("No matching version found"),
"Expected error about version not found, got: {}",
output.stderr
);
}
#[tokio::test]
async fn test_error_on_nonexistent_branch() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", branch = "nonexistent" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
assert!(!output.success, "Expected command to fail but it succeeded");
}
#[tokio::test]
async fn test_frozen_install_uses_lockfile_versions() {
test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("versioned").await.unwrap();
setup_git_repo_with_versions(&source_repo).await.unwrap();
let manifest = format!(
r#"[sources]
versioned = "file://{}"
[agents]
example = {{ source = "versioned", path = "agents/example.md", version = "^1.0.0" }}
"#,
source_repo.path.display().to_string().replace('\\', "/")
);
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile = fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(lockfile.contains("version = \"v1.2.0\""));
fs::remove_dir_all(project.project_path().join(".claude")).await.unwrap();
let output = project.run_agpm(&["install", "--frozen"]).unwrap();
output.assert_success();
let installed =
fs::read_to_string(project.project_path().join(".claude/agents/example.md")).await.unwrap();
assert!(installed.contains("v1.2.0"));
assert!(!installed.contains("v2.0.0"));
}
#[tokio::test]
async fn test_path_collision_detection() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("versioned").await?;
setup_git_repo_with_versions(&source_repo).await?;
let manifest = format!(
r#"[sources]
versioned = "{}"
[agents]
version-one = {{ source = "versioned", path = "agents/example.md", version = "v1.0.0" }}
version-two = {{ source = "versioned", path = "agents/example.md", version = "v2.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(!output.success, "Expected collision for same path");
assert!(
output.stderr.contains("Version conflicts"),
"Expected version conflict error, got: {}",
output.stderr
);
assert!(output.stderr.contains("v1.0.0"));
assert!(output.stderr.contains("v2.0.0"));
let claude_dir = project.project_path().join(".claude");
if claude_dir.exists() {
fs::remove_dir_all(&claude_dir).await?;
}
let lock_file = project.project_path().join("agpm.lock");
if lock_file.exists() {
fs::remove_file(&lock_file).await?;
}
let manifest = format!(
r#"[sources]
versioned = "{}"
[agents]
# Use custom targets to install same source file at different locations
version-one = {{ source = "versioned", path = "agents/example.md", version = "v1.0.0", target = "v1" }}
[snippets]
# Use snippets section to install a different file
version-two = {{ source = "versioned", path = "snippets/utils.md", version = "v1.0.0", target = "v2" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
let v1_path = project.project_path().join(".claude/agents/v1/example.md");
let v2_path = project.project_path().join(".agpm/snippets/v2/utils.md");
let v1 = fs::read_to_string(&v1_path)
.await
.unwrap_or_else(|e| panic!("Failed to read {}: {}", v1_path.display(), e));
let v2 = fs::read_to_string(&v2_path)
.await
.unwrap_or_else(|e| panic!("Failed to read {}: {}", v2_path.display(), e));
assert!(v1.contains("v1.0.0"));
assert!(v2.contains("v1.0.0"));
let claude_dir = project.project_path().join(".claude");
if claude_dir.exists() {
fs::remove_dir_all(&claude_dir).await?;
}
let lock_file = project.project_path().join("agpm.lock");
if lock_file.exists() {
fs::remove_file(&lock_file).await?;
}
let manifest = format!(
r#"[sources]
versioned = "{}"
[agents]
agent-one = {{ source = "versioned", path = "agents/example.md", version = "v1.0.0" }}
[snippets]
snippet-one = {{ source = "versioned", path = "snippets/utils.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
Ok(())
}