use anyhow::Result;
use sha2::{Digest, Sha256};
use crate::common::{ManifestBuilder, TestProject};
fn compute_file_hash(path: &std::path::Path) -> Result<String> {
let content = std::fs::read(path)?;
let hash = Sha256::digest(&content);
Ok(format!("{:x}", hash))
}
fn collect_installed_hashes(base_path: &std::path::Path) -> Result<Vec<(String, String)>> {
let mut hashes = Vec::new();
for entry in walkdir::WalkDir::new(base_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
let relative_path = path.strip_prefix(base_path)?.to_string_lossy().to_string();
if relative_path == "agpm.lock" {
continue;
}
let hash = compute_file_hash(path)?;
hashes.push((relative_path, hash));
}
hashes.sort();
Ok(hashes)
}
#[tokio::test]
async fn test_basic_stability_multiple_installs() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "my-agent", "# My Agent\nSimple agent content").await?;
source_repo.add_resource("snippets", "my-snippet", "# My Snippet\nSnippet content").await?;
source_repo.add_resource("commands", "my-command", "# My Command\nCommand content").await?;
source_repo.commit_all("Add resources")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_agent("my-agent", "test-source", "agents/my-agent.md")
.add_standard_snippet("my-snippet", "test-source", "snippets/my-snippet.md")
.add_standard_command("my-command", "test-source", "commands/my-command.md")
.build();
project.write_manifest(&manifest).await?;
let mut all_hashes = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Install #{} failed: {}", i + 1, output.stderr);
let hashes = collect_installed_hashes(project.project_path())?;
all_hashes.push(hashes);
}
let first_hashes = &all_hashes[0];
for (i, hashes) in all_hashes.iter().enumerate().skip(1) {
assert_eq!(
first_hashes,
hashes,
"Install #{} produced different content than install #1",
i + 1
);
}
Ok(())
}
#[tokio::test]
async fn test_frozen_stability_multiple_installs() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo.add_resource("agents", "my-agent", "# My Agent\nAgent content").await?;
source_repo.commit_all("Add agent")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_agent("my-agent", "test-source", "agents/my-agent.md")
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Initial install failed: {}", output.stderr);
let initial_hashes = collect_installed_hashes(project.project_path())?;
let mut all_frozen_hashes = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--frozen", "--quiet"])?;
assert!(output.success, "Frozen install #{} failed: {}", i + 1, output.stderr);
let hashes = collect_installed_hashes(project.project_path())?;
all_frozen_hashes.push(hashes);
}
for (i, hashes) in all_frozen_hashes.iter().enumerate() {
assert_eq!(
&initial_hashes,
hashes,
"Frozen install #{} produced different content than initial install",
i + 1
);
}
Ok(())
}
#[tokio::test]
async fn test_templating_stability() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo
.add_resource(
"snippets",
"base",
"---\nagpm:\n templating: false\n---\n# Base Content\nThis is base content",
)
.await?;
source_repo
.add_resource(
"commands",
"my-command",
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: base
install: false
path: ../snippets/base.md
---
# My Command
{{ agpm.deps.snippets.base.content }}
"#,
)
.await?;
source_repo.commit_all("Add templated resources")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_command("my-command", "test-source", "commands/my-command.md")
.build();
project.write_manifest(&manifest).await?;
let mut all_hashes = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(output.success, "Install #{} with templating failed: {}", i + 1, output.stderr);
let command_path = project.project_path().join(".claude/commands/agpm/my-command.md");
let command_content = tokio::fs::read_to_string(&command_path).await?;
assert!(
command_content.contains("Base Content"),
"Template was not rendered correctly in install #{}",
i + 1
);
let hashes = collect_installed_hashes(project.project_path())?;
all_hashes.push(hashes);
}
let first_hashes = &all_hashes[0];
for (i, hashes) in all_hashes.iter().enumerate().skip(1) {
assert_eq!(
first_hashes,
hashes,
"Templated install #{} produced different content than install #1.\n\
This indicates a template rendering stability issue.",
i + 1
);
}
Ok(())
}
#[tokio::test]
async fn test_transitive_dependency_stability() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo
.add_resource(
"snippets",
"level1",
"---\nagpm:\n templating: false\n---\n# Level 1\nBase level",
)
.await?;
source_repo
.add_resource(
"snippets",
"level2",
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: level1
install: false
path: snippets/level1.md
---
# Level 2
{{ agpm.deps.snippets.level1.content }}
"#,
)
.await?;
source_repo
.add_resource(
"commands",
"my-command",
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: level2
install: false
path: ../snippets/level2.md
---
# My Command
{{ agpm.deps.snippets.level2.content }}
"#,
)
.await?;
source_repo.commit_all("Add multi-level dependencies")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_command("my-command", "test-source", "commands/my-command.md")
.build();
project.write_manifest(&manifest).await?;
let mut all_hashes = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(
output.success,
"Install #{} with transitive deps failed: {}",
i + 1,
output.stderr
);
let command_path = project.project_path().join(".claude/commands/agpm/my-command.md");
let command_content = tokio::fs::read_to_string(&command_path).await?;
assert!(
command_content.contains("Level 1"),
"Transitive dependency content missing in install #{}",
i + 1
);
let hashes = collect_installed_hashes(project.project_path())?;
all_hashes.push(hashes);
}
let first_hashes = &all_hashes[0];
for (i, hashes) in all_hashes.iter().enumerate().skip(1) {
assert_eq!(
first_hashes,
hashes,
"Install #{} with transitive deps produced different content.\n\
This indicates a transitive dependency resolution issue.",
i + 1
);
}
Ok(())
}
#[tokio::test]
async fn test_custom_name_collision_stability() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
for i in 1..=3 {
source_repo
.add_resource(
"snippets",
&format!("content-{}", i),
&format!("# Content {}\nUnique content {}", i, i),
)
.await?;
}
for i in 1..=3 {
source_repo
.add_resource(
"commands",
&format!("cmd-{}", i),
&format!(
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: base
install: false
path: ../snippets/content-{}.md
---
# Command {}
{{{{ agpm.deps.snippets.base.content }}}}
"#,
i, i
),
)
.await?;
}
source_repo.commit_all("Add commands with name collisions")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_command("cmd-1", "test-source", "commands/cmd-1.md")
.add_standard_command("cmd-2", "test-source", "commands/cmd-2.md")
.add_standard_command("cmd-3", "test-source", "commands/cmd-3.md")
.add_standard_snippet("content-1", "test-source", "snippets/content-1.md")
.add_standard_snippet("content-2", "test-source", "snippets/content-2.md")
.add_standard_snippet("content-3", "test-source", "snippets/content-3.md")
.build();
project.write_manifest(&manifest).await?;
let mut all_hashes = Vec::new();
let mut all_contents = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(
output.success,
"Install #{} with name collisions failed: {}",
i + 1,
output.stderr
);
for j in 1..=3 {
let cmd_path =
project.project_path().join(format!(".claude/commands/agpm/cmd-{}.md", j));
let cmd_content = tokio::fs::read_to_string(&cmd_path).await?;
assert!(
cmd_content.contains(&format!("Unique content {}", j)),
"Command {} got wrong content in install #{}. \
Expected 'Unique content {}', but content was:\n{}",
j,
i + 1,
j,
cmd_content
);
}
let hashes = collect_installed_hashes(project.project_path())?;
all_hashes.push(hashes.clone());
let mut contents = Vec::new();
for j in 1..=3 {
let cmd_path =
project.project_path().join(format!(".claude/commands/agpm/cmd-{}.md", j));
let content = tokio::fs::read_to_string(&cmd_path).await?;
contents.push((format!("cmd-{}.md", j), content));
}
all_contents.push(contents);
}
let first_hashes = &all_hashes[0];
for (i, hashes) in all_hashes.iter().enumerate().skip(1) {
if first_hashes != hashes {
let first_contents = &all_contents[0];
let current_contents = &all_contents[i];
for ((name1, content1), (_name2, content2)) in
first_contents.iter().zip(current_contents.iter())
{
if content1 != content2 {
panic!(
"Install #{} produced different content for {}.\n\
First install:\n{}\n\n\
Install #{}:\n{}",
i + 1,
name1,
content1,
i + 1,
content2
);
}
}
panic!(
"Install #{} produced different hashes but content appears same. Hash issue?",
i + 1
);
}
}
Ok(())
}
#[tokio::test]
async fn test_nested_transitive_custom_names_stability() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("test-source").await?;
source_repo
.add_resource(
"snippets",
"helper",
"---\nagpm:\n templating: false\n---\n# Helper Content\nThis is helper content",
)
.await?;
source_repo
.add_resource(
"snippets",
"base",
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: helper
install: false
path: ../snippets/helper.md
---
# Base Content
{{ agpm.deps.snippets.helper.content }}
"#,
)
.await?;
source_repo
.add_resource(
"commands",
"my-command",
r#"---
agpm:
templating: true
dependencies:
snippets:
- name: base
install: false
path: ../snippets/base.md
- name: helper
install: false
path: ../snippets/helper.md
---
# My Command
Base: {{ agpm.deps.snippets.base.content }}
Helper: {{ agpm.deps.snippets.helper.content }}
"#,
)
.await?;
source_repo.commit_all("Add nested dependencies")?;
source_repo.tag_version("v1.0.0")?;
let manifest = ManifestBuilder::new()
.add_source("test-source", &source_repo.file_url())
.add_standard_command("my-command", "test-source", "commands/my-command.md")
.build();
project.write_manifest(&manifest).await?;
let mut all_hashes = Vec::new();
for i in 0..10 {
let output = project.run_agpm(&["install", "--quiet"])?;
assert!(
output.success,
"Install #{} with nested transitive custom names failed: {}",
i + 1,
output.stderr
);
let command_path = project.project_path().join(".claude/commands/agpm/my-command.md");
let command_content = tokio::fs::read_to_string(&command_path).await?;
assert!(
command_content.contains("Helper Content"),
"Nested transitive custom name 'helper' was not accessible in install #{}",
i + 1
);
let hashes = collect_installed_hashes(project.project_path())?;
all_hashes.push(hashes);
}
let first_hashes = &all_hashes[0];
for (i, hashes) in all_hashes.iter().enumerate().skip(1) {
assert_eq!(
first_hashes,
hashes,
"Install #{} with nested transitive custom names produced different content than install #1.\n\
This indicates a template rendering stability issue with custom name extraction.",
i + 1
);
}
Ok(())
}