1use crate::core::ResourceType;
23use crate::lockfile::{LockFile, LockedResource};
24use crate::manifest::{Manifest, ResourceDependency};
25use std::collections::HashMap;
26
27pub trait ResourceTypeExt {
33 fn all() -> Vec<ResourceType>;
43
44 fn get_lockfile_entries<'a>(&self, lockfile: &'a LockFile) -> &'a [LockedResource];
58
59 fn get_lockfile_entries_mut<'a>(
73 &mut self,
74 lockfile: &'a mut LockFile,
75 ) -> &'a mut Vec<LockedResource>;
76
77 fn get_manifest_entries<'a>(
79 &self,
80 manifest: &'a Manifest,
81 ) -> &'a HashMap<String, ResourceDependency>;
82}
83
84impl ResourceTypeExt for ResourceType {
85 fn all() -> Vec<ResourceType> {
86 vec![
87 Self::Agent,
88 Self::Snippet,
89 Self::Command,
90 Self::McpServer,
91 Self::Script,
92 Self::Hook,
93 Self::Skill,
94 ]
95 }
96
97 fn get_lockfile_entries<'a>(&self, lockfile: &'a LockFile) -> &'a [LockedResource] {
98 match self {
99 Self::Agent => &lockfile.agents,
100 Self::Snippet => &lockfile.snippets,
101 Self::Command => &lockfile.commands,
102 Self::Script => &lockfile.scripts,
103 Self::Hook => &lockfile.hooks,
104 Self::McpServer => &lockfile.mcp_servers,
105 Self::Skill => &lockfile.skills,
106 }
107 }
108
109 fn get_lockfile_entries_mut<'a>(
110 &mut self,
111 lockfile: &'a mut LockFile,
112 ) -> &'a mut Vec<LockedResource> {
113 match self {
114 Self::Agent => &mut lockfile.agents,
115 Self::Snippet => &mut lockfile.snippets,
116 Self::Command => &mut lockfile.commands,
117 Self::Script => &mut lockfile.scripts,
118 Self::Hook => &mut lockfile.hooks,
119 Self::McpServer => &mut lockfile.mcp_servers,
120 Self::Skill => &mut lockfile.skills,
121 }
122 }
123
124 fn get_manifest_entries<'a>(
125 &self,
126 manifest: &'a Manifest,
127 ) -> &'a HashMap<String, ResourceDependency> {
128 match self {
129 Self::Agent => &manifest.agents,
130 Self::Snippet => &manifest.snippets,
131 Self::Command => &manifest.commands,
132 Self::Script => &manifest.scripts,
133 Self::Hook => &manifest.hooks,
134 Self::McpServer => &manifest.mcp_servers,
135 Self::Skill => &manifest.skills,
136 }
137 }
138}
139
140pub struct ResourceIterator;
179
180impl ResourceIterator {
181 pub fn collect_all_entries<'a>(
204 lockfile: &'a LockFile,
205 manifest: &'a Manifest,
206 ) -> Vec<(&'a LockedResource, std::borrow::Cow<'a, str>)> {
207 let mut all_entries = Vec::new();
208
209 for resource_type in ResourceType::all() {
210 if matches!(resource_type, ResourceType::Hook | ResourceType::McpServer) {
213 continue;
214 }
215
216 let entries = resource_type.get_lockfile_entries(lockfile);
217
218 for entry in entries {
219 let tool = entry.tool.as_deref().unwrap_or("claude-code");
221 let artifact_path = manifest
224 .get_artifact_resource_path(tool, *resource_type)
225 .expect("Resource type should be supported by configured tools");
226 let target_dir = std::borrow::Cow::Owned(artifact_path.display().to_string());
227
228 all_entries.push((entry, target_dir));
229 }
230 }
231
232 all_entries
233 }
234
235 pub fn find_resource_by_name<'a>(
243 lockfile: &'a LockFile,
244 name: &str,
245 ) -> Option<(ResourceType, &'a LockedResource)> {
246 for resource_type in ResourceType::all() {
247 if let Some(entry) =
248 resource_type.get_lockfile_entries(lockfile).iter().find(|e| e.name == name)
249 {
250 return Some((*resource_type, entry));
251 }
252 }
253 None
254 }
255
256 pub fn find_resource_by_name_and_source<'a>(
269 lockfile: &'a LockFile,
270 name: &str,
271 source: Option<&str>,
272 ) -> Option<(ResourceType, &'a LockedResource)> {
273 for resource_type in ResourceType::all() {
274 if let Some(entry) = resource_type.get_lockfile_entries(lockfile).iter().find(|e| {
275 if e.source.as_deref() != source {
277 return false;
278 }
279 e.name == name || e.manifest_alias.as_deref() == Some(name)
282 }) {
283 return Some((*resource_type, entry));
284 }
285 }
286 None
287 }
288
289 pub fn count_total_resources(lockfile: &LockFile) -> usize {
291 ResourceType::all().iter().map(|rt| rt.get_lockfile_entries(lockfile).len()).sum()
292 }
293
294 pub fn count_manifest_dependencies(manifest: &Manifest) -> usize {
296 ResourceType::all().iter().map(|rt| rt.get_manifest_entries(manifest).len()).sum()
297 }
298
299 pub fn has_resources(lockfile: &LockFile) -> bool {
301 ResourceType::all().iter().any(|rt| !rt.get_lockfile_entries(lockfile).is_empty())
302 }
303
304 pub fn get_all_resource_names(lockfile: &LockFile) -> Vec<String> {
306 let mut names = Vec::new();
307 for resource_type in ResourceType::all() {
308 for entry in resource_type.get_lockfile_entries(lockfile) {
309 names.push(entry.name.clone());
310 }
311 }
312 names
313 }
314
315 pub fn get_resources_by_source<'a>(
317 lockfile: &'a LockFile,
318 resource_type: ResourceType,
319 source: &str,
320 ) -> Vec<&'a LockedResource> {
321 resource_type
322 .get_lockfile_entries(lockfile)
323 .iter()
324 .filter(|e| e.source.as_deref() == Some(source))
325 .collect()
326 }
327
328 pub fn for_each_resource<F>(lockfile: &LockFile, mut f: F)
330 where
331 F: FnMut(ResourceType, &LockedResource),
332 {
333 for resource_type in ResourceType::all() {
334 for entry in resource_type.get_lockfile_entries(lockfile) {
335 f(*resource_type, entry);
336 }
337 }
338 }
339
340 pub fn map_resources<T, F>(lockfile: &LockFile, mut f: F) -> Vec<T>
342 where
343 F: FnMut(ResourceType, &LockedResource) -> T,
344 {
345 let mut results = Vec::new();
346 Self::for_each_resource(lockfile, |rt, entry| {
347 results.push(f(rt, entry));
348 });
349 results
350 }
351
352 pub fn filter_resources<F>(
354 lockfile: &LockFile,
355 mut predicate: F,
356 ) -> Vec<(ResourceType, LockedResource)>
357 where
358 F: FnMut(ResourceType, &LockedResource) -> bool,
359 {
360 let mut results = Vec::new();
361 Self::for_each_resource(lockfile, |rt, entry| {
362 if predicate(rt, entry) {
363 results.push((rt, entry.clone()));
364 }
365 });
366 results
367 }
368
369 pub fn group_by_source(
371 lockfile: &LockFile,
372 ) -> std::collections::HashMap<String, Vec<(ResourceType, LockedResource)>> {
373 let mut groups = std::collections::HashMap::new();
374
375 Self::for_each_resource(lockfile, |rt, entry| {
376 if let Some(ref source) = entry.source {
377 groups.entry(source.clone()).or_insert_with(Vec::new).push((rt, entry.clone()));
378 }
379 });
380
381 groups
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::lockfile::{LockFile, LockedResource};
389 use crate::manifest::Manifest;
390
391 use crate::utils::normalize_path_for_storage;
392
393 fn create_test_lockfile() -> LockFile {
394 let mut lockfile = LockFile::new();
395
396 lockfile.agents.push(LockedResource {
397 name: "test-agent".to_string(),
398 source: Some("community".to_string()),
399 url: Some("https://github.com/test/repo.git".to_string()),
400 path: "agents/test.md".to_string(),
401 version: Some("v1.0.0".to_string()),
402 resolved_commit: Some("abc123".to_string()),
403 checksum: "sha256:abc".to_string(),
404 installed_at: ".claude/agents/test-agent.md".to_string(),
405 dependencies: vec![],
406 resource_type: crate::core::ResourceType::Agent,
407 context_checksum: None,
408 tool: Some("claude-code".to_string()),
409 manifest_alias: None,
410 applied_patches: std::collections::BTreeMap::new(),
411 install: None,
412 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
413 is_private: false,
414 });
415
416 lockfile.snippets.push(LockedResource {
417 name: "test-snippet".to_string(),
418 source: Some("community".to_string()),
419 url: Some("https://github.com/test/repo.git".to_string()),
420 path: "snippets/test.md".to_string(),
421 version: Some("v1.0.0".to_string()),
422 resolved_commit: Some("def456".to_string()),
423 checksum: "sha256:def".to_string(),
424 installed_at: ".claude/snippets/test-snippet.md".to_string(),
425 dependencies: vec![],
426 resource_type: crate::core::ResourceType::Snippet,
427 context_checksum: None,
428 tool: Some("claude-code".to_string()),
429 manifest_alias: None,
430 applied_patches: std::collections::BTreeMap::new(),
431 install: None,
432 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
433 is_private: false,
434 });
435
436 lockfile
437 }
438
439 fn create_test_manifest() -> Manifest {
440 Manifest::default()
441 }
442
443 fn create_multi_resource_lockfile() -> LockFile {
444 let mut lockfile = LockFile::new();
445
446 lockfile.agents.push(LockedResource {
448 name: "agent1".to_string(),
449 source: Some("source1".to_string()),
450 url: Some("https://github.com/source1/repo.git".to_string()),
451 path: "agents/agent1.md".to_string(),
452 version: Some("v1.0.0".to_string()),
453 resolved_commit: Some("abc123".to_string()),
454 checksum: "sha256:abc1".to_string(),
455 installed_at: ".claude/agents/agent1.md".to_string(),
456 dependencies: vec![],
457 resource_type: crate::core::ResourceType::Agent,
458 context_checksum: None,
459 tool: Some("claude-code".to_string()),
460 manifest_alias: None,
461 applied_patches: std::collections::BTreeMap::new(),
462 install: None,
463 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
464 is_private: false,
465 });
466
467 lockfile.agents.push(LockedResource {
468 name: "agent2".to_string(),
469 source: Some("source2".to_string()),
470 url: Some("https://github.com/source2/repo.git".to_string()),
471 path: "agents/agent2.md".to_string(),
472 version: Some("v2.0.0".to_string()),
473 resolved_commit: Some("def456".to_string()),
474 checksum: "sha256:def2".to_string(),
475 installed_at: ".claude/agents/agent2.md".to_string(),
476 dependencies: vec![],
477 resource_type: crate::core::ResourceType::Agent,
478 context_checksum: None,
479 tool: Some("claude-code".to_string()),
480 manifest_alias: None,
481 applied_patches: std::collections::BTreeMap::new(),
482 install: None,
483 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
484 is_private: false,
485 });
486
487 lockfile.commands.push(LockedResource {
489 name: "command1".to_string(),
490 source: Some("source1".to_string()),
491 url: Some("https://github.com/source1/repo.git".to_string()),
492 path: "commands/command1.md".to_string(),
493 version: Some("v1.1.0".to_string()),
494 resolved_commit: Some("ghi789".to_string()),
495 checksum: "sha256:ghi3".to_string(),
496 installed_at: ".claude/commands/command1.md".to_string(),
497 dependencies: vec![],
498 resource_type: crate::core::ResourceType::Command,
499 context_checksum: None,
500 tool: Some("claude-code".to_string()),
501 manifest_alias: None,
502 applied_patches: std::collections::BTreeMap::new(),
503 install: None,
504 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
505 is_private: false,
506 });
507
508 lockfile.scripts.push(LockedResource {
510 name: "script1".to_string(),
511 source: Some("source1".to_string()),
512 url: Some("https://github.com/source1/repo.git".to_string()),
513 path: "scripts/build.sh".to_string(),
514 version: Some("v1.0.0".to_string()),
515 resolved_commit: Some("jkl012".to_string()),
516 checksum: "sha256:jkl4".to_string(),
517 installed_at: ".claude/scripts/script1.sh".to_string(),
518 dependencies: vec![],
519 resource_type: crate::core::ResourceType::Script,
520 context_checksum: None,
521 tool: Some("claude-code".to_string()),
522 manifest_alias: None,
523 applied_patches: std::collections::BTreeMap::new(),
524 install: None,
525 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
526 is_private: false,
527 });
528
529 lockfile.hooks.push(LockedResource {
531 name: "hook1".to_string(),
532 source: Some("source2".to_string()),
533 url: Some("https://github.com/source2/repo.git".to_string()),
534 path: "hooks/pre-commit.json".to_string(),
535 version: Some("v1.0.0".to_string()),
536 resolved_commit: Some("mno345".to_string()),
537 checksum: "sha256:mno5".to_string(),
538 installed_at: ".claude/hooks/hook1.json".to_string(),
539 dependencies: vec![],
540 resource_type: crate::core::ResourceType::Hook,
541 context_checksum: None,
542 tool: Some("claude-code".to_string()),
543 manifest_alias: None,
544 applied_patches: std::collections::BTreeMap::new(),
545 install: None,
546 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
547 is_private: false,
548 });
549
550 lockfile.mcp_servers.push(LockedResource {
552 name: "mcp1".to_string(),
553 source: Some("source1".to_string()),
554 url: Some("https://github.com/source1/repo.git".to_string()),
555 path: "mcp-servers/filesystem.json".to_string(),
556 version: Some("v1.0.0".to_string()),
557 resolved_commit: Some("pqr678".to_string()),
558 checksum: "sha256:pqr6".to_string(),
559 installed_at: ".mcp-servers/mcp1.json".to_string(),
560 dependencies: vec![],
561 resource_type: crate::core::ResourceType::McpServer,
562 context_checksum: None,
563 tool: Some("claude-code".to_string()),
564 manifest_alias: None,
565 applied_patches: std::collections::BTreeMap::new(),
566 install: None,
567 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
568 is_private: false,
569 });
570
571 lockfile.snippets.push(LockedResource {
573 name: "local-snippet".to_string(),
574 source: None,
575 url: None,
576 path: "local/snippet.md".to_string(),
577 version: None,
578 resolved_commit: None,
579 checksum: "sha256:local".to_string(),
580 installed_at: ".agpm/snippets/local-snippet.md".to_string(),
581 dependencies: vec![],
582 resource_type: crate::core::ResourceType::Snippet,
583 context_checksum: None,
584 tool: Some("claude-code".to_string()),
585 manifest_alias: None,
586 applied_patches: std::collections::BTreeMap::new(),
587 install: None,
588 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
589 is_private: false,
590 });
591
592 lockfile
593 }
594
595 #[test]
596 fn test_resource_type_all() {
597 let all_types = ResourceType::all();
598 assert_eq!(all_types.len(), 7);
599 assert_eq!(all_types[0], ResourceType::Agent);
601 assert_eq!(all_types[1], ResourceType::Snippet);
602 assert_eq!(all_types[2], ResourceType::Command);
603 assert_eq!(all_types[3], ResourceType::McpServer);
604 assert_eq!(all_types[4], ResourceType::Script);
605 assert_eq!(all_types[5], ResourceType::Hook);
606 assert_eq!(all_types[6], ResourceType::Skill);
607 }
608
609 #[test]
610 fn test_get_lockfile_entries_mut() {
611 let mut lockfile = create_test_lockfile();
612
613 let mut agent_type = ResourceType::Agent;
615 let entries = agent_type.get_lockfile_entries_mut(&mut lockfile);
616 assert_eq!(entries.len(), 1);
617 assert_eq!(entries[0].name, "test-agent");
618
619 entries.push(LockedResource {
621 name: "new-agent".to_string(),
622 source: Some("test".to_string()),
623 url: Some("https://example.com/repo.git".to_string()),
624 path: "agents/new.md".to_string(),
625 version: Some("v1.0.0".to_string()),
626 resolved_commit: Some("xyz789".to_string()),
627 checksum: "sha256:xyz".to_string(),
628 installed_at: ".claude/agents/new-agent.md".to_string(),
629 dependencies: vec![],
630 resource_type: crate::core::ResourceType::Agent,
631 context_checksum: None,
632 tool: Some("claude-code".to_string()),
633 manifest_alias: None,
634 applied_patches: std::collections::BTreeMap::new(),
635 install: None,
636 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
637 is_private: false,
638 });
639
640 assert_eq!(lockfile.agents.len(), 2);
642 assert_eq!(lockfile.agents[1].name, "new-agent");
643
644 let mut snippet_type = ResourceType::Snippet;
646 let snippet_entries = snippet_type.get_lockfile_entries_mut(&mut lockfile);
647 assert_eq!(snippet_entries.len(), 1);
648
649 let mut command_type = ResourceType::Command;
650 let command_entries = command_type.get_lockfile_entries_mut(&mut lockfile);
651 assert_eq!(command_entries.len(), 0);
652
653 let mut script_type = ResourceType::Script;
654 let script_entries = script_type.get_lockfile_entries_mut(&mut lockfile);
655 assert_eq!(script_entries.len(), 0);
656
657 let mut hook_type = ResourceType::Hook;
658 let hook_entries = hook_type.get_lockfile_entries_mut(&mut lockfile);
659 assert_eq!(hook_entries.len(), 0);
660
661 let mut mcp_type = ResourceType::McpServer;
662 let mcp_entries = mcp_type.get_lockfile_entries_mut(&mut lockfile);
663 assert_eq!(mcp_entries.len(), 0);
664 }
665
666 #[test]
667 fn test_collect_all_entries() {
668 let lockfile = create_test_lockfile();
669 let manifest = create_test_manifest();
670
671 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
672 assert_eq!(entries.len(), 2);
673
674 assert_eq!(entries[0].0.name, "test-agent");
675 assert_eq!(normalize_path_for_storage(entries[0].1.as_ref()), ".claude/agents/agpm");
677
678 assert_eq!(entries[1].0.name, "test-snippet");
679 assert_eq!(normalize_path_for_storage(entries[1].1.as_ref()), ".claude/snippets/agpm");
682 }
683
684 #[test]
685 fn test_collect_all_entries_empty_lockfile() {
686 let empty_lockfile = LockFile::new();
687 let manifest = create_test_manifest();
688
689 let entries = ResourceIterator::collect_all_entries(&empty_lockfile, &manifest);
690 assert_eq!(entries.len(), 0);
691 }
692
693 #[test]
694 fn test_collect_all_entries_multiple_resources() {
695 let lockfile = create_multi_resource_lockfile();
696 let manifest = create_test_manifest();
697
698 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
699
700 assert_eq!(entries.len(), 5);
703
704 let mut found_types = std::collections::HashSet::new();
706 for (resource, _) in &entries {
707 match resource.name.as_str() {
708 "agent1" | "agent2" => {
709 found_types.insert("agent");
710 }
711 "local-snippet" => {
712 found_types.insert("snippet");
713 }
714 "command1" => {
715 found_types.insert("command");
716 }
717 "script1" => {
718 found_types.insert("script");
719 }
720 "hook1" | "mcp1" => {
722 panic!("Hooks and MCP servers should not be in collected entries");
723 }
724 _ => {}
725 }
726 }
727
728 assert_eq!(found_types.len(), 4);
729 }
730
731 #[test]
732 fn test_find_resource_by_name() {
733 let lockfile = create_test_lockfile();
734
735 let result = ResourceIterator::find_resource_by_name(&lockfile, "test-agent");
736 assert!(result.is_some());
737 let (rt, resource) = result.unwrap();
738 assert_eq!(rt, ResourceType::Agent);
739 assert_eq!(resource.name, "test-agent");
740
741 let result = ResourceIterator::find_resource_by_name(&lockfile, "nonexistent");
742 assert!(result.is_none());
743 }
744
745 #[test]
746 fn test_find_resource_by_name_multiple_types() {
747 let lockfile = create_multi_resource_lockfile();
748
749 let result = ResourceIterator::find_resource_by_name(&lockfile, "agent1");
751 assert!(result.is_some());
752 let (rt, resource) = result.unwrap();
753 assert_eq!(rt, ResourceType::Agent);
754 assert_eq!(resource.name, "agent1");
755
756 let result = ResourceIterator::find_resource_by_name(&lockfile, "command1");
758 assert!(result.is_some());
759 let (rt, resource) = result.unwrap();
760 assert_eq!(rt, ResourceType::Command);
761 assert_eq!(resource.name, "command1");
762
763 let result = ResourceIterator::find_resource_by_name(&lockfile, "script1");
765 assert!(result.is_some());
766 let (rt, resource) = result.unwrap();
767 assert_eq!(rt, ResourceType::Script);
768 assert_eq!(resource.name, "script1");
769
770 let result = ResourceIterator::find_resource_by_name(&lockfile, "hook1");
772 assert!(result.is_some());
773 let (rt, resource) = result.unwrap();
774 assert_eq!(rt, ResourceType::Hook);
775 assert_eq!(resource.name, "hook1");
776
777 let result = ResourceIterator::find_resource_by_name(&lockfile, "mcp1");
779 assert!(result.is_some());
780 let (rt, resource) = result.unwrap();
781 assert_eq!(rt, ResourceType::McpServer);
782 assert_eq!(resource.name, "mcp1");
783
784 let result = ResourceIterator::find_resource_by_name(&lockfile, "local-snippet");
786 assert!(result.is_some());
787 let (rt, resource) = result.unwrap();
788 assert_eq!(rt, ResourceType::Snippet);
789 assert_eq!(resource.name, "local-snippet");
790 assert!(resource.source.is_none());
791 }
792
793 #[test]
794 fn test_count_and_has_resources() {
795 let lockfile = create_test_lockfile();
796 assert_eq!(ResourceIterator::count_total_resources(&lockfile), 2);
797 assert!(ResourceIterator::has_resources(&lockfile));
798
799 let empty_lockfile = LockFile::new();
800 assert_eq!(ResourceIterator::count_total_resources(&empty_lockfile), 0);
801 assert!(!ResourceIterator::has_resources(&empty_lockfile));
802
803 let multi_lockfile = create_multi_resource_lockfile();
804 assert_eq!(ResourceIterator::count_total_resources(&multi_lockfile), 7);
805 assert!(ResourceIterator::has_resources(&multi_lockfile));
806 }
807
808 #[test]
809 fn test_get_all_resource_names() {
810 let lockfile = create_test_lockfile();
811 let names = ResourceIterator::get_all_resource_names(&lockfile);
812
813 assert_eq!(names.len(), 2);
814 assert!(names.contains(&"test-agent".to_string()));
815 assert!(names.contains(&"test-snippet".to_string()));
816 }
817
818 #[test]
819 fn test_get_all_resource_names_empty() {
820 let empty_lockfile = LockFile::new();
821 let names = ResourceIterator::get_all_resource_names(&empty_lockfile);
822 assert_eq!(names.len(), 0);
823 }
824
825 #[test]
826 fn test_get_all_resource_names_multiple() {
827 let lockfile = create_multi_resource_lockfile();
828 let names = ResourceIterator::get_all_resource_names(&lockfile);
829
830 assert_eq!(names.len(), 7);
831 assert!(names.contains(&"agent1".to_string()));
832 assert!(names.contains(&"agent2".to_string()));
833 assert!(names.contains(&"local-snippet".to_string()));
834 assert!(names.contains(&"command1".to_string()));
835 assert!(names.contains(&"script1".to_string()));
836 assert!(names.contains(&"hook1".to_string()));
837 assert!(names.contains(&"mcp1".to_string()));
838 }
839
840 #[test]
841 fn test_get_resources_by_source() {
842 let lockfile = create_multi_resource_lockfile();
843
844 let source1_resources =
846 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source1");
847 assert_eq!(source1_resources.len(), 1);
848 assert_eq!(source1_resources[0].name, "agent1");
849
850 let source1_commands =
851 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Command, "source1");
852 assert_eq!(source1_commands.len(), 1);
853 assert_eq!(source1_commands[0].name, "command1");
854
855 let source1_scripts =
856 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Script, "source1");
857 assert_eq!(source1_scripts.len(), 1);
858 assert_eq!(source1_scripts[0].name, "script1");
859
860 let source1_mcps = ResourceIterator::get_resources_by_source(
861 &lockfile,
862 ResourceType::McpServer,
863 "source1",
864 );
865 assert_eq!(source1_mcps.len(), 1);
866 assert_eq!(source1_mcps[0].name, "mcp1");
867
868 let source2_agents =
870 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source2");
871 assert_eq!(source2_agents.len(), 1);
872 assert_eq!(source2_agents[0].name, "agent2");
873
874 let source2_hooks =
875 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Hook, "source2");
876 assert_eq!(source2_hooks.len(), 1);
877 assert_eq!(source2_hooks[0].name, "hook1");
878
879 let nonexistent = ResourceIterator::get_resources_by_source(
881 &lockfile,
882 ResourceType::Agent,
883 "nonexistent",
884 );
885 assert_eq!(nonexistent.len(), 0);
886
887 let source1_snippets =
889 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Snippet, "source1");
890 assert_eq!(source1_snippets.len(), 0);
891 }
892
893 #[test]
894 fn test_for_each_resource() {
895 let lockfile = create_multi_resource_lockfile();
896 let mut visited_resources = Vec::new();
897
898 ResourceIterator::for_each_resource(&lockfile, |resource_type, resource| {
899 visited_resources.push((resource_type, resource.name.clone()));
900 });
901
902 assert_eq!(visited_resources.len(), 7);
903
904 let expected_resources = vec![
906 (ResourceType::Agent, "agent1".to_string()),
907 (ResourceType::Agent, "agent2".to_string()),
908 (ResourceType::Snippet, "local-snippet".to_string()),
909 (ResourceType::Command, "command1".to_string()),
910 (ResourceType::Script, "script1".to_string()),
911 (ResourceType::Hook, "hook1".to_string()),
912 (ResourceType::McpServer, "mcp1".to_string()),
913 ];
914
915 for expected in expected_resources {
916 assert!(visited_resources.contains(&expected));
917 }
918 }
919
920 #[test]
921 fn test_for_each_resource_empty() {
922 let empty_lockfile = LockFile::new();
923 let mut count = 0;
924
925 ResourceIterator::for_each_resource(&empty_lockfile, |_, _| {
926 count += 1;
927 });
928
929 assert_eq!(count, 0);
930 }
931
932 #[test]
933 fn test_map_resources() {
934 let lockfile = create_multi_resource_lockfile();
935
936 let names = ResourceIterator::map_resources(&lockfile, |_, resource| resource.name.clone());
938
939 assert_eq!(names.len(), 7);
940 assert!(names.contains(&"agent1".to_string()));
941 assert!(names.contains(&"agent2".to_string()));
942 assert!(names.contains(&"local-snippet".to_string()));
943 assert!(names.contains(&"command1".to_string()));
944 assert!(names.contains(&"script1".to_string()));
945 assert!(names.contains(&"hook1".to_string()));
946 assert!(names.contains(&"mcp1".to_string()));
947
948 let type_name_pairs =
950 ResourceIterator::map_resources(&lockfile, |resource_type, resource| {
951 format!("{}:{}", resource_type, resource.name)
952 });
953
954 assert_eq!(type_name_pairs.len(), 7);
955 assert!(type_name_pairs.contains(&"agent:agent1".to_string()));
956 assert!(type_name_pairs.contains(&"agent:agent2".to_string()));
957 assert!(type_name_pairs.contains(&"snippet:local-snippet".to_string()));
958 assert!(type_name_pairs.contains(&"command:command1".to_string()));
959 assert!(type_name_pairs.contains(&"script:script1".to_string()));
960 assert!(type_name_pairs.contains(&"hook:hook1".to_string()));
961 assert!(type_name_pairs.contains(&"mcp-server:mcp1".to_string()));
962 }
963
964 #[test]
965 fn test_map_resources_empty() {
966 let empty_lockfile = LockFile::new();
967
968 let results =
969 ResourceIterator::map_resources(&empty_lockfile, |_, resource| resource.name.clone());
970
971 assert_eq!(results.len(), 0);
972 }
973
974 #[test]
975 fn test_filter_resources() {
976 let lockfile = create_multi_resource_lockfile();
977
978 let source1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
980 resource.source.as_deref() == Some("source1")
981 });
982
983 assert_eq!(source1_resources.len(), 4); let source1_names: Vec<String> =
985 source1_resources.iter().map(|(_, r)| r.name.clone()).collect();
986 assert!(source1_names.contains(&"agent1".to_string()));
987 assert!(source1_names.contains(&"command1".to_string()));
988 assert!(source1_names.contains(&"script1".to_string()));
989 assert!(source1_names.contains(&"mcp1".to_string()));
990
991 let agents = ResourceIterator::filter_resources(&lockfile, |resource_type, _| {
993 resource_type == ResourceType::Agent
994 });
995
996 assert_eq!(agents.len(), 2); let agent_names: Vec<String> = agents.iter().map(|(_, r)| r.name.clone()).collect();
998 assert!(agent_names.contains(&"agent1".to_string()));
999 assert!(agent_names.contains(&"agent2".to_string()));
1000
1001 let no_source_resources =
1003 ResourceIterator::filter_resources(&lockfile, |_, resource| resource.source.is_none());
1004
1005 assert_eq!(no_source_resources.len(), 1); assert_eq!(no_source_resources[0].1.name, "local-snippet");
1007
1008 let v1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
1010 resource.version.as_deref().unwrap_or("").starts_with("v1.")
1011 });
1012
1013 assert_eq!(v1_resources.len(), 5); let no_matches = ResourceIterator::filter_resources(&lockfile, |_, resource| {
1017 resource.name == "nonexistent"
1018 });
1019
1020 assert_eq!(no_matches.len(), 0);
1021 }
1022
1023 #[test]
1024 fn test_filter_resources_empty() {
1025 let empty_lockfile = LockFile::new();
1026
1027 let results = ResourceIterator::filter_resources(&empty_lockfile, |_, _| true);
1028 assert_eq!(results.len(), 0);
1029 }
1030
1031 #[test]
1032 fn test_group_by_source() {
1033 let lockfile = create_multi_resource_lockfile();
1034
1035 let groups = ResourceIterator::group_by_source(&lockfile);
1036
1037 assert_eq!(groups.len(), 2); let source1_group = groups.get("source1").unwrap();
1041 assert_eq!(source1_group.len(), 4); let source1_names: Vec<String> =
1044 source1_group.iter().map(|(_, r)| r.name.clone()).collect();
1045 assert!(source1_names.contains(&"agent1".to_string()));
1046 assert!(source1_names.contains(&"command1".to_string()));
1047 assert!(source1_names.contains(&"script1".to_string()));
1048 assert!(source1_names.contains(&"mcp1".to_string()));
1049
1050 let source2_group = groups.get("source2").unwrap();
1052 assert_eq!(source2_group.len(), 2); let source2_names: Vec<String> =
1055 source2_group.iter().map(|(_, r)| r.name.clone()).collect();
1056 assert!(source2_names.contains(&"agent2".to_string()));
1057 assert!(source2_names.contains(&"hook1".to_string()));
1058
1059 assert!(!groups.contains_key(""));
1061 }
1062
1063 #[test]
1064 fn test_group_by_source_empty() {
1065 let empty_lockfile = LockFile::new();
1066
1067 let groups = ResourceIterator::group_by_source(&empty_lockfile);
1068 assert_eq!(groups.len(), 0);
1069 }
1070
1071 #[test]
1072 fn test_group_by_source_no_sources() {
1073 let mut lockfile = LockFile::new();
1074
1075 lockfile.agents.push(LockedResource {
1077 name: "local-agent".to_string(),
1078 source: None,
1079 url: None,
1080 path: "local/agent.md".to_string(),
1081 version: None,
1082 resolved_commit: None,
1083 checksum: "sha256:local".to_string(),
1084 installed_at: ".claude/agents/local-agent.md".to_string(),
1085 dependencies: vec![],
1086 resource_type: crate::core::ResourceType::Agent,
1087 context_checksum: None,
1088 tool: Some("claude-code".to_string()),
1089 manifest_alias: None,
1090 applied_patches: std::collections::BTreeMap::new(),
1091 install: None,
1092 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1093 is_private: false,
1094 });
1095
1096 let groups = ResourceIterator::group_by_source(&lockfile);
1097 assert_eq!(groups.len(), 0); }
1099
1100 #[test]
1101 fn test_resource_type_ext() {
1102 let lockfile = create_test_lockfile();
1103
1104 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 1);
1105 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1106 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 0);
1107 }
1108
1109 #[test]
1110 fn test_resource_type_ext_all_types() {
1111 let lockfile = create_multi_resource_lockfile();
1112
1113 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 2);
1114 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1115 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 1);
1116 assert_eq!(ResourceType::Script.get_lockfile_entries(&lockfile).len(), 1);
1117 assert_eq!(ResourceType::Hook.get_lockfile_entries(&lockfile).len(), 1);
1118 assert_eq!(ResourceType::McpServer.get_lockfile_entries(&lockfile).len(), 1);
1119 }
1120}