use anyhow::Result;
mod common;
use common::TestProject;
mod fixtures;
#[tokio::test]
async fn test_install_auto_updates_missing_dependency() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "agent-one", "# Agent One\nTest agent one").await?;
source_repo.add_resource("agents", "agent-two", "# Agent Two\nTest agent two").await?;
source_repo.commit_all("Add agents")?;
source_repo.tag_version("v1.0.0")?;
let manifest = format!(
r#"[sources]
test-source = "{}"
[agents]
agent-one = {{ source = "test-source", path = "agents/agent-one.md", version = "v1.0.0" }}
agent-two = {{ source = "test-source", path = "agents/agent-two.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let lockfile = project.read_lockfile().await?;
let start_marker = "[[agents]]\nname = \"agent-two\"";
if let Some(start_pos) = lockfile.find(start_marker) {
let after_start = start_pos + start_marker.len();
let end_pos = lockfile[after_start..]
.find("[[agents]]")
.map(|p| after_start + p)
.unwrap_or(lockfile.len());
let modified_lockfile = format!("{}{}", &lockfile[..start_pos], &lockfile[end_pos..]);
project.write_lockfile(&modified_lockfile).await?;
} else {
panic!("Could not find agent-two in lockfile to remove");
}
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Install should auto-update lockfile: {}", output.stderr);
let updated_lockfile = project.read_lockfile().await?;
assert!(
updated_lockfile.contains("agent-two"),
"Lockfile should have been auto-updated with missing dependency"
);
Ok(())
}
#[tokio::test]
async fn test_install_frozen_detects_version_mismatch() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "test-agent", "# Test Agent v1\nVersion 1").await?;
source_repo.commit_all("Version 1")?;
source_repo.tag_version("v1.0.0")?;
source_repo.add_resource("agents", "test-agent", "# Test Agent v2\nVersion 2").await?;
source_repo.commit_all("Version 2")?;
source_repo.tag_version("v2.0.0")?;
let manifest = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/test-agent.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let manifest_v2 = manifest.replace("v1.0.0", "v2.0.0");
project.write_manifest(&manifest_v2).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Normal install should auto-update: {}", output.stderr);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success);
project.write_manifest(&manifest_v2).await?;
let output = project.run_agpm(&["install", "--frozen"])?;
assert!(
output.success,
"Frozen install should succeed (ignores version changes): {}",
output.stderr
);
Ok(())
}
#[tokio::test]
async fn test_install_detects_removed_dependency() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "agent-one", "# Agent One").await?;
source_repo.add_resource("agents", "agent-two", "# Agent Two").await?;
source_repo.commit_all("Add agents")?;
source_repo.tag_version("v1.0.0")?;
let manifest_two_agents = format!(
r#"[sources]
test-source = "{}"
[agents]
agent-one = {{ source = "test-source", path = "agents/agent-one.md", version = "v1.0.0" }}
agent-two = {{ source = "test-source", path = "agents/agent-two.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest_two_agents).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let manifest_one_agent = format!(
r#"[sources]
test-source = "{}"
[agents]
agent-one = {{ source = "test-source", path = "agents/agent-one.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest_one_agent).await?;
let output = project.run_agpm_with_env(&["install"], &[("CI", "true")])?;
assert!(
output.success,
"Install should succeed with extra lockfile entries for transitive deps: {}",
output.stderr
);
assert!(!output.stderr.contains("stale"), "Should not report lockfile as stale");
Ok(())
}
#[tokio::test]
async fn test_install_detects_path_change() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "old-path", "# Agent at old path").await?;
source_repo.add_resource("agents", "new-path", "# Agent at new path").await?;
source_repo.commit_all("Add agents")?;
source_repo.tag_version("v1.0.0")?;
let manifest_old = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/old-path.md", version = "v1.0.0" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest_old).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let manifest_new = manifest_old.replace("old-path", "new-path");
project.write_manifest(&manifest_new).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Normal install should auto-update path change: {}", output.stderr);
project.write_manifest(&manifest_old).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success);
project.write_manifest(&manifest_new).await?;
let output = project.run_agpm(&["install", "--frozen"])?;
assert!(output.success, "Frozen mode should succeed (ignores path changes): {}", output.stderr);
Ok(())
}
#[tokio::test]
async fn test_install_detects_source_url_change() -> Result<()> {
let project = TestProject::new().await?;
let old_repo = project.create_source_repo("old-repo").await?;
old_repo.add_resource("agents", "test-agent", "# Agent from old repo").await?;
old_repo.commit_all("Add agent")?;
old_repo.tag_version("v1.0.0")?;
let new_repo = project.create_source_repo("new-repo").await?;
new_repo.add_resource("agents", "test-agent", "# Agent from new repo").await?;
new_repo.commit_all("Add agent")?;
new_repo.tag_version("v1.0.0")?;
let manifest_old = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/test-agent.md", version = "v1.0.0" }}
"#,
old_repo.file_url()
);
project.write_manifest(&manifest_old).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let manifest_new = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/test-agent.md", version = "v1.0.0" }}
"#,
new_repo.file_url()
);
project.write_manifest(&manifest_new).await?;
let output = project.run_agpm(&["install", "--frozen"])?;
assert!(!output.success, "Should fail on source URL change (security)");
assert!(
output.stderr.contains("Source repository 'test-source' URL changed")
|| output.stderr.contains("out of sync"),
"Should report URL change, got: {}",
output.stderr
);
Ok(())
}
#[tokio::test]
async fn test_install_detects_duplicate_entries() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "test-agent", "# Test Agent").await?;
source_repo.commit_all("Add agent")?;
source_repo.git.ensure_branch("main")?;
let manifest = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/test-agent.md", version = "main" }}
"#,
source_repo.file_url()
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let lockfile = project.read_lockfile().await?;
if let Some(agents_pos) = lockfile.find("[[agents]]") {
let agent_section = &lockfile[agents_pos..];
let next_section = agent_section[11..].find("[[").unwrap_or(agent_section.len() - 11) + 11;
let agent_entry = &agent_section[..next_section];
let corrupted_lockfile = format!("{}\n{}", lockfile.trim(), agent_entry);
project.write_lockfile(&corrupted_lockfile).await?;
} else {
panic!("Could not find agents section in lockfile");
}
let output = project.run_agpm(&["install", "--frozen"])?;
assert!(!output.success, "Expected failure due to duplicate entries, but command succeeded");
assert!(
output.stderr.contains("duplicate entries")
|| output.stderr.contains("Found") && output.stderr.contains("duplicate")
|| output.stderr.contains("out of sync"),
"Expected duplicate entries error, got: {}",
output.stderr
);
Ok(())
}
#[tokio::test]
async fn test_install_allows_branch_references() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "test-agent", "# Test Agent v1").await?;
source_repo.commit_all("Initial commit")?;
source_repo.git.ensure_branch("main")?;
let bare_url = source_repo.bare_file_url(project.sources_path())?;
let manifest = format!(
r#"[sources]
test-source = "{}"
[agents]
test-agent = {{ source = "test-source", path = "agents/test-agent.md", version = "main" }}
"#,
bare_url
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(
output.success,
"Install should succeed with branch references, got error: {}",
output.stderr
);
Ok(())
}