use crate::core::ResourceType;
use crate::lockfile::{LockFile, LockedResource};
use crate::manifest::{Manifest, ResourceDependency};
use std::collections::HashMap;
pub trait ResourceTypeExt {
fn all() -> Vec<ResourceType>;
fn get_lockfile_entries<'a>(&self, lockfile: &'a LockFile) -> &'a [LockedResource];
fn get_lockfile_entries_mut<'a>(
&mut self,
lockfile: &'a mut LockFile,
) -> &'a mut Vec<LockedResource>;
fn get_manifest_entries<'a>(
&self,
manifest: &'a Manifest,
) -> &'a HashMap<String, ResourceDependency>;
}
impl ResourceTypeExt for ResourceType {
fn all() -> Vec<ResourceType> {
vec![
Self::Agent,
Self::Snippet,
Self::Command,
Self::McpServer,
Self::Script,
Self::Hook,
Self::Skill,
]
}
fn get_lockfile_entries<'a>(&self, lockfile: &'a LockFile) -> &'a [LockedResource] {
match self {
Self::Agent => &lockfile.agents,
Self::Snippet => &lockfile.snippets,
Self::Command => &lockfile.commands,
Self::Script => &lockfile.scripts,
Self::Hook => &lockfile.hooks,
Self::McpServer => &lockfile.mcp_servers,
Self::Skill => &lockfile.skills,
}
}
fn get_lockfile_entries_mut<'a>(
&mut self,
lockfile: &'a mut LockFile,
) -> &'a mut Vec<LockedResource> {
match self {
Self::Agent => &mut lockfile.agents,
Self::Snippet => &mut lockfile.snippets,
Self::Command => &mut lockfile.commands,
Self::Script => &mut lockfile.scripts,
Self::Hook => &mut lockfile.hooks,
Self::McpServer => &mut lockfile.mcp_servers,
Self::Skill => &mut lockfile.skills,
}
}
fn get_manifest_entries<'a>(
&self,
manifest: &'a Manifest,
) -> &'a HashMap<String, ResourceDependency> {
match self {
Self::Agent => &manifest.agents,
Self::Snippet => &manifest.snippets,
Self::Command => &manifest.commands,
Self::Script => &manifest.scripts,
Self::Hook => &manifest.hooks,
Self::McpServer => &manifest.mcp_servers,
Self::Skill => &manifest.skills,
}
}
}
pub struct ResourceIterator;
impl ResourceIterator {
pub fn collect_all_entries<'a>(
lockfile: &'a LockFile,
manifest: &'a Manifest,
) -> Vec<(&'a LockedResource, std::borrow::Cow<'a, str>)> {
let mut all_entries = Vec::new();
for resource_type in ResourceType::all() {
if matches!(resource_type, ResourceType::Hook | ResourceType::McpServer) {
continue;
}
let entries = resource_type.get_lockfile_entries(lockfile);
for entry in entries {
let tool = entry.tool.as_deref().unwrap_or("claude-code");
let artifact_path = manifest
.get_artifact_resource_path(tool, *resource_type)
.expect("Resource type should be supported by configured tools");
let target_dir = std::borrow::Cow::Owned(artifact_path.display().to_string());
all_entries.push((entry, target_dir));
}
}
all_entries
}
pub fn find_resource_by_name<'a>(
lockfile: &'a LockFile,
name: &str,
) -> Option<(ResourceType, &'a LockedResource)> {
for resource_type in ResourceType::all() {
if let Some(entry) =
resource_type.get_lockfile_entries(lockfile).iter().find(|e| e.name == name)
{
return Some((*resource_type, entry));
}
}
None
}
pub fn find_resource_by_name_and_source<'a>(
lockfile: &'a LockFile,
name: &str,
source: Option<&str>,
) -> Option<(ResourceType, &'a LockedResource)> {
for resource_type in ResourceType::all() {
if let Some(entry) = resource_type.get_lockfile_entries(lockfile).iter().find(|e| {
if e.source.as_deref() != source {
return false;
}
e.name == name || e.manifest_alias.as_deref() == Some(name)
}) {
return Some((*resource_type, entry));
}
}
None
}
pub fn count_total_resources(lockfile: &LockFile) -> usize {
ResourceType::all().iter().map(|rt| rt.get_lockfile_entries(lockfile).len()).sum()
}
pub fn count_manifest_dependencies(manifest: &Manifest) -> usize {
ResourceType::all().iter().map(|rt| rt.get_manifest_entries(manifest).len()).sum()
}
pub fn has_resources(lockfile: &LockFile) -> bool {
ResourceType::all().iter().any(|rt| !rt.get_lockfile_entries(lockfile).is_empty())
}
pub fn get_all_resource_names(lockfile: &LockFile) -> Vec<String> {
let mut names = Vec::new();
for resource_type in ResourceType::all() {
for entry in resource_type.get_lockfile_entries(lockfile) {
names.push(entry.name.clone());
}
}
names
}
pub fn get_resources_by_source<'a>(
lockfile: &'a LockFile,
resource_type: ResourceType,
source: &str,
) -> Vec<&'a LockedResource> {
resource_type
.get_lockfile_entries(lockfile)
.iter()
.filter(|e| e.source.as_deref() == Some(source))
.collect()
}
pub fn for_each_resource<F>(lockfile: &LockFile, mut f: F)
where
F: FnMut(ResourceType, &LockedResource),
{
for resource_type in ResourceType::all() {
for entry in resource_type.get_lockfile_entries(lockfile) {
f(*resource_type, entry);
}
}
}
pub fn map_resources<T, F>(lockfile: &LockFile, mut f: F) -> Vec<T>
where
F: FnMut(ResourceType, &LockedResource) -> T,
{
let mut results = Vec::new();
Self::for_each_resource(lockfile, |rt, entry| {
results.push(f(rt, entry));
});
results
}
pub fn filter_resources<F>(
lockfile: &LockFile,
mut predicate: F,
) -> Vec<(ResourceType, LockedResource)>
where
F: FnMut(ResourceType, &LockedResource) -> bool,
{
let mut results = Vec::new();
Self::for_each_resource(lockfile, |rt, entry| {
if predicate(rt, entry) {
results.push((rt, entry.clone()));
}
});
results
}
pub fn group_by_source(
lockfile: &LockFile,
) -> std::collections::HashMap<String, Vec<(ResourceType, LockedResource)>> {
let mut groups = std::collections::HashMap::new();
Self::for_each_resource(lockfile, |rt, entry| {
if let Some(ref source) = entry.source {
groups.entry(source.clone()).or_insert_with(Vec::new).push((rt, entry.clone()));
}
});
groups
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lockfile::{LockFile, LockedResource};
use crate::manifest::Manifest;
use crate::utils::normalize_path_for_storage;
fn create_test_lockfile() -> LockFile {
let mut lockfile = LockFile::new();
lockfile.agents.push(LockedResource {
name: "test-agent".to_string(),
source: Some("community".to_string()),
url: Some("https://github.com/test/repo.git".to_string()),
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:abc".to_string(),
installed_at: ".claude/agents/test-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.snippets.push(LockedResource {
name: "test-snippet".to_string(),
source: Some("community".to_string()),
url: Some("https://github.com/test/repo.git".to_string()),
path: "snippets/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("def456".to_string()),
checksum: "sha256:def".to_string(),
installed_at: ".claude/snippets/test-snippet.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Snippet,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile
}
fn create_test_manifest() -> Manifest {
Manifest::default()
}
fn create_multi_resource_lockfile() -> LockFile {
let mut lockfile = LockFile::new();
lockfile.agents.push(LockedResource {
name: "agent1".to_string(),
source: Some("source1".to_string()),
url: Some("https://github.com/source1/repo.git".to_string()),
path: "agents/agent1.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:abc1".to_string(),
installed_at: ".claude/agents/agent1.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.agents.push(LockedResource {
name: "agent2".to_string(),
source: Some("source2".to_string()),
url: Some("https://github.com/source2/repo.git".to_string()),
path: "agents/agent2.md".to_string(),
version: Some("v2.0.0".to_string()),
resolved_commit: Some("def456".to_string()),
checksum: "sha256:def2".to_string(),
installed_at: ".claude/agents/agent2.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.commands.push(LockedResource {
name: "command1".to_string(),
source: Some("source1".to_string()),
url: Some("https://github.com/source1/repo.git".to_string()),
path: "commands/command1.md".to_string(),
version: Some("v1.1.0".to_string()),
resolved_commit: Some("ghi789".to_string()),
checksum: "sha256:ghi3".to_string(),
installed_at: ".claude/commands/command1.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Command,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.scripts.push(LockedResource {
name: "script1".to_string(),
source: Some("source1".to_string()),
url: Some("https://github.com/source1/repo.git".to_string()),
path: "scripts/build.sh".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("jkl012".to_string()),
checksum: "sha256:jkl4".to_string(),
installed_at: ".claude/scripts/script1.sh".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Script,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.hooks.push(LockedResource {
name: "hook1".to_string(),
source: Some("source2".to_string()),
url: Some("https://github.com/source2/repo.git".to_string()),
path: "hooks/pre-commit.json".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("mno345".to_string()),
checksum: "sha256:mno5".to_string(),
installed_at: ".claude/hooks/hook1.json".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Hook,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.mcp_servers.push(LockedResource {
name: "mcp1".to_string(),
source: Some("source1".to_string()),
url: Some("https://github.com/source1/repo.git".to_string()),
path: "mcp-servers/filesystem.json".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("pqr678".to_string()),
checksum: "sha256:pqr6".to_string(),
installed_at: ".mcp-servers/mcp1.json".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::McpServer,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile.snippets.push(LockedResource {
name: "local-snippet".to_string(),
source: None,
url: None,
path: "local/snippet.md".to_string(),
version: None,
resolved_commit: None,
checksum: "sha256:local".to_string(),
installed_at: ".agpm/snippets/local-snippet.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Snippet,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
lockfile
}
#[test]
fn test_resource_type_all() {
let all_types = ResourceType::all();
assert_eq!(all_types.len(), 7);
assert_eq!(all_types[0], ResourceType::Agent);
assert_eq!(all_types[1], ResourceType::Snippet);
assert_eq!(all_types[2], ResourceType::Command);
assert_eq!(all_types[3], ResourceType::McpServer);
assert_eq!(all_types[4], ResourceType::Script);
assert_eq!(all_types[5], ResourceType::Hook);
assert_eq!(all_types[6], ResourceType::Skill);
}
#[test]
fn test_get_lockfile_entries_mut() {
let mut lockfile = create_test_lockfile();
let mut agent_type = ResourceType::Agent;
let entries = agent_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "test-agent");
entries.push(LockedResource {
name: "new-agent".to_string(),
source: Some("test".to_string()),
url: Some("https://example.com/repo.git".to_string()),
path: "agents/new.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("xyz789".to_string()),
checksum: "sha256:xyz".to_string(),
installed_at: ".claude/agents/new-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
assert_eq!(lockfile.agents.len(), 2);
assert_eq!(lockfile.agents[1].name, "new-agent");
let mut snippet_type = ResourceType::Snippet;
let snippet_entries = snippet_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(snippet_entries.len(), 1);
let mut command_type = ResourceType::Command;
let command_entries = command_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(command_entries.len(), 0);
let mut script_type = ResourceType::Script;
let script_entries = script_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(script_entries.len(), 0);
let mut hook_type = ResourceType::Hook;
let hook_entries = hook_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(hook_entries.len(), 0);
let mut mcp_type = ResourceType::McpServer;
let mcp_entries = mcp_type.get_lockfile_entries_mut(&mut lockfile);
assert_eq!(mcp_entries.len(), 0);
}
#[test]
fn test_collect_all_entries() {
let lockfile = create_test_lockfile();
let manifest = create_test_manifest();
let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].0.name, "test-agent");
assert_eq!(normalize_path_for_storage(entries[0].1.as_ref()), ".claude/agents/agpm");
assert_eq!(entries[1].0.name, "test-snippet");
assert_eq!(normalize_path_for_storage(entries[1].1.as_ref()), ".claude/snippets/agpm");
}
#[test]
fn test_collect_all_entries_empty_lockfile() {
let empty_lockfile = LockFile::new();
let manifest = create_test_manifest();
let entries = ResourceIterator::collect_all_entries(&empty_lockfile, &manifest);
assert_eq!(entries.len(), 0);
}
#[test]
fn test_collect_all_entries_multiple_resources() {
let lockfile = create_multi_resource_lockfile();
let manifest = create_test_manifest();
let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
assert_eq!(entries.len(), 5);
let mut found_types = std::collections::HashSet::new();
for (resource, _) in &entries {
match resource.name.as_str() {
"agent1" | "agent2" => {
found_types.insert("agent");
}
"local-snippet" => {
found_types.insert("snippet");
}
"command1" => {
found_types.insert("command");
}
"script1" => {
found_types.insert("script");
}
"hook1" | "mcp1" => {
panic!("Hooks and MCP servers should not be in collected entries");
}
_ => {}
}
}
assert_eq!(found_types.len(), 4);
}
#[test]
fn test_find_resource_by_name() {
let lockfile = create_test_lockfile();
let result = ResourceIterator::find_resource_by_name(&lockfile, "test-agent");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Agent);
assert_eq!(resource.name, "test-agent");
let result = ResourceIterator::find_resource_by_name(&lockfile, "nonexistent");
assert!(result.is_none());
}
#[test]
fn test_find_resource_by_name_multiple_types() {
let lockfile = create_multi_resource_lockfile();
let result = ResourceIterator::find_resource_by_name(&lockfile, "agent1");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Agent);
assert_eq!(resource.name, "agent1");
let result = ResourceIterator::find_resource_by_name(&lockfile, "command1");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Command);
assert_eq!(resource.name, "command1");
let result = ResourceIterator::find_resource_by_name(&lockfile, "script1");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Script);
assert_eq!(resource.name, "script1");
let result = ResourceIterator::find_resource_by_name(&lockfile, "hook1");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Hook);
assert_eq!(resource.name, "hook1");
let result = ResourceIterator::find_resource_by_name(&lockfile, "mcp1");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::McpServer);
assert_eq!(resource.name, "mcp1");
let result = ResourceIterator::find_resource_by_name(&lockfile, "local-snippet");
assert!(result.is_some());
let (rt, resource) = result.unwrap();
assert_eq!(rt, ResourceType::Snippet);
assert_eq!(resource.name, "local-snippet");
assert!(resource.source.is_none());
}
#[test]
fn test_count_and_has_resources() {
let lockfile = create_test_lockfile();
assert_eq!(ResourceIterator::count_total_resources(&lockfile), 2);
assert!(ResourceIterator::has_resources(&lockfile));
let empty_lockfile = LockFile::new();
assert_eq!(ResourceIterator::count_total_resources(&empty_lockfile), 0);
assert!(!ResourceIterator::has_resources(&empty_lockfile));
let multi_lockfile = create_multi_resource_lockfile();
assert_eq!(ResourceIterator::count_total_resources(&multi_lockfile), 7);
assert!(ResourceIterator::has_resources(&multi_lockfile));
}
#[test]
fn test_get_all_resource_names() {
let lockfile = create_test_lockfile();
let names = ResourceIterator::get_all_resource_names(&lockfile);
assert_eq!(names.len(), 2);
assert!(names.contains(&"test-agent".to_string()));
assert!(names.contains(&"test-snippet".to_string()));
}
#[test]
fn test_get_all_resource_names_empty() {
let empty_lockfile = LockFile::new();
let names = ResourceIterator::get_all_resource_names(&empty_lockfile);
assert_eq!(names.len(), 0);
}
#[test]
fn test_get_all_resource_names_multiple() {
let lockfile = create_multi_resource_lockfile();
let names = ResourceIterator::get_all_resource_names(&lockfile);
assert_eq!(names.len(), 7);
assert!(names.contains(&"agent1".to_string()));
assert!(names.contains(&"agent2".to_string()));
assert!(names.contains(&"local-snippet".to_string()));
assert!(names.contains(&"command1".to_string()));
assert!(names.contains(&"script1".to_string()));
assert!(names.contains(&"hook1".to_string()));
assert!(names.contains(&"mcp1".to_string()));
}
#[test]
fn test_get_resources_by_source() {
let lockfile = create_multi_resource_lockfile();
let source1_resources =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source1");
assert_eq!(source1_resources.len(), 1);
assert_eq!(source1_resources[0].name, "agent1");
let source1_commands =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Command, "source1");
assert_eq!(source1_commands.len(), 1);
assert_eq!(source1_commands[0].name, "command1");
let source1_scripts =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Script, "source1");
assert_eq!(source1_scripts.len(), 1);
assert_eq!(source1_scripts[0].name, "script1");
let source1_mcps = ResourceIterator::get_resources_by_source(
&lockfile,
ResourceType::McpServer,
"source1",
);
assert_eq!(source1_mcps.len(), 1);
assert_eq!(source1_mcps[0].name, "mcp1");
let source2_agents =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source2");
assert_eq!(source2_agents.len(), 1);
assert_eq!(source2_agents[0].name, "agent2");
let source2_hooks =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Hook, "source2");
assert_eq!(source2_hooks.len(), 1);
assert_eq!(source2_hooks[0].name, "hook1");
let nonexistent = ResourceIterator::get_resources_by_source(
&lockfile,
ResourceType::Agent,
"nonexistent",
);
assert_eq!(nonexistent.len(), 0);
let source1_snippets =
ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Snippet, "source1");
assert_eq!(source1_snippets.len(), 0);
}
#[test]
fn test_for_each_resource() {
let lockfile = create_multi_resource_lockfile();
let mut visited_resources = Vec::new();
ResourceIterator::for_each_resource(&lockfile, |resource_type, resource| {
visited_resources.push((resource_type, resource.name.clone()));
});
assert_eq!(visited_resources.len(), 7);
let expected_resources = vec![
(ResourceType::Agent, "agent1".to_string()),
(ResourceType::Agent, "agent2".to_string()),
(ResourceType::Snippet, "local-snippet".to_string()),
(ResourceType::Command, "command1".to_string()),
(ResourceType::Script, "script1".to_string()),
(ResourceType::Hook, "hook1".to_string()),
(ResourceType::McpServer, "mcp1".to_string()),
];
for expected in expected_resources {
assert!(visited_resources.contains(&expected));
}
}
#[test]
fn test_for_each_resource_empty() {
let empty_lockfile = LockFile::new();
let mut count = 0;
ResourceIterator::for_each_resource(&empty_lockfile, |_, _| {
count += 1;
});
assert_eq!(count, 0);
}
#[test]
fn test_map_resources() {
let lockfile = create_multi_resource_lockfile();
let names = ResourceIterator::map_resources(&lockfile, |_, resource| resource.name.clone());
assert_eq!(names.len(), 7);
assert!(names.contains(&"agent1".to_string()));
assert!(names.contains(&"agent2".to_string()));
assert!(names.contains(&"local-snippet".to_string()));
assert!(names.contains(&"command1".to_string()));
assert!(names.contains(&"script1".to_string()));
assert!(names.contains(&"hook1".to_string()));
assert!(names.contains(&"mcp1".to_string()));
let type_name_pairs =
ResourceIterator::map_resources(&lockfile, |resource_type, resource| {
format!("{}:{}", resource_type, resource.name)
});
assert_eq!(type_name_pairs.len(), 7);
assert!(type_name_pairs.contains(&"agent:agent1".to_string()));
assert!(type_name_pairs.contains(&"agent:agent2".to_string()));
assert!(type_name_pairs.contains(&"snippet:local-snippet".to_string()));
assert!(type_name_pairs.contains(&"command:command1".to_string()));
assert!(type_name_pairs.contains(&"script:script1".to_string()));
assert!(type_name_pairs.contains(&"hook:hook1".to_string()));
assert!(type_name_pairs.contains(&"mcp-server:mcp1".to_string()));
}
#[test]
fn test_map_resources_empty() {
let empty_lockfile = LockFile::new();
let results =
ResourceIterator::map_resources(&empty_lockfile, |_, resource| resource.name.clone());
assert_eq!(results.len(), 0);
}
#[test]
fn test_filter_resources() {
let lockfile = create_multi_resource_lockfile();
let source1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
resource.source.as_deref() == Some("source1")
});
assert_eq!(source1_resources.len(), 4); let source1_names: Vec<String> =
source1_resources.iter().map(|(_, r)| r.name.clone()).collect();
assert!(source1_names.contains(&"agent1".to_string()));
assert!(source1_names.contains(&"command1".to_string()));
assert!(source1_names.contains(&"script1".to_string()));
assert!(source1_names.contains(&"mcp1".to_string()));
let agents = ResourceIterator::filter_resources(&lockfile, |resource_type, _| {
resource_type == ResourceType::Agent
});
assert_eq!(agents.len(), 2); let agent_names: Vec<String> = agents.iter().map(|(_, r)| r.name.clone()).collect();
assert!(agent_names.contains(&"agent1".to_string()));
assert!(agent_names.contains(&"agent2".to_string()));
let no_source_resources =
ResourceIterator::filter_resources(&lockfile, |_, resource| resource.source.is_none());
assert_eq!(no_source_resources.len(), 1); assert_eq!(no_source_resources[0].1.name, "local-snippet");
let v1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
resource.version.as_deref().unwrap_or("").starts_with("v1.")
});
assert_eq!(v1_resources.len(), 5);
let no_matches = ResourceIterator::filter_resources(&lockfile, |_, resource| {
resource.name == "nonexistent"
});
assert_eq!(no_matches.len(), 0);
}
#[test]
fn test_filter_resources_empty() {
let empty_lockfile = LockFile::new();
let results = ResourceIterator::filter_resources(&empty_lockfile, |_, _| true);
assert_eq!(results.len(), 0);
}
#[test]
fn test_group_by_source() {
let lockfile = create_multi_resource_lockfile();
let groups = ResourceIterator::group_by_source(&lockfile);
assert_eq!(groups.len(), 2);
let source1_group = groups.get("source1").unwrap();
assert_eq!(source1_group.len(), 4);
let source1_names: Vec<String> =
source1_group.iter().map(|(_, r)| r.name.clone()).collect();
assert!(source1_names.contains(&"agent1".to_string()));
assert!(source1_names.contains(&"command1".to_string()));
assert!(source1_names.contains(&"script1".to_string()));
assert!(source1_names.contains(&"mcp1".to_string()));
let source2_group = groups.get("source2").unwrap();
assert_eq!(source2_group.len(), 2);
let source2_names: Vec<String> =
source2_group.iter().map(|(_, r)| r.name.clone()).collect();
assert!(source2_names.contains(&"agent2".to_string()));
assert!(source2_names.contains(&"hook1".to_string()));
assert!(!groups.contains_key(""));
}
#[test]
fn test_group_by_source_empty() {
let empty_lockfile = LockFile::new();
let groups = ResourceIterator::group_by_source(&empty_lockfile);
assert_eq!(groups.len(), 0);
}
#[test]
fn test_group_by_source_no_sources() {
let mut lockfile = LockFile::new();
lockfile.agents.push(LockedResource {
name: "local-agent".to_string(),
source: None,
url: None,
path: "local/agent.md".to_string(),
version: None,
resolved_commit: None,
checksum: "sha256:local".to_string(),
installed_at: ".claude/agents/local-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
context_checksum: None,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
});
let groups = ResourceIterator::group_by_source(&lockfile);
assert_eq!(groups.len(), 0); }
#[test]
fn test_resource_type_ext() {
let lockfile = create_test_lockfile();
assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 0);
}
#[test]
fn test_resource_type_ext_all_types() {
let lockfile = create_multi_resource_lockfile();
assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 2);
assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::Script.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::Hook.get_lockfile_entries(&lockfile).len(), 1);
assert_eq!(ResourceType::McpServer.get_lockfile_entries(&lockfile).len(), 1);
}
}