use crate::common::{ManifestBuilder, TestProject};
use anyhow::Result;
use std::fs;
#[tokio::test]
async fn test_skill_with_transitive_dependencies() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.add_resource(
"agents",
"base-agent",
r#"---
name: Base Agent
description: A base agent for testing
---
# Base Agent
"#,
)
.await?;
source
.create_file(
"snippets/utils.md",
r#"---
name: Utility Snippets
description: Useful utility snippets
---
# Utility Snippets
"#,
)
.await?;
source
.create_skill(
"complex-skill",
r#"---
name: Complex Skill
description: A skill with dependencies
dependencies:
agents:
- path: agents/base-agent.md
snippets:
- path: snippets/utils.md
---
# Complex Skill
This skill depends on other resources.
"#,
)
.await?;
source.commit_all("Add skill with dependencies")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("complex-skill", |d| {
d.source("test").path("skills/complex-skill").version("HEAD")
})
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
project.run_agpm(&["install"])?;
assert!(project.project_path().join(".claude/skills/agpm/complex-skill").exists());
assert!(project.project_path().join(".claude/agents/agpm/base-agent.md").exists());
assert!(project.project_path().join(".claude/snippets/agpm/utils.md").exists());
Ok(())
}
#[tokio::test]
async fn test_skill_validation() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.create_skill(
"valid-skill",
r#"---
name: Valid Skill
description: A properly formatted skill
---
# Valid Skill
"#,
)
.await?;
source.commit_all("Add valid skill")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("valid-skill", |d| d.source("test").path("skills/valid-skill").version("HEAD"))
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
project.run_agpm(&["install"])?;
let result = project.run_agpm(&["validate", "--paths"])?;
assert!(result.success);
assert!(project.project_path().join(".claude/skills/agpm/valid-skill").exists());
Ok(())
}
#[tokio::test]
async fn test_skill_list_command() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.create_skill(
"skill-a",
r#"---
name: Skill A
description: First skill for listing
---
# Skill A
"#,
)
.await?;
source
.create_skill(
"skill-b",
r#"---
name: Skill B
description: Second skill for listing
---
# Skill B
"#,
)
.await?;
source.commit_all("Add skills for listing")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("skill-a", |d| d.source("test").path("skills/skill-a").version("HEAD"))
.add_skill("skill-b", |d| d.source("test").path("skills/skill-b").version("HEAD"))
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
project.run_agpm(&["install"])?;
let result = project.run_agpm(&["list", "--type", "skill"])?;
assert!(
result.success,
"list command failed: stdout={}, stderr={}",
result.stdout, result.stderr
);
assert!(result.stdout.contains("skill-a"), "skill-a not found in stdout: {}", result.stdout);
assert!(result.stdout.contains("skill-b"), "skill-b not found in stdout: {}", result.stdout);
Ok(())
}
#[tokio::test]
async fn test_remove_skill() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.create_skill(
"removable-skill",
r#"---
name: Removable Skill
description: A skill that can be removed
---
# Removable Skill
"#,
)
.await?;
source.commit_all("Add removable skill")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("removable-skill", |d| {
d.source("test").path("skills/removable-skill").version("HEAD")
})
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
project.run_agpm(&["install"])?;
assert!(project.project_path().join(".claude/skills/agpm/removable-skill").exists());
project.run_agpm(&["remove", "dep", "skill", "removable-skill"])?;
let manifest_content = fs::read_to_string(project.project_path().join("agpm.toml")).unwrap();
assert!(!manifest_content.contains("removable-skill"));
Ok(())
}
#[tokio::test]
async fn test_skill_complete_removal_and_reinstallation() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.create_skill(
"comprehensive-skill",
r#"---
name: Comprehensive Test Skill
description: A skill with multiple files for testing complete removal
model: claude-3-opus
temperature: "0.7"
---
# Comprehensive Test Skill
This skill tests complete removal and reinstallation.
"#,
)
.await?;
let skill_source_dir = source.path.join("skills").join("comprehensive-skill");
fs::write(skill_source_dir.join("config.json"), r#"{"setting": "value"}"#)?;
fs::write(skill_source_dir.join("script.sh"), "#!/bin/bash\necho 'Hello World'")?;
fs::create_dir_all(skill_source_dir.join("utils"))?;
fs::write(skill_source_dir.join("utils/helper.txt"), "Helper content")?;
source.commit_all("Add comprehensive skill with multiple files")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("comprehensive-skill", |d| {
d.source("test").path("skills/comprehensive-skill").version("HEAD")
})
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
project.run_agpm(&["install"])?;
let skill_path = project.project_path().join(".claude/skills/agpm/comprehensive-skill");
assert!(skill_path.exists(), "Skill directory should exist after installation");
assert!(skill_path.is_dir(), "Skill should be a directory");
assert!(skill_path.join("SKILL.md").exists(), "SKILL.md should exist");
assert!(skill_path.join("config.json").exists(), "config.json should exist");
assert!(skill_path.join("script.sh").exists(), "script.sh should exist");
assert!(skill_path.join("utils/helper.txt").exists(), "Nested file should exist");
let lockfile_content = project.read_lockfile().await?;
assert!(lockfile_content.contains("comprehensive-skill"), "Skill should be in lockfile");
assert!(lockfile_content.contains("checksum = \"sha256:"), "Skill should have checksum");
fs::write(skill_path.join("extra-file.txt"), "This should be removed")?;
assert!(skill_path.join("extra-file.txt").exists(), "Extra file should exist initially");
project.run_agpm(&["remove", "dep", "skill", "comprehensive-skill"])?;
assert!(!skill_path.exists(), "Skill directory should be completely removed after removal");
assert!(
!project.project_path().join(".claude/skills/agpm/comprehensive-skill").exists(),
"Skill directory should not exist in any form"
);
let updated_manifest = fs::read_to_string(project.project_path().join("agpm.toml")).unwrap();
assert!(
!updated_manifest.contains("comprehensive-skill"),
"Skill should be removed from manifest"
);
let updated_lockfile = project.read_lockfile().await?;
assert!(
!updated_lockfile.contains("comprehensive-skill"),
"Skill should be removed from lockfile"
);
let skills_dir = project.project_path().join(".claude/skills/agpm");
if skills_dir.exists() {
let entries: Vec<_> = fs::read_dir(skills_dir)?.collect::<Result<Vec<_>, _>>()?;
assert!(
!entries.iter().any(|entry| {
entry.file_name().to_string_lossy().contains("comprehensive-skill")
}),
"No skill-related artifacts should remain"
);
}
let reinstallation_manifest = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("comprehensive-skill", |d| {
d.source("test").path("skills/comprehensive-skill").version("HEAD")
})
.with_claude_code_tool()
.build();
project.write_manifest(&reinstallation_manifest).await?;
project.run_agpm(&["install"])?;
assert!(skill_path.exists(), "Skill directory should exist after reinstallation");
assert!(skill_path.is_dir(), "Skill should be a directory after reinstallation");
assert!(skill_path.join("SKILL.md").exists(), "SKILL.md should exist after reinstallation");
assert!(
skill_path.join("config.json").exists(),
"config.json should exist after reinstallation"
);
assert!(skill_path.join("script.sh").exists(), "script.sh should exist after reinstallation");
assert!(
skill_path.join("utils/helper.txt").exists(),
"Nested file should exist after reinstallation"
);
assert!(
!skill_path.join("extra-file.txt").exists(),
"Extra file should be removed during clean reinstallation"
);
let final_lockfile = project.read_lockfile().await?;
assert!(final_lockfile.contains("comprehensive-skill"), "Skill should be back in lockfile");
assert!(
final_lockfile.contains("checksum = \"sha256:"),
"Skill should have checksum after reinstallation"
);
let skill_content = fs::read_to_string(skill_path.join("SKILL.md")).unwrap();
assert!(skill_content.contains("Comprehensive Test Skill"), "Skill content should be correct");
let config_content = fs::read_to_string(skill_path.join("config.json")).unwrap();
assert!(config_content.contains("\"setting\""), "Config file should be correct");
let helper_content = fs::read_to_string(skill_path.join("utils/helper.txt")).unwrap();
assert_eq!(helper_content, "Helper content", "Nested file content should be correct");
Ok(())
}
#[tokio::test]
async fn test_skill_with_private_patches() -> Result<()> {
let project = TestProject::new().await?;
let source = project.create_source_repo("test").await?;
source
.create_skill(
"patchable-skill",
r#"---
name: Patchable Skill
description: A skill for testing private patches
model: claude-3-opus
---
# Patchable Skill
"#,
)
.await?;
source.commit_all("Add patchable skill")?;
let source_url = source.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("test", &source_url)
.add_skill("patchable-skill", |d| {
d.source("test").path("skills/patchable-skill").version("HEAD")
})
.with_claude_code_tool()
.build();
project.write_manifest(&manifest_content).await?;
let project_patches = r#"
[patch.skills.patchable-skill]
model = "claude-3-sonnet"
temperature = "0.5"
"#;
fs::write(
project.project_path().join("agpm.toml"),
format!("{}\n{}", manifest_content, project_patches),
)?;
let private_patches = r#"
[patch.skills.patchable-skill]
temperature = "0.9"
max_tokens = 1000
"#;
fs::write(project.project_path().join("agpm.private.toml"), private_patches)?;
project.run_agpm(&["install"])?;
let skill_path = project.project_path().join(".claude/skills/agpm/patchable-skill");
let content = fs::read_to_string(skill_path.join("SKILL.md")).unwrap();
assert!(content.contains("model: claude-3-sonnet"));
assert!(content.contains("temperature: '0.9'")); assert!(content.contains("max_tokens: 1000")); Ok(())
}