use crate::common::TestProject;
use anyhow::Result;
use tokio::fs;
fn normalize_timestamps(lockfile: &str) -> String {
let re = regex::Regex::new(r#"fetched_at = "[^"]+""#).unwrap();
re.replace_all(lockfile, r#"fetched_at = "NORMALIZED""#).to_string()
}
#[tokio::test]
async fn test_old_lockfile_detection() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"simple",
r#"---
title: Simple Agent
---
# Simple Agent
I am a simple agent.
"#,
)
.await?;
test_repo.commit_all("Initial version")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
test-repo = "{}"
[agents]
simple = {{ source = "test-repo", path = "agents/simple.md", version = "v1.0.0" }}
"#,
repo_url
);
project.write_manifest(&manifest).await?;
let _old_lockfile_content = format!(
r#"# AGPM Lockfile
# This file is automatically generated. Do not edit.
# Run 'agpm install' to regenerate.
format_version = "1"
[[agents]]
name = "simple"
source = "test-repo"
path = "agents/simple.md"
version = "v1.0.0"
resolved_commit = "{}"
checksum = "sha256:placeholder"
installed_at = ".claude/agents/agpm/simple.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
template_vars = "{{}}" # Old field name (now variant_inputs)
"#,
"placeholder_commit"
);
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Initial install should succeed");
let real_lockfile_content = project.read_lockfile().await?;
let lines: Vec<&str> = real_lockfile_content.lines().collect();
let mut resolved_commit = "unknown";
for line in lines {
if line.trim().starts_with("resolved_commit") {
if let Some(commit) = line.split('=').nth(1) {
resolved_commit = commit.trim().trim_matches('"');
}
break;
}
}
let old_lockfile_content = format!(
r#"# AGPM Lockfile
# This file is automatically generated. Do not edit.
# Run 'agpm install' to regenerate.
format_version = "1"
[[agents]]
name = "simple"
source = "test-repo"
path = "agents/simple.md"
version = "v1.0.0"
resolved_commit = "{}"
checksum = "sha256:placeholder"
installed_at = ".claude/agents/agpm/simple.md"
dependencies = []
resource_type = "Agent"
tool = "claude-code"
template_vars = "{{}}" # Old field name (now variant_inputs)
"#,
resolved_commit
);
let lockfile_path = project.project_path().join("agpm.lock");
fs::write(&lockfile_path, old_lockfile_content).await?;
let output = project.run_agpm(&["install"])?;
assert!(!output.success, "Install should fail with old lockfile format");
let error_output = format!("{}\n{}", output.stdout, output.stderr);
assert!(
error_output.contains("context_checksum")
|| error_output.contains("regenerate")
|| error_output.contains("migrate")
|| error_output.contains("upgrade"),
"Should contain helpful migration message. Output: {}",
error_output
);
Ok(())
}
#[tokio::test]
async fn test_old_template_vars_handling() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"templated",
r#"---
title: "{{ project.name }}"
agpm:
templating: true
---
# {{ project.name }}
This is a templated agent.
"#,
)
.await?;
test_repo.commit_all("Initial version")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
test-repo = "{}"
[agents]
templated = {{ source = "test-repo", path = "agents/templated.md", version = "v1.0.0", template_vars = {{ project = {{ name = "TestProject" }} }} }}
"#,
repo_url
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed with template_vars. Stderr: {}", output.stderr);
let agent_path = project.project_path().join(".claude/agents/agpm/templated.md");
assert!(agent_path.exists(), "Templated agent should be installed");
let lockfile = project.load_lockfile()?;
let has_context_checksum = lockfile.agents.iter().any(|a| a.context_checksum.is_some());
assert!(has_context_checksum, "Lockfile should contain context_checksum");
let has_template_vars = lockfile.agents.iter().any(|a| {
a.variant_inputs.json().as_object().is_some_and(|obj| !obj.is_empty())
});
assert!(has_template_vars, "Lockfile should contain template_vars for backward compatibility");
let content = fs::read_to_string(&agent_path).await?;
assert!(content.contains("TestProject"), "Content should be rendered with template variables");
Ok(())
}
#[tokio::test]
async fn test_install_false_migration() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"installable",
r#"---
title: Installable Snippet
---
# Installable Snippet
This gets installed.
"#,
)
.await?;
test_repo
.add_resource(
"snippets",
"content-only",
r#"---
title: Content Only Snippet
dependencies:
snippets:
- path: snippets/dependency.md
version: "v1.0.0"
agpm:
templating: true
---
# Content Only Snippet
This is content-only.
"#,
)
.await?;
test_repo
.add_resource(
"snippets",
"dependency",
r#"---
title: Dependency Snippet
---
# Dependency Snippet
I am a dependency.
"#,
)
.await?;
test_repo.commit_all("Initial version")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest1 = format!(
r#"[sources]
test-repo = "{}"
[snippets]
installable = {{ source = "test-repo", path = "snippets/installable.md", version = "v1.0.0" }}
content_only = {{ source = "test-repo", path = "snippets/content-only.md", version = "v1.0.0", template_vars = {{}} }}
"#,
repo_url
);
project.write_manifest(&manifest1).await?;
let output1 = project.run_agpm(&["install"])?;
assert!(output1.success, "Initial install should succeed");
let installable_path = project.project_path().join(".agpm/snippets/installable.md");
let content_only_path = project.project_path().join(".agpm/snippets/content-only.md");
let dependency_path = project.project_path().join(".agpm/snippets/dependency.md");
assert!(installable_path.exists(), "Installable snippet should be installed");
assert!(content_only_path.exists(), "Content-only snippet should be installed initially");
assert!(dependency_path.exists(), "Dependency should be installed");
let manifest2 = format!(
r#"[sources]
test-repo = "{}"
[snippets]
installable = {{ source = "test-repo", path = "snippets/installable.md", version = "v1.0.0" }}
content_only = {{ source = "test-repo", path = "snippets/content-only.md", version = "v1.0.0", install = false, template_vars = {{}} }}
"#,
repo_url
);
project.write_manifest(&manifest2).await?;
let output2 = project.run_agpm(&["install"])?;
assert!(output2.success, "Update should succeed");
assert!(
output2.stdout.contains("Cleaned up") || output2.stdout.contains("moved or removed"),
"Should report cleanup. Output: {}",
output2.stdout
);
assert!(installable_path.exists(), "Installable snippet should still exist");
assert!(!content_only_path.exists(), "Content-only snippet should be removed");
assert!(
dependency_path.exists(),
"Dependency should still exist (may be content-only transitive dependency)"
);
Ok(())
}
#[tokio::test]
async fn test_variant_inputs_compatibility() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"commands",
"variant-command",
r#"---
title: "Variant Command"
api_url: "{{ config.api_url }}"
timeout: {{ config.timeout }}
agpm:
templating: true
---
# Variant Command
API: {{ config.api_url }}
Timeout: {{ config.timeout }}s
Features: {{ features | join(sep=", ") }}
"#,
)
.await?;
test_repo.commit_all("Initial version")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
test-repo = "{}"
[commands]
variant = {{ source = "test-repo", path = "commands/variant-command.md", version = "v1.0.0", template_vars = {{ config = {{ api_url = "https://api.example.com", timeout = 30 }}, features = ["auth", "logging"] }} }}
"#,
repo_url
);
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(
output.success,
"Install should succeed with template_vars alias. Stderr: {}",
output.stderr
);
let command_path = project.project_path().join(".claude/commands/agpm/variant-command.md");
assert!(command_path.exists(), "Variant command should be installed");
let content = fs::read_to_string(&command_path).await?;
assert!(content.contains("https://api.example.com"), "API URL should be rendered");
assert!(content.contains("30s"), "Timeout should be rendered");
assert!(content.contains("auth, logging"), "Features should be rendered");
let lockfile = project.load_lockfile()?;
let has_context_checksum = lockfile.commands.iter().any(|c| c.context_checksum.is_some());
assert!(has_context_checksum, "Lockfile should contain context_checksum");
let has_template_vars = lockfile.commands.iter().any(|c| {
c.variant_inputs.json().as_object().is_some_and(|obj| !obj.is_empty())
});
assert!(has_template_vars, "Lockfile should contain template_vars (backward compatibility)");
Ok(())
}
#[tokio::test]
async fn test_post_migration_consistency() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"plain",
r#"---
title: Plain Agent
---
# Plain Agent
No templating.
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"templated",
r#"---
title: "{{ config.name }}"
agpm:
templating: true
---
# {{ config.name }}
Templated agent.
"#,
)
.await?;
test_repo.commit_all("Initial version")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = format!(
r#"[sources]
test-repo = "{}"
[agents]
plain = {{ source = "test-repo", path = "agents/plain.md", version = "v1.0.0" }}
templated = {{ source = "test-repo", path = "agents/templated.md", version = "v1.0.0", template_vars = {{ config = {{ name = "MigratedAgent" }} }} }}
"#,
repo_url
);
project.write_manifest(&manifest).await?;
let mut lockfiles = Vec::new();
for run in 1..=3 {
let lockfile_path = project.project_path().join("agpm.lock");
if lockfile_path.exists() {
fs::remove_file(&lockfile_path).await?;
}
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Run {} should succeed", run);
let lockfile_content = project.read_lockfile().await?;
lockfiles.push(lockfile_content);
let plain_path = project.project_path().join(".claude/agents/agpm/plain.md");
let templated_path = project.project_path().join(".claude/agents/agpm/templated.md");
assert!(plain_path.exists(), "Plain agent should exist in run {}", run);
assert!(templated_path.exists(), "Templated agent should exist in run {}", run);
}
let normalized_lockfiles: Vec<_> = lockfiles.iter().map(|l| normalize_timestamps(l)).collect();
for i in 1..normalized_lockfiles.len() {
assert_eq!(
normalized_lockfiles[0],
normalized_lockfiles[i],
"Lockfiles should be consistent post-migration. Run 1 vs Run {}:\n\nRun 1:\n{}\n\nRun {}:\n{}",
i + 1,
normalized_lockfiles[0],
i + 1,
normalized_lockfiles[i]
);
}
let first_lockfile = toml::from_str::<agpm_cli::lockfile::LockFile>(&lockfiles[0])?;
let context_checksums: Vec<_> =
first_lockfile.agents.iter().filter_map(|a| a.context_checksum.as_ref()).collect();
assert!(!context_checksums.is_empty(), "Lockfile should contain context checksum");
assert_eq!(
context_checksums.len(),
1,
"Should have exactly one context checksum (for templated resource), found {}",
context_checksums.len()
);
Ok(())
}