use crate::common::{ManifestBuilder, TestProject};
use agpm_cli::utils::normalize_path_for_storage;
use tokio::fs;
#[tokio::test]
async fn test_install_with_prefixed_constraint() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("prefixed").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::write(source_repo.path.join("agents/test-agent.md"), "# Test Agent\n\nTest content")
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Add agents").unwrap();
source_repo.git.tag("agents-v1.0.0").unwrap();
source_repo.git.tag("agents-v1.2.0").unwrap();
source_repo.git.tag("agents-v2.0.0").unwrap();
fs::create_dir_all(source_repo.path.join("snippets")).await.unwrap();
fs::write(
source_repo.path.join("snippets/test-snippet.md"),
"# Test Snippet\n\nSnippet content",
)
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Add snippets").unwrap();
source_repo.git.tag("snippets-v1.0.0").unwrap();
source_repo.git.tag("snippets-v2.0.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("prefixed", &source_url)
.add_agent("test-agent", |d| {
d.source("prefixed").path("agents/test-agent.md").version("agents-^v1.0.0")
})
.add_snippet("test-snippet", |d| {
d.source("prefixed").path("snippets/test-snippet.md").version("snippets-^v2.0.0")
})
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(
lockfile_content.contains("agents-v1.2.0"),
"Should resolve agents-^v1.0.0 to agents-v1.2.0 (highest 1.x)\nActual lockfile:\n{}",
lockfile_content
);
assert!(
lockfile_content.contains("snippets-v2.0.0"),
"Should resolve snippets-^v2.0.0 to snippets-v2.0.0"
);
assert!(project.project_path().join(".claude/agents/agpm/test-agent.md").exists());
assert!(
project.project_path().join(".agpm/snippets/test-snippet.md").exists(),
"Snippet file should be installed in .agpm/snippets/"
);
}
#[tokio::test]
async fn test_prefix_isolation() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("prefixed").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nContent").await.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Initial commit").unwrap();
source_repo.git.tag("agents-v1.5.0").unwrap(); source_repo.git.tag("tools-v2.0.0").unwrap(); source_repo.git.tag("v1.0.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("prefixed", &source_url)
.add_agent("agent", |d| {
d.source("prefixed").path("agents/agent.md").version("agents-^v1.0.0")
})
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(lockfile_content.contains("agents-v1.5.0"));
assert!(!lockfile_content.contains("tools-v2.0.0"));
assert!(!lockfile_content.contains("version = \"v1.0.0\""));
}
#[tokio::test]
async fn test_outdated_with_prefixed_versions() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("prefixed").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nContent").await.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Initial commit").unwrap();
source_repo.git.tag("agents-v1.0.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("prefixed", &source_url)
.add_agent("agent", |d| {
d.source("prefixed").path("agents/agent.md").version("agents-^v1.0.0")
})
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nUpdated content")
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Update agent").unwrap();
source_repo.git.tag("agents-v1.5.0").unwrap();
let output = project.run_agpm(&["outdated"]).unwrap();
output.assert_success();
let has_version_info = output.stdout.contains("agents");
let is_up_to_date = output.stdout.contains("up to date");
assert!(
has_version_info || is_up_to_date,
"Expected outdated to either show version info or 'up to date' message.\nGot: {}",
output.stdout
);
}
#[tokio::test]
async fn test_unprefixed_constraint_doesnt_match_prefixed_tags() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("prefixed").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nContent").await.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Initial commit").unwrap();
source_repo.git.tag("agents-v1.0.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("prefixed", &source_url)
.add_agent("agent", |d| d.source("prefixed").path("agents/agent.md").version("^v1.0.0"))
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
assert!(
!output.success,
"Expected install to fail when no unprefixed tags match, but it succeeded"
);
assert!(
output.stderr.contains("No tag found matching") || output.stderr.contains("No tags found"),
"Expected error about no matching tags, got: {}",
output.stderr
);
}
#[tokio::test]
async fn test_multi_prefix_manifest() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("multi").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::create_dir_all(source_repo.path.join("snippets")).await.unwrap();
fs::create_dir_all(source_repo.path.join("commands")).await.unwrap();
fs::write(source_repo.path.join("agents/test-agent.md"), "# Test Agent\n\nAgent content")
.await
.unwrap();
fs::write(
source_repo.path.join("snippets/test-snippet.md"),
"# Test Snippet\n\nSnippet content",
)
.await
.unwrap();
fs::write(
source_repo.path.join("commands/test-command.md"),
"# Test Command\n\nCommand content",
)
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Initial commit").unwrap();
source_repo.git.tag("agents-v1.0.0").unwrap();
source_repo.git.tag("agents-v1.5.0").unwrap();
source_repo.git.tag("agents-v2.0.0").unwrap();
source_repo.git.tag("snippets-v1.0.0").unwrap();
source_repo.git.tag("snippets-v1.5.0").unwrap(); source_repo.git.tag("snippets-v2.0.0").unwrap(); source_repo.git.tag("v1.0.0").unwrap(); source_repo.git.tag("v1.5.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("multi", &source_url)
.add_agent("test-agent", |d| {
d.source("multi").path("agents/test-agent.md").version("agents-^v1.0.0")
})
.add_snippet("test-snippet", |d| {
d.source("multi").path("snippets/test-snippet.md").version("snippets-^v1.0.0")
})
.add_command("test-command", |d| {
d.source("multi").path("commands/test-command.md").version("^v1.0.0")
})
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(
lockfile_content.contains("agents-v1.5.0"),
"Should resolve agents-^v1.0.0 to agents-v1.5.0 (highest compatible in agents namespace)\nLockfile:\n{}",
lockfile_content
);
assert!(
lockfile_content.contains("snippets-v1.5.0"),
"Should resolve snippets-^v1.0.0 to snippets-v1.5.0 (highest compatible in snippets namespace)\nLockfile:\n{}",
lockfile_content
);
assert!(
lockfile_content.contains("version = \"v1.5.0\""),
"Should resolve unprefixed ^v1.0.0 to v1.5.0 (highest compatible unprefixed)\nLockfile:\n{}",
lockfile_content
);
assert!(
!lockfile_content.contains("agents-v2.0.0"),
"Should NOT use agents-v2.0.0 (breaks semver constraint ^v1.0.0)"
);
assert!(
!lockfile_content.contains("snippets-v2.0.0"),
"Should NOT use snippets-v2.0.0 (breaks semver constraint ^v1.0.0)"
);
assert!(
project.project_path().join(".claude/agents/agpm/test-agent.md").exists(),
"Agent file should be installed"
);
assert!(
project.project_path().join(".agpm/snippets/test-snippet.md").exists(),
"Snippet file should be installed in .agpm/snippets/"
);
assert!(
project.project_path().join(".claude/commands/agpm/test-command.md").exists(),
"Command file should be installed"
);
}
#[tokio::test]
async fn test_update_command_with_prefixed_versions() {
crate::test_config::init_test_env();
let project = TestProject::new().await.unwrap();
let source_repo = project.create_source_repo("updatetest").await.unwrap();
fs::create_dir_all(source_repo.path.join("agents")).await.unwrap();
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nInitial content")
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Initial commit").unwrap();
source_repo.git.tag("agents-v1.0.0").unwrap();
source_repo.git.tag("agents-v1.2.0").unwrap();
let source_url = format!("file://{}", normalize_path_for_storage(&source_repo.path));
let manifest = ManifestBuilder::new()
.add_source("updatetest", &source_url)
.add_agent("agent", |d| {
d.source("updatetest").path("agents/agent.md").version("agents-^v1.0.0")
})
.build();
project.write_manifest(&manifest).await.unwrap();
let output = project.run_agpm(&["install"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(lockfile_content.contains("agents-v1.2.0"), "Initial install should use agents-v1.2.0");
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nUpdated content v1.5.0")
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Update to v1.5.0").unwrap();
source_repo.git.tag("agents-v1.5.0").unwrap();
let output = project.run_agpm(&["update"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(
lockfile_content.contains("agents-v1.5.0"),
"Update should upgrade to agents-v1.5.0\nLockfile:\n{}",
lockfile_content
);
assert!(!lockfile_content.contains("agents-v1.2.0"), "Update should remove old agents-v1.2.0");
let installed_content =
fs::read_to_string(project.project_path().join(".claude/agents/agpm/agent.md"))
.await
.unwrap();
assert!(
installed_content.contains("Updated content v1.5.0"),
"Installed file should have updated content"
);
fs::write(source_repo.path.join("agents/agent.md"), "# Agent\n\nUpdated content v2.0.0")
.await
.unwrap();
source_repo.git.add_all().unwrap();
source_repo.git.commit("Update to v2.0.0").unwrap();
source_repo.git.tag("agents-v2.0.0").unwrap();
let output = project.run_agpm(&["update"]).unwrap();
output.assert_success();
let lockfile_content =
fs::read_to_string(project.project_path().join("agpm.lock")).await.unwrap();
assert!(
lockfile_content.contains("agents-v1.5.0"),
"Update should keep agents-v1.5.0 (v2.0.0 breaks ^v1.0.0 constraint)\nLockfile:\n{}",
lockfile_content
);
assert!(
!lockfile_content.contains("agents-v2.0.0"),
"Update should NOT upgrade to agents-v2.0.0 (breaks semver constraint)"
);
}