1use crate::core::ResourceType;
23use crate::lockfile::{LockFile, LockedResource};
24use crate::manifest::{Manifest, ResourceDependency, TargetConfig};
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_target_dir<'a>(&self, targets: &'a TargetConfig) -> &'a str;
91
92 fn get_manifest_entries<'a>(
94 &self,
95 manifest: &'a Manifest,
96 ) -> &'a HashMap<String, ResourceDependency>;
97}
98
99impl ResourceTypeExt for ResourceType {
100 fn all() -> Vec<ResourceType> {
101 vec![Self::Agent, Self::Snippet, Self::Command, Self::McpServer, Self::Script, Self::Hook]
102 }
103
104 fn get_lockfile_entries<'a>(&self, lockfile: &'a LockFile) -> &'a [LockedResource] {
105 match self {
106 Self::Agent => &lockfile.agents,
107 Self::Snippet => &lockfile.snippets,
108 Self::Command => &lockfile.commands,
109 Self::Script => &lockfile.scripts,
110 Self::Hook => &lockfile.hooks,
111 Self::McpServer => &lockfile.mcp_servers,
112 }
113 }
114
115 fn get_lockfile_entries_mut<'a>(
116 &mut self,
117 lockfile: &'a mut LockFile,
118 ) -> &'a mut Vec<LockedResource> {
119 match self {
120 Self::Agent => &mut lockfile.agents,
121 Self::Snippet => &mut lockfile.snippets,
122 Self::Command => &mut lockfile.commands,
123 Self::Script => &mut lockfile.scripts,
124 Self::Hook => &mut lockfile.hooks,
125 Self::McpServer => &mut lockfile.mcp_servers,
126 }
127 }
128
129 fn get_target_dir<'a>(&self, targets: &'a TargetConfig) -> &'a str {
130 match self {
131 Self::Agent => targets.agents.as_str(),
132 Self::Snippet => targets.snippets.as_str(),
133 Self::Command => targets.commands.as_str(),
134 Self::Script => targets.scripts.as_str(),
135 Self::Hook => targets.hooks.as_str(),
136 Self::McpServer => targets.mcp_servers.as_str(),
137 }
138 }
139
140 fn get_manifest_entries<'a>(
141 &self,
142 manifest: &'a Manifest,
143 ) -> &'a HashMap<String, ResourceDependency> {
144 match self {
145 Self::Agent => &manifest.agents,
146 Self::Snippet => &manifest.snippets,
147 Self::Command => &manifest.commands,
148 Self::Script => &manifest.scripts,
149 Self::Hook => &manifest.hooks,
150 Self::McpServer => &manifest.mcp_servers,
151 }
152 }
153}
154
155pub struct ResourceIterator;
194
195impl ResourceIterator {
196 pub fn collect_all_entries<'a>(
219 lockfile: &'a LockFile,
220 manifest: &'a Manifest,
221 ) -> Vec<(&'a LockedResource, std::borrow::Cow<'a, str>)> {
222 let mut all_entries = Vec::new();
223
224 for resource_type in ResourceType::all() {
225 if matches!(resource_type, ResourceType::Hook | ResourceType::McpServer) {
228 continue;
229 }
230
231 let entries = resource_type.get_lockfile_entries(lockfile);
232
233 for entry in entries {
234 let target_dir = if let Some(artifact_path) =
236 manifest.get_artifact_resource_path(&entry.tool, *resource_type)
237 {
238 std::borrow::Cow::Owned(artifact_path.display().to_string())
239 } else {
240 #[allow(deprecated)]
242 std::borrow::Cow::Borrowed(resource_type.get_target_dir(&manifest.target))
243 };
244
245 all_entries.push((entry, target_dir));
246 }
247 }
248
249 all_entries
250 }
251
252 pub fn find_resource_by_name<'a>(
260 lockfile: &'a LockFile,
261 name: &str,
262 ) -> Option<(ResourceType, &'a LockedResource)> {
263 for resource_type in ResourceType::all() {
264 if let Some(entry) =
265 resource_type.get_lockfile_entries(lockfile).iter().find(|e| e.name == name)
266 {
267 return Some((*resource_type, entry));
268 }
269 }
270 None
271 }
272
273 pub fn find_resource_by_name_and_source<'a>(
286 lockfile: &'a LockFile,
287 name: &str,
288 source: Option<&str>,
289 ) -> Option<(ResourceType, &'a LockedResource)> {
290 for resource_type in ResourceType::all() {
291 if let Some(entry) = resource_type
292 .get_lockfile_entries(lockfile)
293 .iter()
294 .find(|e| e.name == name && e.source.as_deref() == source)
295 {
296 return Some((*resource_type, entry));
297 }
298 }
299 None
300 }
301
302 pub fn count_total_resources(lockfile: &LockFile) -> usize {
304 ResourceType::all().iter().map(|rt| rt.get_lockfile_entries(lockfile).len()).sum()
305 }
306
307 pub fn count_manifest_dependencies(manifest: &Manifest) -> usize {
309 ResourceType::all().iter().map(|rt| rt.get_manifest_entries(manifest).len()).sum()
310 }
311
312 pub fn has_resources(lockfile: &LockFile) -> bool {
314 ResourceType::all().iter().any(|rt| !rt.get_lockfile_entries(lockfile).is_empty())
315 }
316
317 pub fn get_all_resource_names(lockfile: &LockFile) -> Vec<String> {
319 let mut names = Vec::new();
320 for resource_type in ResourceType::all() {
321 for entry in resource_type.get_lockfile_entries(lockfile) {
322 names.push(entry.name.clone());
323 }
324 }
325 names
326 }
327
328 pub fn get_resources_by_source<'a>(
330 lockfile: &'a LockFile,
331 resource_type: ResourceType,
332 source: &str,
333 ) -> Vec<&'a LockedResource> {
334 resource_type
335 .get_lockfile_entries(lockfile)
336 .iter()
337 .filter(|e| e.source.as_deref() == Some(source))
338 .collect()
339 }
340
341 pub fn for_each_resource<F>(lockfile: &LockFile, mut f: F)
343 where
344 F: FnMut(ResourceType, &LockedResource),
345 {
346 for resource_type in ResourceType::all() {
347 for entry in resource_type.get_lockfile_entries(lockfile) {
348 f(*resource_type, entry);
349 }
350 }
351 }
352
353 pub fn map_resources<T, F>(lockfile: &LockFile, mut f: F) -> Vec<T>
355 where
356 F: FnMut(ResourceType, &LockedResource) -> T,
357 {
358 let mut results = Vec::new();
359 Self::for_each_resource(lockfile, |rt, entry| {
360 results.push(f(rt, entry));
361 });
362 results
363 }
364
365 pub fn filter_resources<F>(
367 lockfile: &LockFile,
368 mut predicate: F,
369 ) -> Vec<(ResourceType, LockedResource)>
370 where
371 F: FnMut(ResourceType, &LockedResource) -> bool,
372 {
373 let mut results = Vec::new();
374 Self::for_each_resource(lockfile, |rt, entry| {
375 if predicate(rt, entry) {
376 results.push((rt, entry.clone()));
377 }
378 });
379 results
380 }
381
382 pub fn group_by_source(
384 lockfile: &LockFile,
385 ) -> std::collections::HashMap<String, Vec<(ResourceType, LockedResource)>> {
386 let mut groups = std::collections::HashMap::new();
387
388 Self::for_each_resource(lockfile, |rt, entry| {
389 if let Some(ref source) = entry.source {
390 groups.entry(source.clone()).or_insert_with(Vec::new).push((rt, entry.clone()));
391 }
392 });
393
394 groups
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::lockfile::{LockFile, LockedResource};
402 use crate::manifest::{Manifest, TargetConfig};
403
404 fn create_test_lockfile() -> LockFile {
405 let mut lockfile = LockFile::new();
406
407 lockfile.agents.push(LockedResource {
408 name: "test-agent".to_string(),
409 source: Some("community".to_string()),
410 url: Some("https://github.com/test/repo.git".to_string()),
411 path: "agents/test.md".to_string(),
412 version: Some("v1.0.0".to_string()),
413 resolved_commit: Some("abc123".to_string()),
414 checksum: "sha256:abc".to_string(),
415 installed_at: ".claude/agents/test-agent.md".to_string(),
416 dependencies: vec![],
417 resource_type: crate::core::ResourceType::Agent,
418
419 tool: "claude-code".to_string(),
420 });
421
422 lockfile.snippets.push(LockedResource {
423 name: "test-snippet".to_string(),
424 source: Some("community".to_string()),
425 url: Some("https://github.com/test/repo.git".to_string()),
426 path: "snippets/test.md".to_string(),
427 version: Some("v1.0.0".to_string()),
428 resolved_commit: Some("def456".to_string()),
429 checksum: "sha256:def".to_string(),
430 installed_at: ".claude/snippets/test-snippet.md".to_string(),
431 dependencies: vec![],
432 resource_type: crate::core::ResourceType::Snippet,
433
434 tool: "claude-code".to_string(),
435 });
436
437 lockfile
438 }
439
440 #[allow(deprecated)]
441 fn create_test_manifest() -> Manifest {
442 Manifest {
443 target: TargetConfig::default(),
444 ..Default::default()
445 }
446 }
447
448 fn create_multi_resource_lockfile() -> LockFile {
449 let mut lockfile = LockFile::new();
450
451 lockfile.agents.push(LockedResource {
453 name: "agent1".to_string(),
454 source: Some("source1".to_string()),
455 url: Some("https://github.com/source1/repo.git".to_string()),
456 path: "agents/agent1.md".to_string(),
457 version: Some("v1.0.0".to_string()),
458 resolved_commit: Some("abc123".to_string()),
459 checksum: "sha256:abc1".to_string(),
460 installed_at: ".claude/agents/agent1.md".to_string(),
461 dependencies: vec![],
462 resource_type: crate::core::ResourceType::Agent,
463
464 tool: "claude-code".to_string(),
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
479 tool: "claude-code".to_string(),
480 });
481
482 lockfile.commands.push(LockedResource {
484 name: "command1".to_string(),
485 source: Some("source1".to_string()),
486 url: Some("https://github.com/source1/repo.git".to_string()),
487 path: "commands/command1.md".to_string(),
488 version: Some("v1.1.0".to_string()),
489 resolved_commit: Some("ghi789".to_string()),
490 checksum: "sha256:ghi3".to_string(),
491 installed_at: ".claude/commands/command1.md".to_string(),
492 dependencies: vec![],
493 resource_type: crate::core::ResourceType::Command,
494
495 tool: "claude-code".to_string(),
496 });
497
498 lockfile.scripts.push(LockedResource {
500 name: "script1".to_string(),
501 source: Some("source1".to_string()),
502 url: Some("https://github.com/source1/repo.git".to_string()),
503 path: "scripts/build.sh".to_string(),
504 version: Some("v1.0.0".to_string()),
505 resolved_commit: Some("jkl012".to_string()),
506 checksum: "sha256:jkl4".to_string(),
507 installed_at: ".claude/agpm/scripts/script1.sh".to_string(),
508 dependencies: vec![],
509 resource_type: crate::core::ResourceType::Script,
510
511 tool: "claude-code".to_string(),
512 });
513
514 lockfile.hooks.push(LockedResource {
516 name: "hook1".to_string(),
517 source: Some("source2".to_string()),
518 url: Some("https://github.com/source2/repo.git".to_string()),
519 path: "hooks/pre-commit.json".to_string(),
520 version: Some("v1.0.0".to_string()),
521 resolved_commit: Some("mno345".to_string()),
522 checksum: "sha256:mno5".to_string(),
523 installed_at: ".claude/agpm/hooks/hook1.json".to_string(),
524 dependencies: vec![],
525 resource_type: crate::core::ResourceType::Hook,
526
527 tool: "claude-code".to_string(),
528 });
529
530 lockfile.mcp_servers.push(LockedResource {
532 name: "mcp1".to_string(),
533 source: Some("source1".to_string()),
534 url: Some("https://github.com/source1/repo.git".to_string()),
535 path: "mcp-servers/filesystem.json".to_string(),
536 version: Some("v1.0.0".to_string()),
537 resolved_commit: Some("pqr678".to_string()),
538 checksum: "sha256:pqr6".to_string(),
539 installed_at: ".claude/agpm/mcp-servers/mcp1.json".to_string(),
540 dependencies: vec![],
541 resource_type: crate::core::ResourceType::McpServer,
542
543 tool: "claude-code".to_string(),
544 });
545
546 lockfile.snippets.push(LockedResource {
548 name: "local-snippet".to_string(),
549 source: None,
550 url: None,
551 path: "local/snippet.md".to_string(),
552 version: None,
553 resolved_commit: None,
554 checksum: "sha256:local".to_string(),
555 installed_at: ".claude/agpm/snippets/local-snippet.md".to_string(),
556 dependencies: vec![],
557 resource_type: crate::core::ResourceType::Snippet,
558
559 tool: "claude-code".to_string(),
560 });
561
562 lockfile
563 }
564
565 #[test]
566 fn test_resource_type_all() {
567 let all_types = ResourceType::all();
568 assert_eq!(all_types.len(), 6);
569 assert_eq!(all_types[0], ResourceType::Agent);
571 assert_eq!(all_types[1], ResourceType::Snippet);
572 assert_eq!(all_types[2], ResourceType::Command);
573 assert_eq!(all_types[3], ResourceType::McpServer);
574 assert_eq!(all_types[4], ResourceType::Script);
575 assert_eq!(all_types[5], ResourceType::Hook);
576 }
577
578 #[test]
579 fn test_get_lockfile_entries_mut() {
580 let mut lockfile = create_test_lockfile();
581
582 let mut agent_type = ResourceType::Agent;
584 let entries = agent_type.get_lockfile_entries_mut(&mut lockfile);
585 assert_eq!(entries.len(), 1);
586 assert_eq!(entries[0].name, "test-agent");
587
588 entries.push(LockedResource {
590 name: "new-agent".to_string(),
591 source: Some("test".to_string()),
592 url: Some("https://example.com/repo.git".to_string()),
593 path: "agents/new.md".to_string(),
594 version: Some("v1.0.0".to_string()),
595 resolved_commit: Some("xyz789".to_string()),
596 checksum: "sha256:xyz".to_string(),
597 installed_at: ".claude/agents/new-agent.md".to_string(),
598 dependencies: vec![],
599 resource_type: crate::core::ResourceType::Agent,
600
601 tool: "claude-code".to_string(),
602 });
603
604 assert_eq!(lockfile.agents.len(), 2);
606 assert_eq!(lockfile.agents[1].name, "new-agent");
607
608 let mut snippet_type = ResourceType::Snippet;
610 let snippet_entries = snippet_type.get_lockfile_entries_mut(&mut lockfile);
611 assert_eq!(snippet_entries.len(), 1);
612
613 let mut command_type = ResourceType::Command;
614 let command_entries = command_type.get_lockfile_entries_mut(&mut lockfile);
615 assert_eq!(command_entries.len(), 0);
616
617 let mut script_type = ResourceType::Script;
618 let script_entries = script_type.get_lockfile_entries_mut(&mut lockfile);
619 assert_eq!(script_entries.len(), 0);
620
621 let mut hook_type = ResourceType::Hook;
622 let hook_entries = hook_type.get_lockfile_entries_mut(&mut lockfile);
623 assert_eq!(hook_entries.len(), 0);
624
625 let mut mcp_type = ResourceType::McpServer;
626 let mcp_entries = mcp_type.get_lockfile_entries_mut(&mut lockfile);
627 assert_eq!(mcp_entries.len(), 0);
628 }
629
630 #[test]
631 fn test_collect_all_entries() {
632 let lockfile = create_test_lockfile();
633 let manifest = create_test_manifest();
634
635 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
636 assert_eq!(entries.len(), 2);
637
638 assert_eq!(entries[0].0.name, "test-agent");
639 assert_eq!(entries[0].1.replace('\\', "/"), ".claude/agents");
641
642 assert_eq!(entries[1].0.name, "test-snippet");
643 assert_eq!(entries[1].1.replace('\\', "/"), ".claude/agpm/snippets");
645 }
646
647 #[test]
648 fn test_collect_all_entries_empty_lockfile() {
649 let empty_lockfile = LockFile::new();
650 let manifest = create_test_manifest();
651
652 let entries = ResourceIterator::collect_all_entries(&empty_lockfile, &manifest);
653 assert_eq!(entries.len(), 0);
654 }
655
656 #[test]
657 fn test_collect_all_entries_multiple_resources() {
658 let lockfile = create_multi_resource_lockfile();
659 let manifest = create_test_manifest();
660
661 let entries = ResourceIterator::collect_all_entries(&lockfile, &manifest);
662
663 assert_eq!(entries.len(), 5);
666
667 let mut found_types = std::collections::HashSet::new();
669 for (resource, _) in &entries {
670 match resource.name.as_str() {
671 "agent1" | "agent2" => {
672 found_types.insert("agent");
673 }
674 "local-snippet" => {
675 found_types.insert("snippet");
676 }
677 "command1" => {
678 found_types.insert("command");
679 }
680 "script1" => {
681 found_types.insert("script");
682 }
683 "hook1" | "mcp1" => {
685 panic!("Hooks and MCP servers should not be in collected entries");
686 }
687 _ => {}
688 }
689 }
690
691 assert_eq!(found_types.len(), 4);
692 }
693
694 #[test]
695 fn test_find_resource_by_name() {
696 let lockfile = create_test_lockfile();
697
698 let result = ResourceIterator::find_resource_by_name(&lockfile, "test-agent");
699 assert!(result.is_some());
700 let (rt, resource) = result.unwrap();
701 assert_eq!(rt, ResourceType::Agent);
702 assert_eq!(resource.name, "test-agent");
703
704 let result = ResourceIterator::find_resource_by_name(&lockfile, "nonexistent");
705 assert!(result.is_none());
706 }
707
708 #[test]
709 fn test_find_resource_by_name_multiple_types() {
710 let lockfile = create_multi_resource_lockfile();
711
712 let result = ResourceIterator::find_resource_by_name(&lockfile, "agent1");
714 assert!(result.is_some());
715 let (rt, resource) = result.unwrap();
716 assert_eq!(rt, ResourceType::Agent);
717 assert_eq!(resource.name, "agent1");
718
719 let result = ResourceIterator::find_resource_by_name(&lockfile, "command1");
721 assert!(result.is_some());
722 let (rt, resource) = result.unwrap();
723 assert_eq!(rt, ResourceType::Command);
724 assert_eq!(resource.name, "command1");
725
726 let result = ResourceIterator::find_resource_by_name(&lockfile, "script1");
728 assert!(result.is_some());
729 let (rt, resource) = result.unwrap();
730 assert_eq!(rt, ResourceType::Script);
731 assert_eq!(resource.name, "script1");
732
733 let result = ResourceIterator::find_resource_by_name(&lockfile, "hook1");
735 assert!(result.is_some());
736 let (rt, resource) = result.unwrap();
737 assert_eq!(rt, ResourceType::Hook);
738 assert_eq!(resource.name, "hook1");
739
740 let result = ResourceIterator::find_resource_by_name(&lockfile, "mcp1");
742 assert!(result.is_some());
743 let (rt, resource) = result.unwrap();
744 assert_eq!(rt, ResourceType::McpServer);
745 assert_eq!(resource.name, "mcp1");
746
747 let result = ResourceIterator::find_resource_by_name(&lockfile, "local-snippet");
749 assert!(result.is_some());
750 let (rt, resource) = result.unwrap();
751 assert_eq!(rt, ResourceType::Snippet);
752 assert_eq!(resource.name, "local-snippet");
753 assert!(resource.source.is_none());
754 }
755
756 #[test]
757 fn test_count_and_has_resources() {
758 let lockfile = create_test_lockfile();
759 assert_eq!(ResourceIterator::count_total_resources(&lockfile), 2);
760 assert!(ResourceIterator::has_resources(&lockfile));
761
762 let empty_lockfile = LockFile::new();
763 assert_eq!(ResourceIterator::count_total_resources(&empty_lockfile), 0);
764 assert!(!ResourceIterator::has_resources(&empty_lockfile));
765
766 let multi_lockfile = create_multi_resource_lockfile();
767 assert_eq!(ResourceIterator::count_total_resources(&multi_lockfile), 7);
768 assert!(ResourceIterator::has_resources(&multi_lockfile));
769 }
770
771 #[test]
772 fn test_get_all_resource_names() {
773 let lockfile = create_test_lockfile();
774 let names = ResourceIterator::get_all_resource_names(&lockfile);
775
776 assert_eq!(names.len(), 2);
777 assert!(names.contains(&"test-agent".to_string()));
778 assert!(names.contains(&"test-snippet".to_string()));
779 }
780
781 #[test]
782 fn test_get_all_resource_names_empty() {
783 let empty_lockfile = LockFile::new();
784 let names = ResourceIterator::get_all_resource_names(&empty_lockfile);
785 assert_eq!(names.len(), 0);
786 }
787
788 #[test]
789 fn test_get_all_resource_names_multiple() {
790 let lockfile = create_multi_resource_lockfile();
791 let names = ResourceIterator::get_all_resource_names(&lockfile);
792
793 assert_eq!(names.len(), 7);
794 assert!(names.contains(&"agent1".to_string()));
795 assert!(names.contains(&"agent2".to_string()));
796 assert!(names.contains(&"local-snippet".to_string()));
797 assert!(names.contains(&"command1".to_string()));
798 assert!(names.contains(&"script1".to_string()));
799 assert!(names.contains(&"hook1".to_string()));
800 assert!(names.contains(&"mcp1".to_string()));
801 }
802
803 #[test]
804 fn test_get_resources_by_source() {
805 let lockfile = create_multi_resource_lockfile();
806
807 let source1_resources =
809 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source1");
810 assert_eq!(source1_resources.len(), 1);
811 assert_eq!(source1_resources[0].name, "agent1");
812
813 let source1_commands =
814 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Command, "source1");
815 assert_eq!(source1_commands.len(), 1);
816 assert_eq!(source1_commands[0].name, "command1");
817
818 let source1_scripts =
819 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Script, "source1");
820 assert_eq!(source1_scripts.len(), 1);
821 assert_eq!(source1_scripts[0].name, "script1");
822
823 let source1_mcps = ResourceIterator::get_resources_by_source(
824 &lockfile,
825 ResourceType::McpServer,
826 "source1",
827 );
828 assert_eq!(source1_mcps.len(), 1);
829 assert_eq!(source1_mcps[0].name, "mcp1");
830
831 let source2_agents =
833 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Agent, "source2");
834 assert_eq!(source2_agents.len(), 1);
835 assert_eq!(source2_agents[0].name, "agent2");
836
837 let source2_hooks =
838 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Hook, "source2");
839 assert_eq!(source2_hooks.len(), 1);
840 assert_eq!(source2_hooks[0].name, "hook1");
841
842 let nonexistent = ResourceIterator::get_resources_by_source(
844 &lockfile,
845 ResourceType::Agent,
846 "nonexistent",
847 );
848 assert_eq!(nonexistent.len(), 0);
849
850 let source1_snippets =
852 ResourceIterator::get_resources_by_source(&lockfile, ResourceType::Snippet, "source1");
853 assert_eq!(source1_snippets.len(), 0);
854 }
855
856 #[test]
857 fn test_for_each_resource() {
858 let lockfile = create_multi_resource_lockfile();
859 let mut visited_resources = Vec::new();
860
861 ResourceIterator::for_each_resource(&lockfile, |resource_type, resource| {
862 visited_resources.push((resource_type, resource.name.clone()));
863 });
864
865 assert_eq!(visited_resources.len(), 7);
866
867 let expected_resources = vec![
869 (ResourceType::Agent, "agent1".to_string()),
870 (ResourceType::Agent, "agent2".to_string()),
871 (ResourceType::Snippet, "local-snippet".to_string()),
872 (ResourceType::Command, "command1".to_string()),
873 (ResourceType::Script, "script1".to_string()),
874 (ResourceType::Hook, "hook1".to_string()),
875 (ResourceType::McpServer, "mcp1".to_string()),
876 ];
877
878 for expected in expected_resources {
879 assert!(visited_resources.contains(&expected));
880 }
881 }
882
883 #[test]
884 fn test_for_each_resource_empty() {
885 let empty_lockfile = LockFile::new();
886 let mut count = 0;
887
888 ResourceIterator::for_each_resource(&empty_lockfile, |_, _| {
889 count += 1;
890 });
891
892 assert_eq!(count, 0);
893 }
894
895 #[test]
896 fn test_map_resources() {
897 let lockfile = create_multi_resource_lockfile();
898
899 let names = ResourceIterator::map_resources(&lockfile, |_, resource| resource.name.clone());
901
902 assert_eq!(names.len(), 7);
903 assert!(names.contains(&"agent1".to_string()));
904 assert!(names.contains(&"agent2".to_string()));
905 assert!(names.contains(&"local-snippet".to_string()));
906 assert!(names.contains(&"command1".to_string()));
907 assert!(names.contains(&"script1".to_string()));
908 assert!(names.contains(&"hook1".to_string()));
909 assert!(names.contains(&"mcp1".to_string()));
910
911 let type_name_pairs =
913 ResourceIterator::map_resources(&lockfile, |resource_type, resource| {
914 format!("{}:{}", resource_type, resource.name)
915 });
916
917 assert_eq!(type_name_pairs.len(), 7);
918 assert!(type_name_pairs.contains(&"agent:agent1".to_string()));
919 assert!(type_name_pairs.contains(&"agent:agent2".to_string()));
920 assert!(type_name_pairs.contains(&"snippet:local-snippet".to_string()));
921 assert!(type_name_pairs.contains(&"command:command1".to_string()));
922 assert!(type_name_pairs.contains(&"script:script1".to_string()));
923 assert!(type_name_pairs.contains(&"hook:hook1".to_string()));
924 assert!(type_name_pairs.contains(&"mcp-server:mcp1".to_string()));
925 }
926
927 #[test]
928 fn test_map_resources_empty() {
929 let empty_lockfile = LockFile::new();
930
931 let results =
932 ResourceIterator::map_resources(&empty_lockfile, |_, resource| resource.name.clone());
933
934 assert_eq!(results.len(), 0);
935 }
936
937 #[test]
938 fn test_filter_resources() {
939 let lockfile = create_multi_resource_lockfile();
940
941 let source1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
943 resource.source.as_deref() == Some("source1")
944 });
945
946 assert_eq!(source1_resources.len(), 4); let source1_names: Vec<String> =
948 source1_resources.iter().map(|(_, r)| r.name.clone()).collect();
949 assert!(source1_names.contains(&"agent1".to_string()));
950 assert!(source1_names.contains(&"command1".to_string()));
951 assert!(source1_names.contains(&"script1".to_string()));
952 assert!(source1_names.contains(&"mcp1".to_string()));
953
954 let agents = ResourceIterator::filter_resources(&lockfile, |resource_type, _| {
956 resource_type == ResourceType::Agent
957 });
958
959 assert_eq!(agents.len(), 2); let agent_names: Vec<String> = agents.iter().map(|(_, r)| r.name.clone()).collect();
961 assert!(agent_names.contains(&"agent1".to_string()));
962 assert!(agent_names.contains(&"agent2".to_string()));
963
964 let no_source_resources =
966 ResourceIterator::filter_resources(&lockfile, |_, resource| resource.source.is_none());
967
968 assert_eq!(no_source_resources.len(), 1); assert_eq!(no_source_resources[0].1.name, "local-snippet");
970
971 let v1_resources = ResourceIterator::filter_resources(&lockfile, |_, resource| {
973 resource.version.as_deref().unwrap_or("").starts_with("v1.")
974 });
975
976 assert_eq!(v1_resources.len(), 5); let no_matches = ResourceIterator::filter_resources(&lockfile, |_, resource| {
980 resource.name == "nonexistent"
981 });
982
983 assert_eq!(no_matches.len(), 0);
984 }
985
986 #[test]
987 fn test_filter_resources_empty() {
988 let empty_lockfile = LockFile::new();
989
990 let results = ResourceIterator::filter_resources(&empty_lockfile, |_, _| true);
991 assert_eq!(results.len(), 0);
992 }
993
994 #[test]
995 fn test_group_by_source() {
996 let lockfile = create_multi_resource_lockfile();
997
998 let groups = ResourceIterator::group_by_source(&lockfile);
999
1000 assert_eq!(groups.len(), 2); let source1_group = groups.get("source1").unwrap();
1004 assert_eq!(source1_group.len(), 4); let source1_names: Vec<String> =
1007 source1_group.iter().map(|(_, r)| r.name.clone()).collect();
1008 assert!(source1_names.contains(&"agent1".to_string()));
1009 assert!(source1_names.contains(&"command1".to_string()));
1010 assert!(source1_names.contains(&"script1".to_string()));
1011 assert!(source1_names.contains(&"mcp1".to_string()));
1012
1013 let source2_group = groups.get("source2").unwrap();
1015 assert_eq!(source2_group.len(), 2); let source2_names: Vec<String> =
1018 source2_group.iter().map(|(_, r)| r.name.clone()).collect();
1019 assert!(source2_names.contains(&"agent2".to_string()));
1020 assert!(source2_names.contains(&"hook1".to_string()));
1021
1022 assert!(!groups.contains_key(""));
1024 }
1025
1026 #[test]
1027 fn test_group_by_source_empty() {
1028 let empty_lockfile = LockFile::new();
1029
1030 let groups = ResourceIterator::group_by_source(&empty_lockfile);
1031 assert_eq!(groups.len(), 0);
1032 }
1033
1034 #[test]
1035 fn test_group_by_source_no_sources() {
1036 let mut lockfile = LockFile::new();
1037
1038 lockfile.agents.push(LockedResource {
1040 name: "local-agent".to_string(),
1041 source: None,
1042 url: None,
1043 path: "local/agent.md".to_string(),
1044 version: None,
1045 resolved_commit: None,
1046 checksum: "sha256:local".to_string(),
1047 installed_at: ".claude/agents/local-agent.md".to_string(),
1048 dependencies: vec![],
1049 resource_type: crate::core::ResourceType::Agent,
1050
1051 tool: "claude-code".to_string(),
1052 });
1053
1054 let groups = ResourceIterator::group_by_source(&lockfile);
1055 assert_eq!(groups.len(), 0); }
1057
1058 #[test]
1059 fn test_resource_type_ext() {
1060 let lockfile = create_test_lockfile();
1061
1062 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 1);
1063 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1064 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 0);
1065 }
1066
1067 #[test]
1068 fn test_resource_type_ext_all_types() {
1069 let lockfile = create_multi_resource_lockfile();
1070
1071 assert_eq!(ResourceType::Agent.get_lockfile_entries(&lockfile).len(), 2);
1072 assert_eq!(ResourceType::Snippet.get_lockfile_entries(&lockfile).len(), 1);
1073 assert_eq!(ResourceType::Command.get_lockfile_entries(&lockfile).len(), 1);
1074 assert_eq!(ResourceType::Script.get_lockfile_entries(&lockfile).len(), 1);
1075 assert_eq!(ResourceType::Hook.get_lockfile_entries(&lockfile).len(), 1);
1076 assert_eq!(ResourceType::McpServer.get_lockfile_entries(&lockfile).len(), 1);
1077 }
1078
1079 #[test]
1080 #[allow(deprecated)]
1081 fn test_resource_type_get_target_dir() {
1082 let manifest = create_test_manifest();
1083 let targets = &manifest.target;
1084
1085 assert_eq!(ResourceType::Agent.get_target_dir(targets), ".claude/agents");
1086 assert_eq!(ResourceType::Snippet.get_target_dir(targets), ".claude/agpm/snippets");
1087 assert_eq!(ResourceType::Command.get_target_dir(targets), ".claude/commands");
1088 assert_eq!(ResourceType::Script.get_target_dir(targets), ".claude/agpm/scripts");
1089 assert_eq!(ResourceType::Hook.get_target_dir(targets), ".claude/agpm/hooks");
1090 assert_eq!(ResourceType::McpServer.get_target_dir(targets), ".claude/agpm/mcp-servers");
1091 }
1092}